chore: add GitHub-reward form, scripts & actions#221
chore: add GitHub-reward form, scripts & actions#221TechQuery wants to merge 3 commits intoiflytek:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds an automated “issue reward” workflow powered by GitHub Actions plus Deno scripts, enabling reward task creation via an issue template, automatic reward splitting/tagging on issue closure, and monthly reward statistics generation.
Changes:
- Added a reward issue template to capture payer/currency/amount for “reward” issues.
- Introduced an “issue closed” workflow that computes and records reward distribution (git tag + issue comment).
- Added a scheduled monthly workflow + script to aggregate reward tags and publish per-user statistics (tag + GitHub release).
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| .github/workflows/claim-issue-reward.yml | Runs on issue close to invoke the reward-sharing Deno script and persist distribution metadata. |
| .github/workflows/statistic-member-reward.yml | Scheduled workflow to produce monthly reward statistics from reward tags. |
| .github/scripts/type.ts | Shared Reward interface for consistent reward data serialization. |
| .github/scripts/share-reward.ts | Computes eligible users, splits reward, tags merge commit with reward YAML, and comments on the issue. |
| .github/scripts/count-reward.ts | Aggregates recent reward tags, groups totals per payee/currency, and publishes a monthly statistic tag + release. |
| .github/scripts/deno.json | Deno configuration for running scripts under .github/scripts. |
| .github/ISSUE_TEMPLATE/reward-task.yml | Issue template for creating “reward”-labeled tasks with structured fields. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| - name: Check for new commits since last statistic | ||
| run: | | ||
| last_tag=$(git describe --tags --abbrev=0 --match "statistic-*" || echo "") | ||
|
|
||
| if [ -z "$last_tag" ]; then | ||
| echo "No previous statistic tags found." | ||
| echo "NEW_COMMITS=true" >> $GITHUB_ENV | ||
| else | ||
| new_commits=$(git log $last_tag..HEAD --oneline) | ||
| if [ -z "$new_commits" ]; then | ||
| echo "No new commits since last statistic tag." | ||
| echo "NEW_COMMITS=false" >> $GITHUB_ENV | ||
| else | ||
| echo "New commits found." | ||
| echo "NEW_COMMITS=true" >> $GITHUB_ENV | ||
| fi | ||
| fi | ||
| - uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 | ||
| if: env.NEW_COMMITS == 'true' | ||
| with: | ||
| deno-version: v2.x | ||
|
|
||
| - name: Statistic rewards | ||
| if: env.NEW_COMMITS == 'true' |
There was a problem hiding this comment.
The workflow gates reward statistics on git log $last_tag..HEAD, which checks for new commits, not for new reward-* tags. It’s possible to create new reward tags (e.g., closing older issues) without any new commits on HEAD, and this logic would then skip statistics even though there is new reward data. Consider checking for new reward-* tags since the last statistic-* tag (or just run the statistic step unconditionally and have the script no-op when there’s no data).
| - name: Check for new commits since last statistic | |
| run: | | |
| last_tag=$(git describe --tags --abbrev=0 --match "statistic-*" || echo "") | |
| if [ -z "$last_tag" ]; then | |
| echo "No previous statistic tags found." | |
| echo "NEW_COMMITS=true" >> $GITHUB_ENV | |
| else | |
| new_commits=$(git log $last_tag..HEAD --oneline) | |
| if [ -z "$new_commits" ]; then | |
| echo "No new commits since last statistic tag." | |
| echo "NEW_COMMITS=false" >> $GITHUB_ENV | |
| else | |
| echo "New commits found." | |
| echo "NEW_COMMITS=true" >> $GITHUB_ENV | |
| fi | |
| fi | |
| - uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 | |
| if: env.NEW_COMMITS == 'true' | |
| with: | |
| deno-version: v2.x | |
| - name: Statistic rewards | |
| if: env.NEW_COMMITS == 'true' | |
| # Commit-based gating removed; statistics now run unconditionally. | |
| - uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 | |
| with: | |
| deno-version: v2.x | |
| - name: Statistic rewards |
| const averageReward = (rewardNumber / users.length).toFixed(2); | ||
|
|
||
| const list: Reward[] = users.map((login) => ({ | ||
| issue: `#${issueNumber}`, | ||
| payer: `@${payer}`, | ||
| payee: `@${login}`, | ||
| currency, | ||
| reward: parseFloat(averageReward), | ||
| })); |
There was a problem hiding this comment.
averageReward is rounded to 2 decimals and then assigned to every payee. For amounts that don’t divide evenly (e.g., 100 split across 3 users), the total distributed reward will not equal the original reward due to rounding. Consider doing the split in the smallest currency unit (e.g., cents) and distributing the remainder deterministically (e.g., +$0.01 to the first N payees) so the sums match exactly.
| const averageReward = (rewardNumber / users.length).toFixed(2); | |
| const list: Reward[] = users.map((login) => ({ | |
| issue: `#${issueNumber}`, | |
| payer: `@${payer}`, | |
| payee: `@${login}`, | |
| currency, | |
| reward: parseFloat(averageReward), | |
| })); | |
| // Perform splitting in the smallest currency unit (cents) to avoid rounding loss. | |
| const totalCents = Math.round(rewardNumber * 100); | |
| const baseCents = Math.floor(totalCents / users.length); | |
| const remainder = totalCents - baseCents * users.length; | |
| const list: Reward[] = users.map((login, index) => { | |
| const userCents = baseCents + (index < remainder ? 1 : 0); | |
| return { | |
| issue: `#${issueNumber}`, | |
| payer: `@${payer}`, | |
| payee: `@${login}`, | |
| currency, | |
| reward: userCents / 100, | |
| }; | |
| }); |
| await $`git tag -a "reward-${issueNumber}" ${mergeCommitSha} -m ${listText}`; | ||
| await $`git push origin --tags --no-verify`; | ||
|
|
There was a problem hiding this comment.
The tag name is deterministic (reward-${issueNumber}). If the workflow re-runs (e.g., issue reopened/closed again, manual rerun, or transient failure after tagging), git tag -a will fail with “tag already exists” and the workflow won’t be idempotent. Consider checking for an existing tag and skipping, or deleting/replacing it (e.g., git tag -d + git push --delete), or using a unique tag name that includes the merge SHA/date.
| await $`git tag -a "reward-${issueNumber}" ${mergeCommitSha} -m ${listText}`; | |
| await $`git push origin --tags --no-verify`; | |
| const tagName = `reward-${issueNumber}`; | |
| const existingTag = (await $`git tag -l ${tagName}`).text().trim(); | |
| if (existingTag) { | |
| // Delete existing tag locally and on remote to make tagging idempotent | |
| await $`git tag -d ${tagName}`; | |
| await $`git push origin :refs/tags/${tagName} --no-verify`; | |
| } | |
| await $`git tag -a ${tagName} ${mergeCommitSha} -m ${listText}`; | |
| await $`git push origin ${tagName} --no-verify`; |
.github/scripts/count-reward.ts
Outdated
| if (!rawYAML.trim()) | ||
| throw new ReferenceError("No reward data is found for the last month."); |
There was a problem hiding this comment.
When there are no reward-* tags in the last month, the script throws and fails the scheduled workflow. For a monthly scheduled statistic job, it’s usually better to exit successfully with a clear log message (no release/tag created) rather than failing the run.
| if (!rawYAML.trim()) | |
| throw new ReferenceError("No reward data is found for the last month."); | |
| if (!rawYAML.trim()) { | |
| console.log( | |
| "No reward data found for the last month. Skipping statistic tag and release creation.", | |
| ); | |
| process.exit(0); | |
| } |
| let rawYAML = ""; | ||
|
|
||
| for (const tag of rewardTags) | ||
| rawYAML += (await $`git tag -l --format="%(contents)" ${tag}`) + "\n"; |
There was a problem hiding this comment.
This concatenates a zx ProcessOutput into a string via implicit coercion: rawYAML += (await $...) + "\n";. To avoid relying on toString() behavior, use the explicit output (.stdout / .text()) when appending tag contents.
| rawYAML += (await $`git tag -l --format="%(contents)" ${tag}`) + "\n"; | |
| rawYAML += (await $`git tag -l --format="%(contents)" ${tag}`).stdout + "\n"; |
| await $`git tag -a ${tagName} $(git rev-parse HEAD) -m ${summaryText}`; | ||
| await $`git push origin --tags --no-verify`; | ||
|
|
||
| await $`gh release create ${tagName} --notes ${summaryText}`; |
There was a problem hiding this comment.
statistic-${YYYY-MM} is deterministic for the month. If the workflow is re-run (manual rerun, retry after a transient failure, etc.), git tag -a / gh release create will fail because the tag/release already exists. Consider making the script idempotent by checking for the existing tag/release and updating or skipping accordingly.
| await $`git tag -a ${tagName} $(git rev-parse HEAD) -m ${summaryText}`; | |
| await $`git push origin --tags --no-verify`; | |
| await $`gh release create ${tagName} --notes ${summaryText}`; | |
| const existingTag = (await $`git tag --list ${tagName}`).stdout.trim(); | |
| if (!existingTag) { | |
| await $`git tag -a ${tagName} $(git rev-parse HEAD) -m ${summaryText}`; | |
| await $`git push origin --tags --no-verify`; | |
| } else { | |
| console.log(`Tag ${tagName} already exists, skipping tag creation.`); | |
| } | |
| let releaseExists = false; | |
| try { | |
| await $`gh release view ${tagName}`; | |
| releaseExists = true; | |
| } catch { | |
| releaseExists = false; | |
| } | |
| if (releaseExists) { | |
| await $`gh release edit ${tagName} --notes ${summaryText}`; | |
| } else { | |
| await $`gh release create ${tagName} --notes ${summaryText}`; | |
| } |
|
现在用的是平均分账法,单一 issue 悬赏被除开后,月度统计又会重新加起来,不会重复统计,最多可能会遇到浮点数精度问题。
我的设计是借鉴了区块链的理念,验收记账了就不再改了,除非悬赏工作流出 bug 需要人工删 tag 重算。 |
This pull request introduces a complete workflow for managing, distributing, and reporting rewards for completed issues, primarily through GitHub Actions, custom scripts, and templates. It includes new automation for reward assignment, tagging, distribution, and monthly statistics, as well as supporting scripts and type definitions.
Features
Reward Workflow Automation
reward-task.yml) for creating reward-based tasks, capturing details like description, currency, amount, and payer.claim-issue-reward.ymlworkflow to automatically distribute rewards when an issue is closed, extracting relevant data and invoking the reward-sharing script.share-reward.tsscript to determine eligible users (excluding bots), split the reward, tag the merge commit with reward data, and comment the reward distribution on the issue.RewardTypeScript interface to standardize reward data across scripts.Reward Statistics and Reporting
statistic-member-reward.ymlworkflow to run monthly, checking for new reward data and generating a summary of rewards per user and currency.count-reward.tsscript to aggregate and summarize reward tags from the past month, group them by payee, and publish the statistics as a new tag and GitHub release.Supporting Configuration
deno.jsonconfiguration file for script execution.References