vouch-check-pr.yml 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. name: vouch-check-pr
  2. on:
  3. pull_request_target:
  4. types: [opened]
  5. permissions:
  6. contents: read
  7. issues: write
  8. pull-requests: write
  9. jobs:
  10. check:
  11. runs-on: ubuntu-latest
  12. steps:
  13. - name: Check if PR author is denounced
  14. uses: actions/github-script@v7
  15. with:
  16. script: |
  17. const author = context.payload.pull_request.user.login;
  18. const prNumber = context.payload.pull_request.number;
  19. // Skip bots
  20. if (author.endsWith('[bot]')) {
  21. core.info(`Skipping bot: ${author}`);
  22. return;
  23. }
  24. // Read the VOUCHED.td file via API (no checkout needed)
  25. let content;
  26. try {
  27. const response = await github.rest.repos.getContent({
  28. owner: context.repo.owner,
  29. repo: context.repo.repo,
  30. path: '.github/VOUCHED.td',
  31. });
  32. content = Buffer.from(response.data.content, 'base64').toString('utf-8');
  33. } catch (error) {
  34. if (error.status === 404) {
  35. core.info('No .github/VOUCHED.td file found, skipping check.');
  36. return;
  37. }
  38. throw error;
  39. }
  40. // Parse the .td file for vouched and denounced users
  41. const vouched = new Set();
  42. const denounced = new Map();
  43. for (const line of content.split('\n')) {
  44. const trimmed = line.trim();
  45. if (!trimmed || trimmed.startsWith('#')) continue;
  46. const isDenounced = trimmed.startsWith('-');
  47. const rest = isDenounced ? trimmed.slice(1).trim() : trimmed;
  48. if (!rest) continue;
  49. const spaceIdx = rest.indexOf(' ');
  50. const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
  51. const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim();
  52. // Handle platform:username or bare username
  53. // Only match bare usernames or github: prefix (skip other platforms)
  54. const colonIdx = handle.indexOf(':');
  55. if (colonIdx !== -1) {
  56. const platform = handle.slice(0, colonIdx).toLowerCase();
  57. if (platform !== 'github') continue;
  58. }
  59. const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
  60. if (!username) continue;
  61. if (isDenounced) {
  62. denounced.set(username.toLowerCase(), reason);
  63. continue;
  64. }
  65. vouched.add(username.toLowerCase());
  66. }
  67. // Check if the author is denounced
  68. const reason = denounced.get(author.toLowerCase());
  69. if (reason !== undefined) {
  70. // Author is denounced — close the PR
  71. await github.rest.issues.createComment({
  72. owner: context.repo.owner,
  73. repo: context.repo.repo,
  74. issue_number: prNumber,
  75. body: 'This pull request has been automatically closed.',
  76. });
  77. await github.rest.pulls.update({
  78. owner: context.repo.owner,
  79. repo: context.repo.repo,
  80. pull_number: prNumber,
  81. state: 'closed',
  82. });
  83. core.info(`Closed PR #${prNumber} from denounced user ${author}`);
  84. return;
  85. }
  86. // Author is positively vouched — add label
  87. if (!vouched.has(author.toLowerCase())) {
  88. core.info(`User ${author} is not denounced or vouched. Allowing PR.`);
  89. return;
  90. }
  91. await github.rest.issues.addLabels({
  92. owner: context.repo.owner,
  93. repo: context.repo.repo,
  94. issue_number: prNumber,
  95. labels: ['Vouched'],
  96. });
  97. core.info(`Added vouched label to PR #${prNumber} from ${author}`);