Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/ISSUE_TEMPLATE/reward-task.yml
Original file line number Diff line number Diff line change
@@ -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
57 changes: 57 additions & 0 deletions .github/scripts/count-reward.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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()) throw new ReferenceError('No reward data is found for the last month.');

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 --global user.name "github-actions[bot]"`;
await $`git config --global 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 $`gh release create ${tagName} --notes ${summaryText}`;
105 changes: 105 additions & 0 deletions .github/scripts/share-reward.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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 PR_DATA = await $`gh api graphql -f query='{
repository(owner: "${repositoryOwner}", name: "${repositoryName}") {
issue(number: ${issueNumber}) {
closedByPullRequestsReferences(first: 10) {
nodes {
url
merged
mergeCommit {
oid
}
}
}
}
}
}' --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)];
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 --global user.name "github-actions[bot]"`;
await $`git config --global 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`;

const commentBody = `## Reward data

\`\`\`yml
${listText}
\`\`\`
`;
await $`gh issue comment ${issueNumber} --body ${commentBody}`;
7 changes: 7 additions & 0 deletions .github/scripts/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface Reward {
issue: string;
payer: string;
payee: string;
currency: string;
reward: number;
}
40 changes: 40 additions & 0 deletions .github/workflows/claim-issue-reward.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Claim Issue Reward
on:
issues:
types:
- closed
env:
GH_TOKEN: ${{ github.token }}

jobs:
claim-issue-reward:
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: read
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: true

- uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- name: Get Issue details
id: parse_issue
uses: stefanbuck/github-issue-parser@v3
with:
template-path: '.github/ISSUE_TEMPLATE/reward-task.yml'

- name: Calculate & Save Reward
run: |
deno --allow-all .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 }}
90 changes: 54 additions & 36 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,53 +10,71 @@ jobs:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
if: ${{ env.VERCEL_TOKEN && env.VERCEL_ORG_ID && env.VERCEL_PROJECT_ID }}

- uses: actions/setup-node@v6
if: ${{ env.VERCEL_TOKEN && env.VERCEL_ORG_ID && env.VERCEL_PROJECT_ID }}
with:
submodules: recursive
lfs: true
- run: git submodule update --remote
node-version: 24

- name: Deploy to Vercel
id: vercel-deployment
uses: amondnet/vercel-action@v25
if: ${{ env.VERCEL_TOKEN && env.VERCEL_ORG_ID && env.VERCEL_PROJECT_ID }}
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
working-directory: ./
vercel-args: ${{ github.ref == 'refs/heads/main' && '--prod' || '' }}
shell: bash
run: |
set -euo pipefail

npm install vercel -g

if [[ "$GITHUB_REF" == 'refs/heads/main' ]]; then
DeployOutput=$(vercel -t "$VERCEL_TOKEN" --prod)
else
DeployOutput=$(vercel -t "$VERCEL_TOKEN")
fi
echo "$DeployOutput"

ParsedURL=$(echo "$DeployOutput" | grep -Eo 'https://[^[:space:]]*\.vercel\.app' | tail -n 1)

if [[ -z "$ParsedURL" ]]; then
echo "Failed to parse Vercel URL from deploy output"
exit 1
fi
vercel inspect "$ParsedURL" -t "$VERCEL_TOKEN" -F json > vercel-inspect.json

InspectURL=$(jq -r '.url // empty' vercel-inspect.json)

if [[ -z "$InspectURL" ]]; then
echo "Failed to parse inspect url from vercel-inspect.json"
exit 1
fi
if [[ "$InspectURL" != http* ]]; then
InspectURL="https://$InspectURL"
fi
echo "preview-url=$InspectURL" >> "$GITHUB_OUTPUT"

- name: Lark notification
uses: Open-Source-Bazaar/feishu-action@v3
with:
url: ${{ secrets.LARK_CHATBOT_HOOK_URL }}
msg_type: post
msg_type: interactive
content: |
post:
zh_cn:
title: Vercel 预览环境
content:
- - tag: text
text: Git 仓库:
- tag: a
text: ${{ github.server_url }}/${{ github.repository }}
href: ${{ github.server_url }}/${{ github.repository }}
- - tag: text
text: 代码分支:
- tag: a
text: ${{ github.ref }}
href: ${{ github.server_url }}/${{ github.repository }}/tree/${{ github.ref_name }}
- - tag: text
text: 提交作者:
- tag: a
text: ${{ github.actor }}
href: ${{ github.server_url }}/${{ github.actor }}
- - tag: text
text: 预览链接:
- tag: a
text: ${{ steps.vercel-deployment.outputs.preview-url }}
href: ${{ steps.vercel-deployment.outputs.preview-url }}
schema: "2.0"
config:
wide_screen_mode: true
header:
title:
tag: plain_text
content: Vercel 部署通知
template: blue
body:
elements:
- tag: markdown
content: |
**Git 仓库:** [${{ github.server_url }}/${{ github.repository }}](${{ github.server_url }}/${{ github.repository }})
**代码分支:** [${{ github.ref }}](${{ github.server_url }}/${{ github.repository }}/tree/${{ github.ref_name }})
**提交作者:** [${{ github.actor }}](${{ github.server_url }}/${{ github.actor }})
**预览链接:** [${{ steps.vercel-deployment.outputs.preview-url }}](${{ steps.vercel-deployment.outputs.preview-url }})
Loading