Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions .github/workflows/contributor-check-writer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
name: Contributor Reputation Check Writer

on:
workflow_run:
workflows: ["Contributor Reputation Check"]
types: [completed]

permissions:
actions: read
issues: write
pull-requests: read

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