Skip to content

pr-report

pr-report #4

Workflow file for this run

# Posts the result of pr-validate back to the pull request.
# Triggered by workflow_run so it runs in the BASE repo with write permission —
# this is what lets us set a commit status and comment on PRs opened from forks,
# without ever exposing a write token to the untrusted build in pr-validate.
name: pr-report
on:
workflow_run:
workflows: ["pr-validate"]
types: [completed]
permissions:
statuses: write
pull-requests: write
actions: read
jobs:
report:
runs-on: ubuntu-latest
steps:
- name: Download validation report
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: report
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Publish status and comment
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const run = context.payload.workflow_run;
// Only report on PR builds.
if (run.event !== 'pull_request') { core.info('Not a pull_request run; skipping.'); return; }
const buildOk = run.conclusion === 'success';
// report.json comes from the UNTRUSTED PR build (a fork can put anything in
// it). It is used ONLY as sanitized display text — never to choose which
// commit/PR to write to. Those targets come from the trusted event/API.
let report = {};
try { report = JSON.parse(fs.readFileSync('report.json', 'utf8')); }
catch (e) { core.warning('No report.json artifact found: ' + e.message); }
const clean = (s, n = 300) =>
String(s == null ? '' : s).replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, n);
const cleanName = (s) =>
(String(s || '').match(/[A-Za-z0-9._-]+/) || ['container'])[0].slice(0, 128);
// Trusted target: the head commit of the run, and the PR that owns it.
const sha = run.head_sha;
let prNumber = run.pull_requests && run.pull_requests[0] && run.pull_requests[0].number;
if (!prNumber && sha) {
try {
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner, repo: context.repo.repo, commit_sha: sha,
});
const hit = prs.find(p => p.state === 'open') || prs[0];
if (hit) prNumber = hit.number;
} catch (e) { core.warning('PR lookup failed: ' + e.message); }
}
const software = cleanName(report.software || report.container);
const container = cleanName(report.container);
const tag = cleanName(report.tag);
const errors = (Array.isArray(report.errors) ? report.errors : []).slice(0, 20).map(e => clean(e));
const warnings = (Array.isArray(report.warnings) ? report.warnings : []).slice(0, 20).map(w => clean(w));
// Commit status on the trusted head SHA; build conclusion gates pass/fail.
if (sha) {
const state = buildOk && errors.length === 0 ? 'success' : 'failure';
const description = state === 'success'
? 'All checks passed'
: (errors[0] || 'Build or tests failed — see the pr-validate run');
await github.rest.repos.createCommitStatus({
owner: context.repo.owner, repo: context.repo.repo, sha,
state, context: 'biocontainers/status/check/' + software,
description: description.slice(0, 140),
target_url: run.html_url,
});
}
if (!prNumber) { core.info('No PR number resolved; skipping comment.'); return; }
const lines = [];
const passed = buildOk && errors.length === 0;
lines.push(passed
? `✅ **BioContainers CI passed** for \`${software}\`.`
: `❌ **BioContainers CI failed** for \`${software}\`.`);
if (report.tag) lines.push(`Image tag: \`biocontainers/${container}:${tag}\``);
if (!buildOk && errors.length === 0)
lines.push('\nThe build or test step failed. See the ' +
`[pr-validate run](${run.html_url}) for details.`);
if (errors.length) lines.push('\n**Errors (must fix):**\n' + errors.map(e => `- ${e}`).join('\n'));
if (warnings.length) lines.push('\n**Warnings (advisory):**\n' + warnings.map(w => `- ${w}`).join('\n'));
const marker = '<!-- biocontainers-ci -->';
const body = marker + '\n' + lines.join('\n');
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner, repo: context.repo.repo,
comment_id: existing.id, body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: prNumber, body,
});
}