Skip to content

Commit 654f14b

Browse files
committed
[add] GitHub-reward configuration & scripts
1 parent 68d89e4 commit 654f14b

File tree

6 files changed

+289
-0
lines changed

6 files changed

+289
-0
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: 💰 Reward Task
2+
description: Task issue with Reward
3+
title: '[Reward] '
4+
labels:
5+
- reward
6+
body:
7+
- type: textarea
8+
id: description
9+
attributes:
10+
label: Task description
11+
validations:
12+
required: true
13+
14+
- type: dropdown
15+
id: currency
16+
attributes:
17+
label: Reward currency
18+
options:
19+
- 'USD $'
20+
- 'CAD C$'
21+
- 'AUD A$'
22+
- 'GBP £'
23+
- 'EUR €'
24+
- 'CNY ¥'
25+
- 'HKD HK$'
26+
- 'TWD NT$'
27+
- 'SGD S$'
28+
- 'KRW ₩'
29+
- 'JPY ¥'
30+
- 'INR ₹'
31+
- 'UAH ₴'
32+
validations:
33+
required: true
34+
35+
- type: input
36+
id: amount
37+
attributes:
38+
label: Reward amount
39+
validations:
40+
required: true
41+
42+
- type: input
43+
id: payer
44+
attributes:
45+
label: Payer
46+
description: GitHub username of the payer (optional, defaults to issue creator)
47+
validations:
48+
required: false

.github/scripts/count-reward.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { $, YAML } from "npm:zx";
2+
3+
import { Reward } from "./type.ts";
4+
5+
$.verbose = true;
6+
7+
const rawTags =
8+
await $`git tag --list "reward-*" --format="%(refname:short) %(creatordate:short)"`;
9+
10+
const lastMonth = new Date();
11+
lastMonth.setMonth(lastMonth.getMonth() - 1);
12+
const lastMonthStr = lastMonth.toJSON().slice(0, 7);
13+
14+
const rewardTags = rawTags.stdout
15+
.split("\n")
16+
.filter((line) => line.split(/\s+/)[1] >= lastMonthStr)
17+
.map((line) => line.split(/\s+/)[0]);
18+
19+
let rawYAML = "";
20+
21+
for (const tag of rewardTags)
22+
rawYAML += (await $`git tag -l --format="%(contents)" ${tag}`) + "\n";
23+
24+
if (!rawYAML.trim())
25+
throw new ReferenceError("No reward data is found for the last month.");
26+
27+
const rewards = YAML.parse(rawYAML) as Reward[];
28+
29+
const groupedRewards = Object.groupBy(rewards, ({ payee }) => payee);
30+
31+
const summaryList = Object.entries(groupedRewards).map(([payee, rewards]) => {
32+
const reward = rewards!.reduce((acc, { currency, reward }) => {
33+
acc[currency] ??= 0;
34+
acc[currency] += reward;
35+
return acc;
36+
}, {} as Record<string, number>);
37+
38+
return {
39+
payee,
40+
reward,
41+
accounts: rewards!.map(({ payee: _, ...account }) => account),
42+
};
43+
});
44+
45+
const summaryText = YAML.stringify(summaryList);
46+
47+
console.log(summaryText);
48+
49+
const tagName = `statistic-${new Date().toJSON().slice(0, 7)}`;
50+
51+
await $`git config --global user.name "github-actions[bot]"`;
52+
await $`git config --global user.email "github-actions[bot]@users.noreply.github.com"`;
53+
54+
await $`git tag -a ${tagName} $(git rev-parse HEAD) -m ${summaryText}`;
55+
await $`git push origin --tags`;
56+
57+
await $`gh release create ${tagName} --notes ${summaryText}`;

.github/scripts/share-reward.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { components } from "npm:@octokit/openapi-types";
2+
import { $, YAML, argv } from "npm:zx";
3+
4+
import { Reward } from "./type.ts";
5+
6+
$.verbose = true;
7+
8+
const [
9+
repositoryOwner,
10+
repositoryName,
11+
issueNumber,
12+
payer, // GitHub username of the payer (provided by workflow, defaults to issue creator)
13+
currency,
14+
reward,
15+
] = argv._;
16+
17+
interface PRMeta {
18+
author: components["schemas"]["simple-user"];
19+
assignees: components["schemas"]["simple-user"][];
20+
}
21+
22+
const PR_URL = await $`gh api graphql -f query='{
23+
repository(owner: "${repositoryOwner}", name: "${repositoryName}") {
24+
issue(number: ${issueNumber}) {
25+
closedByPullRequestsReferences(first: 10) {
26+
nodes {
27+
url
28+
merged
29+
}
30+
}
31+
}
32+
}
33+
}' --jq '.data.repository.issue.closedByPullRequestsReferences.nodes[] | select(.merged == true) | .url' | head -n 1`;
34+
35+
if (!PR_URL.text().trim())
36+
throw new ReferenceError("No merged PR is found for the given issue number.");
37+
38+
const { author, assignees }: PRMeta = await (
39+
await $`gh pr view ${PR_URL} --json author,assignees`
40+
).json();
41+
42+
// Function to check if a user is a Copilot/bot user
43+
function isCopilotUser(login: string): boolean {
44+
const lowerLogin = login.toLowerCase();
45+
return lowerLogin.includes('copilot') ||
46+
lowerLogin.includes('[bot]') ||
47+
lowerLogin === 'github-actions[bot]' ||
48+
lowerLogin.endsWith('[bot]');
49+
}
50+
51+
// Filter out Copilot and bot users from the list
52+
const allUsers = [author.login, ...assignees.map(({ login }) => login)];
53+
const users = allUsers.filter(login => !isCopilotUser(login));
54+
55+
console.log(`All users: ${allUsers.join(', ')}`);
56+
console.log(`Filtered users (excluding bots/copilot): ${users.join(', ')}`);
57+
58+
// Handle case where all users are bots/copilot
59+
if (users.length === 0) {
60+
console.log("No real users found (all users are bots/copilot). Skipping reward distribution.");
61+
console.log(`Filtered users: ${allUsers.join(', ')}`);
62+
process.exit(0);
63+
}
64+
65+
const rewardNumber = parseFloat(reward);
66+
67+
if (isNaN(rewardNumber) || rewardNumber <= 0)
68+
throw new RangeError(`Reward amount is not a valid number, can not proceed with reward distribution. Received reward value: ${reward}`);
69+
70+
const averageReward = (rewardNumber / users.length).toFixed(2);
71+
72+
const list: Reward[] = users.map((login) => ({
73+
issue: `#${issueNumber}`,
74+
payer: `@${payer}`,
75+
payee: `@${login}`,
76+
currency,
77+
reward: parseFloat(averageReward),
78+
}));
79+
const listText = YAML.stringify(list);
80+
81+
console.log(listText);
82+
83+
await $`git config --global user.name "github-actions[bot]"`;
84+
await $`git config --global user.email "github-actions[bot]@users.noreply.github.com"`;
85+
await $`git tag -a "reward-${issueNumber}" -m ${listText}`;
86+
await $`git push origin --tags`;
87+
88+
const commentBody = `## Reward data
89+
90+
\`\`\`yml
91+
${listText}
92+
\`\`\`
93+
`;
94+
await $`gh issue comment ${issueNumber} --body ${commentBody}`;

.github/scripts/type.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface Reward {
2+
issue: string;
3+
payer: string;
4+
payee: string;
5+
currency: string;
6+
reward: number;
7+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Claim Issue Reward
2+
on:
3+
issues:
4+
types:
5+
- closed
6+
env:
7+
GH_TOKEN: ${{ github.token }}
8+
9+
jobs:
10+
claim-issue-reward:
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: write
14+
issues: write
15+
pull-requests: read
16+
steps:
17+
- uses: actions/checkout@v4
18+
with:
19+
fetch-depth: 0
20+
fetch-tags: true
21+
22+
- uses: denoland/setup-deno@v2
23+
with:
24+
deno-version: v2.x
25+
26+
- name: Get Issue details
27+
id: parse_issue
28+
uses: stefanbuck/github-issue-parser@v3
29+
with:
30+
template-path: ".github/ISSUE_TEMPLATE/reward-task.yml"
31+
32+
- name: Calculate & Save Reward
33+
run: |
34+
deno --allow-all .github/scripts/share-reward.ts \
35+
${{ github.repository_owner }} \
36+
${{ github.event.repository.name }} \
37+
${{ github.event.issue.number }} \
38+
"${{ steps.parse_issue.outputs.issueparser_payer || github.event.issue.user.login }}" \
39+
"${{ steps.parse_issue.outputs.issueparser_currency }}" \
40+
${{ steps.parse_issue.outputs.issueparser_amount }}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Statistic Member Reward
2+
on:
3+
schedule:
4+
- cron: "0 0 1 * *" # Run at 00:00 on the first day of every month
5+
env:
6+
GH_TOKEN: ${{ github.token }}
7+
8+
jobs:
9+
statistic-member-reward:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: write
13+
steps:
14+
- uses: actions/checkout@v4
15+
with:
16+
fetch-depth: 0
17+
fetch-tags: true
18+
19+
- name: Check for new commits since last statistic
20+
run: |
21+
last_tag=$(git describe --tags --abbrev=0 --match "statistic-*" || echo "")
22+
23+
if [ -z "$last_tag" ]; then
24+
echo "No previous statistic tags found."
25+
echo "NEW_COMMITS=true" >> $GITHUB_ENV
26+
else
27+
new_commits=$(git log $last_tag..HEAD --oneline)
28+
if [ -z "$new_commits" ]; then
29+
echo "No new commits since last statistic tag."
30+
echo "NEW_COMMITS=false" >> $GITHUB_ENV
31+
else
32+
echo "New commits found."
33+
echo "NEW_COMMITS=true" >> $GITHUB_ENV
34+
fi
35+
fi
36+
- uses: denoland/setup-deno@v2
37+
if: env.NEW_COMMITS == 'true'
38+
with:
39+
deno-version: v2.x
40+
41+
- name: Statistic rewards
42+
if: env.NEW_COMMITS == 'true'
43+
run: deno --allow-all .github/scripts/count-reward.ts

0 commit comments

Comments
 (0)