Move every Node.js service from Node 16 (EOL) to Node 20 (active LTS).
For each repository:
1. Update `package.json`:
- `"engines": { "node": ">=20.0.0" }`
- Bump any dependency with a hard Node 20 floor (e.g., `eslint` 9, `vite` 5,
`typescript` 5.x)
2. Update Dockerfiles:
- `FROM node:16-*` → `FROM node:20-*` (preserve the variant suffix:
`-alpine`, `-slim`, `-bookworm`)
- Apply to every `FROM` in multi-stage builds
3. Update CI workflows in `.github/workflows/*.yml`:
- `actions/setup-node@v4` with `node-version: '20'`
- Update matrix entries that pin `16.x` to `20.x`
4. Update `.nvmrc` and `.node-version` files to `20`.
5. Patch known breaking-change call sites:
- `crypto.createCipher` / `createDecipher` (removed) →
`createCipheriv` / `createDecipheriv`
- `url.parse` (deprecated) → `new URL(...)`
- `fs.rmdir(..., { recursive: true })` → `fs.rm(..., { recursive: true, force: true })`
- Replace `node-fetch` with the global `fetch` where the codebase no longer
needs streaming workarounds
6. Run `npm ci && npm test` (or yarn / pnpm equivalent). Surface failing repos in
a triage list. The Problem
Node 16 reached end-of-life in September 2023. Every service still on it is unsupported, blocked from current versions of `eslint`, `vite`, `typescript`, and most modern tooling, and accumulating a steady security-vulnerability backlog. Worse, the runtime is encoded in at least four different places per repo (`engines` in `package.json`, the base image in every Dockerfile `FROM`, the `node-version` in every GitHub Actions workflow, and `.nvmrc` / `.node-version` files for local dev) and they're never quite in sync.
Bumping to Node 20 also surfaces a handful of breaking-change call sites: `crypto.createCipher` was removed, `url.parse` is deprecated, `fs.rmdir({ recursive: true })` no longer accepts recursive directories. Most repos have one or two of these; some have a dozen. Finding them, patching them, and verifying the test suite still passes (across hundreds of repos with slightly different Docker layouts and CI patterns) is the kind of tedious cross-cutting work that defeats manual migration efforts.
What Tidra Does
- Updates
package.jsonengines.nodeto>=20.0.0and bumps any dependency with a hard Node 20 floor (eslint9,vite5, currenttypescript) - Rewrites every
FROM node:16-*in Dockerfiles toFROM node:20-*, preserving the variant suffix (-alpine,-slim,-bookworm) across multi-stage builds - Updates
.github/workflows/*.yml,actions/setup-nodeversions,node-versionkeys, and matrix entries pinned to 16.x - Bumps
.nvmrcand.node-versionfiles to20 - Patches known breaking-change call sites (
crypto.createCipher→createCipheriv,url.parse→new URL(...),fs.rmdir→fs.rm) and removesnode-fetchwhere globalfetchsuffices - Runs the test suite per repo and surfaces failing repos in a triage list, leaving complex failures for human review
Before & After
Customization Tips
- Target Node version: Node 20 is current LTS; Node 22 is the next active LTS. Pick once for the org. Mixing 20 and 22 across services causes the same drift you're trying to eliminate.
- Native module rebuilds: Repos using native modules (
sharp,node-gyp,bcrypt) may need their lockfiles regenerated for prebuilt binaries that match the new Node ABI. - Dependency floor scope: Decide whether to also bump
eslint9,typescript5.x, and other tools to their current majors as part of the runtime bump, or stage them as follow-ups to keep diffs reviewable. - Test coverage gate: Require
npm testto pass per repo before merging. Repos with low coverage should also get a manual smoke test of the entry point. Runtime bumps surface latent bugs the tests miss.