Files
unleashed-firmware/.github/workflows/pr-comment.yml
T
Mykhailo ShevchukandClaude Opus 5 a56750ffe7 Fix deprecated CodeQL action and harden the PR report commenter (#1046)
CodeQL runs are still reporting green, but every run carries a
failure-level annotation: "CodeQL Action major versions v1 and v2 have
been deprecated." It is running on a compatibility shim. Move all four
pins to v4, and bump checkout from v3 to v6 to match the rest of the
repo and clear the Node 20 deprecation warning. Drop
setup-python-dependencies, which the run log confirms has had no effect
since CodeQL 2.16. Add a concurrency group so back-to-back pushes to dev
no longer run several full firmware builds plus analyses at once.

pr-comment.yml trusted pr_number.txt from the report artifact and
validated it only as an integer. That artifact is written by a job that
checks out and runs the PR's own code, so both the comment body and its
destination were attacker-controlled: a malicious fork PR could make the
privileged commenter post arbitrary markdown onto any open thread in the
repo. Check the named PR's head SHA against the triggering run's
head_sha, which comes from the event payload and cannot be forged, and
refuse to comment on a mismatch.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 13:28:47 +03:00

113 lines
5.4 KiB
YAML

# 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, whose body it treats strictly as text to post - and whose PR
# number it does not trust at all: the destination is checked against the triggering run's
# head SHA before commenting, so a PR cannot redirect the comment onto another thread.
#
# 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) || issue_number < 1) {
core.setFailed('Invalid PR number in artifact.');
return;
}
const { owner, repo } = context.repo;
// The artifact is written by a job that checked out and ran the PR's own
// code, so the number inside it is attacker-controlled: on its own it
// would let a malicious PR aim this privileged comment at any other
// thread in the repo. workflow_run.head_sha comes from the event payload
// and cannot be forged, so require the named PR to actually be the one
// that was built before posting anything.
const headSha = context.payload.workflow_run.head_sha;
let pr;
try {
({ data: pr } = await github.rest.pulls.get(
{ owner, repo, pull_number: issue_number }));
} catch (e) {
core.setFailed(`Could not load PR #${issue_number}: ${e.message}`);
return;
}
if (pr.head.sha !== headSha) {
core.setFailed(
`PR #${issue_number} is at ${pr.head.sha} but this build ran ${headSha}; ` +
'refusing to comment.');
return;
}
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 });
}