Chore: declare a supported Node.js range (>=20.19.0) and exercise the floor in CI - #571
Open
AmaadMartin wants to merge 2 commits into
Open
Chore: declare a supported Node.js range (>=20.19.0) and exercise the floor in CI#571AmaadMartin wants to merge 2 commits into
AmaadMartin wants to merge 2 commits into
Conversation
The published packages carried no machine-readable statement of the Node.js runtime they support, so npm could not warn a consumer installing on an unsupported runtime, and CI ran on whatever Node the GitHub-hosted runner image happened to ship that week. Declare engines.node ">=20.19.0" on the three published manifests and mirror it into the lockfile's workspace entries. The value is the single number that is true for both audiences, measured from package-lock.json: the production dependency closure is satisfied from 20.6.0 (@opentelemetry/core), and the full closure including the dev toolchain is satisfied from exactly 20.19.0 (vite via vitest, eslint-visitor-keys). No entry in either closure requires anything above 20. Give both workflows an explicit Node version so the runner default stops deciding, and add a fourth validation job pinned to the declared floor so the floor is actually executed rather than merely asserted. A drift-guard test fails if the three manifests ever stop agreeing. It checks presence and mutual agreement rather than the literal range, so a deliberate bump does not require editing the test.
AmaadMartin
force-pushed
the
feat/engines-node-floor-ci-matrix
branch
from
August 3, 2026 12:19
a94dbe3 to
3a877ad
Compare
AmaadMartin
changed the base branch from
feat/declare-engines-node-range
to
main
August 3, 2026 12:19
An include entry that overwrites an original matrix value cannot be merged into any existing combination, so it creates a new one: a base of os x ['24.x'] plus the single floor entry still yields four jobs, without hand-writing the three OS rows. Leaves the os: line untouched.
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):
Related: Docs: document minimum supported Node.js version in README google/adk-js#526
Or, if no issue exists, describe the change:
Problem: The published packages carry no machine-readable statement of the Node.js runtime they support.
npmcannot warn a consumer who installs@google/adkon an unsupported Node, and CI cannot catch a change that silently requires a newer runtime than the project claims to support, because both workflows callactions/setup-node@v6with nowith:block and therefore run on whatever Node the GitHub-hosted runner image happens to ship that week. The README prerequisite added in google#526 ("ADK for TypeScript requires a current Node.js LTS release") has nothing machine-checkable behind it.adk-pythonpairs the same prose with an enforceablerequires-python = ">=3.10".Solution: Three coordinated changes.
"engines": {"node": ">=20.19.0"}on the three published manifests (core,dev,integrations), mirrored into the lockfile's workspace entries.validation.yamlbecomes an explicitinclude:matrix — the three OSes on the current line, plus one extra job pinned to the declared floor, so the floor is executed rather than merely asserted.Verified on this PR: those four jobs are exactly what CI produced, and the floor job's log shows
setup-nodeacquiringv20.19.0and running the fulltest:coverage(225 test files passed),lint,format:checkanddocs:check-- not a skipped or no-op job.The matrix is a base (
osx['24.x']) plus a singleincludeentry for the floor. Per the workflow syntax docs, anincludeobject whose keys would overwrite an original matrix value "cannot be added to any of the matrix combinations", so "a new matrix combination will be created instead" -- 3 + 1 = 4 jobs, not 6. This keeps the existingos:line untouched.cross-language-integration.ymlhas no matrix and does not exercise the floor; it is pinned for the same reason the other workflow is -- so that no workflow silently inherits whatever Node the runner image ships that week. Without it the repository would still have one workflow whose Node version drifts with the runner image.Root
package.jsondeliberately gets noengines: npm already validates each workspace package's engines during a root install, so a root block is never published and is just a fourth copy of the number to drift.Why
>=20.19.0This is the one judgement call here, so it is measured from
package-lock.jsonrather than chosen. Walking every lockfile entry that declaresengines.node(workspace roots excluded, which would be circular):dev: trueexcluded, 426 entries)@opentelemetry/core^18.19.0 || >=20.6.0vite(viavitest)^20.19.0 || >=22.12.0;eslint-visitor-keys^20.19.0 || ^22.13.0 || >=24>=20.19.0is the single value simultaneously true for consumers and contributors: at or above the runtime floor, and exactly the toolchain floor on the 20 line. Nothing in either closure requires anything above 20. Verified empirically —npm cion Node 20.19.0 produces zeroEBADENGINEwarnings.Supporting reasons:
@types/nodeis^20.12.7in the root anddevmanifests, so the codebase is type-checked against the Node 20 stdlib and a higher floor would be incoherent without also bumping it (a separate change); and@google/genai@2.9.0, which@google/adkis built on, declares>=20.0.0— a library should not advertise a stricter floor than the SDK it wraps.Why not
>=22. Node 20 is past end-of-life, which is a real argument for a 22 floor, butenginesis a compatibility floor ("will this break?"), not a lifecycle recommendation ("what should you run?"); the README already carries the recommendation, and the two are complementary. A 22 floor would emitEBADENGINEat users who work fine today and contradict@types/node@^20. Note also that>=22.12.0specifically is not the floor of either closure — production is satisfied at 22.0.0 and the full lockfile not until 22.13.0, so 22.12.0 (vite's dev-only bound) leaves 5EBADENGINEwarnings fromeslint-visitor-keyson the floor job itself. If maintainers do want 22, it is a one-value edit in three manifests plus the matrix entry, and it should ride with the@types/nodebump.Collision check
Checked before writing anything (
gh pr list --repo AmaadMartin/adk-js --state open --limit 1000, 470 open PRs). This area is contested: #445, #508 and #544/#549 each implement the manifest half independently, and #509/#133/#510/#406/#416/#428/#467 overlap the CI-pin portion. They disagree on the floor (>=22.12.0,>=20,22.13.0), and none of them both declares a floor and runs it. This PR uses the measured value and is self-contained; it is intended to supersede those, and they should be closed rather than merged alongside it.Consequences
npm WARN EBADENGINEfor consumers below 20.19.0, from the first release containing this change. Installs still succeed — there is no.npmrcwithengine-strict, so this never hard-fails. Users on 20.6–20.18 will see a warning while continuing to work; that is the deliberate cost of a single number.run-tests (ubuntu-latest)torun-tests (ubuntu-latest, 24.x). If branch-protection required checks are pinned to the old names, a maintainer must update them.README.md:55still reads "requires a current Node.js LTS release". That prose is deliberately left alone (it landed in Docs: document minimum supported Node.js version in README google/adk-js#526, and restating a number there re-litigates that review), but note it is now a vaguer fourth statement of the same fact and reads as slightly at odds with a maintenance-LTS floor. Tightening it is worth a follow-up.enginesis not a field release-please manages. Suggested squash title is the commit subject (chore:). Because theEBADENGINEwarning is user-visible, retitle tofix:if you want a changelog line and a patch release — that is a maintainer call, not made 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.
tests/integration/package_manifests/engines_test.tsasserts the three published manifests declareengines.nodeand agree. It deliberately does not pin the literal'>=20.19.0': that would make the test a fourth copy of the number and force a test edit on every deliberate bump, whereas mutual agreement is the property that actually breaks by accident. Asserting on the whole object (rather than looping) makes the vitest diff name the offending workspace.This change adds no production TypeScript. Coverage
includeinvitest.config.tsis{core,dev,integrations}/src/**/*.ts, so the new file is outside the denominator and cannot move the existing thresholds (statements: 86,branches: 87,functions: 88,lines: 86). The coverage requirement is satisfied vacuously; the real regression signal is the guard test plus the CI floor job.[x] All unit tests pass locally.
Prove the test can fail — three mutations, each run against the committed tree, then reverted:
dev/package.json→">=22.0.0"- "dev": ">=20.19.0"/+ "dev": ">=22.0.0"enginesblock fromintegrations/package.json- "integrations": ">=20.19.0"/+ "integrations": undefinedenginesblock fromcore/package.json(the reference entry)AssertionError: expected undefined to be definedatexpect(reference).toBeDefined()Unmutated:
Test Files 1 passed (1) / Tests 1 passed (1).Everything below was run on Node 20.19.0 — the exact runtime the new floor job pins (downloaded from
nodejs.org/dist/v20.19.0/), withGOOGLE_CLOUD_*unset to match a clean runner:Note: with an ambient
GOOGLE_CLOUD_PROJECTexported,dev/test/cli/cli_create_test.ts > should handle Vertex AI selection with gcloud defaultsfails. That is a pre-existing env-hermeticity bug unrelated to this diff — it reproduces identically on Node 22 and is the subject of separate open PRs. It passes on every runtime once the variable is unset.Manual End-to-End (E2E) Tests:
The lockfile diff is exactly what the change implies.
npm install --package-lock-only && git diff --stat package-lock.json→+9 −0, three lines per workspace entry, nothing else. Re-checked after committing to confirm prettier did not reformat it.The warning actually reaches a consumer below the floor. On Node 18.20.8,
npm installat the repo root:All three published packages are named, and the install still succeeds (
exit=0) — confirming the advisory-only behaviour.The field actually ships.
npm pack --workspace @google/adk, then readingpackage/package.jsonout of the tarball →engines: {'node': '>=20.19.0'}. It reaches consumers rather than stopping at the repo.The floor job is not vacuous. Confirm on this PR that the job list contains
run-tests (ubuntu-latest, 20.19.0)and that it ran the full build and test suite. The pin is to the exact patch rather than20.xso that a Node API added later in the line cannot pass CI while breaking a consumer who is still inside the declared range.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.