mirror of
https://github.com/DarkFlippers/unleashed-firmware.git
synced 2026-11-13 10:27:37 +00:00
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>
113 lines
5.4 KiB
YAML
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 });
|
|
}
|