mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
289 lines
11 KiB
YAML
289 lines
11 KiB
YAML
---
|
|
name: Contributor Rewards
|
|
|
|
on:
|
|
pull_request_target:
|
|
types: [closed, labeled]
|
|
issues:
|
|
types: [labeled]
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: read
|
|
issues: write
|
|
pull-requests: read
|
|
|
|
concurrency:
|
|
group: contributor-rewards-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
reward:
|
|
name: Invite eligible contributor
|
|
runs-on: ubuntu-latest
|
|
if: >-
|
|
github.event_name == 'workflow_dispatch' ||
|
|
(github.event_name == 'pull_request_target' &&
|
|
github.event.action == 'closed' &&
|
|
github.event.pull_request.merged == true) ||
|
|
(github.event.action == 'labeled' &&
|
|
github.event.label.name == 'reward user')
|
|
steps:
|
|
- name: Post reward invite when eligible
|
|
uses: actions/github-script@v8
|
|
env:
|
|
CONTRIBUTOR_REWARD_TRIGGER_LABEL: reward user
|
|
CONTRIBUTOR_REWARD_SENT_LABEL: reward sent
|
|
CONTRIBUTOR_REWARD_DISCORD_URL: ${{ vars.CONTRIBUTOR_REWARD_DISCORD_URL }}
|
|
CONTRIBUTOR_REWARD_MERCH_URL: ${{ vars.CONTRIBUTOR_REWARD_MERCH_URL }}
|
|
CONTRIBUTOR_REWARD_MESSAGE: ${{ vars.CONTRIBUTOR_REWARD_MESSAGE }}
|
|
with:
|
|
script: |
|
|
const { owner, repo } = context.repo;
|
|
const triggerLabel = (process.env.CONTRIBUTOR_REWARD_TRIGGER_LABEL || 'reward user').trim();
|
|
const sentLabel = (process.env.CONTRIBUTOR_REWARD_SENT_LABEL || 'reward sent').trim();
|
|
const discordUrl =
|
|
(process.env.CONTRIBUTOR_REWARD_DISCORD_URL || 'https://discord.tinyhumans.ai/').trim();
|
|
const merchUrl = (process.env.CONTRIBUTOR_REWARD_MERCH_URL || '').trim();
|
|
const customMessage = (process.env.CONTRIBUTOR_REWARD_MESSAGE || '').trim();
|
|
|
|
const normalize = value => String(value || '').trim().toLowerCase();
|
|
const triggerLabelKey = normalize(triggerLabel);
|
|
|
|
function markerFor(login) {
|
|
return `<!-- openhuman-contributor-reward:user=${normalize(login)} -->`;
|
|
}
|
|
|
|
async function ensureLabel(name, color, description) {
|
|
try {
|
|
await github.rest.issues.getLabel({ owner, repo, name });
|
|
return;
|
|
} catch (error) {
|
|
if (error.status !== 404) throw error;
|
|
}
|
|
|
|
try {
|
|
await github.rest.issues.createLabel({ owner, repo, name, color, description });
|
|
core.info(`Created label "${name}".`);
|
|
} catch (error) {
|
|
if (error.status !== 422) throw error;
|
|
core.info(`Label "${name}" already exists.`);
|
|
}
|
|
}
|
|
|
|
function labeledWithTrigger() {
|
|
return normalize(context.payload.label?.name) === triggerLabelKey;
|
|
}
|
|
|
|
function actorIsBot(target) {
|
|
return target.userType === 'Bot' || /\[bot\]$/i.test(target.login);
|
|
}
|
|
|
|
function targetFromEvent() {
|
|
if (context.eventName === 'workflow_dispatch') {
|
|
return { bootstrapOnly: true };
|
|
}
|
|
|
|
if (context.eventName === 'pull_request_target') {
|
|
const pr = context.payload.pull_request;
|
|
if (!pr) return null;
|
|
|
|
if (context.payload.action === 'closed') {
|
|
if (!pr.merged) {
|
|
core.info(`PR #${pr.number} was closed without merge; skipping reward flow.`);
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
kind: 'pull request',
|
|
number: pr.number,
|
|
htmlUrl: pr.html_url,
|
|
login: pr.user.login,
|
|
userType: pr.user.type,
|
|
reason: 'first merged pull request',
|
|
manualOverride: false,
|
|
requireFirstMergedPr: true,
|
|
};
|
|
}
|
|
|
|
if (context.payload.action === 'labeled') {
|
|
if (!labeledWithTrigger()) {
|
|
core.info(`Label "${context.payload.label?.name}" is not "${triggerLabel}"; skipping.`);
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
kind: 'pull request',
|
|
number: pr.number,
|
|
htmlUrl: pr.html_url,
|
|
login: pr.user.login,
|
|
userType: pr.user.type,
|
|
reason: `maintainer-applied "${triggerLabel}" label on a pull request`,
|
|
manualOverride: true,
|
|
requireFirstMergedPr: false,
|
|
};
|
|
}
|
|
}
|
|
|
|
if (context.eventName === 'issues') {
|
|
const issue = context.payload.issue;
|
|
if (!issue) return null;
|
|
|
|
if (issue.pull_request) {
|
|
core.info(`Issue event is for PR #${issue.number}; pull_request_target handles PR labels.`);
|
|
return null;
|
|
}
|
|
|
|
if (!labeledWithTrigger()) {
|
|
core.info(`Label "${context.payload.label?.name}" is not "${triggerLabel}"; skipping.`);
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
kind: 'issue',
|
|
number: issue.number,
|
|
htmlUrl: issue.html_url,
|
|
login: issue.user.login,
|
|
userType: issue.user.type,
|
|
reason: `maintainer-applied "${triggerLabel}" label on an issue`,
|
|
manualOverride: true,
|
|
requireFirstMergedPr: false,
|
|
};
|
|
}
|
|
|
|
core.info(`Unsupported event "${context.eventName}"; skipping.`);
|
|
return null;
|
|
}
|
|
|
|
async function isFirstMergedPullRequest(login, currentNumber) {
|
|
const query = `repo:${owner}/${repo} is:pr is:merged author:${login}`;
|
|
const response = await github.rest.search.issuesAndPullRequests({
|
|
q: query,
|
|
per_page: 100,
|
|
});
|
|
const previous = response.data.items.filter(item => item.number !== currentNumber);
|
|
core.info(
|
|
`Found ${previous.length} earlier merged PR(s) for ${login} while evaluating #${currentNumber}.`
|
|
);
|
|
return previous.length === 0;
|
|
}
|
|
|
|
async function targetAlreadyRewarded(target) {
|
|
const marker = markerFor(target.login);
|
|
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
owner,
|
|
repo,
|
|
issue_number: target.number,
|
|
per_page: 100,
|
|
});
|
|
return comments.some(comment => comment.body?.includes(marker));
|
|
}
|
|
|
|
async function contributorAlreadyRewardedElsewhere(target) {
|
|
const markerNeedle = `openhuman-contributor-reward:user=${normalize(target.login)}`;
|
|
const query = `repo:${owner}/${repo} "${markerNeedle}" in:comments`;
|
|
const response = await github.rest.search.issuesAndPullRequests({
|
|
q: query,
|
|
per_page: 10,
|
|
});
|
|
return response.data.items.some(item => item.number !== target.number);
|
|
}
|
|
|
|
function renderCustomMessage(target) {
|
|
const replacements = {
|
|
'{user}': `@${target.login}`,
|
|
'{login}': target.login,
|
|
'{discord_url}': discordUrl,
|
|
'{merch_url}': merchUrl,
|
|
'{reason}': target.reason,
|
|
'{target_url}': target.htmlUrl,
|
|
};
|
|
|
|
let body = customMessage;
|
|
for (const [token, value] of Object.entries(replacements)) {
|
|
body = body.split(token).join(value);
|
|
}
|
|
return `${markerFor(target.login)}\n${body}`;
|
|
}
|
|
|
|
function renderDefaultMessage(target) {
|
|
const merchLine = merchUrl
|
|
? `3. Claim merch: ${merchUrl}`
|
|
: '3. For merch, ask a maintainer in Discord. Keep shipping details out of GitHub comments.';
|
|
|
|
return [
|
|
markerFor(target.login),
|
|
`Thanks @${target.login} for contributing to OpenHuman.`,
|
|
'',
|
|
'You are eligible for the contributor reward flow:',
|
|
'',
|
|
`1. Join the OpenHuman Discord: ${discordUrl}`,
|
|
`2. Share this ${target.kind} link so maintainers can verify your GitHub handle: ${target.htmlUrl}`,
|
|
merchLine,
|
|
'',
|
|
`*Reason: ${target.reason}. Automatic rewards are idempotent per GitHub user; maintainer-applied "${triggerLabel}" labels can intentionally start the flow for special cases.*`,
|
|
].join('\n');
|
|
}
|
|
|
|
const target = targetFromEvent();
|
|
|
|
await ensureLabel(triggerLabel, 'fbca04', 'Maintainer-triggered contributor reward invite');
|
|
await ensureLabel(sentLabel, '0e8a16', 'Contributor reward invite has been posted');
|
|
|
|
if (!target) {
|
|
await core.summary.addHeading('Contributor Rewards').addRaw('No eligible target for this event.').write();
|
|
return;
|
|
}
|
|
|
|
if (target.bootstrapOnly) {
|
|
await core.summary
|
|
.addHeading('Contributor Rewards')
|
|
.addRaw(`Labels "${triggerLabel}" and "${sentLabel}" are ready.`)
|
|
.write();
|
|
return;
|
|
}
|
|
|
|
if (actorIsBot(target)) {
|
|
core.info(`Skipping bot account ${target.login}.`);
|
|
return;
|
|
}
|
|
|
|
if (target.requireFirstMergedPr) {
|
|
const firstMergedPr = await isFirstMergedPullRequest(target.login, target.number);
|
|
if (!firstMergedPr) {
|
|
core.info(`${target.login} already has a merged PR; skipping automatic reward.`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (await targetAlreadyRewarded(target)) {
|
|
core.info(`#${target.number} already has a reward marker for ${target.login}; skipping.`);
|
|
return;
|
|
}
|
|
|
|
if (!target.manualOverride && (await contributorAlreadyRewardedElsewhere(target))) {
|
|
core.info(`${target.login} already has a reward marker elsewhere; skipping automatic reward.`);
|
|
return;
|
|
}
|
|
|
|
const body = customMessage ? renderCustomMessage(target) : renderDefaultMessage(target);
|
|
|
|
await github.rest.issues.createComment({
|
|
owner,
|
|
repo,
|
|
issue_number: target.number,
|
|
body,
|
|
});
|
|
await github.rest.issues.addLabels({
|
|
owner,
|
|
repo,
|
|
issue_number: target.number,
|
|
labels: [sentLabel],
|
|
});
|
|
|
|
core.info(`Posted contributor reward invite for ${target.login} on #${target.number}.`);
|
|
await core.summary
|
|
.addHeading('Contributor Rewards')
|
|
.addRaw(`Posted reward invite for @${target.login} on ${target.htmlUrl}.`)
|
|
.write();
|