Contributor Reputation Check Writer #110
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: Contributor Reputation Check Writer | |
| on: | |
| workflow_run: | |
| workflows: ["Contributor Reputation Check"] | |
| types: [completed] | |
| permissions: | |
| actions: read | |
| issues: write | |
| pull-requests: write | |
| concurrency: | |
| group: ccw-${{ github.event.workflow_run.head_repository.id || 'unknown-repo' }}-${{ github.event.workflow_run.head_branch || 'unknown-branch' }} | |
| cancel-in-progress: true | |
| jobs: | |
| sync-pr-state: | |
| runs-on: ubuntu-latest | |
| if: github.event.workflow_run.event == 'pull_request' | |
| steps: | |
| - name: Download PR result artifact | |
| id: download-result | |
| continue-on-error: true | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: contributor-check-result | |
| path: ${{ runner.temp }}/contributor-check-result | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ github.token }} | |
| - name: Sync risk labels and comment | |
| if: steps.download-result.outcome == 'success' | |
| uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const workflowRun = context.payload.workflow_run; | |
| const resultPath = path.join(process.env.RUNNER_TEMP, 'contributor-check-result', 'result.json'); | |
| const raw = fs.readFileSync(resultPath, 'utf8'); | |
| const result = JSON.parse(raw); | |
| const allowedRisks = new Set(['HIGH', 'MEDIUM', 'LOW', 'NONE', 'UNKNOWN']); | |
| function fail(message) { | |
| throw new Error(`Invalid contributor check artifact: ${message}`); | |
| } | |
| if (result.schema_version !== 'contributor-check-result/v1') fail('unexpected schema_version'); | |
| if (result.event !== 'pull_request') fail('unexpected event'); | |
| if (!Number.isInteger(result.pr_number) || result.pr_number < 1) fail('invalid pr_number'); | |
| if (!/^[0-9a-f]{40}$/i.test(String(result.head_sha || ''))) fail('invalid head_sha'); | |
| if (workflowRun.event !== 'pull_request') fail('unexpected workflow_run event'); | |
| if (String(result.run_id || '') !== String(workflowRun.id)) fail('run_id did not match workflow_run'); | |
| if (result.head_sha !== workflowRun.head_sha) fail('head_sha did not match workflow_run'); | |
| for (const key of ['profile_risk', 'credential_risk', 'overall_risk']) { | |
| if (!allowedRisks.has(result[key])) fail(`invalid ${key}`); | |
| } | |
| const { data: pr } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: result.pr_number, | |
| }); | |
| if (pr.state !== 'open') { | |
| core.info(`Skipping contributor result for non-open PR #${result.pr_number}.`); | |
| return; | |
| } | |
| const expectedBaseRepository = `${context.repo.owner}/${context.repo.repo}`.toLowerCase(); | |
| const runHeadRepository = String(workflowRun.head_repository?.full_name || ''); | |
| const runHeadRepositoryParts = runHeadRepository.split('/'); | |
| const runHeadRef = String(workflowRun.head_branch || ''); | |
| if (String(pr.base?.repo?.full_name || '').toLowerCase() !== expectedBaseRepository) { | |
| fail(`PR #${result.pr_number} does not target this repository`); | |
| } | |
| if (pr.head.sha !== workflowRun.head_sha) { | |
| core.warning(`Skipping stale contributor result for PR #${result.pr_number}: artifact head ${result.head_sha}, current head ${pr.head.sha}`); | |
| return; | |
| } | |
| if ( | |
| runHeadRepositoryParts.length !== 2 || | |
| !runHeadRepositoryParts[0] || | |
| !runHeadRepositoryParts[1] || | |
| !runHeadRef || | |
| String(pr.head?.repo?.full_name || '').toLowerCase() !== runHeadRepository.toLowerCase() || | |
| String(pr.head?.ref || '') !== runHeadRef | |
| ) { | |
| fail(`PR #${result.pr_number} head did not match workflow_run`); | |
| } | |
| const workflowRunPullRequests = Array.isArray(workflowRun.pull_requests) ? workflowRun.pull_requests : []; | |
| if (workflowRunPullRequests.length > 0) { | |
| if (!workflowRunPullRequests.some((pullRequest) => pullRequest.number === result.pr_number)) { | |
| fail(`PR #${result.pr_number} was not present in workflow_run.pull_requests`); | |
| } | |
| } else { | |
| const candidatePullRequests = await github.paginate(github.rest.pulls.list, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| head: `${runHeadRepositoryParts[0]}:${runHeadRef}`, | |
| per_page: 100, | |
| }); | |
| const trustedMatches = candidatePullRequests.filter((candidate) => | |
| candidate.head?.sha === workflowRun.head_sha && | |
| String(candidate.head?.ref || '') === runHeadRef && | |
| String(candidate.head?.repo?.full_name || '').toLowerCase() === runHeadRepository.toLowerCase() && | |
| String(candidate.base?.repo?.full_name || '').toLowerCase() === expectedBaseRepository | |
| ); | |
| if (trustedMatches.length !== 1 || trustedMatches[0].number !== result.pr_number) { | |
| fail(`PR #${result.pr_number} could not be uniquely associated with workflow_run`); | |
| } | |
| } | |
| const issueNumber = pr.number; | |
| const risk = result.overall_risk; | |
| const marker = '<!-- agt-contributor-check -->'; | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| per_page: 100, | |
| }); | |
| const matchingComments = comments.filter((comment) => | |
| comment.user?.login === 'github-actions[bot]' && String(comment.body || '').includes(marker) | |
| ); | |
| if (risk !== 'MEDIUM' && risk !== 'HIGH') { | |
| for (const comment of matchingComments) { | |
| await github.rest.issues.deleteComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: comment.id, | |
| }).catch((error) => core.warning(`Could not delete comment ${comment.id}: ${error.message}`)); | |
| } | |
| } else { | |
| const icon = risk === 'HIGH' ? '🔴' : '🟡'; | |
| const runUrl = context.payload.workflow_run.html_url; | |
| const body = [ | |
| marker, | |
| `${icon} **Contributor Reputation Check: ${risk} risk**`, | |
| '', | |
| '| Check | Risk |', | |
| '|-------|------|', | |
| `| Profile | ${result.profile_risk} |`, | |
| `| Credential audit | ${result.credential_risk} |`, | |
| '', | |
| 'Maintainers: please review this contributor before merging.', | |
| `See the [workflow run](${runUrl}) for full details.`, | |
| '*Automated check powered by [AGT](https://github.com/microsoft/agent-governance-toolkit).*', | |
| ].join('\n'); | |
| const [canonical, ...duplicates] = matchingComments; | |
| if (canonical) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: canonical.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| body, | |
| }); | |
| } | |
| for (const duplicate of duplicates) { | |
| await github.rest.issues.deleteComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: duplicate.id, | |
| }).catch(() => {}); | |
| } | |
| } | |
| for (const label of ['needs-review:MEDIUM', 'needs-review:HIGH']) { | |
| if (label !== `needs-review:${risk}`) { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| name: label, | |
| }).catch(() => {}); | |
| } | |
| } | |
| if (risk === 'MEDIUM' || risk === 'HIGH') { | |
| const label = `needs-review:${risk}`; | |
| await github.rest.issues.getLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: label, | |
| }).catch(async () => { | |
| await github.rest.issues.createLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: label, | |
| description: `Contributor reputation check flagged ${risk} risk`, | |
| color: 'FFA500', | |
| }); | |
| }); | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| labels: [label], | |
| }); | |
| } | |
| - name: Note missing artifact | |
| if: steps.download-result.outcome != 'success' | |
| run: echo "No contributor-check-result artifact was available; nothing to synchronize." |