From 5d6f874af471a4fe077bdcc90d0026cf940460ec Mon Sep 17 00:00:00 2001 From: KJGBot Date: Thu, 8 Oct 2026 16:04:12 -0700 Subject: [PATCH] ci: harden trunk gating - CI gates also require the head repo to be this repository and the base to be main (a fork branch named trunk, or trunk into another branch, no longer runs CI). - Trunk guard runs via pull_request_target (loaded from the base branch, so a PR cannot edit it to pass), re-runs on base edits, and checks head repo. - Workflows that only react to a PR closing are no longer gated. --- .github/workflows/ci.yml | 16 ++++++++-------- .github/workflows/trunk-guard.yml | 15 ++++++++++----- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c86c6853e..07ce75fb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ permissions: jobs: verify: - if: (github.event_name != 'pull_request' && github.event_name != 'pull_request_target') || github.head_ref == 'trunk' + if: (github.event_name != 'pull_request' && github.event_name != 'pull_request_target') || (github.head_ref == 'trunk' && github.event.pull_request.head.repo.full_name == github.repository && github.base_ref == 'main') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -102,7 +102,7 @@ jobs: # fails on any `rusqlite` type in the listing: an embedder never holds a # raw connection. See docs/sourcing-sdk.md. public-api: - if: (github.event_name != 'pull_request' && github.event_name != 'pull_request_target') || github.head_ref == 'trunk' + if: (github.event_name != 'pull_request' && github.event_name != 'pull_request_target') || (github.head_ref == 'trunk' && github.event.pull_request.head.repo.full_name == github.repository && github.base_ref == 'main') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -133,7 +133,7 @@ jobs: # published-crate-consumer workflow builds the same example against the # crate crates.io actually serves. rust-consumer: - if: (github.event_name != 'pull_request' && github.event_name != 'pull_request_target') || github.head_ref == 'trunk' + if: (github.event_name != 'pull_request' && github.event_name != 'pull_request_target') || (github.head_ref == 'trunk' && github.event.pull_request.head.repo.full_name == github.repository && github.base_ref == 'main') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -188,7 +188,7 @@ jobs: # tell a regression here from breakage in burn. Making the job a required # check is a branch-protection setting. burn-contract-drift: - if: (github.event_name != 'pull_request' && github.event_name != 'pull_request_target') || github.head_ref == 'trunk' + if: (github.event_name != 'pull_request' && github.event_name != 'pull_request_target') || (github.head_ref == 'trunk' && github.event.pull_request.head.repo.full_name == github.repository && github.base_ref == 'main') runs-on: ubuntu-latest timeout-minutes: 45 steps: @@ -235,7 +235,7 @@ jobs: run: node scripts/burn-guardrails.mjs run-parity --burn-dir burn msrv: - if: (github.event_name != 'pull_request' && github.event_name != 'pull_request_target') || github.head_ref == 'trunk' + if: (github.event_name != 'pull_request' && github.event_name != 'pull_request_target') || (github.head_ref == 'trunk' && github.event.pull_request.head.repo.full_name == github.repository && github.base_ref == 'main') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -244,7 +244,7 @@ jobs: run: cargo check -p ai-hist --all-features --locked node-22-sdk: - if: (github.event_name != 'pull_request' && github.event_name != 'pull_request_target') || github.head_ref == 'trunk' + if: (github.event_name != 'pull_request' && github.event_name != 'pull_request_target') || (github.head_ref == 'trunk' && github.event.pull_request.head.repo.full_name == github.repository && github.base_ref == 'main') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -264,7 +264,7 @@ jobs: run: npm ci && npm test optional-history-plugins: - if: (github.event_name != 'pull_request' && github.event_name != 'pull_request_target') || github.head_ref == 'trunk' + if: (github.event_name != 'pull_request' && github.event_name != 'pull_request_target') || (github.head_ref == 'trunk' && github.event.pull_request.head.repo.full_name == github.repository && github.base_ref == 'main') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -313,7 +313,7 @@ jobs: node scripts/smoke-history-packages.mjs --helpers helper-packs windows-helper-cancellation: - if: (github.event_name != 'pull_request' && github.event_name != 'pull_request_target') || github.head_ref == 'trunk' + if: (github.event_name != 'pull_request' && github.event_name != 'pull_request_target') || (github.head_ref == 'trunk' && github.event.pull_request.head.repo.full_name == github.repository && github.base_ref == 'main') runs-on: windows-latest defaults: run: diff --git a/.github/workflows/trunk-guard.yml b/.github/workflows/trunk-guard.yml index 2ddb49758..fbe379e4b 100644 --- a/.github/workflows/trunk-guard.yml +++ b/.github/workflows/trunk-guard.yml @@ -1,17 +1,22 @@ name: Trunk guard -# Only the `trunk` branch may merge into main. Feature branches merge into -# `trunk` (via the merge train); CI runs only on the trunk -> main PR. +# Only this repository's `trunk` branch may merge into main. Feature branches +# merge into `trunk`; CI runs only on the trunk -> main PR. +# pull_request_target loads this workflow from the base branch, so a PR cannot +# edit the guard to pass itself. It never checks out PR code. on: - pull_request: + pull_request_target: branches: [main] + types: [opened, synchronize, reopened, edited] + +permissions: {} jobs: only-trunk-merges-to-main: - if: github.head_ref != 'trunk' + if: github.head_ref != 'trunk' || github.event.pull_request.head.repo.full_name != github.repository runs-on: ubuntu-latest timeout-minutes: 1 steps: - run: | - echo "::error::Only the 'trunk' branch may merge into main. Retarget this PR to 'trunk'." + echo "::error::Only this repository's 'trunk' branch may merge into main. Retarget this PR to 'trunk'." exit 1