Feat: declare a supported Node.js floor via engines and pin the CI matrix - #593
Open
AmaadMartin wants to merge 2 commits into
Open
Feat: declare a supported Node.js floor via engines and pin the CI matrix#593AmaadMartin wants to merge 2 commits into
AmaadMartin wants to merge 2 commits into
Conversation
added 2 commits
August 3, 2026 14:10
…trix
The repository published three npm packages without stating which Node.js
runtimes they support in any machine-checkable form. The only statement was
prose in the README, and both workflows called actions/setup-node with no
node-version, so CI silently ran on whatever Node the runner image shipped.
Declare "engines": {"node": ">=22.0.0"} in the root manifest and all three
published workspace manifests, pin the validation matrix to the supported LTS
lines ('22', '24'), and pin the cross-language workflow to the floor.
The floor is 22 because Node 20 Iron reached end of life on 2026-04-30, so 22
and 24 are the LTS lines still in support. It is also the lowest value with no
gaps: vitest@3.2.6 declares ^18.0.0 || ^20.0.0 || >=22.0.0, so a >=20 floor
would nominally admit a Node 21 that vitest refuses.
A repo-config test binds the three declarations together so they cannot drift:
it fails if the manifests disagree, if any setup-node step stops pinning a
node-version, if a workflow pins below the floor, or if the matrix stops
exercising every supported line.
…ting The new repo-config test imports js-yaml from tests/, but the root manifest declared only @types/js-yaml. The runtime package resolved purely by npm hoisting it out of core and dev, so the import would break if either workspace dropped the dependency, or under any strict or isolated node_modules layout. Declare it explicitly at the root, matching the ^4.1.1 range core and dev already use. No new package is installed; the lockfile delta is the declaration mirror only.
This was referenced Aug 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
Link to an existing issue (if applicable):
N/A — no existing issue.
Or, if no issue exists, describe the change:
Problem: The repository publishes three npm packages (
@google/adk,@google/adk-devtools,@google/adk-integrations) but never states whichNode.js runtimes it supports in a form any tool can check.
enginesfield in the root manifest or any of the three publishedworkspace manifests (
grep -rn '"engines"' --include=package.jsonreturnednothing outside
node_modules)..github/workflows/validation.yaml:24and.github/workflows/cross-language-integration.yml:18both calledactions/setup-node@v6with nowith:block, so every job silently ranon whatever Node the runner image happened to ship.
Two consequences. Consumers got no
EBADENGINEsignal at install time, so anunsupported runtime surfaced later as an unexplained
TypeErroron a missingbuilt-in. And nothing in the repo exercised a declared floor, so a change
relying on a newer Node API could go green in CI and still break a supported
consumer.
Solution: Turn the README's prose into a machine-checkable contract.
"engines": {"node": ">=22.0.0"}in the root manifest and all threepublished workspace manifests, byte-identical.
node: ['22', '24']dimension to thevalidationmatrix and pass itto
setup-node; pin the cross-language workflow to the floor ('22').matrix together so they cannot drift apart.
engines.nodeand document the bump policy inCONTRIBUTING.md.Why 22, and why a bare
>=floor. Every value here is derived from a sourceof truth in the repo or from the official release schedule, not from memory:
nodejs/Releaseschedule.jsongives v20 Iron"end": "2026-04-30"and v18 Hydrogen"end": "2025-04-30". The LTS linesstill in support are 22 (
end: 2027-04-30) and 24(
end: 2028-04-30), so 22 is the floor consistent with the README's existing"a current Node.js LTS release" wording.
package-lock.json:@google/genai@2.9.0>=20.0.0,commander@14.0.3>=20,secretlint@11.7.1>=20.0.0,lint-staged@16.4.0>=20.17. A>=18declaration would have been false on day one.
>=20would have a hole in it.vitest@3.2.6declares^18.0.0 || ^20.0.0 || >=22.0.0— it skips Node 21. A>=20.0.0floornominally admits a runtime vitest refuses;
>=22.0.0does not.^22 || ^24would reject Node 26 whenit becomes LTS on 2026-10-28 and would need an edit every release. A bare
>=floor plus the documented bump policy is the intent.Note for reviewers who remember #526. That PR removed a hard-coded Node
version from the README on the grounds that it goes stale. This change respects
that outcome rather than re-litigating it: no number is added to the README,
which keeps its version-free wording and only gains a pointer to
engines.node.CONTRIBUTING.mdlikewise states the rule ("the floor tracksthe oldest LTS line still in support") and no version. The concrete number lives
only where it must be machine-readable, and the new test binds those places
together so there is effectively one source of truth.
Collision check (required by our process, recorded here). Ran
gh pr list --state open --limit 1000(490 open PRs) and diffed every plausiblyadjacent one. This area is heavily contested — 10 open PRs touch it: #571,
#572, #574, #508, #509, #510, #544, #549, #445, #133. The closest is #571
(
feat/engines-node-floor-ci-matrix), which covers the same surface. I did notstack on it because this change revises the lines it adds rather than building
on them, and the differences are substantive:
>=20.19.0— an EOL line, and it pins a20.19.0CI leg, so CI actively tests an unsupported runtime>=22.0.0, the oldest LTS still in supportsetup-node, a workflow pinned below the floor, and matrix driftMaintainers should land one of these and close the rest.
Breaking change disclosure — this is not "metadata only." Declaring
engineschanges install behaviour for consumers below the floor:engines.nodeEBADENGINEwarning; install proceedsengine-strict=true.npmrcshipped)--ignore-enginesREADME.mddocumentsyarn add @google/adk, so for a Yarn 1 user on Node 20this converts a working install into a hard failure. That is the intended
enforcement — Node 20 has been EOL since 2026-04-30 — but it is a real
behaviour change, not a no-op.
Considered and rejected:
.npmrcwithengine-strict=true— turns a warning into a hard failurefor every contributor and consumer. That is a much larger policy decision than
this change carries. The CI matrix is the enforcement mechanism here: it
actually runs the suite on the floor.
node-version-file: package.json—setup-noderesolves a semver rangeto the newest satisfying version, so
">=22.0.0"would install the latestNode and the floor would never be exercised. Explicit majors are required.
Lockfile.
package-lock.jsonis touched only because npm mirrors manifestfields into its lockfile entries: the four
enginesblocks, plus the onejs-yamldeclaration below. No package was installed, removed, orversion-bumped.
Phantom dependency fixed (second commit). The new test imports
js-yamlfrom
tests/, but the root manifest declared only@types/js-yaml— theruntime package resolved purely by npm hoisting it out of
core/package.jsonand
dev/package.json. That would break if either workspace dropped it, orunder any strict / isolated
node_moduleslayout. It is now declaredexplicitly at the root at
^4.1.1, matching the rangecoreanddevalready use. Worth fixing here specifically, since the premise of this PR is
making the repo's runtime contract explicit rather than incidental. Node ships
no YAML parser, so the dependency itself is warranted — only its declaration
was missing.
Two notes for maintainers before merging:
from
run-tests (ubuntu-latest)torun-tests (ubuntu-latest, 22), and soon. If branch protection pins the old names as required checks, they will
stop reporting and PRs would silently lose that gate until the rules are
updated. Please update branch protection alongside this merge.
secretlint,lint,format:checkanddocs:checknow execute in six jobs instead of three.Narrowing that with
matrix.includewould make the YAML longer and wouldbreak the straightforward matrix assertion in the test, so the uniform
cross-product is kept as the simpler expression.
On keeping the README pointer version-free. It was suggested that the two
added README lines either fold the number in directly (
requires Node.js 22 or newer) or be dropped. Keeping the indirection is deliberate: writing the numberinto the README is exactly what was reverted in #526 ("any hard-coded minimum
version goes stale over time"), and the new test cannot guard prose, so a number
there would be the one claim in this change with nothing enforcing it. The
pointer tells a reader where the authoritative, enforced value lives without
creating a second source of truth that can drift.
Out of scope (each is a separate risk class and is left alone deliberately):
@types/nodeis still pinned at^20.12.7in the root anddevmanifests;bumping it to
^22is a major typings upgrade that can force production-sourcechanges, so it is not bundled here.
Testing Plan
Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.
Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.
Added
tests/integration/repo_config/node_engines_test.ts(5 tests), placedunder
tests/integration/**so it is picked up by theintegrationproject,which
npm run test:coverage— and therefore thevalidationworkflow —actually runs. It asserts the five invariants:
engines.nodeis present and identical in the root manifest and in everydirectory listed in the root
workspacesarray (the workspace list is readfrom the manifest, not hard-coded, so a new workspace is covered
automatically).
actions/setup-nodestep in every workflow pins an explicitnode-version.node-versionbelow the floor (matrix referencescontaining
${{are skipped — they are covered by invariant 5).validationmatrix exercises every supported LTS line.ENGINES_NODE_RANGEis derived fromSUPPORTED_NODE_MAJORS[0], so the twoconstants cannot disagree with each other.
Proof each test can fail. Every test was run against mutated code and
confirmed to FAIL. Each mutation was reverted afterwards:
enginesblock fromintegrations/package.jsonAssertionError: integrations/package.json engines.node: expected undefined to be '>=22.0.0'engines.node→">=20.0.0"AssertionError: ./package.json engines.node: expected '>=20.0.0' to be '>=22.0.0'with: node-versionfromcross-language-integration.ymlAssertionError: cross-language-integration.yml job run-tests: setup-node must pin node-version: expected undefined to be truthynode: ['24']AssertionError: expected [ '24' ] to deeply equal [ '22', '24' ]'20'(below floor)AssertionError: cross-language-integration.yml job run-tests: node-version 20: expected 20 to be greater than or equal to 22SUPPORTED_NODE_MAJORS[0]→'99'(above the running runtime)AssertionError: expected 22 to be greater than or equal to 99M3 is the important one: it restores the exact defect this change exists to
fix — a floating
setup-node— and proves the test would have caught it.Coverage.
vitest.config.tsrestrictscoverage.includetocore/src/**,dev/src/**,integrations/src/**. This change adds zero lines under thosepaths, so the configured thresholds are unaffected and no threshold was edited.
The new test file executes every one of its own lines on every run.
Manual End-to-End (E2E) Tests:
Run from the repository root on Node v22.22.2:
To confirm the four manifests agree, and see the consumer-facing value:
CI result on this PR. All six matrix jobs pass (3 OS × Node 22 and 24),
plus the cross-language
run-testsjob:The new test passed in every job, on both majors and all three OSes.
Getting there took re-runs, and the reason is worth flagging to maintainers
because this change makes it more visible, not less:
core/test/code_executors/unsafe_local_code_executor_test.ts > should execute shell code and return stdouttimes out at 5000ms onwindows-latest. It is areal-subprocess spawn racing a 5s budget, it is unrelated to this diff (which
touches no source that test exercises), and Fix: give the real-subprocess cases in unsafe_local_code_executor_test.ts an explicit 60s timeout #498 is already open to give
those cases an explicit 60s timeout.
tests/integration/app_loader/app_loader_test.tsflaked once onmacos-latest, then passed.b390217ealready failswindows-latestvalidation on
main(run 30669370416,adk_web/webui_test.ts) with none ofthese changes present.
I confirmed Node 24 itself is not the problem: #428 pins the identical
node: v24.18.0onwindows-latestand its job passes. So the retries weregenuine flakiness, not a Node 24 incompatibility introduced here.
One observation, left unfixed deliberately: this change takes the matrix from 3
jobs to 6, and the matrix still uses the default
fail-fast: true. A singleflaky leg therefore cancels the other five, which is why
windows-latest, 22reported
cancelledrather than a result on several attempts. Addingfail-fast: falsewould make the wider matrix considerably more informative,but #235 is already open for exactly that, so it is left out of this diff
rather than duplicated.
Known pre-existing failure, not introduced here:
npm run ts:checkreports281 errors. That count and the exact set of failing files are identical on the
base commit with this change stashed, and none of them is in the new file
(
grep -c node_engines_test→ 0).ts:checkis not run by thevalidationworkflow. Several open PRs address it separately; it is out of scope here.
Checklist
[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas.
[x] I have added tests that prove my fix is effective or that my feature works.
[x] New and existing unit tests pass locally with my changes.