package-lock의 resolved가 옛 레지스트리를 가리킴 — npm ci 인증 실패

CI·CD2분조회

비공개 패키지를 GitHub Packages 에서 내부 레지스트리(Nexus)로 옮기는 작업을 했습니다. 각 프로젝트의 .npmrc 를 새 레지스트리로 바꿨고, 로컬에서 설치도 잘 됐습니다. 그런데 CI 에서 npm ci 가 일부 패키지에서 401/403 으로 계속 실패했습니다.

증상 — .npmrc는 바꿨는데 옛 주소로

.npmrc 는 분명히 새 레지스트리를 가리키는데, 에러에 찍힌 요청 주소는 옛 GitHub Packages 였습니다.

npm error 401 Unauthorized - GET https://npm.pkg.github.com/@scope%2f...

GitHub 토큰이 없는 새 환경이라 401 이 나는 건데, 애초에 왜 아직도 GitHub Packages 로 가느냐가 문제였습니다.

원인 — 옛 URL로 고정된 lockfile resolved

package-lock.json 은 의존성마다 어디서 받아왔는지를 resolved URL 로 박아 둡니다. 이 lock 이 GitHub Packages 를 쓰던 시절에 만들어졌다 보니, 그 URL 들이 전부 npm.pkg.github.com 을 가리키고 있었습니다.

"node_modules/@scope/pkg": {
  "resolved": "https://npm.pkg.github.com/@scope/pkg/-/pkg-1.2.3.tgz",
  ...
}

여기에 npm ci 의 동작이 겹칩니다. npm ci 는 lockfile 을 신뢰하고 그대로 재현하는 명령이라, .npmrc 의 레지스트리보다 lock 의 resolved URL 을 우선해서 그 주소로 직접 받으러 갑니다. 그러니 .npmrc 를 아무리 바꿔도, lock 안의 옛 GitHub 주소로 가서 토큰이 없어 401 이 났던 겁니다. (npm install 은 필요하면 lock 을 갱신하니 로컬에선 문제가 안 보였고, lock 을 고정하는 CI 의 npm ci 에서만 터졌습니다.)

해결 — 새 레지스트리 기준으로 lockfile 재생성

.npmrc 만 바꾸는 걸로는 부족하고, lock 의 resolved 도 새 레지스트리로 옮겨야 합니다. lock 을 재생성하면 됩니다.

rm package-lock.json
npm install          # 새 .npmrc 기준으로 resolved가 다시 써짐

이후 package-lock.jsonresolved 가 내부 레지스트리 주소로 바뀐 걸 확인하고 커밋했습니다. 스코프 패키지는 .npmrc 에 스코프별 레지스트리도 명시해 둡니다.

@scope:registry=https://<내부-레지스트리>/repository/npm-group/

lockfile의 resolved와 npm ci의 lock 고정

레지스트리를 옮길 때 사람들은 .npmrc 만 바꾸고 끝났다고 생각하지만, lockfile 에도 레지스트리 주소가 resolved 로 박혀 있습니다. 그리고 npm ci 는 lock 을 그대로 따라가므로, lock 을 재생성하지 않으면 옛 주소로 계속 갑니다. “로컬 npm install 은 되는데 CI npm ci 만 실패한다” 는 이 둘의 동작 차이(lock 갱신 vs lock 고정)를 정확히 가리키는 신호였습니다.

  1. 불러오는 중