mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-11 22:51:04 +00:00
chore: add GitHub-reward form, scripts & actions (#221)
* chore: add GitHub-reward form, scripts & actions * fix: Deno system permissions in GitHub actions * fix: 3 detail bugs
This commit is contained in:
parent
c2981836bb
commit
66acc6e57b
7 changed files with 338 additions and 0 deletions
48
.github/ISSUE_TEMPLATE/reward-task.yml
vendored
Normal file
48
.github/ISSUE_TEMPLATE/reward-task.yml
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
name: 💰 Reward Task
|
||||
description: Task issue with Reward
|
||||
title: '[Reward] '
|
||||
labels:
|
||||
- reward
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Task description
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: currency
|
||||
attributes:
|
||||
label: Reward currency
|
||||
options:
|
||||
- 'USD $'
|
||||
- 'CAD C$'
|
||||
- 'AUD A$'
|
||||
- 'GBP £'
|
||||
- 'EUR €'
|
||||
- 'CNY ¥'
|
||||
- 'HKD HK$'
|
||||
- 'TWD NT$'
|
||||
- 'SGD S$'
|
||||
- 'KRW ₩'
|
||||
- 'JPY ¥'
|
||||
- 'INR ₹'
|
||||
- 'UAH ₴'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: amount
|
||||
attributes:
|
||||
label: Reward amount
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: payer
|
||||
attributes:
|
||||
label: Reward payer
|
||||
description: GitHub username of the payer (optional, defaults to issue creator)
|
||||
validations:
|
||||
required: false
|
||||
66
.github/scripts/count-reward.ts
vendored
Normal file
66
.github/scripts/count-reward.ts
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { $, YAML } from "npm:zx";
|
||||
|
||||
import { Reward } from "./type.ts";
|
||||
|
||||
$.verbose = true;
|
||||
|
||||
const rawTags =
|
||||
await $`git tag --list "reward-*" --format="%(refname:short) %(creatordate:short)"`;
|
||||
|
||||
const lastMonth = new Date();
|
||||
lastMonth.setMonth(lastMonth.getMonth() - 1);
|
||||
const lastMonthStr = lastMonth.toJSON().slice(0, 7);
|
||||
|
||||
const rewardTags = rawTags.stdout
|
||||
.split("\n")
|
||||
.filter((line) => line.split(/\s+/)[1] >= lastMonthStr)
|
||||
.map((line) => line.split(/\s+/)[0]);
|
||||
|
||||
let rawYAML = "";
|
||||
|
||||
for (const tag of rewardTags)
|
||||
rawYAML += (await $`git tag -l --format="%(contents)" ${tag}`) + "\n";
|
||||
|
||||
if (!rawYAML.trim()) {
|
||||
console.warn("No reward data is found for the last month.");
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const rewards = YAML.parse(rawYAML) as Reward[];
|
||||
|
||||
const groupedRewards = Object.groupBy(rewards, ({ payee }) => payee);
|
||||
|
||||
const summaryList = Object.entries(groupedRewards).map(([payee, rewards]) => {
|
||||
const reward = rewards!.reduce(
|
||||
(acc, { currency, reward }) => {
|
||||
acc[currency] ??= 0;
|
||||
acc[currency] += reward;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
);
|
||||
|
||||
return {
|
||||
payee,
|
||||
reward,
|
||||
accounts: rewards!.map(({ payee: _, ...account }) => account),
|
||||
};
|
||||
});
|
||||
|
||||
const summaryText = YAML.stringify(summaryList);
|
||||
|
||||
console.log(summaryText);
|
||||
|
||||
const tagName = `statistic-${new Date().toJSON().slice(0, 7)}`;
|
||||
|
||||
await $`git config user.name "github-actions[bot]"`;
|
||||
await $`git config user.email "github-actions[bot]@users.noreply.github.com"`;
|
||||
|
||||
await $`git tag -a ${tagName} $(git rev-parse HEAD) -m ${summaryText}`;
|
||||
await $`git push origin --tags --no-verify`;
|
||||
|
||||
await $`git config unset user.name`;
|
||||
await $`git config unset user.email`;
|
||||
|
||||
await $`gh release create ${tagName} --notes ${summaryText}`;
|
||||
3
.github/scripts/deno.json
vendored
Normal file
3
.github/scripts/deno.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"nodeModulesDir": "none"
|
||||
}
|
||||
125
.github/scripts/share-reward.ts
vendored
Normal file
125
.github/scripts/share-reward.ts
vendored
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import "npm:array-unique-proposal";
|
||||
|
||||
import { components } from "npm:@octokit/openapi-types";
|
||||
import { $, argv, YAML } from "npm:zx";
|
||||
|
||||
import { Reward } from "./type.ts";
|
||||
|
||||
$.verbose = true;
|
||||
|
||||
const [
|
||||
repositoryOwner,
|
||||
repositoryName,
|
||||
issueNumber,
|
||||
payer, // GitHub username of the payer (provided by workflow, defaults to issue creator)
|
||||
currency,
|
||||
reward,
|
||||
] = argv._;
|
||||
|
||||
interface PRMeta {
|
||||
author: components["schemas"]["simple-user"];
|
||||
assignees: components["schemas"]["simple-user"][];
|
||||
}
|
||||
|
||||
const graphqlQuery = `
|
||||
query($owner: String!, $name: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
issue(number: $number) {
|
||||
closedByPullRequestsReferences(first: 10) {
|
||||
nodes {
|
||||
url
|
||||
merged
|
||||
mergeCommit {
|
||||
oid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const PR_DATA = await $`gh api graphql \
|
||||
-f query=${graphqlQuery} \
|
||||
-f owner=${repositoryOwner} \
|
||||
-f name=${repositoryName} \
|
||||
-F number=${issueNumber} \
|
||||
--jq '.data.repository.issue.closedByPullRequestsReferences.nodes[] | select(.merged == true) | {url: .url, mergeCommitSha: .mergeCommit.oid}' | head -n 1`;
|
||||
|
||||
const prData = PR_DATA.text().trim();
|
||||
|
||||
if (!prData)
|
||||
throw new ReferenceError("No merged PR is found for the given issue number.");
|
||||
|
||||
const { url: PR_URL, mergeCommitSha } = JSON.parse(prData);
|
||||
|
||||
if (!PR_URL || !mergeCommitSha)
|
||||
throw new Error("Missing required fields in PR data");
|
||||
|
||||
console.table({ PR_URL, mergeCommitSha });
|
||||
|
||||
const { author, assignees }: PRMeta = await (
|
||||
await $`gh pr view ${PR_URL} --json author,assignees`
|
||||
).json();
|
||||
|
||||
function isBotUser(login: string) {
|
||||
const lowerLogin = login.toLowerCase();
|
||||
return (
|
||||
lowerLogin.includes("copilot") ||
|
||||
lowerLogin.includes("[bot]") ||
|
||||
lowerLogin === "github-actions[bot]" ||
|
||||
lowerLogin.endsWith("[bot]")
|
||||
);
|
||||
}
|
||||
|
||||
// Filter out Bot users from the list
|
||||
const allUsers = [
|
||||
author.login,
|
||||
...assignees.map(({ login }) => login),
|
||||
].uniqueBy();
|
||||
|
||||
const users = allUsers.filter((login) => !isBotUser(login));
|
||||
|
||||
console.log(`All users: ${allUsers.join(", ")}`);
|
||||
console.log(`Filtered users (excluding bots): ${users.join(", ")}`);
|
||||
|
||||
if (!users[0])
|
||||
throw new ReferenceError(
|
||||
"No real users found (all users are bots). Skipping reward distribution.",
|
||||
);
|
||||
|
||||
const rewardNumber = parseFloat(reward);
|
||||
|
||||
if (isNaN(rewardNumber) || rewardNumber <= 0)
|
||||
throw new RangeError(
|
||||
`Reward amount is not a valid number, can not proceed with reward distribution. Received reward value: ${reward}`,
|
||||
);
|
||||
|
||||
const averageReward = (rewardNumber / users.length).toFixed(2);
|
||||
|
||||
const list: Reward[] = users.map((login) => ({
|
||||
issue: `#${issueNumber}`,
|
||||
payer: `@${payer}`,
|
||||
payee: `@${login}`,
|
||||
currency,
|
||||
reward: parseFloat(averageReward),
|
||||
}));
|
||||
const listText = YAML.stringify(list);
|
||||
|
||||
console.log(listText);
|
||||
|
||||
await $`git config user.name "github-actions[bot]"`;
|
||||
await $`git config user.email "github-actions[bot]@users.noreply.github.com"`;
|
||||
|
||||
await $`git tag -a "reward-${issueNumber}" ${mergeCommitSha} -m ${listText}`;
|
||||
await $`git push origin --tags --no-verify`;
|
||||
|
||||
await $`git config unset user.name`;
|
||||
await $`git config unset user.email`;
|
||||
|
||||
const commentBody = `## Reward data
|
||||
|
||||
\`\`\`yml
|
||||
${listText}
|
||||
\`\`\`
|
||||
`;
|
||||
await $`gh issue comment ${issueNumber} --body ${commentBody}`;
|
||||
7
.github/scripts/type.ts
vendored
Normal file
7
.github/scripts/type.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export interface Reward {
|
||||
issue: string;
|
||||
payer: string;
|
||||
payee: string;
|
||||
currency: string;
|
||||
reward: number;
|
||||
}
|
||||
46
.github/workflows/claim-issue-reward.yml
vendored
Normal file
46
.github/workflows/claim-issue-reward.yml
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
name: Claim Issue Reward
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- closed
|
||||
|
||||
concurrency:
|
||||
group: claim-issue-reward-${{ github.event.issue.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
claim-issue-reward:
|
||||
runs-on: ubuntu-latest
|
||||
if: contains(github.event.issue.labels.*.name, 'reward')
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4
|
||||
with:
|
||||
deno-version: v2.x
|
||||
|
||||
- name: Get Issue details
|
||||
id: parse_issue
|
||||
uses: stefanbuck/github-issue-parser@10dcc54158ba4c137713d9d69d70a2da63b6bda3 # v3.2.3
|
||||
with:
|
||||
template-path: ".github/ISSUE_TEMPLATE/reward-task.yml"
|
||||
|
||||
- name: Calculate & Save Reward
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
deno --allow-run --allow-sys --allow-env --allow-read --allow-net=api.github.com \
|
||||
.github/scripts/share-reward.ts \
|
||||
"${{ github.repository_owner }}" \
|
||||
"${{ github.event.repository.name }}" \
|
||||
"${{ github.event.issue.number }}" \
|
||||
"${{ steps.parse_issue.outputs.issueparser_payer || github.event.issue.user.login }}" \
|
||||
"${{ steps.parse_issue.outputs.issueparser_currency }}" \
|
||||
"${{ steps.parse_issue.outputs.issueparser_amount }}"
|
||||
43
.github/workflows/statistic-member-reward.yml
vendored
Normal file
43
.github/workflows/statistic-member-reward.yml
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
name: Statistic Member Reward
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 1 * *" # Run at 00:00 on the first day of every month
|
||||
|
||||
jobs:
|
||||
statistic-member-reward:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- 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'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: deno --allow-run --allow-sys --allow-env --allow-read --allow-net=api.github.com .github/scripts/count-reward.ts
|
||||
Loading…
Add table
Reference in a new issue