Skip to content

chore: optimize CI#8199

Merged
eduardoboucas merged 5 commits intomainfrom
chore/optimise-ci
Apr 23, 2026
Merged

chore: optimize CI#8199
eduardoboucas merged 5 commits intomainfrom
chore/optimise-ci

Conversation

@eduardoboucas
Copy link
Copy Markdown
Member

Currently, shipping a single feature ends up triggering the full cross-platform test matrix roughly four times — once on the feature PR, once on the main merge, once on the release-please PR, and once more on the release merge.

That's ~200+ jobs per released feature, most of them duplicating work that had already passed on a parent commit.

Combined with the slower and flakier Windows runners, the feedback loop on PRs had grown long and noisy, and the release workflow was spending a lot of CI time re-verifying code that was already green.

This PR restructures when and where tests run across the development lifecycle so that we only pay for the coverage that actually tells us something new.

Problems this fixes

  • Redundant test runs across the release lifecycle. The same commit was being tested on the PR, on the main push after merge, on the release PR, on the release merge, and again inside the publish job. We now test each commit at most twice (feature PR + release PR), and the publish job trusts the green release PR instead of re-running E2E.
  • Slow feedback on feature PRs. Every PR had to wait for the full 3 OS × 3 Node matrix across unit, integration (sharded 4 ways), and E2E — while most issues are catchable on Linux alone. Feature PRs now run Linux-only, trading a small amount of platform coverage at PR time for dramatically faster iteration.
  • Flaky Windows E2E gating every PR. Windows-specific flakiness was a common reason PRs got stuck. Windows (and macOS) now only run on release PRs and on the nightly schedule — they're still part of our safety net, just moved to where their cost is acceptable.
  • A broken release-PR filter. The existing !release-please--** filter in the test workflows was targeting the PR base branch and therefore never actually skipped anything. Fixed by detecting release-please PRs via github.head_ref and using that to switch matrix strategies.
  • Docs-only PRs running the full test suite. Added a paths-filter step so changes that only touch markdown, LICENSE, and template files skip the test jobs entirely.
  • Hardcoded Node versions scattered across eleven workflow files. Bumping Node versions used to mean edits in many places with inconsistencies creeping in (e.g. 22.x vs 22). Consolidated into a single config file (see below).

When each workflow now runs

Unit, Integration, E2E tests

Trigger Matrix
Feature PR Ubuntu × 3 Node versions
Release-please PR Ubuntu × 3 Node versions + macOS/Windows on primary Node
Nightly schedule (E2E only) Full matrix (same as release PR)
Push to main Not run — the PR already tested this commit
Publish job Not run — the release PR already tested this commit

If a PR only changes documentation (markdown, LICENSE, issue/PR templates, Vale config), the test jobs are skipped and a lightweight status check passes, so required checks still resolve.

Release pipeline

  • Feature PR merges to main → no test run. Release-please opens or updates the release PR.
  • Release-please PR → runs the full cross-platform matrix as the last line of defense.
  • Release-please PR merges → publish job builds and publishes without re-running E2E. It trusts the release PR's checks.

Single-version workflows (lint, typecheck, format, verify-docs, benchmark, pre-release, release publish)

Same triggers as before. Each now reads its Node version from the shared config rather than hardcoding '24'.

Practical implications

  • Faster PR feedback. Feature PRs now run roughly one-third of the jobs they used to. The mean wait-for-CI should drop significantly, and Windows flakes no longer block unrelated work.
  • A new tradeoff. Because Windows/macOS aren't tested on every feature PR, a platform-specific regression can slip through to the release PR. The expectation is that this is rare and cheap to fix — but the first few times it happens, the follow-up is a small PR rather than a blocked release.
  • Release PR is now genuinely the final gate. If its checks are green, the publish job will ship without re-testing. Do not merge the release PR if anything in that matrix is failing or was manually bypassed.

Centralized source of truth: .github/test-matrix.yml

All Node versions and cross-platform OS lists are now declared once, in a short YAML config file at .github/test-matrix.yml. Each key has a comment explaining what it drives and which workflows consume it. The three fields are:

  • node_versions — the list of Node versions that run on Linux in every PR.
  • extra_oses_on_release — the additional operating systems added on release PRs and on the nightly E2E schedule.
  • primary_node_version — the single "primary" Node version used both for the extra OSes in the release matrix and as the pinned version in every single-version workflow (lint, typecheck, publish, etc.).

Each workflow reads this file at run time via yq (pre-installed on GitHub-hosted Ubuntu runners). To add or drop a Node version, or to rotate the primary version, edit this one file — no workflow changes required.

@eduardoboucas eduardoboucas requested a review from a team as a code owner April 23, 2026 10:51
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Summary by CodeRabbit

  • Chores
    • Centralized CI config for Node.js versions and OS coverage.
    • Workflows now read the primary Node version dynamically instead of using fixed values.
    • Test workflows (unit, integration, E2E, benchmark, lint, format, typecheck) run conditionally based on code-change detection and use computed test matrices.
    • Consolidated result reporting with new status/aggregation steps and simplified triggers (fewer push events).
    • Streamlined release flow by removing unnecessary test/setup steps.

Walkthrough

Adds a new CI config file .github/test-matrix.yml to centralize Node.js versions, a primary Node version, and extra OS targets. Many GitHub Actions workflows (benchmark, format, lint, pre-release, pre-release-sha, release-please, typecheck, verify-docs) now read the primary Node version from that file. The e2e-tests, integration-tests, and unit-tests workflows were reworked: triggers simplified to pull_request/schedule, new setup jobs detect code-relevant changes and emit dynamic matrices, test jobs are gated on those results, and status jobs consolidate failures.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'chore: optimize CI' is too vague and does not convey the significant restructuring and optimization changes made across the CI workflows. Consider a more descriptive title such as 'chore: centralize CI configuration and optimize test matrix' to better reflect the scope of changes.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description is comprehensive and directly related to the changeset, providing clear context about the problems being fixed and the new CI structure.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/optimise-ci

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 23, 2026

📊 Benchmark results

Comparing with 901060d

  • Dependency count: 1,061 (no change)
  • Package size: 355 MB (no change)
  • Number of ts-expect-error directives: 356 (no change)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/integration-tests.yml (1)

104-106: ⚠️ Potential issue | 🟡 Minor

Remove dead reference to non-existent release-check step.

Line 106 references steps.release-check.outputs.IS_RELEASE, but there is no release-check step defined in this workflow or any other workflow file. The condition will always evaluate to truthy, meaning the build step always runs. Remove the dead condition.

Proposed fix
       - name: Build project
         run: npm run build
-        if: '${{!steps.release-check.outputs.IS_RELEASE}}'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/integration-tests.yml around lines 104 - 106, The "Build
project" step references a non-existent step output
steps.release-check.outputs.IS_RELEASE, so remove the dead condition from the
"Build project" step (the step named "Build project" that runs npm run build) —
simply delete the if: '${{!steps.release-check.outputs.IS_RELEASE}}' line so the
step no longer depends on the missing release-check output; if you intended to
gate the build, replace the condition with a valid existing step output or
create a proper release-check step that emits IS_RELEASE.
🧹 Nitpick comments (2)
.github/workflows/typecheck.yml (1)

19-26: Consider extracting the repeated “read primary Node version” step into a reusable unit.

This exact snippet now exists across many workflows; a composite action (or reusable workflow) would reduce copy/paste maintenance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/typecheck.yml around lines 19 - 26, Extract the duplicated
"Read primary Node version" step (the job step with name "Read primary Node
version" and id "node" that sets GITHUB_OUTPUT from yq) into a reusable unit
(either a composite action or a reusable workflow) and replace in this workflow
by calling that unit; ensure the composite exposes the same output name
"version" so the downstream step that runs "uses: actions/setup-node@v6" can
continue to reference ${{ steps.node.outputs.version }} (or update callers to
reference the composite/workflow output if you change the output name).
.github/workflows/e2e-tests.yml (1)

92-101: Divergent pnpm setup across platforms is reasonable.

Using corepack enable on Windows and pnpm/action-setup@v4 elsewhere is a valid approach. Consider adding a brief comment explaining why the split exists (e.g., corepack reliability differences) for future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/e2e-tests.yml around lines 92 - 101, Add a short
explanatory comment above the two GitHub Actions steps to clarify why we enable
corepack on Windows but use pnpm/action-setup@v4 on non-Windows runners:
reference the "Enable corepack (Windows)" job step that runs corepack enable
when matrix.os == 'windows-2025' and the "Setup pnpm (non-Windows)" step that
uses pnpm/action-setup@v4 when matrix.os != 'windows-2025'; the comment should
note the reliability/version-management rationale (e.g., corepack preferred on
Windows for bundled package manager, action-setup used elsewhere for explicit
pnpm version control) so future maintainers understand the platform split.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/e2e-tests.yml:
- Around line 48-52: The script directly interpolates the user-controlled
expression ${{ github.head_ref }} into the shell, which is a script-injection
risk; change the workflow to pass github.head_ref into the runner as a dedicated
environment variable (e.g., HEAD_REF: ${{ github.head_ref }}) and update the
shell block to test the env var (use quoted "$HEAD_REF") instead of embedding
the GitHub expression; specifically, replace occurrences of github.head_ref in
the conditional with the env var and use safe quoted comparisons (e.g., [[
"${GITHUB_EVENT_NAME}" == "schedule" ]] || [[ "$HEAD_REF" == release-please--*
]]) while preserving population of OS_NODE from extra_oses and primary.

In @.github/workflows/integration-tests.yml:
- Around line 45-49: The workflow reads the user-controlled github.head_ref
directly in a shell conditional which risks script injection; change the
job/step to pass github.head_ref into the shell via an environment variable
(e.g., HEAD_REF) and then use that env var in the conditional instead of ${{
github.head_ref }}; update the block that examines extra_oses/OS_NODE/primary so
it references the env var (HEAD_REF) in the [[ "$HEAD_REF" == release-please--*
]] check and ensure any downstream uses of that value also read the sanitized
env var.

In @.github/workflows/pre-release-sha.yml:
- Around line 23-25: The workflow step named "Read primary Node version"
currently unconditionally runs yq against .github/test-matrix.yml which will
fail for historical SHAs that don't contain that file; modify that step's run so
it first checks for the file's existence (test -f .github/test-matrix.yml) and
only runs yq if present, otherwise emit a safe fallback like echo "version=" (or
a default version) to GITHUB_OUTPUT and optionally a warning; ensure the same
conditional/fallback logic is applied to any other step that reads the same file
(e.g., the subsequent "setup-node" usage), so missing .github/test-matrix.yml
does not break the prerelease workflow for older commits.

In @.github/workflows/unit-tests.yml:
- Around line 45-49: The workflow currently interpolates the attacker-controlled
expression "${{ github.head_ref }}" directly into the shell causing a
command-injection risk; change it to read the value from a safe environment
variable (e.g. export HEAD_REF="${{ github.head_ref }}") and then use the quoted
variable in the conditional and loop (if [[ "$HEAD_REF" == release-please--* ]];
then ...) so the shell does not re-evaluate metacharacters; update the block
referencing OS_NODE and extra_oses to use the quoted "$HEAD_REF" and ensure all
variable expansions are quoted to prevent word-splitting or injection.

---

Outside diff comments:
In @.github/workflows/integration-tests.yml:
- Around line 104-106: The "Build project" step references a non-existent step
output steps.release-check.outputs.IS_RELEASE, so remove the dead condition from
the "Build project" step (the step named "Build project" that runs npm run
build) — simply delete the if: '${{!steps.release-check.outputs.IS_RELEASE}}'
line so the step no longer depends on the missing release-check output; if you
intended to gate the build, replace the condition with a valid existing step
output or create a proper release-check step that emits IS_RELEASE.

---

Nitpick comments:
In @.github/workflows/e2e-tests.yml:
- Around line 92-101: Add a short explanatory comment above the two GitHub
Actions steps to clarify why we enable corepack on Windows but use
pnpm/action-setup@v4 on non-Windows runners: reference the "Enable corepack
(Windows)" job step that runs corepack enable when matrix.os == 'windows-2025'
and the "Setup pnpm (non-Windows)" step that uses pnpm/action-setup@v4 when
matrix.os != 'windows-2025'; the comment should note the
reliability/version-management rationale (e.g., corepack preferred on Windows
for bundled package manager, action-setup used elsewhere for explicit pnpm
version control) so future maintainers understand the platform split.

In @.github/workflows/typecheck.yml:
- Around line 19-26: Extract the duplicated "Read primary Node version" step
(the job step with name "Read primary Node version" and id "node" that sets
GITHUB_OUTPUT from yq) into a reusable unit (either a composite action or a
reusable workflow) and replace in this workflow by calling that unit; ensure the
composite exposes the same output name "version" so the downstream step that
runs "uses: actions/setup-node@v6" can continue to reference ${{
steps.node.outputs.version }} (or update callers to reference the
composite/workflow output if you change the output name).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 623c0bee-8e1b-4800-ba71-75fc4a2cad3f

📥 Commits

Reviewing files that changed from the base of the PR and between 0bc12bd and 826e098.

📒 Files selected for processing (12)
  • .github/test-matrix.yml
  • .github/workflows/benchmark.yml
  • .github/workflows/e2e-tests.yml
  • .github/workflows/format.yml
  • .github/workflows/integration-tests.yml
  • .github/workflows/lint.yml
  • .github/workflows/pre-release-sha.yml
  • .github/workflows/pre-release.yml
  • .github/workflows/release-please.yml
  • .github/workflows/typecheck.yml
  • .github/workflows/unit-tests.yml
  • .github/workflows/verify-docs.yml

Comment thread .github/workflows/e2e-tests.yml Outdated
Comment thread .github/workflows/integration-tests.yml Outdated
Comment on lines +23 to +25
- name: Read primary Node version
id: node
run: echo "version=$(yq eval '.primary_node_version' .github/test-matrix.yml)" >> "$GITHUB_OUTPUT"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify whether commits before matrix-file introduction exist (and would fail current logic)
intro_commit=$(git log --diff-filter=A --format=%H -- .github/test-matrix.yml | head -n1 || true)

if [[ -z "${intro_commit}" ]]; then
  echo "No introduction commit for .github/test-matrix.yml found in available history."
  exit 0
fi

parent_commit=$(git rev-parse "${intro_commit}^")

echo "Matrix file introduced at: ${intro_commit}"
echo "Parent commit to test:     ${parent_commit}"

if git cat-file -e "${parent_commit}:.github/test-matrix.yml" 2>/dev/null; then
  echo "Unexpected: matrix file exists in parent commit."
else
  echo "Confirmed: parent commit lacks .github/test-matrix.yml."
  echo "Current pre-release-sha logic would fail for that SHA."
fi

Repository: netlify/cli

Length of output: 278


🏁 Script executed:

#!/bin/bash
# Find when .github/test-matrix.yml was introduced using git log
git log --follow --format="%H %ai %s" -- .github/test-matrix.yml | tail -5
echo "---"
# Check if file exists in current HEAD
if [[ -f .github/test-matrix.yml ]]; then
  echo "File exists in current HEAD"
  head -5 .github/test-matrix.yml
else
  echo "File does not exist in current HEAD"
fi

Repository: netlify/cli

Length of output: 419


🏁 Script executed:

#!/bin/bash
# Check what would happen if yq tries to read a non-existent file
if ! command -v yq &> /dev/null; then
  echo "yq not available in sandbox"
else
  # Test yq behavior with non-existent file
  yq eval '.primary_node_version' /nonexistent/path.yml 2>&1 || true
fi

Repository: netlify/cli

Length of output: 82


🏁 Script executed:

#!/bin/bash
# Read the workflow file to see full context around lines 18-29
cat -n .github/workflows/pre-release-sha.yml | sed -n '15,35p'

Repository: netlify/cli

Length of output: 919


Handle SHAs that predate .github/test-matrix.yml introduction.

Line 23 reads config from the checked-out ref (line 18), but the file was only recently added (2026-04-23). Any older SHA will fail the yq read, breaking setup-node at line 29. This makes the SHA-based prerelease workflow fail for historical commits.

Add a conditional fallback to handle missing file:

Proposed fix
-      - name: Read primary Node version
-        id: node
-        run: echo "version=$(yq eval '.primary_node_version' .github/test-matrix.yml)" >> "$GITHUB_OUTPUT"
+      - name: Read primary Node version
+        id: node
+        run: |
+          if [[ -f .github/test-matrix.yml ]]; then
+            version="$(yq eval '.primary_node_version' .github/test-matrix.yml)"
+          else
+            # fallback for historical SHAs that predate the matrix file
+            version="24"
+          fi
+          echo "version=$version" >> "$GITHUB_OUTPUT"

Also applies to: 29-29

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/pre-release-sha.yml around lines 23 - 25, The workflow
step named "Read primary Node version" currently unconditionally runs yq against
.github/test-matrix.yml which will fail for historical SHAs that don't contain
that file; modify that step's run so it first checks for the file's existence
(test -f .github/test-matrix.yml) and only runs yq if present, otherwise emit a
safe fallback like echo "version=" (or a default version) to GITHUB_OUTPUT and
optionally a warning; ensure the same conditional/fallback logic is applied to
any other step that reads the same file (e.g., the subsequent "setup-node"
usage), so missing .github/test-matrix.yml does not break the prerelease
workflow for older commits.

Comment thread .github/workflows/unit-tests.yml Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/integration-tests.yml:
- Around line 36-49: Before expanding the OS matrix (the block that builds
OS_NODE using HEAD_REF and extra_oses), add a same-repo guard that verifies the
PR source repo is the base repo: extract the pull_request head repo full name
from the event payload (e.g., via
GITHUB_EVENT_PATH/.pull_request.head.repo.full_name) and compare it to
GITHUB_REPOSITORY, and only if they match proceed to run the existing
branch-prefix check that uses HEAD_REF (the existing if [[ "$HEAD_REF" ==
release-please--* ]] block); this ensures the extra_oses / OS_NODE expansion
(and subsequent sharding) runs only for same-repo PRs.

In @.github/workflows/unit-tests.yml:
- Around line 36-49: The release-please branch check using HEAD_REF can be
spoofed by forked PRs; modify the condition that adds extra_oses to also verify
the PR is from the same repository before appending to OS_NODE. In the workflow
step where HEAD_REF, extra_oses, primary and OS_NODE are used, derive a flag
(e.g. IS_SAME_REPO) from the event JSON (GITHUB_EVENT_PATH) by comparing
github.event.pull_request.head.repo.full_name or owner to github.repository (or
github.repository_owner) and only run the extra_oses loop when both HEAD_REF
matches release-please--* and IS_SAME_REPO is true; keep using the same
variables (HEAD_REF, extra_oses, primary, OS_NODE) so the change is localized
and straightforward.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7059f13e-5fcf-4afe-b505-1763a2d63b39

📥 Commits

Reviewing files that changed from the base of the PR and between 62c1e2b and c02bd42.

📒 Files selected for processing (3)
  • .github/workflows/e2e-tests.yml
  • .github/workflows/integration-tests.yml
  • .github/workflows/unit-tests.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/e2e-tests.yml

Comment on lines +36 to +49
env:
HEAD_REF: ${{ github.head_ref }}
run: |
node_versions=$(yq eval '.node_versions | join(" ")' .github/test-matrix.yml)
extra_oses=$(yq eval '.extra_oses_on_release | join(" ")' .github/test-matrix.yml)
primary=$(yq eval '.primary_node_version' .github/test-matrix.yml)

OS_NODE=()
for node in $node_versions; do
OS_NODE+=("ubuntu-latest:$node")
done
if [[ "$HEAD_REF" == release-please--* ]]; then
for os in $extra_oses; do
OS_NODE+=("$os:$primary")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use a same-repo check before expanding the integration matrix.

This branch-prefix check is spoofable on forked PRs for the same reason as in the unit workflow: github.head_ref only carries the PR source branch name, and forked pull_request events run in the base repository. Here the impact is larger because each extra OS also fans out across 4 shards. (docs.github.com)

🔧 Suggested guard
       - name: Compute matrix
         id: matrix
         env:
           HEAD_REF: ${{ github.head_ref }}
+          HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
+          BASE_REPO: ${{ github.repository }}
         run: |
           node_versions=$(yq eval '.node_versions | join(" ")' .github/test-matrix.yml)
           extra_oses=$(yq eval '.extra_oses_on_release | join(" ")' .github/test-matrix.yml)
           primary=$(yq eval '.primary_node_version' .github/test-matrix.yml)

           OS_NODE=()
           for node in $node_versions; do
             OS_NODE+=("ubuntu-latest:$node")
           done
-          if [[ "$HEAD_REF" == release-please--* ]]; then
+          if [[ "$HEAD_REPO" == "$BASE_REPO" && "$HEAD_REF" == release-please--* ]]; then
             for os in $extra_oses; do
               OS_NODE+=("$os:$primary")
             done
           fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
env:
HEAD_REF: ${{ github.head_ref }}
run: |
node_versions=$(yq eval '.node_versions | join(" ")' .github/test-matrix.yml)
extra_oses=$(yq eval '.extra_oses_on_release | join(" ")' .github/test-matrix.yml)
primary=$(yq eval '.primary_node_version' .github/test-matrix.yml)
OS_NODE=()
for node in $node_versions; do
OS_NODE+=("ubuntu-latest:$node")
done
if [[ "$HEAD_REF" == release-please--* ]]; then
for os in $extra_oses; do
OS_NODE+=("$os:$primary")
env:
HEAD_REF: ${{ github.head_ref }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
BASE_REPO: ${{ github.repository }}
run: |
node_versions=$(yq eval '.node_versions | join(" ")' .github/test-matrix.yml)
extra_oses=$(yq eval '.extra_oses_on_release | join(" ")' .github/test-matrix.yml)
primary=$(yq eval '.primary_node_version' .github/test-matrix.yml)
OS_NODE=()
for node in $node_versions; do
OS_NODE+=("ubuntu-latest:$node")
done
if [[ "$HEAD_REPO" == "$BASE_REPO" && "$HEAD_REF" == release-please--* ]]; then
for os in $extra_oses; do
OS_NODE+=("$os:$primary")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/integration-tests.yml around lines 36 - 49, Before
expanding the OS matrix (the block that builds OS_NODE using HEAD_REF and
extra_oses), add a same-repo guard that verifies the PR source repo is the base
repo: extract the pull_request head repo full name from the event payload (e.g.,
via GITHUB_EVENT_PATH/.pull_request.head.repo.full_name) and compare it to
GITHUB_REPOSITORY, and only if they match proceed to run the existing
branch-prefix check that uses HEAD_REF (the existing if [[ "$HEAD_REF" ==
release-please--* ]] block); this ensures the extra_oses / OS_NODE expansion
(and subsequent sharding) runs only for same-repo PRs.

Comment on lines +36 to +49
env:
HEAD_REF: ${{ github.head_ref }}
run: |
node_versions=$(yq eval '.node_versions | join(" ")' .github/test-matrix.yml)
extra_oses=$(yq eval '.extra_oses_on_release | join(" ")' .github/test-matrix.yml)
primary=$(yq eval '.primary_node_version' .github/test-matrix.yml)

OS_NODE=()
for node in $node_versions; do
OS_NODE+=("ubuntu-latest:$node")
done
if [[ "$HEAD_REF" == release-please--* ]]; then
for os in $extra_oses; do
OS_NODE+=("$os:$primary")
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restrict the release-please matrix to same-repo PRs.

github.head_ref is only the source branch name, and pull_request events for forks are still delivered to the base repository. A forked PR can therefore use a release-please--* branch name and unintentionally trigger the extra macOS/Windows matrix, which defeats the Linux-only optimization for regular PRs. (docs.github.com)

🔧 Suggested guard
       - name: Compute matrix
         id: matrix
         env:
           HEAD_REF: ${{ github.head_ref }}
+          HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
+          BASE_REPO: ${{ github.repository }}
         run: |
           node_versions=$(yq eval '.node_versions | join(" ")' .github/test-matrix.yml)
           extra_oses=$(yq eval '.extra_oses_on_release | join(" ")' .github/test-matrix.yml)
           primary=$(yq eval '.primary_node_version' .github/test-matrix.yml)

           OS_NODE=()
           for node in $node_versions; do
             OS_NODE+=("ubuntu-latest:$node")
           done
-          if [[ "$HEAD_REF" == release-please--* ]]; then
+          if [[ "$HEAD_REPO" == "$BASE_REPO" && "$HEAD_REF" == release-please--* ]]; then
             for os in $extra_oses; do
               OS_NODE+=("$os:$primary")
             done
           fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/unit-tests.yml around lines 36 - 49, The release-please
branch check using HEAD_REF can be spoofed by forked PRs; modify the condition
that adds extra_oses to also verify the PR is from the same repository before
appending to OS_NODE. In the workflow step where HEAD_REF, extra_oses, primary
and OS_NODE are used, derive a flag (e.g. IS_SAME_REPO) from the event JSON
(GITHUB_EVENT_PATH) by comparing github.event.pull_request.head.repo.full_name
or owner to github.repository (or github.repository_owner) and only run the
extra_oses loop when both HEAD_REF matches release-please--* and IS_SAME_REPO is
true; keep using the same variables (HEAD_REF, extra_oses, primary, OS_NODE) so
the change is localized and straightforward.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would a fork PR with head_ref release-please--* actually enable, though?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eduardoboucas — fair pushback. The practical impact is limited to CI resource consumption: a fork PR with a branch named release-please--something would cause the base repo to spin up macOS and Windows runners in addition to the normal Linux ones, burning GitHub Actions minutes unnecessarily.

That's it — there's no privilege escalation or secret exposure risk. Fork PRs run with a read-only GITHUB_TOKEN and secrets are never shared with forks, so the extra matrix entries give the fork author no additional capability or information.

Whether that's worth guarding against depends on your threat model:

  • If the repository is public and you're concerned about bad actors burning your CI quota, the same-repo guard is a cheap safeguard.
  • If it's an internal/low-traffic repo, or you're not worried about CI budget abuse, the current code is fine.

I'd downgrade my original severity assessment — this is a minor cost/efficiency concern rather than a security issue. Happy to resolve the comment if you'd prefer to leave the code as-is.

@eduardoboucas eduardoboucas enabled auto-merge (squash) April 23, 2026 16:31
@eduardoboucas eduardoboucas merged commit c9f23c2 into main Apr 23, 2026
39 checks passed
@eduardoboucas eduardoboucas deleted the chore/optimise-ci branch April 23, 2026 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants