diff --git a/.github/workflows/cross-language-integration.yml b/.github/workflows/cross-language-integration.yml index 9ba2a2d7b..e679af069 100644 --- a/.github/workflows/cross-language-integration.yml +++ b/.github/workflows/cross-language-integration.yml @@ -16,6 +16,8 @@ jobs: - name: Use Node.js uses: actions/setup-node@v6 + with: + node-version: '24.x' - name: Setup Go uses: actions/setup-go@v5 diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 7bb00098e..a926b2309 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -15,6 +15,14 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] + node: ['24.x'] + include: + # The exact lower bound of engines.node in core/dev/integrations + # package.json. Pinned to the patch so a Node API added later in the + # 20.x line cannot pass CI while breaking a consumer who is still + # inside the declared range. Keep the two in sync. + - os: ubuntu-latest + node: '20.19.0' steps: - name: Checkout code @@ -22,6 +30,8 @@ jobs: - name: Use Node.js uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} - name: Setup Python uses: actions/setup-python@v5 diff --git a/core/package.json b/core/package.json index 678f8c0c9..3b22cf762 100644 --- a/core/package.json +++ b/core/package.json @@ -8,6 +8,9 @@ "type": "git", "url": "https://github.com/google/adk-js" }, + "engines": { + "node": ">=20.19.0" + }, "files": [ "package.json", "README.md", diff --git a/dev/package.json b/dev/package.json index 6117df13a..a81465f2d 100644 --- a/dev/package.json +++ b/dev/package.json @@ -8,6 +8,9 @@ "type": "git", "url": "https://github.com/google/adk-js" }, + "engines": { + "node": ">=20.19.0" + }, "files": [ "package.json", "README.md", diff --git a/integrations/package.json b/integrations/package.json index b7c467b4c..cbf6cc338 100644 --- a/integrations/package.json +++ b/integrations/package.json @@ -8,6 +8,9 @@ "type": "git", "url": "https://github.com/google/adk-js" }, + "engines": { + "node": ">=20.19.0" + }, "files": [ "package.json", "README.md", diff --git a/package-lock.json b/package-lock.json index e29a4a190..b314c866a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,6 +81,9 @@ "@types/lodash-es": "^4.17.12", "openapi-types": "^12.1.3" }, + "engines": { + "node": ">=20.19.0" + }, "peerDependencies": { "@mikro-orm/mariadb": "^6.6.6", "@mikro-orm/mssql": "^6.6.6", @@ -130,6 +133,9 @@ "tslib": "^2.6.2", "typescript": "^5.9.2", "vitest": "^3.2.4" + }, + "engines": { + "node": ">=20.19.0" } }, "dev/node_modules/@eslint/js": { @@ -335,6 +341,9 @@ "license": "Apache-2.0", "dependencies": { "@google/adk": "^1.5.0" + }, + "engines": { + "node": ">=20.19.0" } }, "node_modules/@a2a-js/sdk": { diff --git a/tests/integration/package_manifests/engines_test.ts b/tests/integration/package_manifests/engines_test.ts new file mode 100644 index 000000000..e4e5121a1 --- /dev/null +++ b/tests/integration/package_manifests/engines_test.ts @@ -0,0 +1,45 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +import {readFile} from 'node:fs/promises'; +import path from 'node:path'; +import {describe, expect, it} from 'vitest'; + +/** + * Workspace directories published to npm. The root manifest is the workspace + * container, is never published, and deliberately declares no engines: npm + * already validates a workspace package's engines during a root install. + */ +const PUBLISHED_WORKSPACES = ['core', 'dev', 'integrations']; + +interface Manifest { + engines?: {node?: string}; +} + +async function readEnginesNode(workspace: string): Promise { + const manifestPath = path.join(process.cwd(), workspace, 'package.json'); + const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as Manifest; + return manifest.engines?.node; +} + +describe('published package manifests', () => { + it('declare one agreed engines.node range', async () => { + const entries = await Promise.all( + PUBLISHED_WORKSPACES.map( + async (workspace) => + [workspace, await readEnginesNode(workspace)] as const, + ), + ); + const declared = Object.fromEntries(entries); + const [reference] = Object.values(declared); + + expect(reference).toBeDefined(); + expect(declared).toEqual( + Object.fromEntries( + PUBLISHED_WORKSPACES.map((workspace) => [workspace, reference]), + ), + ); + }); +});