From a6cac6d4a3f1e842d6abbece7df87a4a9079c548 Mon Sep 17 00:00:00 2001 From: rishigupta1599 Date: Wed, 24 Jun 2026 21:01:33 +0530 Subject: [PATCH 1/2] ci(release): skip CI and the approval gate on version-bump PRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Create Release PR workflow opens a `release/X.Y.Z` PR that only bumps version files and posts passing check runs for every required context, so the full CI suite is redundant on these PRs. Two problems remained: 1. create-pull-request defaults the commit *author* to `github.actor` (the human who ran the workflow), so the PR commit looked user-authored. That defeated GitHub's GITHUB_TOKEN recursion guard, so the on:pull_request CI workflows were queued and parked in `action_required` — surfacing the unwanted "Approve and run workflows" prompt and, once approved, running the full ~20-min suite redundantly. 2. Even if approved, that CI is pointless for a version-only PR. Fixes: - Fully attribute the release commit to github-actions[bot] (author + committer) so the recursion guard suppresses the pull_request runs entirely — no approval gate; the synthetic checks satisfy branch protection on their own. - Add a `!startsWith(github.head_ref, 'release/')` guard to every PR-triggered CI job (test, windows, typecheck, lint, semgrep, executable-check) as defense-in-depth: if a run is ever triggered/approved on a release PR, the heavy jobs skip instead of running. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/Semgrep.yml | 6 ++++-- .github/workflows/executable-check.yml | 3 +++ .github/workflows/lint.yml | 3 +++ .github/workflows/test.yml | 5 +++++ .github/workflows/typecheck.yml | 3 +++ .github/workflows/version-bump.yml | 8 ++++++++ .github/workflows/windows.yml | 4 ++++ 7 files changed, 30 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Semgrep.yml b/.github/workflows/Semgrep.yml index b08eda5ac..b66a987df 100644 --- a/.github/workflows/Semgrep.yml +++ b/.github/workflows/Semgrep.yml @@ -33,8 +33,10 @@ jobs: # suppressions were ignored. See https://semgrep.dev/docs CHANGELOG v1.164.0. image: semgrep/semgrep:1.164.0 - # Skip any PR created by dependabot to avoid permission issues: - if: (github.actor != 'dependabot[bot]') + # Skip any PR created by dependabot to avoid permission issues, and skip + # release ("Release X.Y.Z") version-bump PRs (the Create Release PR workflow + # posts the required checks as passing for those). + if: ${{ github.actor != 'dependabot[bot]' && !startsWith(github.head_ref, 'release/') }} steps: # Fetch project source with GitHub Actions Checkout. diff --git a/.github/workflows/executable-check.yml b/.github/workflows/executable-check.yml index c3f320eb5..ac7f61ea7 100644 --- a/.github/workflows/executable-check.yml +++ b/.github/workflows/executable-check.yml @@ -23,6 +23,9 @@ concurrency: jobs: verify: name: Build & verify executable + # Skip for release ("Release X.Y.Z") version-bump PRs; the Create Release PR + # workflow posts the required checks as passing for those. + if: ${{ !startsWith(github.head_ref, 'release/') }} runs-on: macos-latest steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e39ebe855..093e9c149 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,6 +10,9 @@ permissions: jobs: lint: name: Lint + # Skip for release ("Release X.Y.Z") version-bump PRs; the Create Release PR + # workflow posts the required checks as passing for those. + if: ${{ !startsWith(github.head_ref, 'release/') }} runs-on: ubuntu-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ada4b288c..9caae47f0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,9 @@ on: jobs: build: name: Build + # Release ("Release X.Y.Z") PRs only bump version files; CI is skipped and the + # required checks are posted as passing by the Create Release PR workflow. + if: ${{ !startsWith(github.head_ref, 'release/') }} runs-on: ubuntu-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -35,6 +38,7 @@ jobs: test: name: Test ${{ matrix.package }} + if: ${{ !startsWith(github.head_ref, 'release/') }} needs: [build] strategy: matrix: @@ -137,6 +141,7 @@ jobs: regression: name: Regression + if: ${{ !startsWith(github.head_ref, 'release/') }} needs: [build] runs-on: ubuntu-latest timeout-minutes: 15 diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 466fb94b7..9ecd32380 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -10,6 +10,9 @@ permissions: jobs: typecheck: name: Typecheck + # Skip for release ("Release X.Y.Z") version-bump PRs; the Create Release PR + # workflow posts the required checks as passing for those. + if: ${{ !startsWith(github.head_ref, 'release/') }} runs-on: ubuntu-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index feb9d19e2..a7cbf6cfd 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -97,6 +97,14 @@ jobs: uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 with: token: ${{ secrets.GITHUB_TOKEN }} + # Fully attribute the commit to the GitHub Actions bot (not the human + # who clicked "Run workflow", which is create-pull-request's default + # author). A bot-authored, GITHUB_TOKEN-pushed commit lets GitHub's + # recursion guard suppress the on:pull_request CI runs entirely, so the + # release PR never sits in the "Approve and run workflows" gate — the + # synthetic check runs posted below satisfy branch protection on their own. + author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" + committer: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" base: master branch: release/${{ steps.bump.outputs.version }} # Only commit version files — never workflow files (GITHUB_TOKEN may diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 94e057d6a..f1fea0c1c 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -7,6 +7,9 @@ on: jobs: build: name: Build + # Skip CI for release ("Release X.Y.Z") version-bump PRs; the Create Release + # PR workflow posts the required checks as passing for those. + if: ${{ !startsWith(github.head_ref, 'release/') }} runs-on: windows-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -35,6 +38,7 @@ jobs: test: name: Test ${{ matrix.package }} + if: ${{ !startsWith(github.head_ref, 'release/') }} needs: [build] strategy: fail-fast: false From 3647f9fae15e818ce97a299aa42f0f144dd9df54 Mon Sep 17 00:00:00 2001 From: rishigupta1599 Date: Wed, 24 Jun 2026 21:05:53 +0530 Subject: [PATCH 2/2] ci(release): gate release-PR CI skip on PR author, not branch name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit keyed the CI skip on `github.head_ref` (the branch name), which any PR author controls. A contributor could open a PR from a branch named `release/anything` and have every required check (Lint, Typecheck, Test, Semgrep, executable verify) skip — a required-status-check bypass. Re-gate the skip on identity that an attacker cannot forge: the PR must be opened by github-actions[bot], from this same repository (not a fork), on a release/* branch. Verified the skip fires only for the genuine automated release PR; fork PRs, same-repo non-bot PRs, and normal PRs all run CI as before. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/Semgrep.yml | 14 +++++++++---- .github/workflows/executable-check.yml | 12 +++++++++--- .github/workflows/lint.yml | 12 +++++++++--- .github/workflows/test.yml | 27 +++++++++++++++++++++----- .github/workflows/typecheck.yml | 12 +++++++++--- .github/workflows/windows.yml | 19 ++++++++++++++---- 6 files changed, 74 insertions(+), 22 deletions(-) diff --git a/.github/workflows/Semgrep.yml b/.github/workflows/Semgrep.yml index b66a987df..f31369bec 100644 --- a/.github/workflows/Semgrep.yml +++ b/.github/workflows/Semgrep.yml @@ -33,10 +33,16 @@ jobs: # suppressions were ignored. See https://semgrep.dev/docs CHANGELOG v1.164.0. image: semgrep/semgrep:1.164.0 - # Skip any PR created by dependabot to avoid permission issues, and skip - # release ("Release X.Y.Z") version-bump PRs (the Create Release PR workflow - # posts the required checks as passing for those). - if: ${{ github.actor != 'dependabot[bot]' && !startsWith(github.head_ref, 'release/') }} + # Skip any PR created by dependabot to avoid permission issues, and skip the + # automated release PR (opened by github-actions[bot], same repo, release/* + # branch — the Create Release PR workflow posts the required checks for it). + # The release gate is keyed on the PR author, not the branch name (which any + # contributor controls), so a "release/…" branch can't bypass required checks. + if: >- + ${{ github.actor != 'dependabot[bot]' + && !(github.event.pull_request.user.login == 'github-actions[bot]' + && github.event.pull_request.head.repo.full_name == github.repository + && startsWith(github.head_ref, 'release/')) }} steps: # Fetch project source with GitHub Actions Checkout. diff --git a/.github/workflows/executable-check.yml b/.github/workflows/executable-check.yml index ac7f61ea7..335905545 100644 --- a/.github/workflows/executable-check.yml +++ b/.github/workflows/executable-check.yml @@ -23,9 +23,15 @@ concurrency: jobs: verify: name: Build & verify executable - # Skip for release ("Release X.Y.Z") version-bump PRs; the Create Release PR - # workflow posts the required checks as passing for those. - if: ${{ !startsWith(github.head_ref, 'release/') }} + # Skip CI only for the automated release PR: opened by github-actions[bot], + # from this same repo, on a release/* branch (the Create Release PR workflow + # posts the required checks as passing for it). Keyed on the PR author, not the + # branch name, so a "release/…" branch can't be used to bypass required checks. + if: >- + ${{ !(github.event_name == 'pull_request' + && github.event.pull_request.user.login == 'github-actions[bot]' + && github.event.pull_request.head.repo.full_name == github.repository + && startsWith(github.head_ref, 'release/')) }} runs-on: macos-latest steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 093e9c149..c86ab011a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,9 +10,15 @@ permissions: jobs: lint: name: Lint - # Skip for release ("Release X.Y.Z") version-bump PRs; the Create Release PR - # workflow posts the required checks as passing for those. - if: ${{ !startsWith(github.head_ref, 'release/') }} + # Skip CI only for the automated release PR: opened by github-actions[bot], + # from this same repo, on a release/* branch (the Create Release PR workflow + # posts the required checks as passing for it). Keyed on the PR author, not the + # branch name, so a "release/…" branch can't be used to bypass required checks. + if: >- + ${{ !(github.event_name == 'pull_request' + && github.event.pull_request.user.login == 'github-actions[bot]' + && github.event.pull_request.head.repo.full_name == github.repository + && startsWith(github.head_ref, 'release/')) }} runs-on: ubuntu-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9caae47f0..d83506518 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,9 +7,16 @@ on: jobs: build: name: Build - # Release ("Release X.Y.Z") PRs only bump version files; CI is skipped and the - # required checks are posted as passing by the Create Release PR workflow. - if: ${{ !startsWith(github.head_ref, 'release/') }} + # Skip CI only for the automated release PR: opened by github-actions[bot], + # from this same repo, on a release/* branch (the Create Release PR workflow + # posts the required checks as passing for it). The gate is keyed on the PR + # author — NOT the branch name, which any contributor controls — so a PR from + # a "release/…" branch can't be used to bypass required checks. + if: >- + ${{ !(github.event_name == 'pull_request' + && github.event.pull_request.user.login == 'github-actions[bot]' + && github.event.pull_request.head.repo.full_name == github.repository + && startsWith(github.head_ref, 'release/')) }} runs-on: ubuntu-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -38,7 +45,12 @@ jobs: test: name: Test ${{ matrix.package }} - if: ${{ !startsWith(github.head_ref, 'release/') }} + # Skip only for the automated release PR (see the build job for rationale). + if: >- + ${{ !(github.event_name == 'pull_request' + && github.event.pull_request.user.login == 'github-actions[bot]' + && github.event.pull_request.head.repo.full_name == github.repository + && startsWith(github.head_ref, 'release/')) }} needs: [build] strategy: matrix: @@ -141,7 +153,12 @@ jobs: regression: name: Regression - if: ${{ !startsWith(github.head_ref, 'release/') }} + # Skip only for the automated release PR (see the build job for rationale). + if: >- + ${{ !(github.event_name == 'pull_request' + && github.event.pull_request.user.login == 'github-actions[bot]' + && github.event.pull_request.head.repo.full_name == github.repository + && startsWith(github.head_ref, 'release/')) }} needs: [build] runs-on: ubuntu-latest timeout-minutes: 15 diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 9ecd32380..28284162e 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -10,9 +10,15 @@ permissions: jobs: typecheck: name: Typecheck - # Skip for release ("Release X.Y.Z") version-bump PRs; the Create Release PR - # workflow posts the required checks as passing for those. - if: ${{ !startsWith(github.head_ref, 'release/') }} + # Skip CI only for the automated release PR: opened by github-actions[bot], + # from this same repo, on a release/* branch (the Create Release PR workflow + # posts the required checks as passing for it). Keyed on the PR author, not the + # branch name, so a "release/…" branch can't be used to bypass required checks. + if: >- + ${{ !(github.event_name == 'pull_request' + && github.event.pull_request.user.login == 'github-actions[bot]' + && github.event.pull_request.head.repo.full_name == github.repository + && startsWith(github.head_ref, 'release/')) }} runs-on: ubuntu-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index f1fea0c1c..6ab2edb02 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -7,9 +7,15 @@ on: jobs: build: name: Build - # Skip CI for release ("Release X.Y.Z") version-bump PRs; the Create Release - # PR workflow posts the required checks as passing for those. - if: ${{ !startsWith(github.head_ref, 'release/') }} + # Skip CI only for the automated release PR: opened by github-actions[bot], + # from this same repo, on a release/* branch (the Create Release PR workflow + # posts the required checks as passing for it). Keyed on the PR author, not the + # branch name, so a "release/…" branch can't be used to bypass required checks. + if: >- + ${{ !(github.event_name == 'pull_request' + && github.event.pull_request.user.login == 'github-actions[bot]' + && github.event.pull_request.head.repo.full_name == github.repository + && startsWith(github.head_ref, 'release/')) }} runs-on: windows-latest steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -38,7 +44,12 @@ jobs: test: name: Test ${{ matrix.package }} - if: ${{ !startsWith(github.head_ref, 'release/') }} + # Skip only for the automated release PR (see the build job for rationale). + if: >- + ${{ !(github.event_name == 'pull_request' + && github.event.pull_request.user.login == 'github-actions[bot]' + && github.event.pull_request.head.repo.full_name == github.repository + && startsWith(github.head_ref, 'release/')) }} needs: [build] strategy: fail-fast: false