Conversation
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds a new CI config file Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
📊 Benchmark resultsComparing with 901060d
|
There was a problem hiding this comment.
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 | 🟡 MinorRemove dead reference to non-existent
release-checkstep.Line 106 references
steps.release-check.outputs.IS_RELEASE, but there is norelease-checkstep 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 enableon Windows andpnpm/action-setup@v4elsewhere 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
📒 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
| - name: Read primary Node version | ||
| id: node | ||
| run: echo "version=$(yq eval '.primary_node_version' .github/test-matrix.yml)" >> "$GITHUB_OUTPUT" |
There was a problem hiding this comment.
🧩 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."
fiRepository: 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"
fiRepository: 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
fiRepository: 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.
There was a problem hiding this comment.
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
📒 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
| 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") |
There was a problem hiding this comment.
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.
| 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.
| 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") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
What would a fork PR with head_ref release-please--* actually enable, though?
There was a problem hiding this comment.
@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.
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
mainmerge, 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
mainpush 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.!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 viagithub.head_refand using that to switch matrix strategies.paths-filterstep so changes that only touch markdown, LICENSE, and template files skip the test jobs entirely.22.xvs22). Consolidated into a single config file (see below).When each workflow now runs
Unit, Integration, E2E tests
mainIf 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
main→ no test run. Release-please opens or updates the release PR.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
Centralized source of truth:
.github/test-matrix.ymlAll 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.