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/review-swarm.yml b/.github/workflows/review-swarm.yml index e77d5bc7d..5e43eadda 100644 --- a/.github/workflows/review-swarm.yml +++ b/.github/workflows/review-swarm.yml @@ -258,30 +258,7 @@ jobs: done 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 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"