vouch-check-issue.yml 4.0 KB

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