Skip to content

chore(ci): standardize the repository on Node 22 (PNI-186) - #2325

Open
mme wants to merge 4 commits into
mainfrom
markus/pni-186-standardize-the-ag-ui-repository-on-node-22
Open

chore(ci): standardize the repository on Node 22 (PNI-186)#2325
mme wants to merge 4 commits into
mainfrom
markus/pni-186-standardize-the-ag-ui-repository-on-node-22

Conversation

@mme

@mme mme commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Fixes PNI-186.

The problem

Node versions were declared in several places and didn't agree. Ten actions/setup-node invocations named "22" inline; two read package.json, whose engines.node range (>=18) resolves to whatever the newest satisfying Node happens to be — so those two jobs were silently running Node 24/25. Nothing said which Node this repository is developed and tested with, and nothing was watching for drift.

What this does

  • Adds .node-version at the root containing 22. One declaration, read by CI and by local version managers alike.
  • Points all 12 actions/setup-node invocations across 8 workflows at it via node-version-file. None names a version inline anymore. Each of the 12 already checks the repository out first, so the file resolves.
  • Moves corepack enable to after setup-node in both publish-commit.yml jobs. It ran before, so that one command used whatever Node the runner image shipped.
  • Adds .github/scripts/check-toolchain-consistency.sh, which fails on a setup-node that names a version inline, reads the wrong version file, or names no version at all — and on a .node-version that isn't 22.
  • Covers the check with seven fixtures (miniature repository roots), each asserting on the failure message rather than just the exit code, so a check that fails for the wrong reason doesn't count as green.
  • Runs both in CI via a new Toolchain Consistency workflow, fixtures first — a check that no longer discriminates is worse than no check.
  • Points the repo-development docs that said "Node 20+" at .node-version, and documents the toolchain in CONTRIBUTING.md.

package.json#engines.node stays at >=18. That range states what consumers of the published packages may use; narrowing it is a separate compatibility decision and explicitly out of scope here.

On the check's design

It is a line scan, not a YAML parse, and its header says so. It catches a version written into a workflow in every style anyone in this repository writes. Unusual-but-legal YAML — anchors, folded scalars, quoted keys — can slip past it. That's an accepted limit rather than an oversight: the drift being guarded against is someone copying a step and typing a version into it, not deliberate evasion.

An earlier draft of this PR also flagged jobs that run Node tooling with no setup-node at all. That rule carried nearly all of the implementation's complexity, found zero findings against the 24 real workflows, and was the only source of false positives — so it was cut rather than hardened, taking the script from 219 lines to 114.

Verification

Under Node 22.20.0 with pnpm 10.33.4, via the repository's Nx targets:

Command Result
toolchain check + fixtures ✅ pass (7/7 fixtures)
pnpm run build ✅ 35 projects
pnpm run check-types ✅ pass
pnpm run test ✅ 33 projects, 1 pre-existing failure (below)
pnpm run lint ❌ pre-existing failure (below)
pnpm run test:exports ❌ pre-existing failure (below)
release script tests ✅ 91/91
verify-nx-release-allowlist / verify-release-scope-dropdowns / verify-config-manifest-names ✅ pass
shellcheck, actionlint ✅ clean

Run against the parent tree, the check reports 13 findings — the 12 drifted call sites plus the absent root file — and passes on this branch.

Pre-existing failures, red both before and after

None relate to Node resolution. Recorded here for separate repair, per the ticket:

  1. @ag-ui/core:lint — its lint script calls eslint, which neither core nor the root declares, so it isn't on PATH for that project. Reproduced on a pristine tree.
  2. @ag-ui/cross-language-tests:test — needs a local .NET SDK. CI runs this suite (dotnet / cross-language (TS <-> C#)) and it passes.
  3. @ag-ui/mastra:test:exportstypesVersions maps copilotkit but not a2ui, so node10 resolution of @ag-ui/mastra/a2ui fails.

Worth flagging separately: neither lint nor test:exports runs in CI at all. That's why the PNI-188 baseline recorded CI as fully green without seeing #1 or #3. Probably its own ticket.

🤖 Generated with Claude Code

@mme
mme requested a review from a team as a code owner August 5, 2026 15:05
Comment on lines +12 to +27
on:
push:
branches: [main]
paths:
- ".node-version"
- ".github/workflows/**"
# The whole scripts directory, fixtures included: a change that weakens a
# fixture has to re-run the suite that fixture belongs to.
- ".github/scripts/**"
pull_request:
paths:
- ".node-version"
- ".github/workflows/**"
# The whole scripts directory, fixtures included: a change that weakens a
# fixture has to re-run the suite that fixture belongs to.
- ".github/scripts/**"
contents: read

jobs:
toolchain-consistency:
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Python Preview Packages

Version 0.0.0.dev1785947872 published to TestPyPI.

Warning: These packages are built from contributor code that may not yet have been vetted for correctness or security. Install at your own risk and do not use in production.

Install with uv

Add the TestPyPI index to your pyproject.toml:

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = true

Then install the packages you need:

# Core SDK
uv add 'ag-ui-protocol==0.0.0.dev1785947872' --index testpypi

# Integrations (each already depends on the matching ag-ui-protocol preview)
uv add 'ag-ui-langgraph==0.0.0.dev1785947872' --index testpypi
uv add 'ag-ui-crewai==0.0.0.dev1785947872' --index testpypi
# NOTE: ag-ui-agent-spec depends on pyagentspec (git-only, not on PyPI).
# You will need to install pyagentspec separately from its git repo.
uv add 'ag-ui-agent-spec==0.0.0.dev1785947872' --index testpypi
uv add 'ag_ui_adk==0.0.0.dev1785947872' --index testpypi
uv add 'ag_ui_strands==0.0.0.dev1785947872' --index testpypi

Install with pip

pip install \
  --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple/ \
  ag-ui-protocol==0.0.0.dev1785947872

Use --extra-index-url https://pypi.org/simple/ so pip can resolve
transitive dependencies (pydantic, fastapi, etc.) from real PyPI.


Commit: 3e10374

@pkg-pr-new

pkg-pr-new Bot commented Aug 5, 2026

Copy link
Copy Markdown

Open in StackBlitz

@ag-ui/a2a-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a-middleware@2325

@ag-ui/a2ui-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-middleware@2325

@ag-ui/event-throttle-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/event-throttle-middleware@2325

@ag-ui/mcp-apps-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-apps-middleware@2325

@ag-ui/mcp-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-middleware@2325

@ag-ui/a2a

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a@2325

@ag-ui/adk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/adk@2325

@ag-ui/ag2

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/ag2@2325

@ag-ui/agno

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/agno@2325

@ag-ui/aws-strands

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/aws-strands@2325

@ag-ui/claude-agent-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-agent-sdk@2325

@ag-ui/claude-managed-agents

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-managed-agents@2325

@ag-ui/crewai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/crewai@2325

@ag-ui/langchain

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langchain@2325

@ag-ui/langgraph

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langgraph@2325

@ag-ui/llamaindex

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/llamaindex@2325

@ag-ui/mastra

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mastra@2325

@ag-ui/pydantic-ai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/pydantic-ai@2325

@ag-ui/vercel-ai-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/vercel-ai-sdk@2325

@ag-ui/watsonx

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/watsonx@2325

@ag-ui/a2ui-toolkit

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-toolkit@2325

create-ag-ui-app

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/create-ag-ui-app@2325

@ag-ui/client

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/client@2325

@ag-ui/core

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/core@2325

@ag-ui/encoder

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/encoder@2325

@ag-ui/proto

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/proto@2325

commit: 7f086b8

mme and others added 4 commits August 5, 2026 18:36
Node versions were declared in several places and didn't agree. Ten
`actions/setup-node` invocations had "22" typed into them and two read
`package.json`, whose `engines.node` range (">=18") resolves to whatever
the newest satisfying Node happens to be — so those two jobs silently ran
a different major from the other ten. The ten agreed with each other by
luck, and no file said which Node this repository is developed and tested
with.

Add `.node-version` at the root containing 22, and point all 12
invocations across 8 workflows at it via `node-version-file`. None names
a version inline anymore.

Every one of the 12 already checks the repository out earlier in the same
job, and no checkout uses `sparse-checkout`, a `path:` subdirectory or a
container, so the file is at the workspace root when `setup-node` runs —
which is where it resolves a relative `node-version-file` from,
regardless of any step's `working-directory`. A bare major is a valid
`.node-version`: setup-node reads it as a semver range and resolves the
latest 22.x.

One consequence worth knowing: a prerelease dispatch of
publish-release.yml against a ref that predates this commit will fail at
`setup-node`, because the file it now reads is not in that tree.
publish-commit.yml already had that property via `package.json`.

`package.json#engines.node` is untouched. That manifest is private and
never published, so its range is a floor on installs in this repository
rather than anything consumers see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both jobs ran `corepack enable` alongside `pnpm/action-setup`, which
already resolves `packageManager` from package.json. Two mechanisms were
installing the same pnpm 10.33.4, and which one won depended on step
order.

It used to run before `setup-node`, so its shims landed in the runner
image's Node bin directory and were shadowed the moment `setup-node`
prepended the toolcache Node — making the corepack step dead code and
leaving `pnpm/action-setup` in charge. Moving it after `setup-node` would
have inverted that: the shims would land in the Node 22 bin directory and
win, making `pnpm/action-setup` inert instead, and adding a registry
fetch to a job whose pnpm binary was already on disk.

Neither arrangement is worth keeping, so remove the step.
`pnpm/action-setup` self-installs pnpm and puts it on PATH, and resolves
its version from `packageManager` when given no `version:` input, which is
the case here. Node 22's bin directory ships `corepack` and `npm` but no
`pnpm` shim unless `corepack enable` creates one, so `pnpm` in the later
steps resolves to the action's binary. The repository's other ten
setup-node jobs already work exactly this way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pointing the 12 call sites at one file fixes today. Without a guard, the
next site someone adds re-creates the problem, because copying an existing
step and typing a version into it is the natural way to add one. No
existing tool has an opinion here — actionlint and zizmor do not care
which Node you select.

Add `.github/scripts/check-toolchain-consistency.sh`, which fails on a
`setup-node` that names a version inline, reads the wrong version file, or
has no version input, and on a `.node-version` that is missing, empty or
not 22. Run it in CI from a new Toolchain Consistency workflow, fixtures
first — a check that no longer discriminates is worse than no check,
because it reports green either way.

Version inputs are attributed to the `setup-node` step they are written
in, by indentation. Counting matching lines file-wide was the obvious
shortcut and it fails open: any stray `node-version-file:` line — in a
`run:` heredoc, in another action's `with:` — pays for a step that has no
version input, and the check goes green on real drift. An inline
`node-version` is reported even when a correct `node-version-file` sits
beside it, because setup-node gives the inline value precedence, so that
is the version the step actually runs.

Reading nothing and finding nothing are kept distinct, which is where a
guard like this usually rots. Each of these is reported rather than passed
over: an unreadable file; an absent `.github/workflows`; a directory whose
contents cannot be listed; a directory holding no workflows; an entry that
is not a regular file, including a dangling symlink; a file with no YAML
keys the scan can read, such as one in the wrong encoding; and a scan that
did not run to completion, detected by the absence of its own end-of-file
record. The success message states how many invocations across how many
files it examined, so a green result cannot mislead by omission.

Cover it with 24 cases over 22 fixture roots — miniature repositories the
check is pointed at via its optional root argument. Each failing case
asserts the message AND the exact problem count; each passing case asserts
the coverage numbers, not just the success sentence. Without the first, a
check that fails for the wrong reason passes; without the second, one that
also reports spurious problems passes; without the third, one that quietly
stops seeing steps or whole files keeps printing success with smaller
numbers. Both helpers require their fixture to exist, since
`missing-version-file` is defined by an absent file.

Fixtures were chosen against mutants that survived earlier drafts: drift
in the second workflow file (a scan that stops after the first), drift in
the last step (only an end-of-file flush evaluates it), drift written in
the repository's own SHA-pinned `# v7.0.0` notation (a scan that skips any
line with a comment), a correct step followed by a bare one (per-step state
that is not reset), a lone-dash sequence entry, a quoted `"node-version"`
key, the action's name appearing as data inside a `run:` block, and an
action whose name merely starts the same. Each of those made a real
green-while-broken hole before it had a fixture.

It is a line scan, not a YAML parse, and the header says so along with
what that costs. Verified: 24/24 on this tree; 13 findings against the
parent tree (the 12 drifted call sites plus the absent root file); and
every rule's report, when removed, makes the suite go red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add a Toolchain section to CONTRIBUTING.md covering which version
managers read `.node-version` and how: fnm resolves a bare major, nvm
reads only `.nvmrc` so the version has to be passed through, and nodenv
and asdf resolve the file to an exactly-installed version — asdf only with
`legacy_version_file = yes`.

Point the two repo-development docs that said "Node 20+" at the file
rather than at a number. Naming the version in prose would have added two
more places to update on every bump, and nothing checks prose.

State the check's scope honestly rather than letting its name imply more
than it does: Node in `.github/workflows/` only. pnpm is not guarded, and
9 of 11 `pnpm/action-setup` steps pin `version:` inline while 2 rely on
`package.json#packageManager` — the same drift shape, still unguarded,
recorded here rather than fixed under a Node ticket.

Document the bump, which touches more than the obvious file: the constant
in the check moves with `.node-version`, and so do the fixture pins and
the expected-message string in the test suite, while
`wrong-pinned-version` has to stay on a different major. Then
publish-release.yml's npm-OIDC workaround needs review, because it exists
only to work around the npm that Node 22 bundles.

Correct what `engines.node` is while describing it. The root manifest is
`private: true` and never published, so its `>=18` is a floor on installs
in this repository — and it is what `node-version-file: "package.json"`
resolved before this change, making it a toolchain input rather than a
consumer promise. Of the published packages only `@ag-ui/watsonx` and
`@ag-ui/claude-managed-agents` declare a range at all, both `>=20.3.0` and
set independently; the six core packages declare none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mme
mme force-pushed the markus/pni-186-standardize-the-ag-ui-repository-on-node-22 branch from f6823df to 7f086b8 Compare August 5, 2026 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants