Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/cross-language-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ jobs:

- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: '22'

- name: Setup Go
uses: actions/setup-go@v5
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/validation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: ['22', '24']

steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}

- name: Setup Python
uses: actions/setup-python@v5
Expand Down
12 changes: 12 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ To set up your local development environment for contributing:
npm test
```

### Supported Node.js versions

The minimum supported runtime is declared as `engines.node` in the root
`package.json` and in each published workspace manifest, and the `validation`
workflow runs the test suite on every supported Node.js LTS line.

The floor tracks the oldest Node.js LTS line that is still in support. When
that line reaches end of life, raise `engines.node`, drop the retired major
from the workflow matrix, and update the constants in
`tests/integration/repo_config/node_engines_test.ts` — the test fails until all
three agree.

### Code Quality

To maintain high code quality and consistency:
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ web runtimes.
## 🚀 Installation

> **Prerequisite:** ADK for TypeScript requires a current Node.js LTS release.
> The exact minimum is declared as `engines.node` in each package's
> `package.json`.

```bash
npm install @google/adk
Expand Down
3 changes: 3 additions & 0 deletions core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
"format": "prettier --write 'src/**/*.ts'",
"prepublishOnly": "npm run build"
},
"engines": {
"node": ">=22.0.0"
},
"dependencies": {
"@a2a-js/sdk": "^0.3.10",
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
Expand Down
3 changes: 3 additions & 0 deletions dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
"format": "prettier --write 'src/**/*.ts'",
"prepublishOnly": "npm run build"
},
"engines": {
"node": ">=22.0.0"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^4.17.21",
Expand Down
3 changes: 3 additions & 0 deletions integrations/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
"format": "prettier --write 'src/**/*.ts'",
"prepublishOnly": "npm run build"
},
"engines": {
"node": ">=22.0.0"
},
"dependencies": {
"@google/adk": "^1.5.0"
}
Expand Down
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
"dev",
"integrations"
],
"engines": {
"node": ">=22.0.0"
},
"devDependencies": {
"@eslint/js": "^9.37.0",
"@google/genai": "^2.9.0",
Expand All @@ -52,6 +55,7 @@
"gts": "^5.3.1",
"http-server": "^14.1.1",
"husky": "^9.1.7",
"js-yaml": "^4.1.1",
"lint-staged": "^16.2.7",
"prettier": "^3.6.2",
"prettier-plugin-organize-imports": "^4.3.0",
Expand Down
137 changes: 137 additions & 0 deletions tests/integration/repo_config/node_engines_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import yaml from 'js-yaml';
import {readFileSync, readdirSync} from 'node:fs';
import * as path from 'node:path';
import {describe, expect, it} from 'vitest';

/**
* Node.js LTS majors the project supports, oldest first. The first entry is the
* declared floor; raise it when that line reaches end of life.
*/
const SUPPORTED_NODE_MAJORS = ['22', '24'];

/** The `engines.node` range every published manifest must declare. */
const ENGINES_NODE_RANGE = `>=${SUPPORTED_NODE_MAJORS[0]}.0.0`;

interface Manifest {
workspaces?: string[];
engines?: {node?: string};
}

interface WorkflowStep {
uses?: string;
with?: Record<string, string>;
}

interface WorkflowJob {
strategy?: {matrix?: Record<string, string[]>};
steps?: WorkflowStep[];
}

interface Workflow {
jobs?: Record<string, WorkflowJob>;
}

/** A `setup-node` step paired with a human-readable location for failures. */
interface LocatedStep {
label: string;
step: WorkflowStep;
}

const repoRoot = process.cwd();
const WORKFLOW_DIR = path.join(repoRoot, '.github', 'workflows');

function readManifest(dir: string): Manifest {
return JSON.parse(
readFileSync(path.join(repoRoot, dir, 'package.json'), 'utf8'),
) as Manifest;
}

function readWorkflow(file: string): Workflow {
return yaml.load(
readFileSync(path.join(WORKFLOW_DIR, file), 'utf8'),
) as Workflow;
}

function workflowFiles(): string[] {
return readdirSync(WORKFLOW_DIR).filter(
(f) => f.endsWith('.yml') || f.endsWith('.yaml'),
);
}

/** Strips a leading range operator or `v` and returns the major version. */
function majorOf(version: string): number {
return Number.parseInt(version.replace(/^[^\d]*/, ''), 10);
}

function setupNodeSteps(): LocatedStep[] {
const steps: LocatedStep[] = [];
for (const file of workflowFiles()) {
const jobs = readWorkflow(file).jobs ?? {};
for (const [jobName, job] of Object.entries(jobs)) {
for (const step of job.steps ?? []) {
if (step.uses?.startsWith('actions/setup-node')) {
steps.push({label: `${file} job ${jobName}`, step});
}
}
}
}
return steps;
}

describe('Node.js engines declaration', () => {
it('declares the same engines.node in the root and every workspace', () => {
const root = readManifest('.');
const workspaces = root.workspaces ?? [];
expect(workspaces.length).toBeGreaterThan(0);

for (const dir of ['.', ...workspaces]) {
expect(
readManifest(dir).engines?.node,
`${dir}/package.json engines.node`,
).toBe(ENGINES_NODE_RANGE);
}
});

it('runs on a Node version that satisfies the declared floor', () => {
expect(majorOf(process.versions.node)).toBeGreaterThanOrEqual(
majorOf(ENGINES_NODE_RANGE),
);
});
});

describe('CI Node.js pinning', () => {
it('pins an explicit node-version in every setup-node step', () => {
const steps = setupNodeSteps();
expect(steps.length).toBeGreaterThan(0);

for (const {label, step} of steps) {
expect(
step.with?.['node-version'],
`${label}: setup-node must pin node-version`,
).toBeTruthy();
}
});

it('never pins a workflow below the declared floor', () => {
const floor = majorOf(ENGINES_NODE_RANGE);
for (const {label, step} of setupNodeSteps()) {
const version = step.with?.['node-version'];
// Matrix references are covered by the matrix assertion below.
if (!version || version.includes('${{')) continue;
expect(
majorOf(version),
`${label}: node-version ${version}`,
).toBeGreaterThanOrEqual(floor);
}
});

it('exercises every supported LTS line in the validation matrix', () => {
const job = readWorkflow('validation.yaml').jobs?.['run-tests'];
expect(job?.strategy?.matrix?.['node']).toEqual(SUPPORTED_NODE_MAJORS);
});
});
Loading