Skip to content

PR report comment

PR report comment #27

Workflow file for this run

# Post the "Build & analyze" report as a PR comment — including on fork PRs.
#
# The build workflow (pr-build.yml) runs on `pull_request`; for fork PRs GitHub hands it
# a read-only token, so it cannot comment itself. It instead uploads the assembled report
# as the `pr-report` artifact. This workflow is triggered by `workflow_run` when that build
# completes: because `workflow_run` always runs the copy of this file on the DEFAULT BRANCH,
# in the base-repo context, its requested write permissions are actually granted (unlike the
# fork's read-only `pull_request` token). It downloads the artifact and posts the comment.
#
# Security: this job must NOT check out or execute the PR's code. Its only untrusted input
# is the report artifact, which it treats strictly as text to post.
#
# Note: `workflow_run` workflows only ever run from the default branch, so changes here take
# effect once merged to dev and can't be exercised from a PR branch.
name: PR report comment
on:
workflow_run:
workflows: ["Build & analyze"] # must match pr-build.yml's `name:`
types: [completed]
# Cross-run artifact download needs `actions: read`; posting the comment needs
# `pull-requests: write`. An explicit block sets every unlisted scope to `none`.
permissions:
actions: read
pull-requests: write # honored here: this runs in the base-repo context, not the fork's
# Serialize comment runs for the same build so a re-run can't race the find-or-create
# below into duplicate comments. workflow_run.pull_requests is empty for fork PRs, so key
# on the run id (unique per triggering build) rather than the PR number.
concurrency:
group: pr-comment-${{ github.event.workflow_run.id }}
cancel-in-progress: false
jobs:
comment:
name: Post report comment
runs-on: ubuntu-latest
# Only PR builds that actually produced a report: skip push/dispatch (no PR) and
# cancelled/skipped builds (no artifact — would otherwise warn spuriously below).
if: >-
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion != 'cancelled' &&
github.event.workflow_run.conclusion != 'skipped'
steps:
# Cross-run: pulls the artifact from the build's run, not this workflow's.
# continue-on-error so a missing/expired artifact doesn't fail the job; the next
# step surfaces it as a warning instead of silently dropping the comment.
- name: Download report artifact
id: dl
continue-on-error: true
uses: actions/download-artifact@v7
with:
name: pr-report
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Warn if report unavailable
if: steps.dl.outcome != 'success'
run: echo "::warning::pr-report artifact could not be downloaded (outcome=${{ steps.dl.outcome }}); no PR comment posted."
- name: Post or update comment
if: steps.dl.outcome == 'success'
uses: actions/github-script@v9
with:
script: |
const fs = require('fs');
if (!fs.existsSync('pr_report.md') || !fs.existsSync('pr_number.txt')) {
core.warning('Report artifact incomplete; nothing to post.');
return;
}
const marker = '<!-- unleashed-pr-report -->';
const body = fs.readFileSync('pr_report.md', 'utf8');
const issue_number = parseInt(fs.readFileSync('pr_number.txt', 'utf8').trim(), 10);
if (!Number.isInteger(issue_number)) {
core.setFailed('Invalid PR number in artifact.');
return;
}
const { owner, repo } = context.repo;
const comments = await github.paginate(github.rest.issues.listComments,
{ owner, repo, issue_number, per_page: 100 });
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
} else {
await github.rest.issues.createComment({ owner, repo, issue_number, body });
}