Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/review-swarm-wrapper-guard.yml
Original file line number Diff line number Diff line change
@@ -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"
31 changes: 7 additions & 24 deletions .github/workflows/review-swarm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -276,30 +282,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

Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/scripts/swarm-status-diagnostic.sh
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions .github/workflows/scripts/swarm-wrapper-guard.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail

pr_number=${1:?usage: swarm-wrapper-guard.sh PR_NUMBER}

# 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

echo "REVIEW_SWARM_WRAPPER_GUARD_OK"
Loading