chore: add vouch trust management system

This commit is contained in:
Tony 2026-04-16 07:11:37 +08:00
parent 8fe52b1173
commit ecffcd3d95
5 changed files with 180 additions and 2 deletions

16
.github/VOUCHED.td vendored Normal file
View File

@ -0,0 +1,16 @@
# Vouched contributors for this project.
#
# See https://github.com/mitchellh/vouch for details.
#
# Syntax:
# - One handle per line (without @), sorted alphabetically.
# - Optional platform prefix: platform:username (e.g., github:user).
# - Denounce with minus prefix: -username or -platform:username.
# - Optional details after a space following the handle.
DIYgod
HenryQW
hyoban
NeverBehave
pseudoyu
TonyRL
zhenlonghe

View File

@ -36,6 +36,29 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
trigger: '/wip'
vouch-manage:
name: Vouch Manage
if: ${{ startsWith(github.event.comment.body, 'vouch') || startsWith(github.event.comment.body, 'unvouch') || startsWith(github.event.comment.body, 'denounce') }}
runs-on: ubuntu-slim
timeout-minutes: 5
permissions:
contents: write
issues: write
pull-requests: read
concurrency:
group: vouch-manage
cancel-in-progress: false
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: mitchellh/vouch/action/manage-by-issue@c6d80ead49839655b61b422700b7a3bc9d0804a9 # v1.4.2
with:
issue-id: ${{ github.event.issue.number }}
comment-id: ${{ github.event.comment.id }}
roles: admin,maintain,write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
test-on-demand:
name: Test route on demand
if: startsWith(github.event.comment.body, '/test')

View File

@ -68,3 +68,23 @@ jobs:
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
vouch-check-pr:
name: Vouch check PR
if: ${{ github.event_name == 'pull_request_target' && github.event.action == 'opened' && github.repository == 'DIYgod/RSSHub' }}
permissions:
contents: read
issues: write
pull-requests: write
runs-on: ubuntu-slim
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check if PR author is denounced
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { default: checkPr } = await import('${{ github.workspace }}/scripts/workflow/vouch/check-pr.mjs')
await checkPr({ github, context, core })

View File

@ -206,13 +206,17 @@
"yaml-eslint-parser": "2.0.0"
},
"lint-staged": {
"!(*.ts|*.tsx|*.js|*.yml)": [
"!(*.ts|*.tsx|*.js|*.cjs|*.mjs|*.yml)": [
"oxfmt --no-error-on-unmatched-pattern"
],
"*.{ts,tsx,js,yml}": [
"*.{ts,tsx,js,cjs,mjs}": [
"oxlint --type-aware --fix",
"eslint --cache --fix --concurrency auto",
"oxfmt --no-error-on-unmatched-pattern"
],
"*.yml": [
"eslint --cache --fix --concurrency auto",
"oxfmt --no-error-on-unmatched-pattern"
]
},
"engines": {

View File

@ -0,0 +1,115 @@
/**
* @param {{ github: ReturnType<typeof import('@actions/github').getOctokit>, context: typeof import('@actions/github').context, core: typeof import('@actions/core') }} githubScript
* @returns {Promise<void>}
*/
export default async function checkPr({ github, context, core }) {
const author = context.payload.pull_request.user.login;
const prNumber = context.payload.pull_request.number;
// Skip bots
if (author.endsWith('[bot]')) {
core.info(`Skipping bot: ${author}`);
return;
}
// Read the VOUCHED.td file via API (no checkout needed)
let content;
try {
const response = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: '.github/VOUCHED.td',
});
content = Buffer.from(response.data.content, 'base64').toString('utf-8');
} catch (error) {
if (error.status === 404) {
core.info('No .github/VOUCHED.td file found, skipping check.');
return;
}
throw error;
}
// Parse the .td file for vouched and denounced users
const vouched = new Set();
const denounced = new Map();
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) {
continue;
}
const isDenounced = trimmed.startsWith('-');
const rest = isDenounced ? trimmed.slice(1).trim() : trimmed;
if (!rest) {
continue;
}
const spaceIdx = rest.indexOf(' ');
const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim();
// Handle platform:username or bare username
// Only match bare usernames or github: prefix (skip other platforms)
const colonIdx = handle.indexOf(':');
if (colonIdx !== -1) {
const platform = handle.slice(0, colonIdx).toLowerCase();
if (platform !== 'github') {
continue;
}
}
const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
if (!username) {
continue;
}
if (isDenounced) {
denounced.set(username.toLowerCase(), reason);
continue;
}
vouched.add(username.toLowerCase());
}
// Check if the author is denounced
const reason = denounced.get(author.toLowerCase());
if (reason !== undefined) {
// Author is denounced — close the PR
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: 'This pull request has been automatically closed.',
});
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
state: 'closed',
});
core.info(`Closed PR #${prNumber} from denounced user ${author}`);
return;
}
// Author is positively vouched — add label
if (!vouched.has(author.toLowerCase())) {
core.info(`User ${author} is not denounced or vouched. Allowing PR.`);
return;
}
const association = context.payload.pull_request.author_association;
if (association === 'OWNER' || association === 'MEMBER' || association === 'COLLABORATOR') {
core.info(`Skipping vouched label for collaborator ${author} (${association}).`);
return;
}
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
labels: ['vouched'],
});
core.info(`Added vouched label to PR #${prNumber} from ${author}`);
}