mirror of
https://gitee.com/mirrors_PX4/PX4-Autopilot.git
synced 2026-04-14 10:07:39 +08:00
Adds a stand-alone workflow that posts or updates sticky PR comments on behalf of any analysis workflow, including those triggered by fork PRs. The poster runs on `workflow_run` in the base repo context, which is the standard GitHub-sanctioned way to get a write token on events that originate from untrusted forks without ever checking out fork code. All validation, GitHub API interaction, and upsert logic lives in Tools/ci/pr-comment-poster.py (Python 3 stdlib only, two subcommands: `validate` and `post`). The workflow file itself is a thin orchestrator: sparse-checkout the script, download the pr-comment artifact via github-script, unzip, then invoke the script twice. No inline jq, no inline bash validation, no shell-interpolated marker strings. The sparse-checkout ensures only Tools/ci/pr-comment-poster.py lands in the workspace, never the rest of the repo. Artifact contract: a producer uploads an artifact named exactly `pr-comment` containing `manifest.json` (with `pr_number`, `marker`, and optional `mode`) and `body.md`. The script validates the manifest (positive integer pr_number, printable-ASCII marker bounded 1..200 chars, UTF-8 body under 60000 bytes, mode in an allowlist), finds any existing comment containing the marker via the comments REST API, and either edits it in place or creates a new one. The workflow file header documents six security invariants that any future change MUST preserve, most importantly: NEVER check out PR code, NEVER execute anything from the artifact, and treat all artifact contents as opaque data. Why a generic poster and not `pull_request_target`: `pull_request_target` is the tool people reach for first and the one that most often turns into a supply-chain vulnerability, because it hands a write token to a workflow that is then tempted to check out the PR head. `workflow_run` gives the same write token without any check-out temptation, because the only input is a pre-produced artifact treated as opaque data. Producer migrations =================== flash_analysis.yml: - Drop the fork gate on the `post_pr_comment` job. - Drop the obsolete TODO pointing at issue #24408 (the fork-comment workflow does not error anymore; it just no-ops). - Keep the existing "comment only if threshold crossed or previous comment exists" behaviour verbatim. peter-evans/find-comment@v3 stays as a read-only probe (forks can read issue comments just fine); its body-includes is updated to search for the new marker `<!-- pr-comment-poster:flash-analysis -->` instead of the old "FLASH Analysis" heading substring. - Replace the peter-evans/create-or-update-comment@v4 step with two new steps that write pr-comment/manifest.json and pr-comment/body.md and then upload them as artifact pr-comment. The body markdown is byte-for-byte identical to the previous heredoc, with the marker prepended as the first line so subsequent runs can find it. - The threshold-or-existing-comment gate is preserved on both new steps. When the gate does not fire no artifact is uploaded and the poster no-ops. docs-orchestrator.yml (link-check job): - Drop the fork gate on the sticky-comment step. - Replace marocchino/sticky-pull-request-comment@v2 with two new steps that copy logs/filtered-link-check-results.md into pr-comment/body.md, write a pr-comment/manifest.json with the marker `<!-- pr-comment-poster:docs-link-check -->`, and upload the directory as artifact pr-comment. - The prepare step checks `test -s` on the results file and emits a prepared step output; the upload step is gated on that output. In practice the existing link-check step always writes a placeholder ("No broken links found in changed files.") into the file when empty, so the guard is defensive but not load-bearing today. - Tighten the link-check job's permissions from `pull-requests: write` down to `contents: read`; writing PR comments now happens in the poster workflow. The poster's workflows allowlist is seeded with the two active producers: "FLASH usage analysis" and "Docs - Orchestrator". clang-tidy (workflow name "Static Analysis") is not in the list because platisd/clang-tidy-pr-comments posts line-level review comments, a different REST API from issue comments that the poster script does not handle. Extending the poster to cover review comments is a follow-up. Signed-off-by: Ramon Roche <mrpollo@gmail.com>
148 lines
6.4 KiB
YAML
148 lines
6.4 KiB
YAML
name: PR Comment Poster
|
|
|
|
# Generic PR comment poster. Any analysis workflow (clang-tidy, flash_analysis,
|
|
# fuzz coverage, SITL perf, etc.) can produce a `pr-comment` artifact and this
|
|
# workflow will post or update a sticky PR comment with its contents. Designed
|
|
# so that analysis jobs running on untrusted fork PRs can still get their
|
|
# results posted back to the PR.
|
|
#
|
|
# ==============================================================================
|
|
# SECURITY INVARIANTS
|
|
# ==============================================================================
|
|
# This workflow runs on `workflow_run` which means it runs in the BASE REPO
|
|
# context with a WRITE token, even when the triggering PR comes from a fork.
|
|
# That is the entire reason it exists, and also the reason it is a loaded
|
|
# footgun. Anyone modifying this file MUST preserve the following invariants:
|
|
#
|
|
# 1. NEVER check out PR code. No `actions/checkout` with a ref. No git clone
|
|
# of a fork branch. No execution of scripts from the downloaded artifact.
|
|
# The ONLY things read from the artifact are `manifest.json` and `body.md`,
|
|
# and both are treated as opaque data (JSON parsed by the poster script
|
|
# and markdown posted verbatim via the GitHub API).
|
|
#
|
|
# 2. `pr_number` is validated to be a positive integer before use.
|
|
# `marker` is validated to be printable ASCII only before use. Validation
|
|
# happens inside Tools/ci/pr-comment-poster.py which is checked out from
|
|
# the base branch, not from the artifact.
|
|
#
|
|
# 3. The comment body is passed to the GitHub API as a JSON field, never
|
|
# interpolated into a shell command string.
|
|
#
|
|
# 4. This workflow file lives on the default branch. `workflow_run` only
|
|
# loads workflow files from the default branch, so a fork cannot modify
|
|
# THIS file as part of a PR. The fork CAN cause this workflow to fire
|
|
# by triggering a producer workflow that uploads a `pr-comment` artifact.
|
|
# That is intended.
|
|
#
|
|
# 5. The artifact-name filter (`pr-comment`) is the only gate on which
|
|
# workflow runs get processed. Any workflow in this repo that uploads
|
|
# an artifact named `pr-comment` is trusted to have written the
|
|
# manifest and body itself, NOT copied fork-controlled content into
|
|
# them. Producer workflows are responsible for that.
|
|
#
|
|
# 6. `actions/checkout@v4` below uses NO ref (so it pulls the base branch,
|
|
# the default-branch commit this workflow file was loaded from) AND uses
|
|
# sparse-checkout to materialize ONLY Tools/ci/pr-comment-poster.py. The
|
|
# rest of the repo never touches the workspace. This is safe: the only
|
|
# file the job executes is a base-repo Python script that was reviewed
|
|
# through normal code review, never anything from the PR.
|
|
#
|
|
# ==============================================================================
|
|
# ARTIFACT CONTRACT
|
|
# ==============================================================================
|
|
# Producers upload an artifact named exactly `pr-comment` containing:
|
|
#
|
|
# manifest.json:
|
|
# {
|
|
# "pr_number": 12345, // required, int > 0
|
|
# "marker": "<!-- pr-comment-poster:flash-analysis -->", // required, printable ASCII
|
|
# "mode": "upsert" // optional, default "upsert"
|
|
# }
|
|
#
|
|
# body.md: the markdown content of the comment. Posted verbatim.
|
|
#
|
|
# The `marker` string is used to find an existing comment to update. It MUST
|
|
# be unique per producer (e.g. include the producer name). If no existing
|
|
# comment contains the marker, a new one is created. If the marker is found
|
|
# in an existing comment, that comment is edited in place.
|
|
#
|
|
# Producers MUST write `pr_number` from their own workflow context
|
|
# (`github.event.pull_request.number`) and MUST NOT read it from any
|
|
# fork-controlled source.
|
|
|
|
on:
|
|
workflow_run:
|
|
# Producers that may upload a `pr-comment` artifact. When a new producer
|
|
# is wired up, add its workflow name here. Runs of workflows not in this
|
|
# list will never trigger the poster. Every run of a listed workflow will
|
|
# trigger the poster, which will no-op if no `pr-comment` artifact exists.
|
|
workflows:
|
|
- "FLASH usage analysis"
|
|
- "Docs - Orchestrator"
|
|
types:
|
|
- completed
|
|
|
|
permissions:
|
|
pull-requests: write
|
|
actions: read
|
|
contents: read
|
|
|
|
jobs:
|
|
post:
|
|
name: Post PR Comment
|
|
runs-on: ubuntu-latest
|
|
if: github.event.workflow_run.conclusion != 'cancelled'
|
|
steps:
|
|
# Checkout runs first so the poster script is available AND so that
|
|
# actions/checkout@v4's default clean step does not delete the artifact
|
|
# zip that the next step writes into the workspace. Sparse-checkout
|
|
# restricts the materialized tree to just the poster script.
|
|
- name: Checkout poster script only
|
|
uses: actions/checkout@v4
|
|
with:
|
|
sparse-checkout: |
|
|
Tools/ci/pr-comment-poster.py
|
|
sparse-checkout-cone-mode: false
|
|
|
|
- name: Download pr-comment artifact
|
|
id: download
|
|
uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
run_id: context.payload.workflow_run.id,
|
|
});
|
|
const match = artifacts.data.artifacts.find(a => a.name === 'pr-comment');
|
|
if (!match) {
|
|
core.info('No pr-comment artifact on this run; nothing to post.');
|
|
core.setOutput('found', 'false');
|
|
return;
|
|
}
|
|
const download = await github.rest.actions.downloadArtifact({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
artifact_id: match.id,
|
|
archive_format: 'zip',
|
|
});
|
|
const fs = require('fs');
|
|
fs.writeFileSync('pr-comment.zip', Buffer.from(download.data));
|
|
core.setOutput('found', 'true');
|
|
|
|
- name: Unpack artifact
|
|
if: steps.download.outputs.found == 'true'
|
|
run: |
|
|
mkdir -p pr-comment
|
|
unzip -q pr-comment.zip -d pr-comment
|
|
|
|
- name: Validate artifact
|
|
if: steps.download.outputs.found == 'true'
|
|
run: python3 Tools/ci/pr-comment-poster.py validate pr-comment
|
|
|
|
- name: Upsert sticky comment
|
|
if: steps.download.outputs.found == 'true'
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: python3 Tools/ci/pr-comment-poster.py post pr-comment
|