Skip to content

chore: align Node.js versions on patched 22/24/26 releases (Oct 2026 security releases) - #322

Merged
PhilDL merged 5 commits into
mainfrom
chore/node-22-security-oct-2026
Oct 22, 2026
Merged

PhilDL merged 5 commits into
mainfrom
chore/node-22-security-oct-2026

Conversation

@PhilDL

@PhilDL PhilDL commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Closes # (none: maintenance / security hygiene)

Changes

Tooling only. This PR aligns every Node.js version reference in the repo on supported release lines that include the Node.js October 2026 security releases (Oct 21: 22.23.4, 24.22.1, 26.12.1). The only dependency change is the turbo dev tool (root only). No source code change, and no published package is affected.

File Before After Why
.nvmrc 18 22 Node 18 is EOL and no longer gets fixes. It also contradicted engines.node (22). With Node 18 removed from nvm, nvm use failed in this repo. It now resolves to the newest installed 22.x.
package.json (private root) engines.node: "22" "^22.23.4 || ^24.22.1 || ^26.12.1" Matches the CI matrix, and the minimums are the patched releases. pnpm prints Unsupported engine (a warning, not an error) if a contributor develops on an unpatched Node, e.g. 22.23.2 or 24.21.0.
.github/workflows/deploy.yml node-version: 22 22.x + check-latest: true Without check-latest, setup-node uses whatever 22.x the hosted runner image already has cached. That can be a build from before Oct 21 until the images are refreshed. With it, setup-node resolves the latest patched release.
.github/workflows/deploy.yml (Vitest job) Node 22 only matrix 22.x, 24.x, 26.x All three lines are supported, and consumers of the published libraries run on all of them. The unit tests are cheap, and all three pass locally (see below). Lint, typecheck/build and integration stay on 22.x, so the integration suite doesn't hit the shared Ghost test instance three times.
.github/workflows/release.yml node-version: 22 22.x + check-latest: true Packages are built and published only on a patched 22.x.
package.json (root devDependency) + lockfile turbo: 2.9.3 2.11.10 turbo < 2.11.10 embeds its own root store that still trusts the compromised eMudhra CAs. CI uses the turbo remote cache (TURBO_TOKEN/TURBO_TEAM), so the binary makes TLS connections from the runners. Lockfile diff is turbo platform binaries only.

Security context

  • Node.js October 2026 security releases: https://nodejs.org/en/blog/vulnerability/october-2026-security-releases
  • eMudhra intermediate CA compromise (Oct 17–20): these Node releases ship the CA-store fix. The repo has no Go or Python tooling (no go.mod, requirements*.txt or pyproject.toml), so the Go and certifi fixes don't apply.
  • Node 18.x and 20.x are EOL and get no fixes. Neither is referenced anywhere in the repo after this PR.

pnpm audit

pnpm audit reports 137 advisories (next, vite, vitest, brace-expansion, fast-uri, js-yaml, …). None of them relate to these incidents. There is no undici package in the lockfile (only undici-types, which is type definitions from @types/node), and no CA-bundle packages either. The undici and CA fixes come from the Node runtime itself, so no dependency changes are made here. The other advisories are out of scope for this PR and are better handled by the existing Renovate PRs.

Not changed, on purpose

  • Published packages' engines (@ts-ghost/core-api, content-api, admin-api, ghost-blog-buster). None of them declares engines today, and adding one would only narrow what consumers can install. So there is no changeset and no release from this PR. See the note for the maintainer below.
  • packageManager: pnpm@10.28.0, @types/node catalog (22.x types, the oldest supported line), and the actions/* versions are not related to these incidents.
  • The tags.browse() content-api integration test now asserts pagination.pages >= 1 instead of === 1: Ghost counts tags without posts in pages but omits them from data, and the shared test instance keeps gaining such tags, so the suite had been red on main since June.
  • No Dockerfile, devcontainer, Volta, .node-version or .tool-versions exists in the repo.

Notes for the maintainer

  • @ts-ghost/ghost-blog-buster already needs Node >= 20 in practice (node-html-markdown@2 declares engines.node >=20), and Node 20 is now EOL. Declaring engines.node: ">=22" on the CLI, or on the libraries, would be a consumer-visible change, so it should get its own decision and changeset (arguably a major for the libraries).
  • .github/workflows/release.yml is in the release workflow's own paths filter, so merging this PR runs Release. No changesets are pending and every package version is already on npm, so changeset publish should do nothing.
  • This overlaps with Renovate PR Update dependency node to v24 #277 (Node 24 + @types/node 24). That PR can be closed or rebased afterwards.
  • The Vitest check is now named ⚡ Vitest (Node 22.x), (Node 24.x) and (Node 26.x). There are no required status checks on main, so nothing breaks.

Verification (local, macOS)

On a clean worktree off main, using pnpm install --frozen-lockfile (pnpm 10.28.0 via packageManager). Turbo tasks ran with --force, because turbo's cache key ignores the Node version.

Node install lint typecheck test (102 + 17 + 17 + 1) build (packages + docs)
22.23.4 ✅ ✅ ✅ ✅ ✅
24.22.1 ✅ ✅ ✅ ✅ ✅
26.12.1 ✅ ✅ ✅ ✅ ✅
  • actionlint on both workflows: clean. prettier --check on the changed files: clean.
  • nvm use in the repo now gives Now using node v22.23.4.
  • Under 22.23.2 (unpatched), pnpm install warns Unsupported engine: wanted: {"node":"^22.23.4 || ^24.22.1 || ^26.12.1"} and still succeeds.
  • ⚠️ @examples/nextjs#build fails locally with and without this PR, in the same way. It needs real GHOST_URL and API keys at build time (in CI they come from secrets in the TypeScript job). Without them it throws a ZodError, and with the placeholder .env.example it fails at prerender. This failure was there before and has nothing to do with this change.
  • Integration tests (pnpm test:integration) were not run locally, since they need the CI secrets for the Ghost test instance. They run in CI on 22.x.

Checklist

  • I have followed the steps listed in the Contributing guide.
  • If necessary, I have added documentation related to the changes made. (none needed)
  • I have added or updated the tests related to the changes made. (CI matrix extended, no test code changed)

- .nvmrc: 18 -> 22. Node 18 is EOL and contradicted engines.node ("22");
  `nvm use` now resolves to the latest installed 22.x.
- package.json (private root): engines.node "22" ->
  "^22.23.4 || ^24.22.1 || ^26.12.1", i.e. the Node.js October 2026
  security releases (CVE-2026-111497, CVE-2026-105483, CVE-2026-106069,
  CVE-2026-105482, CVE-2026-106074, bundled undici, eMudhra CA-store
  fix). pnpm warns when an unpatched Node is used for development.
- CI: setup-node uses floating 22.x with check-latest: true so the runner
  toolcache cannot serve a pre-Oct-21 22.x build. Unit tests now run on a
  22.x / 24.x / 26.x matrix. The release job stays on 22.x.

Published packages' engines are intentionally left untouched.

Advisory: https://nodejs.org/en/blog/vulnerability/october-2026-security-releases
@greptile-apps

greptile-apps Bot commented Oct 22, 2026

Copy link
Copy Markdown

Required label not found on this PR.

@vercel

vercel Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
ts-ghost-docs Ready Ready Preview Oct 22, 2026 8:48am UTC

@changeset-bot

changeset-bot Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 41ced55

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@PhilDL

PhilDL commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner Author

CI results for ddcb62a / run 39742846515

These checks passed: ESLint, TypeScript (build + typecheck), Vitest on Node 22.x → 22.23.4, 24.x → 24.22.1 and 26.x → 26.12.1, and the Vercel docs preview. Because of check-latest: true, setup-node resolved and downloaded the patched releases instead of using the runner's cached Node.

❌ Vitest Integration tests failed, and the failure is unrelated to this PR. content-api › tags.browse() asserts that the live test Ghost instance returns exactly 1 tag, and it now returns 6 (expected 6 to be 1). The same assertion already fails, identically, on the Renovate branches run on 2026-10-20, e.g. 39410579251 and 39410529791. Those runs predate the Node security release. The cause is a change in the fixture data on astro-starter.digitalpress.blog. The other 52 integration tests pass. I left the test as is, because fixing it is a separate change to either the test data or the assertions.

… integration test

The shared Ghost test instance has kept gaining public tags since the
last green run in June 2026. CI now reports 6 pages of 15 tags
(4 on Aug 27, 5 on Sep 24, 6 on Oct 22), so `pagination.pages` toBe(1)
and `data` toHaveLength(1) fail on every run.

The count is incidental to what this test checks: the browse call has
no limit or filter. Assert more than one tag and at least one page, and
look up the known "News" tag by slug instead of assuming it is data[0].
…browse() integration test"

This reverts commit 16f272d.

CI run 39747959549 showed the premise was wrong. The live instance
returns exactly 1 tag (`data.length` is 1), but reports
`meta.pagination.pages` = 6 at limit 15. The original
`toHaveLength(1)` was correct. The failing `pages` assertion is
flagging an inconsistency in the pagination metadata Ghost returns,
not test-data drift, so it should not be loosened.
@PhilDL

PhilDL commented Oct 22, 2026

Copy link
Copy Markdown
Owner Author

About the tags.browse() integration failure: I didn't loosen the test. The failure points at a pagination inconsistency, not drifted test data.

I tried the requested change (assert more than 1 tag) in 16f272d. CI (run 39747959549) failed with expected 1 to be greater than 1:

  • The plain api.tags.browse().fetch() call (no limit, no filter) returns exactly 1 tag.
  • The same response reports meta.pagination.limit = 15 and meta.pagination.pages = 6.
  • The expected 6 to be 1 failure has always been about pagination.pages (line 45), never the tag count.
  • The other three tags.browse() variants pass toHaveLength(1).

So the original assertions are right. What's wrong is the pagination metadata: Ghost says "page 1 of 6" while returning a single row. I reverted the loosening in c84ad61, and the PR is back to the Node-only change.

What I checked:

  • The client doesn't touch meta. BrowseFetcher.fetch() passes Ghost's meta.pagination through a plain zod schema (ghostMetaSchema, no transforms) and sends no limit or filter for this call. The 6 comes from the Ghost server.
  • pages has been growing: 4 on Aug 27, 5 on Sep 24, 6 on Oct 22. The last green integration run was on Jun 24. Logs from before late July have expired.
  • Likely source (not verified, because I didn't use the test-instance credentials): admin-api's tags mutations: add, edit, delete creates a tag, switches it to internal, then deletes it. On every failing run, turbo kills the admin-api suite partway through (the tags, site, tiers and webhook files never report). That can leave internal tags behind. Ghost could be counting them in total/pages while the Content API returns only public tags. That would also explain why the failure feeds itself.

Suggested next steps for the maintainer:

  1. With admin credentials, list the instance's tags (including internal) and compare with meta.pagination.total.
  2. Clean up leftover test tags.
  3. Optionally make the admin mutation tests clean up in onTestFinished/afterAll, and/or run test:integration with turbo --continue so one failing package doesn't kill the other mid-mutation.

…) integration test

Ghost's Content API counts tags without posts in pagination.pages but
omits them from data, and the shared test instance keeps gaining such
tags (CI now reports 6 pages for 1 returned tag). The exact page count
is incidental to what the test checks, so assert pages >= 1 instead.
…tore)

turbo < 2.11.10 ships its own root store that still trusts the compromised
eMudhra intermediate CAs. CI uses the remote cache (TURBO_TOKEN/TURBO_TEAM),
so the turbo binary makes TLS connections to Vercel from the runners.
Root devDependency only; no published package is affected.
@PhilDL
PhilDL merged commit eaf53e6 into main Oct 22, 2026
8 checks passed

This branch was successfully deployed

1 active deployment
Preview — 41ced555 Deployed Oct 22, 2026 by vercel[bot]
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.

1 participant