Skip to content

Commit bfeff65

Browse files
committed
[add] GitHub actions & issue form of GitHub-reward
1 parent b44ea57 commit bfeff65

File tree

6 files changed

+293
-0
lines changed

6 files changed

+293
-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: Reward 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 = await $`git tag --list "reward-*" --format="%(refname:short) %(creatordate:short)"`;
8+
9+
const lastMonth = new Date();
10+
lastMonth.setMonth(lastMonth.getMonth() - 1);
11+
const lastMonthStr = lastMonth.toJSON().slice(0, 7);
12+
13+
const rewardTags = rawTags.stdout
14+
.split('\n')
15+
.filter(line => line.split(/\s+/)[1] >= lastMonthStr)
16+
.map(line => line.split(/\s+/)[0]);
17+
18+
let rawYAML = '';
19+
20+
for (const tag of rewardTags) rawYAML += (await $`git tag -l --format="%(contents)" ${tag}`) + '\n';
21+
22+
if (!rawYAML.trim()) throw new ReferenceError('No reward data is found for the last month.');
23+
24+
const rewards = YAML.parse(rawYAML) as Reward[];
25+
26+
const groupedRewards = Object.groupBy(rewards, ({ payee }) => payee);
27+
28+
const summaryList = Object.entries(groupedRewards).map(([payee, rewards]) => {
29+
const reward = rewards!.reduce(
30+
(acc, { currency, reward }) => {
31+
acc[currency] ??= 0;
32+
acc[currency] += reward;
33+
return acc;
34+
},
35+
{} as Record<string, number>,
36+
);
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: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { components } from 'npm:@octokit/openapi-types';
2+
import { $, argv, YAML } 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 (
46+
lowerLogin.includes('copilot') ||
47+
lowerLogin.includes('[bot]') ||
48+
lowerLogin === 'github-actions[bot]' ||
49+
lowerLogin.endsWith('[bot]')
50+
);
51+
}
52+
53+
// Filter out Copilot and bot users from the list
54+
const allUsers = [author.login, ...assignees.map(({ login }) => login)];
55+
const users = allUsers.filter(login => !isCopilotUser(login));
56+
57+
console.log(`All users: ${allUsers.join(', ')}`);
58+
console.log(`Filtered users (excluding bots/copilot): ${users.join(', ')}`);
59+
60+
// Handle case where all users are bots/copilot
61+
if (users.length === 0) {
62+
console.log('No real users found (all users are bots/copilot). Skipping reward distribution.');
63+
console.log(`Filtered users: ${allUsers.join(', ')}`);
64+
process.exit(0);
65+
}
66+
67+
const rewardNumber = parseFloat(reward);
68+
69+
if (isNaN(rewardNumber) || rewardNumber <= 0)
70+
throw new RangeError(
71+
`Reward amount is not a valid number, can not proceed with reward distribution. Received reward value: ${reward}`,
72+
);
73+
74+
const averageReward = (rewardNumber / users.length).toFixed(2);
75+
76+
const list: Reward[] = users.map(login => ({
77+
issue: `#${issueNumber}`,
78+
payer: `@${payer}`,
79+
payee: `@${login}`,
80+
currency,
81+
reward: parseFloat(averageReward),
82+
}));
83+
const listText = YAML.stringify(list);
84+
85+
console.log(listText);
86+
87+
await $`git config --global user.name "github-actions[bot]"`;
88+
await $`git config --global user.email "github-actions[bot]@users.noreply.github.com"`;
89+
await $`git tag -a "reward-${issueNumber}" -m ${listText}`;
90+
await $`git push origin --tags`;
91+
92+
const commentBody = `## Reward data
93+
94+
\`\`\`yml
95+
${listText}
96+
\`\`\`
97+
`;
98+
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@v6
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@v6
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)