chore: declare supported Node version and pin it in CI - #572
Open
AmaadMartin wants to merge 2 commits into
Open
Conversation
added 2 commits
August 3, 2026 05:05
Nothing in the repository stated which Node.js runtime it supports: no engines field in any of the four manifests, no .nvmrc, no .node-version. The three published packages therefore shipped without an engines field, so npm could not warn a user installing on an unsupported runtime. Declare engines.node ">=20.19.0" identically in the root manifest and in each independently published workspace (@google/adk, @google/adk-devtools, @google/adk-integrations). The floor is measured, not guessed: evaluating every engines.node range in package-lock.json against candidate versions leaves 10 packages unsatisfied at 20.17.0 and 0 at 20.19.0. The binding constraints are vite@7.3.5 (^20.19.0 || >=22.12.0) and eslint-visitor-keys@5.0.1 (^20.19.0 || ^22.13.0 || >=24). Node 20 is also the major the repository type-checks against (@types/node ^20.12.7). A floor rather than an exact pin keeps users on Node 22/24 supported; engines is a consumer-facing compatibility statement, and with npm's default engine-strict=false an older runtime warns rather than fails. package-lock.json carries the same four blocks because npm mirrors workspace engines into the lockfile; it is a fixed point after the edit.
Both workflows called actions/setup-node with neither node-version nor node-version-file. That input has no default: with nothing supplied the action installs nothing and every later run: step executes on whatever Node the runner image happens to ship. Three consequences, all live: the tested runtime drifted with the image without any commit here; the ubuntu/windows/macos legs are independently versioned images and could run different Node majors in the same matrix; and the executed runtime was decoupled from the Node 20 API surface the code is type-checked against. Pin node-version: '20.x' on both steps. The value is quoted so YAML reads it as a string, and it pins the major while letting the patch float to the newest Node 20 (currently 20.20.2), which satisfies the >=20.19.0 floor declared in the manifests. node-version-file pointing at package.json was rejected deliberately: it resolves engines.node, which is the range >=20.19.0, and with the default check-latest: false the action first looks for a cached semver match, so the image's newer Node would satisfy it and reproduce the drift this change removes. engines stays the consumer-facing floor; node-version is the pin.
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
Closes: #issue_number
Related: #issue_number
Problem:
The repository never states which Node.js runtime it supports, and CI never selects one.
Both workflows that run JavaScript call
actions/setup-nodewith neithernode-versionnornode-version-file:.github/workflows/validation.yaml- name: Use Node.js/uses: actions/setup-node@v6.github/workflows/cross-language-integration.yml- name: Use Node.js/uses: actions/setup-node@v6These are the only two
setup-nodecall sites (grep -rn "setup-node" .github/returns exactly those two).node-versionhas no default: with nothing supplied the action installs nothing and every laterrun:step executes on whatever Node the runner image happens to ship.Nothing else pins it either — before this change,
grep -rn '"engines"' --include=package.json . | grep -v node_modulesreturned nothing across all four manifests, and there is no.nvmrcor.node-version.Four consequences, all live before this PR:
The tested runtime drifts with the runner image, with no commit in this repository.
Different legs of the same matrix run different Node majors —
ubuntu-latest,windows-latestandmacos-latestare independently versioned images, so a green matrix does not mean "green on Node X". This is not hypothetical; it is observable in this repository's own CI right now. A recentvalidationrun on an unrelated branch that still has the baresetup-nodestep reported:Two Node majors in one green matrix, chosen by the image rather than by this repository.
The type surface and the executed runtime are decoupled. The repo pins
@types/node: ^20.12.7— code is type-checked against the Node 20 API — while tests execute on an unrelated, unstated major.Consumers are told nothing.
@google/adk,@google/adk-devtoolsand@google/adk-integrationsshipped with noenginesfield, sonpmcould not warn a user installing on an unsupported Node.Solution:
Two declarations, seven files, 28 added lines. No TypeScript, no runtime code, no new files.
A.
engines.node: ">=20.19.0"— added identically to the root manifest and to each of the three independently published workspaces. Each workspace has its ownfilesarray containingpackage.jsonand its ownprepublishOnly, so each must carry the declaration for it to reach consumers.The floor is measured, not guessed. Evaluating every
engines.noderange inpackage-lock.jsonagainst candidate versions withsemver.satisfies:engines.nodeis unsatisfiedThe binding constraints at the top of the Node 20 line are
vite@7.3.5(^20.19.0 || >=22.12.0) andeslint-visitor-keys@5.0.1(^20.19.0 || ^22.13.0 || >=24), pulled in transitively byvitestandtypescript-eslint. 20.19.0 is the lowest version at which the current tree is fully satisfied, so it is the lowest honest value. It is also still within the Node 20 major that@types/node: ^20.12.7pins.A floor rather than an exact pin is deliberate:
enginesis a consumer-facing compatibility statement."20.x"there would tell every user on Node 22/24 they are unsupported. The floor leaves them supported; the CI pin below is what makes the tested version deterministic.B.
node-version: '20.x'— added to the existingsetup-nodestep in both workflows. Nothing else about either step changed, and the action version was not bumped. The value is quoted so YAML reads it as a string (node-version: 20.10unquoted parses as the float20.1).'20.x'pins the major and lets the patch float to the newest Node 20, currently 20.20.2, which satisfies the declared>=20.19.0floor.Why not
node-version-file: pointing it atpackage.jsonmakes the action resolveengines.node, which is the range>=20.19.0. With the defaultcheck-latest: falsethe action first checks the local tool cache for a semver match, so the image's newer cached Node satisfies the range — reproducing exactly the drift this PR removes. Makingnode-version-fileactually pin would require narrowingengines.nodeto"20.x", which breaks the consumer contract above. Hence:engines= consumer floor,node-version= CI pin.Deliberately out of scope (each would turn a reviewable chore into a behaviour change): no
.nvmrc/.node-version(a third place for the version to live and drift); nopackageManager/devEngines(either would silently switch on npm caching insetup-node@v6); nocache:input, nonpm ci, nots:checkCI step; no Node-version matrix dimension; no dependency or action-version bumps.Note on the diff size — 7 files, not the 6 you might expect.
package-lock.jsonis included because npm mirrors workspaceenginesinto the lockfile; the diff is exactly the same fourenginesblocks and nothing else — no dependency change, no version churn, noresolvedURL churn. It is a fixed point (a secondnpm installproduces no further change). Committing it keeps the lockfile in sync with the manifests; omitting it would mean every contributor's firstnpm installimmediately dirties their working tree.Known caveat — the pinned major is EOL. Node 20 "Iron" reached end-of-life on 2026-04-30 (
nodejs/Releaseschedule.json:v20.end = "2026-04-30"), so this pins CI to a runtime that no longer receives security updates. That is deliberate and correctly scoped: this PR makes the repository's existing, implicit support claim (@types/node: ^20.12.7) explicit and testable, without changing it. Moving the project to Node 22 (end: 2027-04-30) or 24 (end: 2028-04-30) is a consumer-visible policy decision that requires bumping@types/nodein the same change and absorbing the type fallout; it is tracked as a separate follow-up and must not ride along here. The three values that must move together areengines.node, the two workflow pins, and@types/node— all Node 20 today.Collision check. Run before writing any code, as required:
470 open PRs on the fork. Several are adjacent and pin the CI Node version and/or declare
engines.node— notably #133 (.nvmrc22+engines >=22.0.0+node-version-filein both workflows), #509 (node-versionliteral + rootengines >=22.13.0+ a['22','24']matrix), and #406/#416/#428/#445/#467/#508/#510/#544/#549. Verified at the git level that none of them has merged: onfork/mainthere is no.nvmrc,enginesis absent from all four manifests, and both workflows still call a bareactions/setup-node@v6— i.e. the defect described above is still live onmain. This PR is branched from currentfork/main, not stacked on any of them. They are mutually exclusive by construction — only one Node pin can land — so whichever is reviewed first should be merged and the rest closed. The substantive difference is the value: the siblings pin 22 or 24, this one pins 20 to match the@types/nodethe repo actually type-checks against, keeping the three values consistent rather than making a support-policy change inside a CI chore. If the maintainers prefer to move the support policy in one step instead, #509 or #133 is the better base and this PR should be closed.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.
No tests were added, and that is intentional, not an oversight. This change contains zero executable lines — it is four JSON
enginesblocks and two YAMLwith:blocks. There is no unit under test and no new-line coverage to report. A test that readpackage.jsonand assertedengines.node === '>=20.19.0', or that parsed the workflow YAML, would only restate a constant back to itself: it could never fail for a reason a reader cares about, so it would be noise rather than signal. The real verification for a runtime pin is executing the existing suites on the pinned runtime, which is what the protocol below does, plus the CI matrix itself.Unit Tests:
[x] I have added or updated unit tests for my change. — N/A, see above: zero executable lines. Existing suites are unmodified.
[x] All unit tests pass locally.
Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
Everything below was run on the pinned runtime. Reproduce with any Node 20 >= 20.19.0:
1. Runtime used for every step below —
node -v->v20.20.2(npm -v->10.8.2). Latest Node 20 release, confirmed againsthttps://nodejs.org/dist/index.json.2.
npm install— succeeded. NoEBADENGINEwarning, which is the live confirmation that the declared>=20.19.0floor is satisfiable by the tree.git status --short package-lock.jsonafter install shows only the fourenginesmirrors described above, and a secondnpm installproduces no further change (fixed point).3.
npm run ts:check— fails, and fails identically without this change. Measured both ways on the same Node 20.20.2, withtsc --noEmit --pretty false | grep -c ": error TS":ts:checkis broken onmainindependently of this PR (predominantly TS2345/TS2322 incore/test/**andtests/integration/**). This change is JSON and YAML only and cannot affecttsc.ts:checkis not a CI gate today and this PR does not add it. Fixing it is out of scope here.4.
npm run build— passes for all three workspaces on Node 20.5.
npm run test:coverage— this is the highest-risk step, since the thresholds were measured on a different Node and V8 coverage can shift across V8 versions. Passes on Node 20 with headroom:No threshold was touched, and none needed to be.
One environment note, because a naive local run looks alarming: on a workstation with
GOOGLE_CLOUD_PROJECTexported, 22tests/e2e/**suites de-skip (describe.skipIf(!hasAKey)treats that variable as credentials) and then fail with "API key must be provided…", andtests/e2e/live_model_test.tsde-skips because it is gated ondescribe.skipIf(process.env.CI === 'true'). Neither is related to the Node version. The numbers above are from a run with the CI environment replicated —CI=true,GOOGLE_CLOUD_PROJECT/GEMINI_API_KEY/GOOGLE_GENAI_API_KEYunset — which is what the GitHub runner actually provides.Did pinning to Node 20 regress anything? Directly measured, rather than assumed. Ran the same suite on Node 20.20.2 and on Node 22.22.2 (this machine's system Node, standing in for the unpinned "before" runtime) and diffed the failure sets:
Zero failures are unique to Node 20. Node 20 is strictly no worse than the runtime CI has been silently using.
6.
npm run lint— passes.npm run format:check— passes ("All matched files use Prettier code style!").format:checkcovers only**/*.ts, so the JSON edits are not in its scope, but it gates CI and was run anyway. Separately,prettier --checkon the workflow file reports one pre-existing style nit (NODE_OPTIONS: "…"double quotes on line 10) that is present on pristinefork/maintoo and is untouched here; the lines this PR adds use single quotes, matching Prettier's preference.7.
npm run docs:check— passes (typedoc --emit none --treatWarningsAsErrors).8. YAML sanity — both workflows re-parsed with a YAML loader to confirm the
with:block attached to the correct step and thatnode-versionis a string, not a float:No other step in either file was disturbed.
9. Node version observed in CI — "the CI logs show the intended Node version" is an explicit acceptance criterion, so here are the four versions actually reported by the runners on this PR's commit:
validation/run-tests (ubuntu-latest)validation/run-tests (windows-latest)validation/run-tests (macos-latest)Cross-Language Tests/run-tests(macos)All three OS legs now agree, and the version is chosen by this repository rather than by the runner image. The
setup-nodestep log confirms the resolution path:node-version: 20.x->Attempting to download 20.x...->Acquiring 20.20.2.10. One CI failure, and why it is not caused by this change. The
run-tests (windows-latest)leg failed on its first attempt with exactly one test:That test shells out to run
echo "Hello, Shell!"and hit the 5s default timeout — a process-spawn timing issue on the Windows runner, not a Node API incompatibility. Rather than assume, I checked whether it predates this change: avalidationrun on the unrelated branchfeat/hoist-vertex-placeholder-constants-core-tests, which still uses the bare, unpinnedsetup-nodestep and ran on Node v22.23.1, failed with the identical signature — same file, same line 161, same 5000ms timeout, same1 failed | 223 passed | 20 skipped (244)tally. It is therefore a pre-existing Windows flake, reproducible on a different branch on a different Node major, not a Node 20 regression. Per the error-handling protocol for this change I did not react by looseningengines, moving the pin to a newer major, or editing that test's timeout; fixing the flake is separate work. Re-running that job on the same commit passes:Test Files 224 passed | 20 skipped (244), onnode: v20.20.2. All four test jobs (ubuntu / windows / macos / cross-language) are now green.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. — N/A: the change is declarative config with no code; the rationale is in this description rather than in comments.
[x] I have added tests that prove my fix is effective or that my feature works. — N/A: zero executable lines, see the Testing Plan for why a config-restating test would be noise; the change is proven by running the existing suites on the pinned runtime.
[x] New and existing unit tests pass locally with my changes.