name: Approve Contributor on: issue_comment: types: [created] jobs: approve: if: ${{ !github.event.issue.pull_request }} runs-on: ubuntu-latest permissions: contents: write issues: write steps: - name: Checkout uses: actions/checkout@v4 with: ref: ${{ github.event.repository.default_branch }} - name: Update contributor approval id: update uses: actions/github-script@v7 with: script: | const fs = require('fs'); const APPROVED_FILE = '.github/APPROVED_CONTRIBUTORS'; const VALID_CAPABILITIES = new Set(['issue', 'pr']); const issueAuthor = context.payload.issue.user.login; const commenter = context.payload.comment.user.login; const commentBody = (context.payload.comment.body || '').trim(); let targetCapability; if (/\blgtmi\b/i.test(commentBody)) { targetCapability = 'issue'; } else if (/\blgtm\b/i.test(commentBody)) { targetCapability = 'pr'; } else { console.log('Comment does not match lgtm or lgtmi'); core.setOutput('status', 'skipped'); return; } try { const { data: permissionLevel } = await github.rest.repos.getCollaboratorPermissionLevel({ owner: context.repo.owner, repo: context.repo.repo, username: commenter, }); if (!['admin', 'maintain', 'write'].includes(permissionLevel.permission)) { console.log(`${commenter} does not have write access`); core.setOutput('status', 'skipped'); return; } } catch { console.log(`${commenter} does not have collaborator access`); core.setOutput('status', 'skipped'); return; } function parseApprovedUsers(content) { const lines = content.split('\n'); const entries = []; const users = new Map(); for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) { entries.push({ type: 'other', line }); continue; } const parts = trimmed.split(/\s+/); if (parts.length !== 2) { console.log(`Skipping malformed line: ${line}`); entries.push({ type: 'other', line }); continue; } const [username, capability] = parts; const normalizedCapability = capability.toLowerCase(); if (!VALID_CAPABILITIES.has(normalizedCapability)) { console.log(`Skipping line with invalid capability: ${line}`); entries.push({ type: 'other', line }); continue; } const normalizedUser = username.toLowerCase(); const entry = { type: 'user', username, normalizedUser, capability: normalizedCapability }; entries.push(entry); users.set(normalizedUser, entry); } return { entries, users }; } function stringifyApprovedUsers(entries) { const normalizedEntries = [...entries]; while (normalizedEntries.length > 0) { const lastEntry = normalizedEntries[normalizedEntries.length - 1]; if (lastEntry.type !== 'other' || lastEntry.line.trim() !== '') { break; } normalizedEntries.pop(); } return `${normalizedEntries .map((entry) => (entry.type === 'user' ? `${entry.username} ${entry.capability}` : entry.line)) .join('\n')}\n`; } const content = fs.readFileSync(APPROVED_FILE, 'utf8'); const { entries, users } = parseApprovedUsers(content); const normalizedAuthor = issueAuthor.toLowerCase(); const existingEntry = users.get(normalizedAuthor); const existingCapability = existingEntry?.capability ?? null; if (existingCapability === 'pr' || existingCapability === targetCapability) { core.setOutput('status', 'already'); core.setOutput('capability', existingCapability); console.log(`${issueAuthor} is already approved for ${existingCapability}`); return; } if (existingEntry) { existingEntry.capability = targetCapability; } else { entries.push({ type: 'user', username: issueAuthor, normalizedUser: normalizedAuthor, capability: targetCapability }); } fs.writeFileSync(APPROVED_FILE, stringifyApprovedUsers(entries)); core.setOutput('status', existingCapability ? 'updated' : 'added'); core.setOutput('capability', targetCapability); console.log(`Set ${issueAuthor} capability to ${targetCapability}`); - name: Commit and push if: steps.update.outputs.status == 'added' || steps.update.outputs.status == 'updated' run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add .github/APPROVED_CONTRIBUTORS git diff --staged --quiet || git commit -m "chore: approve contributor ${{ github.event.issue.user.login }}" git push - name: Comment on issue if: steps.update.outputs.status == 'added' || steps.update.outputs.status == 'updated' || steps.update.outputs.status == 'already' uses: actions/github-script@v7 with: script: | const issueAuthor = context.payload.issue.user.login; const capability = '${{ steps.update.outputs.capability }}'; const defaultBranch = context.payload.repository.default_branch; let body; if ('${{ steps.update.outputs.status }}' === 'already') { body = `@${issueAuthor} is already approved.`; } else if (capability === 'issue') { body = [ `@${issueAuthor} approved for issues. Your future issues will not be auto-closed. PRs still require \`lgtm\`.`, '', `See [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md).`, ].join('\n'); } else { body = [ `@${issueAuthor} approved for issues and PRs. Your future issues and PRs will not be auto-closed.`, '', `See [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md).`, ].join('\n'); } await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body, });