Files
awesome-copilot/.github/workflows/contributor-check-writer.yml
T
Michael Recachinas f48b84e6a2 Fix PRT writer permissions for fork PRs
Restore label and comment synchronization after the PRT migration by granting the downstream writer workflows the pull request permission required for fork-originated PRs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 512eb347-ec89-4250-8bf1-87048974b01d
2026-08-14 09:26:45 -04:00

218 lines
9.6 KiB
YAML

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."