From ab409428bb2ce8419a531fcf6fa74a378053cf8b Mon Sep 17 00:00:00 2001 From: Akash Sinha Date: Mon, 15 Jun 2026 18:28:16 +0530 Subject: [PATCH 1/4] ci: skip heavy test suites on version-only version-bump PRs (PER-9560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated version-bump PRs (opened by version-bump.yml from `release/` branches) only touch lerna.json + package.json and have no code impact, yet they block ~1h on the full Linux + Windows test matrices. Skip those suites for these PRs. Skip only when BOTH hold: the head branch is `release/*` AND the PR diff is confined to the version files version-bump.yml is allowed to commit (lerna.json + packages/**/package.json), computed by a `changes` job via dorny/paths-filter (predicate-quantifier: every). So a `release/*` PR that touches source still runs the full suite — the skip can't be abused to land untested code behind a green-looking "skipped" check. - Gate `build`, `test`, `regression` in test.yml and `build` + `test` in windows.yml on `!(startsWith(github.head_ref,'release/') && needs.changes.outputs.version_only=='true')`. - Branch pattern is `release/*` (slash) — the convention set by version-bump.yml — not `release-*` (hyphen) as the ticket guessed. - Done as a job-level `if`, not an `on:` branch filter: a skipped job posts a "skipped" check that satisfies required status checks, whereas an `on:`-level skip leaves required checks "pending" and would block the release PR from merging. - lint, typecheck, and Semgrep (all ~1m) keep running on release PRs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/test.yml | 44 +++++++++++++++++++++++++++++++++-- .github/workflows/windows.yml | 32 ++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3045d61cc..1b77dfc6c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,8 +5,44 @@ on: pull_request: workflow_dispatch: jobs: + # Detect whether a PR's diff is confined to the version files that version-bump.yml + # is allowed to commit (lerna.json + packages/**/package.json). Combined with the + # `release/` branch check on the heavy jobs below, this lets us skip the ~1h matrix + # for genuine version-bump PRs while still running it for any `release/*` PR that + # touches source — so the skip can't be abused to land untested code behind a + # green-looking "skipped" check (PER-9560). + changes: + name: Detect version-only changes + runs-on: ubuntu-latest + outputs: + version_only: ${{ steps.filter.outputs.version_only }} + steps: + - uses: actions/checkout@v5 + - uses: dorny/paths-filter@v3 + id: filter + # Only meaningful for PRs; on push/workflow_dispatch the step is skipped, the + # output is empty, and the branch check below already evaluates to "run". + if: github.event_name == 'pull_request' + with: + # `every`: version_only is true ONLY when every changed file matches a pattern. + predicate-quantifier: every + filters: | + version_only: + - 'lerna.json' + - 'packages/**/package.json' + build: name: Build + # Skip the ~1h test matrix on automated version-bump PRs. version-bump.yml opens + # these from `release/` branches and commits only lerna.json + + # packages/**/package.json, so there's nothing to test. We skip only when BOTH hold + # — branch is `release/*` AND the diff is version-only — so a `release/*` PR that + # touches source still runs the full suite (PER-9560). Implemented as a job-level + # `if` (not an `on:` branch filter) so the check still reports as "skipped" — which + # satisfies a required status check — instead of staying "pending" and blocking the + # release PR from merging. + needs: [changes] + if: ${{ !(startsWith(github.head_ref, 'release/') && needs.changes.outputs.version_only == 'true') }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -35,7 +71,9 @@ jobs: test: name: Test ${{ matrix.package }} - needs: [build] + needs: [build, changes] + # Skip on version-only version-bump PRs — see the build job and PER-9560. + if: ${{ !(startsWith(github.head_ref, 'release/') && needs.changes.outputs.version_only == 'true') }} strategy: matrix: os: [ubuntu-latest] @@ -137,7 +175,9 @@ jobs: regression: name: Regression - needs: [build] + needs: [build, changes] + # Skip on version-only version-bump PRs — see the build job and PER-9560. + if: ${{ !(startsWith(github.head_ref, 'release/') && needs.changes.outputs.version_only == 'true') }} runs-on: ubuntu-latest timeout-minutes: 15 steps: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 8aa1ba4c2..fbbcdfb9f 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -5,8 +5,36 @@ on: pull_request: workflow_dispatch: jobs: + # See test.yml for the rationale. Detects whether a PR's diff is confined to the + # version files version-bump.yml is allowed to commit, so the skip below can't be + # abused to land untested source behind a green-looking "skipped" check (PER-9560). + changes: + name: Detect version-only changes + runs-on: ubuntu-latest + outputs: + version_only: ${{ steps.filter.outputs.version_only }} + steps: + - uses: actions/checkout@v5 + - uses: dorny/paths-filter@v3 + id: filter + if: github.event_name == 'pull_request' + with: + # `every`: version_only is true ONLY when every changed file matches a pattern. + predicate-quantifier: every + filters: | + version_only: + - 'lerna.json' + - 'packages/**/package.json' + build: name: Build + # Skip the ~60m Windows matrix on version-only version-bump PRs (branch `release/*` + # AND a diff confined to lerna.json + packages/**/package.json). A `release/*` PR that + # touches source still runs the full suite (PER-9560). Job-level `if` (not an `on:` + # branch filter) so the check posts a "skipped" result that satisfies required status + # checks, instead of staying "pending" and blocking the release PR. + needs: [changes] + if: ${{ !(startsWith(github.head_ref, 'release/') && needs.changes.outputs.version_only == 'true') }} runs-on: windows-latest steps: - uses: actions/checkout@v5 @@ -35,7 +63,9 @@ jobs: test: name: Test ${{ matrix.package }} - needs: [build] + needs: [build, changes] + # Skip on version-only version-bump PRs — see the build job and PER-9560. + if: ${{ !(startsWith(github.head_ref, 'release/') && needs.changes.outputs.version_only == 'true') }} strategy: fail-fast: false matrix: From 927e16b2560df6c465e67006d93b70b84fe09928 Mon Sep 17 00:00:00 2001 From: Akash Sinha Date: Mon, 15 Jun 2026 21:00:34 +0530 Subject: [PATCH 2/4] ci: gate test skip on github-actions[bot] author; drop paths-filter (PER-9560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refines the version-bump test skip from the earlier version-only diff approach to a check on the PR author. Skipping now requires the head branch to be `release/*` AND the PR to be opened by `github-actions[bot]`. The bot-identity check is native to GitHub — no extra `changes` job that could fail and silently skip tests — and fully closes the "a human names a branch `release/*` to dodge CI" hole; only the release bot's PRs skip. - Replace the per-job `if` with `!(startsWith(github.head_ref,'release/') && github.event.pull_request.user.login=='github-actions[bot]')` on build/test/regression (test.yml) and build/test (windows.yml). - Drop the `changes` job and the `dorny/paths-filter` dependency. - Keep a least-privilege `permissions: contents: read` block on both workflows (no longer need `pull-requests: read`). - Remove the explanatory comments per review. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/test.yml | 51 +++++++---------------------------- .github/workflows/windows.yml | 38 +++++--------------------- 2 files changed, 16 insertions(+), 73 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1b77dfc6c..c70db8d77 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,45 +4,14 @@ on: branches: [master] pull_request: workflow_dispatch: -jobs: - # Detect whether a PR's diff is confined to the version files that version-bump.yml - # is allowed to commit (lerna.json + packages/**/package.json). Combined with the - # `release/` branch check on the heavy jobs below, this lets us skip the ~1h matrix - # for genuine version-bump PRs while still running it for any `release/*` PR that - # touches source — so the skip can't be abused to land untested code behind a - # green-looking "skipped" check (PER-9560). - changes: - name: Detect version-only changes - runs-on: ubuntu-latest - outputs: - version_only: ${{ steps.filter.outputs.version_only }} - steps: - - uses: actions/checkout@v5 - - uses: dorny/paths-filter@v3 - id: filter - # Only meaningful for PRs; on push/workflow_dispatch the step is skipped, the - # output is empty, and the branch check below already evaluates to "run". - if: github.event_name == 'pull_request' - with: - # `every`: version_only is true ONLY when every changed file matches a pattern. - predicate-quantifier: every - filters: | - version_only: - - 'lerna.json' - - 'packages/**/package.json' +permissions: + contents: read + +jobs: build: name: Build - # Skip the ~1h test matrix on automated version-bump PRs. version-bump.yml opens - # these from `release/` branches and commits only lerna.json + - # packages/**/package.json, so there's nothing to test. We skip only when BOTH hold - # — branch is `release/*` AND the diff is version-only — so a `release/*` PR that - # touches source still runs the full suite (PER-9560). Implemented as a job-level - # `if` (not an `on:` branch filter) so the check still reports as "skipped" — which - # satisfies a required status check — instead of staying "pending" and blocking the - # release PR from merging. - needs: [changes] - if: ${{ !(startsWith(github.head_ref, 'release/') && needs.changes.outputs.version_only == 'true') }} + if: ${{ !(startsWith(github.head_ref, 'release/') && github.event.pull_request.user.login == 'github-actions[bot]') }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -71,9 +40,8 @@ jobs: test: name: Test ${{ matrix.package }} - needs: [build, changes] - # Skip on version-only version-bump PRs — see the build job and PER-9560. - if: ${{ !(startsWith(github.head_ref, 'release/') && needs.changes.outputs.version_only == 'true') }} + needs: [build] + if: ${{ !(startsWith(github.head_ref, 'release/') && github.event.pull_request.user.login == 'github-actions[bot]') }} strategy: matrix: os: [ubuntu-latest] @@ -175,9 +143,8 @@ jobs: regression: name: Regression - needs: [build, changes] - # Skip on version-only version-bump PRs — see the build job and PER-9560. - if: ${{ !(startsWith(github.head_ref, 'release/') && needs.changes.outputs.version_only == 'true') }} + needs: [build] + if: ${{ !(startsWith(github.head_ref, 'release/') && github.event.pull_request.user.login == 'github-actions[bot]') }} runs-on: ubuntu-latest timeout-minutes: 15 steps: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index fbbcdfb9f..c918f8fa3 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -4,37 +4,14 @@ on: branches: [master] pull_request: workflow_dispatch: -jobs: - # See test.yml for the rationale. Detects whether a PR's diff is confined to the - # version files version-bump.yml is allowed to commit, so the skip below can't be - # abused to land untested source behind a green-looking "skipped" check (PER-9560). - changes: - name: Detect version-only changes - runs-on: ubuntu-latest - outputs: - version_only: ${{ steps.filter.outputs.version_only }} - steps: - - uses: actions/checkout@v5 - - uses: dorny/paths-filter@v3 - id: filter - if: github.event_name == 'pull_request' - with: - # `every`: version_only is true ONLY when every changed file matches a pattern. - predicate-quantifier: every - filters: | - version_only: - - 'lerna.json' - - 'packages/**/package.json' +permissions: + contents: read + +jobs: build: name: Build - # Skip the ~60m Windows matrix on version-only version-bump PRs (branch `release/*` - # AND a diff confined to lerna.json + packages/**/package.json). A `release/*` PR that - # touches source still runs the full suite (PER-9560). Job-level `if` (not an `on:` - # branch filter) so the check posts a "skipped" result that satisfies required status - # checks, instead of staying "pending" and blocking the release PR. - needs: [changes] - if: ${{ !(startsWith(github.head_ref, 'release/') && needs.changes.outputs.version_only == 'true') }} + if: ${{ !(startsWith(github.head_ref, 'release/') && github.event.pull_request.user.login == 'github-actions[bot]') }} runs-on: windows-latest steps: - uses: actions/checkout@v5 @@ -63,9 +40,8 @@ jobs: test: name: Test ${{ matrix.package }} - needs: [build, changes] - # Skip on version-only version-bump PRs — see the build job and PER-9560. - if: ${{ !(startsWith(github.head_ref, 'release/') && needs.changes.outputs.version_only == 'true') }} + needs: [build] + if: ${{ !(startsWith(github.head_ref, 'release/') && github.event.pull_request.user.login == 'github-actions[bot]') }} strategy: fail-fast: false matrix: From e52547b63a042d3b426d05553e4212703d5206db Mon Sep 17 00:00:00 2001 From: Akash Sinha Date: Mon, 15 Jun 2026 21:10:03 +0530 Subject: [PATCH 3/4] ci: also require version-only diff to skip tests (PER-9560) Author + branch alone is not a safe skip gate: github.event.pull_request .user.login is fixed at PR creation, so commits pushed to a release/* branch *after* the bot opens the PR keep the same author and branch and would skip CI on untested source. Re-add the `changes` job (dorny/paths-filter, SHA-pinned to v3.0.3) and require version_only on every heavy job, so the skip now needs all three: branch is release/* AND author is github-actions[bot] AND the diff is confined to lerna.json + packages/**/package.json. paths-filter re-evaluates the whole PR diff on each push, so any non-version file flips version_only to false and tests run again. - Restore `permissions: pull-requests: read` (paths-filter reads PR files). - build needs [changes]; test/regression need [build, changes]. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/test.yml | 29 ++++++++++++++++++++++++----- .github/workflows/windows.yml | 25 ++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c70db8d77..6db6c36ad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,11 +7,30 @@ on: permissions: contents: read + pull-requests: read jobs: + changes: + name: Detect version-only changes + runs-on: ubuntu-latest + outputs: + version_only: ${{ steps.filter.outputs.version_only }} + steps: + - uses: actions/checkout@v5 + - uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3.0.3 + id: filter + if: github.event_name == 'pull_request' + with: + predicate-quantifier: every + filters: | + version_only: + - 'lerna.json' + - 'packages/**/package.json' + build: name: Build - if: ${{ !(startsWith(github.head_ref, 'release/') && github.event.pull_request.user.login == 'github-actions[bot]') }} + needs: [changes] + if: ${{ !(startsWith(github.head_ref, 'release/') && github.event.pull_request.user.login == 'github-actions[bot]' && needs.changes.outputs.version_only == 'true') }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -40,8 +59,8 @@ jobs: test: name: Test ${{ matrix.package }} - needs: [build] - if: ${{ !(startsWith(github.head_ref, 'release/') && github.event.pull_request.user.login == 'github-actions[bot]') }} + needs: [build, changes] + if: ${{ !(startsWith(github.head_ref, 'release/') && github.event.pull_request.user.login == 'github-actions[bot]' && needs.changes.outputs.version_only == 'true') }} strategy: matrix: os: [ubuntu-latest] @@ -143,8 +162,8 @@ jobs: regression: name: Regression - needs: [build] - if: ${{ !(startsWith(github.head_ref, 'release/') && github.event.pull_request.user.login == 'github-actions[bot]') }} + needs: [build, changes] + if: ${{ !(startsWith(github.head_ref, 'release/') && github.event.pull_request.user.login == 'github-actions[bot]' && needs.changes.outputs.version_only == 'true') }} runs-on: ubuntu-latest timeout-minutes: 15 steps: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index c918f8fa3..60768ca94 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -7,11 +7,30 @@ on: permissions: contents: read + pull-requests: read jobs: + changes: + name: Detect version-only changes + runs-on: ubuntu-latest + outputs: + version_only: ${{ steps.filter.outputs.version_only }} + steps: + - uses: actions/checkout@v5 + - uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3.0.3 + id: filter + if: github.event_name == 'pull_request' + with: + predicate-quantifier: every + filters: | + version_only: + - 'lerna.json' + - 'packages/**/package.json' + build: name: Build - if: ${{ !(startsWith(github.head_ref, 'release/') && github.event.pull_request.user.login == 'github-actions[bot]') }} + needs: [changes] + if: ${{ !(startsWith(github.head_ref, 'release/') && github.event.pull_request.user.login == 'github-actions[bot]' && needs.changes.outputs.version_only == 'true') }} runs-on: windows-latest steps: - uses: actions/checkout@v5 @@ -40,8 +59,8 @@ jobs: test: name: Test ${{ matrix.package }} - needs: [build] - if: ${{ !(startsWith(github.head_ref, 'release/') && github.event.pull_request.user.login == 'github-actions[bot]') }} + needs: [build, changes] + if: ${{ !(startsWith(github.head_ref, 'release/') && github.event.pull_request.user.login == 'github-actions[bot]' && needs.changes.outputs.version_only == 'true') }} strategy: fail-fast: false matrix: From 6a84443dfb149b7c025a7161bd80ce2865783b52 Mon Sep 17 00:00:00 2001 From: Akash Sinha Date: Tue, 16 Jun 2026 02:04:37 +0530 Subject: [PATCH 4/4] =?UTF-8?q?ci:=20fix=20version-only=20check=20?= =?UTF-8?q?=E2=80=94=20list=20PR=20files=20instead=20of=20paths-filter=20(?= =?UTF-8?q?PER-9560)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dorny/paths-filter gate never matched: predicate-quantifier 'every' means a file must match *every* pattern, so no file could be both lerna.json AND packages/**/package.json -> version_only was always false -> the skip never fired. Replace it with an explicit check that lists the PR's changed files via `gh api .../pulls/N/files` and confirms every one is lerna.json or a top-level packages//package.json. Fail-safe: any API error or a non-version file -> version_only=false -> full CI runs. Also drops the third-party action (and its SHA pin / Node-20 deprecation). Verified end-to-end on a fork of percy/cli: - bot version-bump PR (release/*, version-only diff) -> Build/Test/ Regression (Linux) and Build/Test (Windows) reported "skipped". - normal PR (non-version file) -> heavy jobs ran. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/test.yml | 28 ++++++++++++++++++++-------- .github/workflows/windows.yml | 28 ++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6db6c36ad..e64b638a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,16 +16,28 @@ jobs: outputs: version_only: ${{ steps.filter.outputs.version_only }} steps: - - uses: actions/checkout@v5 - - uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3.0.3 + - name: Check the PR changes only version files id: filter if: github.event_name == 'pull_request' - with: - predicate-quantifier: every - filters: | - version_only: - - 'lerna.json' - - 'packages/**/package.json' + env: + GH_TOKEN: ${{ github.token }} + PR: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + run: | + if ! files=$(gh api "repos/$REPO/pulls/$PR/files" --paginate --jq '.[].filename'); then + echo "Could not list PR files — running full CI to be safe." + echo "version_only=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + printf 'Changed files:\n%s\n' "$files" + # version_only=true only if EVERY changed file is lerna.json or a top-level + # packages//package.json (exactly what version-bump.yml commits). + others=$(printf '%s\n' "$files" | grep -vE '^(lerna\.json|packages/[^/]+/package\.json)$' || true) + if [ -n "$files" ] && [ -z "$others" ]; then + echo "version_only=true" >> "$GITHUB_OUTPUT" + else + echo "version_only=false" >> "$GITHUB_OUTPUT" + fi build: name: Build diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 60768ca94..026ed0e30 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -16,16 +16,28 @@ jobs: outputs: version_only: ${{ steps.filter.outputs.version_only }} steps: - - uses: actions/checkout@v5 - - uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3.0.3 + - name: Check the PR changes only version files id: filter if: github.event_name == 'pull_request' - with: - predicate-quantifier: every - filters: | - version_only: - - 'lerna.json' - - 'packages/**/package.json' + env: + GH_TOKEN: ${{ github.token }} + PR: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + run: | + if ! files=$(gh api "repos/$REPO/pulls/$PR/files" --paginate --jq '.[].filename'); then + echo "Could not list PR files — running full CI to be safe." + echo "version_only=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + printf 'Changed files:\n%s\n' "$files" + # version_only=true only if EVERY changed file is lerna.json or a top-level + # packages//package.json (exactly what version-bump.yml commits). + others=$(printf '%s\n' "$files" | grep -vE '^(lerna\.json|packages/[^/]+/package\.json)$' || true) + if [ -n "$files" ] && [ -z "$others" ]; then + echo "version_only=true" >> "$GITHUB_OUTPUT" + else + echo "version_only=false" >> "$GITHUB_OUTPUT" + fi build: name: Build