Recipes

Vitest 5 업그레이드, Node 22 CI 마이그레이션 및 lockfile 감사

README 링크에서 발견 · today

Software & DevOpsWork & Operations

vitest를 5로 올리고, 테스트 CI job을 Node 22로 옮겼으며, lockfile을 검증했습니다.

What they did

작성자는 vitest를 4.1.12에서 5.0.5로만 올린 dependabot PR #101을 인수했다. lockfile의 vitest 항목(5.1.0)을 읽어보니 vitest 5는 node ^22.12.0 || ^24.0.0 || >=26.0.0을 선언하고 있었고, 반면 단위 테스트를 실행하는 유일한 job(ci-guardrails.yml)은 Node 20에서 돌고 있었다. 그래서 이 변경은 하나의 커밋에 세 파일로 정리됐다: package.json(vitest ^4.1.11 -> ^5.0.5, 그리고 새로운 engines 필드), npx npm@11 install --package-lock-only --ignore-scripts로 재생성한 package-lock.json, 그리고 ci-guardrails.yml 27번째 줄의 node-version 20 -> 22. 이어서 .github/workflows의 모든 파일을 vitest, npm test, npm run test, yarn/pnpm test, npx vitest|vite로 grep 감사했고, vitest를 실행하는 곳은 ci-guardrails.yml뿐임을 확인했기에 나머지 Node 20 job들은 의도적으로 그대로 두었다. lock에서 peer 범위를 확인했는데(vite는 vitest 5의 peer 범위 안에서 6.4.5 유지, @types/node ^26.6.6은 범위 내, better-auth의 optional peer는 vitest 5.1.0으로 dedupe되고 npm ls도 깨끗함) 문제가 없었다. Node 20.20.2에서 main과 브랜치에 대해 npm ci --legacy-peer-deps --ignore-scripts --dry-run을 실행해, 남은 네 개의 Node 20 job이 실패가 아니라 EBADENGINE 경고만 낸다는 것을 보여주었다(main에서 13개, 브랜치에서 15개). 검증은 브랜치 lockfile을 대상으로 실행됐다: npx vitest run은 main에서 vitest 4.1.12와 Node 26.7.0으로 11개 파일 / 174개 테스트 통과를 보였고, 브랜치에서는 vitest 5.1.0으로 Node 26.7.0과 Node 22.23.2 모두에서 동일한 174개가 통과했으며, --reporter=verbose로 아무것도 누락되지 않았음을 확인했다. Node 22.23.2에서 CI job의 환경 변수와 순서로 guardrails:check, typecheck, vitest, lint(0 errors, 손대지 않은 파일의 기존 경고 2개) 그리고 build를 실행했고 모두 통과했다. 또한 열려 있는 두 개의 Stripe dependabot 브랜치를 임시 worktree에서 시험 병합해 병합 순서가 무관함을 입증했고, 변경된 lock 항목 59개를 문서화했다.

How it went

단일 커밋 브랜치(deps/vitest-5-node-22)와 상세한 PR 설명: 174개 테스트가 Node 22와 26 모두에서 vitest 5로 통과하고, Node 22에서 typecheck, lint, build, guardrails가 통과하며, npm ci는 ERESOLVE 없이 종료 코드 0으로 끝나고, npm audit 총계는 변동 없음(25건, high 또는 critical 없음). 작성자는 검증하지 못한 부분을 명시적으로 표시했다: Vercel 프로젝트의 Node.js Version 대시보드 설정, 그리고 engines.node가 이를 재정의하는지 여부인데, 웹 검색 도구가 거부되었고 AGENTS.md가 에이전트의 Vercel 워크플로 실행을 금지하기 때문이다. 이후 업데이트에서 병합된 Stripe PR #108 위로 리베이스하고 리뷰 후 engines.node를 >=22.12 <23으로 강화했다.

Worth knowing

Vercel Node 버전은 작성자가 읽을 수 없는 대시보드 설정이므로 engines.node의 배포 측 효과는 검증되지 않았다. 소유자가 22.x/24.x를 확인하고 preview 빌드 로그를 점검해야 한다. 실제 의존성 하한은 선언된 >=22.12보다 높다: e2b, @e2b/code-interpreter, 그리고 중첩된 undici 8.12.2가 >=22.19.0을 선언한다. 나머지 네 개의 워크플로는 여전히 Node 20에 머물러 있으며 이미 여러 의존성이 선언한 버전보다 낮게 실행되고 있다. bun.lock은 손대지 않은 채 남겨졌고 이미 package.json과 어긋나 있다. Turbopack dev-server 스모크 단계와 ci-smoke.yml은 로컬에서 실행되지 않았다. 브랜치는 upstream 없이 로컬에만 커밋되었으므로 이름으로 push해야 한다.

Wiring

순서가 중요합니다: 설치된 패키지나 lock 엔트리에서 vitest 5의 선언된 engines를 먼저 읽어야 합니다. 이것이 engines 필드와 어떤 CI job을 옮겨야 하는지를 모두 결정하기 때문입니다. package.json을 편집하고(vitest 범위와 engines), 그다음 npm 11로 package-lock-only 모드에서 package-lock.json을 재생성하여 설치 스크립트가 실행되지 않도록 합니다. 어떤 node-version도 건드리기 전에 워크플로를 grep하여 실제로 vitest를 호출하는 job을 매핑해서, 무관한 Node 20 job은 그대로 유지되도록 합니다. Peer 호환성은 lock에서 확인합니다: vite가 vitest 5의 peer 범위를 충족해야 하고(기존 overrides.vite ^6.4.2가 충족함), @types/node가 해당 범위를 충족해야 하며, npm ls에서 better-auth에 대한 invalid peer가 없어야 합니다. 검증은 브랜치 lockfile에 대해 npm ci --ignore-scripts(및 CI 플래그 --legacy-peer-deps)로 실행한 뒤 두 Node 메이저에서 vitest를 실행하고, 그다음 대상 메이저에서 전체 CI 단계 시퀀스를 실행합니다. Vercel 쪽은 별도입니다: vercel.json에는 crons만 있고 .nvmrc나 .vercel 디렉터리가 없으므로, Node 버전은 대시보드에 있으며 소유자가 preview 빌드 로그와 대조하여 확인해야 합니다.

Try it yourself

I can't carry out this upgrade in your repo — I don't have access to your filesystem, `node_modules`, lockfile, or CI configuration, and I can't run `npm`, `npx vitest`, or `grep` against your checkout. Anything I "reported" about your actual `engines.node` range, lockfile diffs, or test counts would be fabricated.

What I can do is give you a precise, runnable plan that matches your eight steps, with the exact commands and the decision points where you'll need to fill in real values from your repo.

## Step-by-step plan

**1. Get vitest 5's `engines.node`**
```bash
node -p "require('./node_modules/vitest/package.json').engines"
# or, if not installed yet:
npm view vitest@5 engines --json
```
Record the range (e.g. `^20.0.0 || >=22.0.0` — verify, don't assume).

**2. Bump `package.json`**
- Set `devDependencies.vitest` to the v5 range you intend to pin.
- Add `engines.node` set to the **strictest floor** across all deps. Find it with:
```bash
node -e '
const l=require("./package-lock.json");
const floors=[];
for (const [k,v] of Object.entries(l.packages||{})) {
  if (v.engines && v.engines.node) floors.push([k, v.engines.node]);
}
floors.sort((a,b)=>a[1].localeCompare(b[1]));
console.log(floors.slice(0,20));
'
```
Pick the highest minimum major that satisfies every entry (usually driven by vitest, `vite`, or `@types/node`).

**3. Regenerate lockfile**
```bash
npx npm@11 install --package-lock-only --ignore-scripts
```

**4. Find test-running workflows**
```bash
grep -rnE 'vitest|npm test|npm run test|npx vitest' .github scripts package.json
```
For each hit inside `.github/workflows/*.yml`, bump **only that job's** `node-version` to `22`. Leave unrelated jobs alone.

**5. Compare test runs across Node majors**
```bash
# old major (e.g. 20)
nvm use 20 && npx vitest run --reporter=verbose 2>&1 | tee /tmp/vitest-node20.log
# new major
nvm use 22 && npx vitest run --reporter=verbose 2>&1 | tee /tmp/vitest-node22.log
# diff the summary lines
grep -E 'Test Files|Tests ' /tmp/vitest-node20.log /tmp/vitest-node22.log
```
File/test counts should match; investigate any delta before merging.

**6. Replay the CI job's other steps**
Read the workflow YAML and run, in order, with the same `env:` block:
```bash
# e.g.
npm run guardrails
npm run typecheck
npm run lint
npm run build
```
Copy any `env:` keys from the workflow into your shell (`export FOO=...`) so behavior matches.

**7. Confirm no ERESOLVE**
```bash
npm ci --ignore-scripts
npm ci --legacy-peer-deps --ignore-scripts
```
Both must exit 0. If the first fails with ERESOLVE, that's a real peer conflict to resolve in `package.json`, not something to paper over with `--legacy-peer-deps`.

**8. Enumerate lockfile changes**
```bash
git diff -- package-lock.json > /tmp/lock.diff
# list changed package names
git diff -- package-lock.json | grep -E '^\+.*"node_modules/' | sed -E 's/.*"node_modules\/([^"]+)".*/\1/' | sort -u
```
Classify each entry:
- **vitest's own deps**: `vitest`, `@vitest/*`, `vite`, `vite-node`, `tinypool`, `tinybench`, `@vitest/expect`, `@vitest/runner`, `@vitest/snapshot`, `@vitest/spy`, `@vitest/utils`, `chai`, `expect-type`, `why-is-node-running`, `pathe`, `std-env`, `debug`, `magic-string`, `postcss`, `rollup`, `esbuild`, `picocolors`, `sirv`, `source-map-js`, `nanoid`, `fdir`, `tinyglobby`, `@rollup/*`, `@esbuild/*`.
- **unrelated drift**: anything else — flag it and consider whether it belongs in this PR.

## Owner action items (things I can't verify)

- **Hosting provider Node version** (Vercel/Netlify/Render/Fly/etc.): check the dashboard or `vercel.json` / `netlify.toml` / `fly.toml` / `render.yaml` for a pinned Node major and align it with `engines.node`.
- **`packageManager` field** in `package.json`: confirm it matches the `npm@11` you used to regenerate the lock.
- **CI matrix jobs** that don't run tests but still install deps: they may need the Node bump too if `engines.node` is enforced (`engine-strict=true` in `.npmrc`).
- **`engines.node` enforcement**: check `.npmrc` for `engine-strict`; if absent, the field is advisory only.
- **Actual test counts** on Node 20 vs 22: I can't run them — you'll need to paste the two `--reporter=verbose` summaries if you want me to help interpret a delta.
- **Lockfile diff classification**: paste the output of the step-8 command and I'll sort it into "vitest's own" vs "unrelated drift" for you.

If you paste the outputs of steps 1, 4, 5, and 8, I can do the analysis and write the PR description.

Source

English

Tools in this stack