From 6b1c8d28f157248e6a246d429e33054aa5d264c9 Mon Sep 17 00:00:00 2001 From: Relayflow Lead Date: Thu, 10 Sep 2026 19:51:42 +0200 Subject: [PATCH 1/3] ci(review-swarm): guard wrapper from candidates Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82 --- .../workflows/review-swarm-wrapper-guard.yml | 26 +++++++++++++++++++ .../workflows/scripts/swarm-wrapper-guard.sh | 12 +++++++++ 2 files changed, 38 insertions(+) create mode 100644 .github/workflows/review-swarm-wrapper-guard.yml create mode 100755 .github/workflows/scripts/swarm-wrapper-guard.sh diff --git a/.github/workflows/review-swarm-wrapper-guard.yml b/.github/workflows/review-swarm-wrapper-guard.yml new file mode 100644 index 000000000..3637ffee6 --- /dev/null +++ b/.github/workflows/review-swarm-wrapper-guard.yml @@ -0,0 +1,26 @@ +name: Review swarm wrapper guard + +on: + pull_request_target: + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: read + pull-requests: read + +jobs: + guard: + runs-on: ubuntu-latest + steps: + # pull_request_target always reads this workflow and script from the PR + # base. A candidate therefore cannot relax the guard that evaluates it. + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.sha }} + persist-credentials: false + + - name: Reject candidate-owned wrapper changes + env: + GH_TOKEN: ${{ github.token }} + REVIEW_PR_NUMBER: ${{ github.event.pull_request.number }} + run: .github/workflows/scripts/swarm-wrapper-guard.sh "$REVIEW_PR_NUMBER" diff --git a/.github/workflows/scripts/swarm-wrapper-guard.sh b/.github/workflows/scripts/swarm-wrapper-guard.sh new file mode 100755 index 000000000..174455ddc --- /dev/null +++ b/.github/workflows/scripts/swarm-wrapper-guard.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail + +pr_number=${1:?usage: swarm-wrapper-guard.sh PR_NUMBER} +changed_files=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}/files" --jq '.[].filename') + +if grep -Fxq '.github/workflows/review-swarm.yml' <<<"$changed_files"; then + echo "candidate changes review-swarm.yml; wrapper enforcement is base-owned and immutable" >&2 + exit 1 +fi + +echo "REVIEW_SWARM_WRAPPER_GUARD_OK" From 75ff9b2db040fd04ab5102bf864b07c8cc2c4626 Mon Sep 17 00:00:00 2001 From: Relayflow Lead Date: Thu, 10 Sep 2026 19:52:39 +0200 Subject: [PATCH 2/3] refactor(review-swarm): externalize terminal diagnostics Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82 --- .github/workflows/review-swarm.yml | 25 +------------------ .../scripts/swarm-status-diagnostic.sh | 11 ++++++++ 2 files changed, 12 insertions(+), 24 deletions(-) create mode 100755 .github/workflows/scripts/swarm-status-diagnostic.sh diff --git a/.github/workflows/review-swarm.yml b/.github/workflows/review-swarm.yml index 3210e38e3..c1f7becab 100644 --- a/.github/workflows/review-swarm.yml +++ b/.github/workflows/review-swarm.yml @@ -276,30 +276,7 @@ jobs: fi echo "swarm_status=$status" >> "$GITHUB_OUTPUT" if [ "$status" != completed ]; then - # The status word alone does not say why the swarm failed, and the - # reason never reaches this log: it sits in the run payload we just - # fetched. A quota rejection reads here as a bare "failed", which - # sent one reader inferring for days before querying the run by - # hand. Print what we already have. - reason=$(jq -r '.result.error // .error // empty' <<<"${response:-}" 2>/dev/null) - if [ -n "$reason" ]; then - # The reason is not fully trusted. It can carry agent output, - # which can carry content from the PR under review. Two ways that - # bites: a line starting with `::` is parsed by Actions as a - # workflow command, and a line of three backticks would close a - # fenced block early and render the rest as markup. - # Indenting every line defeats both at once — Actions only parses - # a command at the start of a line, and an indented block is a - # Markdown code block with no fence to break. - safe_reason=$(printf '%s\n' "$reason" | sed 's/^/ /') - echo "swarm failure reason:" >&2 - printf '%s\n' "$safe_reason" >&2 - { - echo "### Swarm failure reason" - echo - printf '%s\n' "$safe_reason" - } >> "$GITHUB_STEP_SUMMARY" - fi + ../gate-files/.github/workflows/scripts/swarm-status-diagnostic.sh "${response:-}" fi exit 0 diff --git a/.github/workflows/scripts/swarm-status-diagnostic.sh b/.github/workflows/scripts/swarm-status-diagnostic.sh new file mode 100755 index 000000000..0f2f073c5 --- /dev/null +++ b/.github/workflows/scripts/swarm-status-diagnostic.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail + +response=${1:-} +reason=$(jq -r '.result.error // .error // empty' <<<"$response" 2>/dev/null) +if [ -n "$reason" ]; then + safe_reason=$(printf '%s\n' "$reason" | sed 's/^/ /') + echo "swarm failure reason:" >&2 + printf '%s\n' "$safe_reason" >&2 + { echo "### Swarm failure reason"; echo; printf '%s\n' "$safe_reason"; } >> "$GITHUB_STEP_SUMMARY" +fi From 058e6724ac8215c322f9538a71c19ff067a4d3db Mon Sep 17 00:00:00 2001 From: kjgbot Date: Thu, 10 Sep 2026 21:40:33 +0200 Subject: [PATCH 3/3] fix(review-swarm): address Cursor Bugbot findings on wrapper guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two findings on flows#285: - HIGH: the `Wait for cloud swarm` step called `../gate-files/...swarm-status-diagnostic.sh` without `working-directory: pr-head`. The path then resolved outside the workspace, and `set +e` at the top of the step swallowed the miss — every failed swarm run would go without its diagnostic line. Add the matching `working-directory` (every other gate-files call from this workflow already uses it) with a comment naming why. - MEDIUM: the wrapper guard only inspected `.filename` from the pulls-files listing. A candidate could rename `.github/workflows/review-swarm.yml` to another path, moving the guarded wrapper off the branch without a modification the guard would catch — `previous_filename` carries the old path in that case. Read both keys and grep the union. Both fixes are minimal and preserve the wrapper's other semantics. Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82 --- .github/workflows/review-swarm.yml | 6 ++++++ .github/workflows/scripts/swarm-wrapper-guard.sh | 14 +++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/review-swarm.yml b/.github/workflows/review-swarm.yml index c1f7becab..d5655263f 100644 --- a/.github/workflows/review-swarm.yml +++ b/.github/workflows/review-swarm.yml @@ -236,6 +236,12 @@ jobs: - name: Wait for cloud swarm id: wait if: always() && steps.launch.outputs.run_id != '' + # `../gate-files/` below resolves relative to this step's cwd. Every + # other step that reaches into `gate-files/` runs from `pr-head/`; + # without a matching `working-directory` here, the diagnostic call + # resolves outside the workspace and `set +e` silently swallows the + # miss (Cursor Bugbot flagged as HIGH on #285). + working-directory: pr-head run: | set +e # Ordering invariant: swarm 60m < this poll deadline 65m < job 75m. diff --git a/.github/workflows/scripts/swarm-wrapper-guard.sh b/.github/workflows/scripts/swarm-wrapper-guard.sh index 174455ddc..e7d2cf231 100755 --- a/.github/workflows/scripts/swarm-wrapper-guard.sh +++ b/.github/workflows/scripts/swarm-wrapper-guard.sh @@ -2,10 +2,18 @@ set -euo pipefail pr_number=${1:?usage: swarm-wrapper-guard.sh PR_NUMBER} -changed_files=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}/files" --jq '.[].filename') -if grep -Fxq '.github/workflows/review-swarm.yml' <<<"$changed_files"; then - echo "candidate changes review-swarm.yml; wrapper enforcement is base-owned and immutable" >&2 +# Read BOTH filename and previous_filename. A rename of +# `.github/workflows/review-swarm.yml` sets previous_filename to the guarded +# path and filename to the new location, which lets a candidate move the +# wrapper off the branch without failing a `filename`-only check +# (Cursor Bugbot flagged as MEDIUM on #285). Any touch of that path in either +# axis is a modification of the guarded wrapper. +touched_paths=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}/files" \ + --jq '.[] | .filename, (.previous_filename // empty)') + +if grep -Fxq '.github/workflows/review-swarm.yml' <<<"$touched_paths"; then + echo "candidate changes review-swarm.yml (add, edit, or rename); wrapper enforcement is base-owned and immutable" >&2 exit 1 fi