Housekeeping – Approval Labels #810
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Housekeeping – Approval Labels | |
| on: | |
| schedule: | |
| - cron: '0 * * * *' | |
| workflow_dispatch: | |
| permissions: | |
| pull-requests: write | |
| issues: write | |
| jobs: | |
| sync-labels: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Generate App token | |
| id: app-token | |
| uses: actions/create-github-app-token@v1 | |
| with: | |
| app-id: ${{ secrets.APP_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| - name: Sync approval labels on all open PRs | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ steps.app-token.outputs.token }} | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| // Load trusted authors from TRUSTED_AGENTS.md (main branch) | |
| const { data: repoContent } = await github.rest.repos.getContent({ | |
| owner, repo, path: 'TRUSTED_AGENTS.md', ref: 'main', | |
| }); | |
| const normalize = (n) => n ? n.replace(/\[bot\]$/, '') : n; | |
| const trusted = Buffer.from(repoContent.content, 'base64').toString() | |
| .split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')); | |
| const trustedSet = new Set(['thepagent', 'copilot', 'github-actions', ...trusted]); | |
| // Fetch all open PRs | |
| const prs = await github.paginate(github.rest.pulls.list, { | |
| owner, repo, state: 'open', per_page: 100, | |
| }); | |
| for (const pr of prs) { | |
| const prNumber = pr.number; | |
| // Count latest approval state per user | |
| const reviews = await github.paginate(github.rest.pulls.listReviews, { | |
| owner, repo, pull_number: prNumber, per_page: 100, | |
| }); | |
| const latestByUser = new Map(); | |
| for (const r of reviews) { | |
| if (r.user?.login) latestByUser.set(r.user.login, r.state); | |
| } | |
| const trustedApprovals = [...latestByUser.entries()] | |
| .filter(([login, state]) => state === 'APPROVED' && trustedSet.has(normalize(login))) | |
| .length; | |
| core.info(`PR #${prNumber} trusted approvals: ${trustedApprovals}`); | |
| const labels = (pr.labels || []).map(l => l.name); | |
| if (trustedApprovals >= 2) { | |
| if (labels.includes('pending-trusted-approvals')) { | |
| await github.rest.issues.removeLabel({ | |
| owner, repo, issue_number: prNumber, name: 'pending-trusted-approvals', | |
| }); | |
| } | |
| if (!labels.includes('pending-final-approval')) { | |
| await github.rest.issues.addLabels({ | |
| owner, repo, issue_number: prNumber, labels: ['pending-final-approval'], | |
| }); | |
| } | |
| } else { | |
| if (!labels.includes('pending-trusted-approvals')) { | |
| await github.rest.issues.addLabels({ | |
| owner, repo, issue_number: prNumber, labels: ['pending-trusted-approvals'], | |
| }); | |
| } | |
| if (labels.includes('pending-final-approval')) { | |
| await github.rest.issues.removeLabel({ | |
| owner, repo, issue_number: prNumber, name: 'pending-final-approval', | |
| }); | |
| } | |
| } | |
| } |