Skip to content

P1: Upgrade to graphql-yoga 5 and Node 22 LTS (#176) - #194

Open
dkijania wants to merge 2 commits into
mainfrom
feat/yoga5-node22
Open

P1: Upgrade to graphql-yoga 5 and Node 22 LTS (#176)#194
dkijania wants to merge 2 commits into
mainfrom
feat/yoga5-node22

Conversation

@dkijania

Copy link
Copy Markdown
Contributor

What & why

Part of the production-readiness epic (#163). Closes #176.

Key server deps were a major behind. This brings them current:

  • graphql-yoga 4 → 5
  • @envelop/core 4 → 5, @envelop/graphql-jit 6 → 11, @envelop/disable-introspection 5 → 9, @envelop/opentelemetry 5 → 9
  • Node 20 → 22 LTS across the Dockerfile, Volta pin, and the lint / unit-test / publish / smoke-load workflows

Code impact

Minimal — a single targeted cast: @envelop/opentelemetry@9 types its provider argument against a duplicate @opentelemetry/api copy, so provider is cast to the exact parameter type useOpenTelemetry expects. Everything else compiled unchanged.

Verification

  • npm run build — clean
  • npm run test:unit — all pass
  • npm run lint / npx prettier --debug-check . — clean
  • Built the node:22 image locally — builds and runs v22.23.1
  • (The lightnet Run-Tests + Docker build-and-deploy in CI are the full integration proof.)

Follow-up (not in this PR)

The OpenTelemetry SDK is intentionally not bumped — modern OTel drops the Jaeger exporter for OTLP, so clearing the remaining @opentelemetry/* audit highs is a separate Jaeger→OTLP migration. With Yoga on 5, the graphql-armor meta package (#164 currently uses the individual sub-plugins due to the old envelop-4 peer) could also be revisited.

🤖 Generated with Claude Code

Bring the server stack onto current majors:

- graphql-yoga 4 → 5, @envelop/core 4 → 5, @envelop/graphql-jit 6 → 11,
  @envelop/disable-introspection 5 → 9, @envelop/opentelemetry 5 → 9.
- Node 20 → 22 LTS across the Dockerfile, Volta pin, and the lint / unit-test /
  publish / smoke-load workflows.

The only code change required is a targeted cast where useOpenTelemetry now types
its provider argument against a duplicate @opentelemetry/api copy; the cast
targets the exact expected parameter type. Build, unit tests, lint, and prettier
all pass; the node:22 image builds and runs v22.x locally.

Note: the OpenTelemetry SDK is intentionally not bumped here — the modern OTel
packages drop the Jaeger exporter in favour of OTLP, so clearing the remaining
@opentelemetry/* audit advisories is a separate Jaeger→OTLP migration.

Closes #176.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSuak9smCHbp4N17xjjLF6
@dkijania dkijania added production-readiness Work toward making the API production-ready / publicly available P1 Strongly recommended before GA labels Jun 28, 2026
@SanabriaRusso

Copy link
Copy Markdown
Collaborator

Nice upgrade — clean and minimal (great that it came down to a single typed cast for the OTel provider), and thanks for the thorough build/lint/image verification.

Since this is the one change in the production-readiness train that could subtly affect the mina-explorer client, I verified the highest-risk axis empirically: yoga 5's GraphQL-over-HTTP status codes. The Explorer's archive client throws on any non-2xx before it reads the GraphQL body (if (!response.ok) throw 'HTTP error…' in client.ts), then keys its graceful-degradation on the exact Cannot query field message — so if yoga 5 had started returning 400 for validation errors, the Explorer's fallback chains (transactions.ts:488/660/909) would break into blank sections.

Good news — it's safe. I ran both 4.0.4 and 5.21.2 with the Explorer's exact request shape (POST, Content-Type: application/json, no Accept header):

yoga 4.0.4 yoga 5.21.2
Cannot query field validation error HTTP 200 {errors:[…]} HTTP 200 {errors:[…]}
resolver throws (masked) HTTP 200 "Unexpected error." HTTP 200 "Unexpected error."

Both only switch to 400 under Accept: application/graphql-response+json, which the Explorer never sends. The error message strings are byte-identical (5.x just adds an additive extensions.code, which the Explorer ignores), and CORS / /healthcheck behave identically — so no client impact.

One suggestion to keep it safe: the 200-on-validation-error behavior is an implicit content-negotiation default, so a future yoga bump could flip it unnoticed. A tiny integration test alongside the existing ones would pin the Explorer contract:

// the exact shape the mina-explorer client sends (no Accept header)
const res = await fetch(endpoint, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ query: '{ __definitelyNotAField }' }),
});
assert.equal(res.status, 200);                                // must NOT be 400
const body = await res.json();
assert.match(body.errors[0].message, /Cannot query field/);   // Explorer fallback keys on this

Minor nit while here: @types/node stays ^20.5.7 even though the runtime moves to 22 — worth bumping to ^22 so the types match the runtime.

@SanabriaRusso

Copy link
Copy Markdown
Collaborator

One more small thing, on the Docker side: this bumps both stages to a floating node:22-alpine tag, which would drop the @sha256 digest pin that #189 adds for reproducible / tamper-evident builds. If #189 lands first, this quietly reverts that hardening. Worth pinning the 22 image by digest here too, so the two changes don't fight:

FROM node:22-alpine@sha256:<digest> AS build
# ...
FROM node:22-alpine@sha256:<digest>

(Once #192 lands, its Dependabot docker ecosystem will keep that digest fresh automatically.)

Pins both Dockerfile stages to node:22-alpine@sha256:16e22a55… (v22.23.1,
verified by pull and build). Without this, moving to a floating 22 tag
would silently drop the digest pin #189 adds, reverting its
reproducible/tamper-evident build hardening depending on merge order.
Once #192 lands, Dependabot's docker ecosystem keeps the digest fresh.

Bumps @types/node to ^22 so the types match the runtime rather than
staying on 20.

Adds tests pinning the GraphQL-over-HTTP behaviour the mina-explorer
depends on: validation errors return 200 (its client throws on non-2xx
before reading the body) and carry the literal "Cannot query field" (its
fallbacks key on that string). Both were verified by hand across the
4 → 5 upgrade and are unchanged, but they rest on an implicit
content-negotiation default — yoga only returns 400 under an Accept
header that client never sends — so a later bump could flip them
unnoticed. Now a standing guard rather than a one-time check.

Addresses review feedback on #194.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dkijania

Copy link
Copy Markdown
Contributor Author

Thanks @SanabriaRusso — and thank you for actually running both versions against the Explorer's request shape rather than reasoning about it. All three done in 20cf867.

Digest pin. Both stages now pin node:22-alpine@sha256:16e22a550f3863206a3f701448c45f7912c6896a62de43add43bb9c86130c3e2. Resolved from the registry, then verified by pulling it and building the image: reports v22.23.1, builds clean. Your point was the important one — without this, whichever of #189/#194 landed second would silently undo the other, and a reverted digest pin is the kind of thing that reads as "still pinned" in review. Comment on both stages says to bump them together, and #192's Dependabot docker ecosystem will keep it fresh once it lands.

@types/node^22 (22.20.1). Typecheck and build clean against it.

The contract test. Added tests/unit/yoga-http-contract.test.ts, pinning both halves of what you verified — 200-not-400 on validation errors, and the literal Cannot query field — using the Explorer's exact shape (POST, JSON, no Accept header). Independently reproduces your result on yoga 5.21.2. It's a unit test via yoga.fetch rather than a full integration test, which needs no server and runs on every PR; buildYoga doesn't exist on this branch yet (it arrives with #195), so it constructs yoga directly.

Your reasoning for why it's worth pinning is in the file: the 200 is an implicit content-negotiation default, so nothing about a future bump would announce a flip. This turns "someone checked once" into a standing guard — and since this PR is the upgrade itself, the guard belongs here rather than downstream.

SanabriaRusso added a commit that referenced this pull request Jul 30, 2026
Closes #206.

## Problem

The `v0.0.9` tag push failed at the **`Update npm`** step — [run
30540591005](https://github.com/o1-labs/Archive-Node-API/actions/runs/30540591005)
— before dependencies were installed, before tests, and before `npm
publish`:

```
npm error code EBADENGINE
npm error Not compatible with your version of node/npm: npm@12.0.2
npm error notsup Required: {"node":"^22.22.2 || ^24.15.0 || >=26.0.0"}
npm error notsup Actual:   {"npm":"10.8.2","node":"v20.20.2"}
```

`npm@latest` has moved to 12.0.2, which dropped Node 20. The job pins
`node-version: '20'`, so the install is refused. Nothing was published —
npm still shows only `0.0.6`.

This is an upstream change, not a repo regression: it breaks **any** tag
pushed from today onward.

## Change

```diff
-      # Ensure npm 11.5.1 or later is installed
       - name: Update npm
-        run: npm install -g npm@latest
+        run: npm install -g npm@11
```

`npm@11` is currently 11.19.0, engines `^20.17.0 || >=22.9.0` —
satisfied by the job's Node 20.20.2, and well past the 11.5.1 that
trusted publishing / OIDC requires. Pinning the major keeps patch
updates flowing without another silent engine break; the unqualified
`latest` is what made this a time bomb.

## Why not bump Node to 22

More future-proof, but wider: the publish job also runs the full `npm
test`, so changing its Node version changes the runtime the release is
validated against. #194 (`P1: Upgrade to graphql-yoga 5 and Node 22
LTS`) is already open and is the right place for that. Once it lands,
this pin can be revisited.

## Verification

This workflow only runs on `v*` tag pushes and `workflow_dispatch`, so
PR CI cannot exercise it. The engine claim is checked directly against
the registry:

```
$ npm view npm@11 version   ->  11.19.0
$ npm view npm@11 engines   ->  { node: '^20.17.0 || >=22.9.0' }
$ npm view npm@12 engines   ->  { node: '^22.22.2 || ^24.15.0 || >=26.0.0' }
```

Node 20.20.2 satisfies `^20.17.0` and does not satisfy `^22.22.2`, which
is exactly the observed pass/fail split. Real proof comes from the
retriggered `v0.0.9` publish after this merges.

## Follow-up

`v0.0.9` is tagged at `3d254f7` but published nothing. Re-running the
failed run will not help — a tag-push event uses the workflow file as it
exists at the tagged commit, which still has the broken step. The tag
needs to be moved to the commit containing this fix and re-pushed. Since
no npm version was consumed, that is a clean operation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P1 Strongly recommended before GA production-readiness Work toward making the API production-ready / publicly available

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P1: Dependency upgrades — graphql-yoga 4→5, Node 20→22 LTS

2 participants