Skip to content

Commit 7bb07d0

Browse files
committed
chore: add GitHub-reward form, scripts & actions
1 parent 56788ff commit 7bb07d0

7 files changed

Lines changed: 359 additions & 0 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: 💰 Reward Task
2+
description: Task issue with Reward
3+
title: '[Reward] '
4+
labels:
5+
- reward
6+
body:
7+
- type: markdown
8+
attributes:
9+
value: This form is created from https://github.com/idea2app/GitHub-reward
10+
11+
- type: textarea
12+
id: description
13+
attributes:
14+
label: Task description
15+
validations:
16+
required: true
17+
18+
- type: input
19+
id: source
20+
attributes:
21+
label: Task source
22+
description: URL from an External System
23+
placeholder: https://example.com/task/123
24+
25+
- type: dropdown
26+
id: currency
27+
attributes:
28+
label: Reward currency
29+
options:
30+
- 'USD $'
31+
- 'CAD C$'
32+
- 'AUD A$'
33+
- 'GBP £'
34+
- 'EUR €'
35+
- 'CNY ¥'
36+
- 'HKD HK$'
37+
- 'TWD NT$'
38+
- 'SGD S$'
39+
- 'KRW ₩'
40+
- 'JPY ¥'
41+
- 'INR ₹'
42+
- 'UAH ₴'
43+
validations:
44+
required: true
45+
46+
- type: input
47+
id: amount
48+
attributes:
49+
label: Reward amount
50+
validations:
51+
required: true
52+
53+
- type: input
54+
id: payer
55+
attributes:
56+
label: Reward payer
57+
description: GitHub username of the payer (optional, defaults to issue creator)
58+
validations:
59+
required: false

.github/scripts/count-reward.ts

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

.github/scripts/deno.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"nodeModulesDir": "none"
3+
}

.github/scripts/share-reward.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import "npm:array-unique-proposal";
2+
3+
import { components } from "npm:@octokit/openapi-types";
4+
import { $, argv, YAML } from "npm:zx";
5+
6+
import { Reward } from "./type.ts";
7+
8+
$.verbose = true;
9+
10+
const [
11+
repositoryOwner,
12+
repositoryName,
13+
issueNumber,
14+
payer, // GitHub username of the payer (provided by workflow, defaults to issue creator)
15+
currency,
16+
reward,
17+
source,
18+
] = argv._;
19+
20+
interface PRMeta {
21+
author: components["schemas"]["simple-user"];
22+
assignees: components["schemas"]["simple-user"][];
23+
}
24+
25+
const graphqlQuery = `
26+
query($owner: String!, $name: String!, $number: Int!) {
27+
repository(owner: $owner, name: $name) {
28+
issue(number: $number) {
29+
closedByPullRequestsReferences(first: 10) {
30+
nodes {
31+
url
32+
merged
33+
mergeCommit {
34+
oid
35+
}
36+
}
37+
}
38+
}
39+
}
40+
}
41+
`;
42+
const PR_DATA = await $`gh api graphql \
43+
-f query=${graphqlQuery} \
44+
-f owner=${repositoryOwner} \
45+
-f name=${repositoryName} \
46+
-F number=${issueNumber} \
47+
--jq '.data.repository.issue.closedByPullRequestsReferences.nodes[] | select(.merged == true) | {url: .url, mergeCommitSha: .mergeCommit.oid}' | head -n 1`;
48+
49+
const prData = PR_DATA.text().trim();
50+
51+
if (!prData)
52+
throw new ReferenceError("No merged PR is found for the given issue number.");
53+
54+
const { url: PR_URL, mergeCommitSha } = JSON.parse(prData);
55+
56+
if (!PR_URL || !mergeCommitSha)
57+
throw new Error("Missing required fields in PR data");
58+
59+
console.table({ PR_URL, mergeCommitSha });
60+
61+
const { author, assignees }: PRMeta = await (
62+
await $`gh pr view ${PR_URL} --json author,assignees`
63+
).json();
64+
65+
function isBotUser(login: string) {
66+
const lowerLogin = login.toLowerCase();
67+
return (
68+
lowerLogin.includes("copilot") ||
69+
lowerLogin.includes("[bot]") ||
70+
lowerLogin === "github-actions[bot]" ||
71+
lowerLogin.endsWith("[bot]")
72+
);
73+
}
74+
75+
// Filter out Bot users from the list
76+
const allUsers = [
77+
author.login,
78+
...assignees.map(({ login }) => login),
79+
].uniqueBy();
80+
81+
const users = allUsers.filter((login) => !isBotUser(login));
82+
83+
console.log(`All users: ${allUsers.join(", ")}`);
84+
console.log(`Filtered users (excluding bots): ${users.join(", ")}`);
85+
86+
if (!users[0])
87+
throw new ReferenceError(
88+
"No real users found (all users are bots). Skipping reward distribution.",
89+
);
90+
91+
const rewardNumber = parseFloat(reward);
92+
93+
if (isNaN(rewardNumber) || rewardNumber <= 0)
94+
throw new RangeError(
95+
`Reward amount is not a valid number, can not proceed with reward distribution. Received reward value: ${reward}`,
96+
);
97+
98+
const averageReward = (rewardNumber / users.length).toFixed(2);
99+
100+
const list: Reward[] = users.map((login) => ({
101+
issue: `#${issueNumber}`,
102+
payer: `@${payer}`,
103+
payee: `@${login}`,
104+
currency,
105+
reward: parseFloat(averageReward),
106+
source,
107+
}));
108+
const listText = YAML.stringify(list);
109+
110+
console.log(listText);
111+
112+
const tagName = `reward-${issueNumber}`;
113+
114+
await $`git config user.name "github-actions[bot]"`;
115+
await $`git config user.email "github-actions[bot]@users.noreply.github.com"`;
116+
117+
await $`git tag -a ${tagName} ${mergeCommitSha} -m ${listText}`;
118+
await $`git push origin ${tagName} --no-verify`;
119+
120+
await $`git config unset user.name`;
121+
await $`git config unset user.email`;
122+
123+
const commentBody = `## Reward data
124+
125+
\`\`\`yml
126+
${listText}
127+
\`\`\`
128+
`;
129+
await $`gh issue comment ${issueNumber} --body ${commentBody}`;

.github/scripts/type.ts

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

0 commit comments

Comments
 (0)