Skip to content

chore: Node 24 LTS 승격 + 런타임·패키지매니저 버전 단일 출처화 - #14

Merged
NariP merged 4 commits into
mainfrom
chore/node-24-lts
Sep 6, 2026
Merged

NariP merged 4 commits into
mainfrom
chore/node-24-lts

Conversation

@NariP

@NariP NariP commented Sep 6, 2026

Copy link
Copy Markdown
Owner

무엇이 바뀌나

Node 런타임을 20(EOL) → 24 LTS 로 올리고, 런타임·패키지매니저 버전을 단일 출처로 묶는다.

⚠️ 파괴적 변경이다. engines 가 >=24.0.0 이 되어 Node 20/22 사용자는 EBADENGINE 을 받고 install.sh 가 설치를 거부한다.

배경

Closes #13

engines 선언이 이미 거짓이었다

의존성이 선언보다 높은 버전을 요구하고 있었다.

의존성 요구
commander@15.0.0 >=22.12.0
@modelcontextprotocol/server@2.0.0 >=20
open@11.0.1 >=20

즉 Node 20 에서는 애초에 설치가 성립하지 않는데 README·install.sh·engines 가 20.9 를 안내하고 있었다. 이 PR 은 새로 제약을 거는 게 아니라 이미 틀린 광고를 바로잡는 쪽에 가깝다. 다만 npm 이 EBADENGINE 을 내보내는 것은 사실이므로 릴리스 노트에 명시가 필요하다.

Node 20 은 EOL

버전 상태 EOL
v24 Krypton 최신 LTS 2028-04
v22 Jod LTS 2027-04
v20 Iron EOL 2026-04 (지남)

22 로 가면 1년여 뒤 같은 작업을 반복한다. 릴리스 직후이고 사용자가 아직 없는 지금이 가장 싼 시점이라 24 로 갔다.

버전이 9곳에 흩어져 있었다

engines 2곳, CI/release 워크플로 2곳, tsup target, README 영·한, getting-started, install.sh(안내 문구 + 게이트 로직). .nvmrc 도 .node-version 도 없어 개발자 로컬은 제각각이었다.

pnpm 도 이중 관리였다 — packageManager: pnpm@10.29.3 이 있는데 CI 는 그걸 안 읽고 pnpm/action-setup 에 따로 적었고, 문서는 corepack enable 을 안내하는데 CI 는 corepack 을 쓰지 않았다.

#11 에서 고친 버전 하드코딩과 같은 구조의 문제다.

작업 내용

Node 24 승격

  • .nvmrc (신규, 24) — canonical. CI·릴리스가 node-version-file 로 읽어 하드코딩 제거
  • engines >=24.0.0 (루트 + packages/pixel-cli)
  • tsup.config.ts target: "node24"
  • install.sh — 안내 문구 및 게이트 로직. >=24.0.0 은 whole major 이므로 minor 항이 죽은 코드가 되어 제거했다
  • README(영·한), docs/getting-started.md, CONTRIBUTING.md, bug_report.yml 갱신

corepack 으로 pnpm 단일화

CI 가 pnpm/action-setup 대신 corepack 을 쓴다. packageManager 필드가 단일 출처가 된다.

여기서 실제 버그를 하나 잡았다. 처음엔 바레 corepack enable 을 썼는데 test:distribution 이 이렇게 실패했다:

This project is configured to use pnpm because .../package.json has a "packageManager" field

바레 형태는 npm 까지 shim 하는데, 그 shim 은 packageManager: pnpm 인 저장소 안에서 실행을 거부한다. 그게 install.sh 의 npm install --global 을 깨뜨린다. corepack enable pnpm 으로 스코프를 좁혀 pnpm/pnpx shim 만 만들고 npm 은 진짜를 남겼다.

코드 리뷰가 이게 테스트만의 문제가 아님을 짚었다 — release.yml:60 이 npm publish 를 실행하므로 스코프 없는 enable 이었다면 발행 경로에도 pnpm-abort shim 이 깔렸을 것이다.

문서를 CI 와 일치시킴

문서 4곳이 여전히 바레 corepack enable 을 안내하고 있었다. 그대로 두면 기여자가 CI 주석이 "install.sh 를 깨뜨린다"고 명시한 바로 그 shim 을 설치하게 되고, pnpm test:distribution 이 혼란스러운 에러로 실패한다. 전부 corepack enable pnpm 으로 바꾸고 왜 스코프가 필요한지 한 줄씩 덧붙였다.

CONTRIBUTING.md:11 의 sharp 잔재도 고쳤다 — #7 에서 @jsquash wasm 으로 교체했으므로 네이티브 툴체인이 필요 없다는 게 오히려 장점인데 정반대로 적혀 있었다.

새 가드

tests/manifests/runtime-versions.test.ts (신규, 10 tests) — .nvmrc·engines 2곳·워크플로 2곳·tsup target·install.sh 게이트·문서 Node 버전·문서 pnpm 버전을 전부 canonical 과 대조한다. 모든 비교가 디스크-대-디스크이고 파일 안에 버전 리터럴이 없다.

자가체크

루프 — 1회차 구현 + 2회차 코드 리뷰 반영(4건).

코드 리뷰 — 위반 0건. corepack 스코핑·게이트 로직·tautology 수정 셋 다 소스에서 검증됐다. 개선 4건(문서 corepack 불일치, sharp 잔재, 게이트가 나중에 느슨해질 여지, 문서 pnpm 하드코딩) 전부 반영했다.

QA 감사 — pass. 10개 단언을 독립 재검증했고, vacuous pass 방어가 전 항목에 있음을 확인했다 (파일 부재 시 ENOENT throw, engines 누락 시 타입 단언 선행, 정규식 0-match 시 null 체크, matchAll 은 length 먼저).

QA 가 withoutComments 에 대해 중요한 사실을 밝혔다:

ci.yml:23 의 주석 # 1. Corepack runs before actions/setup-node... 는 실제 corepack enable pnpm(:34)보다 앞선 오프셋에 actions/setup-node 문자열을 갖고 있다. 스트립이 없으면 순서 단언이 정반대로 뒤집혀 올바른 파일에서 실패하고 뒤집힌 파일에서 통과한다.

즉 구현자가 스스로 발견해 고친 tautology 는 장식이 아니라 실제 반전이었다.

테스트 — 전부 Node v24.18.1 + 진짜 npm 11.16.0 으로 실행:

명령 결과
pnpm verify exit 0 — tests 44 (runtime-versions 10 포함)
pnpm test:distribution exit 0
pnpm test:source-install exit 0
pnpm test:e2e exit 0 — 23 passed

게이트 실측 — 실제 런타임 3종으로 확인:

v20.11.1  → requires Node.js 24 or newer   (거부)
v22.23.2  → requires Node.js 24 or newer   (거부)
v24.18.1  → 통과

mutation check:

변형 결과
engines → >=24.5.0 실패 — install.sh 게이트가 major 만 인코딩하므로 24.0~24.4 를 허용하게 됨
문서 pnpm → 9.9.9 (3파일 각각) 실패 — documents pnpm 9.9.9, packageManager is 10.29.3
문서에서 pnpm 언급 삭제 실패 — vacuous pass 대신 no "pnpm x.y.z" mention to check
.nvmrc 를 floor 미만으로 실패
게이트를 major < 20 으로 두고 메시지만 갱신 실패 — 조용한 드리프트 차단
node-version 재하드코딩 / pnpm/action-setup 복구 / corepack 순서 반전 / 바레 corepack enable 전부 실패
tsup target 을 floor 위로 실패

전부 원복 후 green 재확인.

제거한 테스트 — 없음. 기존 기대값 수정도 없음.

로컬 환경 주의사항 (개발자용)

이 저장소를 로컬에서 검증할 때 npm 이 corepack shim 인지 확인해라. 구현자와 메인 세션 모두 여기 걸려 test:distribution 이 한 번 실패했다.

$ npm -v
This project is configured to use pnpm because .../package.json has a "packageManager" field

이 메시지가 나오면 진짜 npm 이 아니다. CI 는 corepack enable pnpm 으로 스코프를 좁히므로 해당 없다.

CI 에서 확인할 것

self-check 2건은 성격상 로컬에서 닫을 수 없다. 머지 전 CI 로그에서 아래를 확인한다.

  1. verify (ubuntu-latest) / verify (windows-latest) 둘 다 green (fail-fast: false 이므로 각각 확인)
  2. "Enable Corepack" 단계에서 pnpm --version 이 10.29.3 출력 — 다른 값이면 packageManager 가 단일 출처가 아니라는 뜻
  3. actions/setup-node 로그에 Cache restored from key: ...pnpm... (첫 실행이면 job 끝에 Cache saved with key:). Unable to locate executable file: pnpm 이 뜨거나 캐시 섹션이 아예 없으면 corepack-before-setup-node 순서가 깨진 것
  4. Setup Node.js 가 .nvmrc 를 24.x 로 해석
  5. pnpm test:distribution 이 양쪽 OS 에서 통과

알려진 갭 (의도적)

  • commander@15 같은 전이 의존성이 자기 floor 를 24 위로 올리면 아무도 못 잡는다. 이번 승격을 촉발한 게 바로 그 제약인데, 다음번엔 설치 시점까지 모른다. 별도 이슈감이다.
  • .node-version 파일은 만들지 않았다. .nvmrc 를 canonical 로 택했고 현재 이 저장소에 둘 다 없다.
  • 문서 pnpm 대조는 하드코딩된 파일 목록에 대해서만 돈다. 새 문서에 낡은 pnpm 버전을 적으면 안 잡힌다.

리뷰 포인트

  • Node 24 승격 시점과 engines 파괴적 변경에 동의하는가
  • .nvmrc 를 24(bare major)로 둔 선택이 적절한가 — 보안 패치 자동 추종 vs CI 재현성
  • corepack 스코핑(enable pnpm)이 올바른 해법인가
  • 워크플로 YAML·셸을 정규식으로 파싱하는 테스트를 자산으로 볼 수 있는가

머지 후

engines 상향은 파괴적 변경이므로 1.0.4 릴리스가 필요하다. 이 PR 에는 버전 범프를 포함하지 않았다 — #11 로 이제 package.json 한 곳만 올리고 pnpm sync-version 을 돌리면 된다.

NariP and others added 4 commits September 6, 2026 11:55
engines said >=20.9.0, but commander@15 already required >=22.12.0 — so
Node 20 could not install this package and the docs said otherwise. Node
20 also reached end of life in April. 24 is the current LTS, and going
there now costs nothing while nobody depends on the old floor.

The version was written in nine places with nothing keeping them in
step. .nvmrc is now canonical: both workflows read it through
node-version-file, and a new guard ties engines, the tsup target, the
install.sh gate, and the prose in four documents back to it. The gate
needed its logic changed, not just its message — a whole-major floor
makes the minor term dead code that would otherwise rot.

CI now activates pnpm through corepack so packageManager is the only
place its version lives. Scoping that to `corepack enable pnpm` matters:
the bare form also shims npm, and that shim aborts inside a repository
whose packageManager is pnpm, which breaks both install.sh and the npm
publish step in the release workflow.
Every GitHub Action was still on the v4 it was written with at
c4f814f, three majors behind. setup-node v7 is the one that matters:
actions/setup-node#531 tracks the corepack chicken-and-egg problem
where cache: pnpm needs pnpm on PATH before setup-node runs, and
enabling corepack that early binds pnpm to the runner's default Node
on Windows, where .cmd shims resolve against their own directory.
v7 is reported to make the double-setup-node workaround unnecessary.

Dependencies were the same story. Eleven move; five do not, each for
a reason rather than caution: TypeScript 7 reports node: specifiers
as unresolved identifiers, which the same tree compiles clean under
5.9.3, and vite, plugin-react, vitest and jsdom are one coupled unit
whose newest jsdom changes accessible-name computation in a way that
needs test expectations rewritten, not a version bumped.

eslint.config.mjs now ignores artifacts/, a gitignored scratch tree
carrying its own ESLint 9 install that made ESLint 10 crash mixing
two majors in one run.
Two separate Windows failures, one loud and one quiet.

The loud one: git checks out LF as CRLF on Windows by default, so the
.nvmrc assertion compared '24\r\n' against '24\n' and failed the job.
The repository had no .gitattributes at all, so line endings were
whatever each platform decided. It now pins them, and the assertion
checks what nvm actually parses — one terminated line, no padding —
rather than which bytes terminate it.

The quiet one: Windows CI was testing on Node 22 while claiming to
test 24. setup-node installed 24 correctly, but Corepack had already
written its pnpm shim next to the runner's default Node, and a
Windows .cmd shim resolves against its own directory rather than
PATH. v7 did not change this, so the double-setup-node workaround
from actions/setup-node#531 goes in: install Node, enable Corepack on
it, then run setup-node again for the cache. This one only warned, so
the fix is verifiable from a `node --version` line rather than from a
green check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KUF3P9ehRWYZqZ1nw3BuZY
base64url includes `-` and `_`, so roughly one session id in 64 began
with a character the CLI's parser treats as an option. `--session <id>`
then failed with `unknown option`, and CI went red at random — twice in
one afternoon, on two different tests.

The leading byte is now redrawn until it lands on an alphanumeric
digit. Every other position keeps the full 64-symbol alphabet and the
length is unchanged, so the cost is about 0.046 bits out of 144. Auth
tokens are untouched: they never reach an argument vector.

The test drives create() rather than the generator, because asserting
on the generator alone still passed when the call site was reverted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KUF3P9ehRWYZqZ1nw3BuZY
@NariP
NariP merged commit 959f8c9 into main Sep 6, 2026
2 checks passed
@NariP
NariP deleted the chore/node-24-lts branch September 6, 2026 09:05
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.

chore: Node 24 LTS 승격 + 런타임·패키지매니저 버전 단일 출처화

1 participant