name: vouch-check-issue on: issues: types: [opened] permissions: contents: read issues: write jobs: check: runs-on: ubuntu-latest steps: - name: Check if issue author is denounced uses: actions/github-script@v7 with: script: | const author = context.payload.issue.user.login; const issueNumber = context.payload.issue.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 issue const body = 'This issue has been automatically closed.'; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, body, }); await github.rest.issues.update({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, state: 'closed', state_reason: 'not_planned', }); core.info(`Closed issue #${issueNumber} 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 issue.`); return; } await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, labels: ['Vouched'], }); core.info(`Added vouched label to issue #${issueNumber} from ${author}`);