모노레포에서 아이콘 패키지 5개의 스코프를 @kwonmory (GitHub Packages) → @in-gabia (npm public registry)로 마이그레이션했다. Changesets + GitHub Actions로 자동 배포를 구성했는데, main에 push할 때마다 Release 워크플로우가 실패했다. 3번 연속.
에러 메시지가 매번 달랐는데, 알고 보니 원인이 2개였다.
ENEEDAUTH This command requires you to be logged in to https://registry.npmjs.org
You need to authorize this machine using `npm adduser`로그인이 안 됐다는 건데, 분명 NPM_TOKEN은 등록해놨다. GitHub 레포의 Settings → Environments → Production에 시크릿으로.
그런데 CI 로그를 자세히 보니까 토큰이 아예 비어 있었다:
NODE_AUTH_TOKEN: # 빈 값
NPM_TOKEN: # 빈 값시크릿이 등록되어 있으면 ***로 마스킹돼야 정상인데, 빈 칸이면 값 자체가 안 들어간 거다.
GitHub에서 시크릿을 등록하는 곳이 두 군데 있다:
위치 | 접근 방법 | 조건 |
|---|---|---|
Repository secrets |
| 모든 job에서 접근 가능 |
Environment secrets |
| job에 |
접근 문법이 똑같다. 둘 다 ${{ secrets.NPM_TOKEN }}이다.
문제는 내가 시크릿을 Environment "Production"에 등록해놓고, 워크플로우에서는 environment를 지정하지 않은 것이다.
jobs:
release:
runs-on: ubuntu-latest
# environment: Production ← 이게 없었다Environment secrets는 해당 environment를 명시한 job에서만 접근할 수 있다. 지정 안 하면 그냥 빈 값이 들어간다. 에러도 안 나고 조용히 빈 값.
한 줄 추가.
jobs:
release:
runs-on: ubuntu-latest
environment: Production같은 Environment 설정 페이지에 Secrets와 Variables 두 섹션이 있다. 다음 차이를 보인다.
Secrets: 암호화 저장, 로그에서 ***로 마스킹, ${{ secrets.NAME }}
Variables: 평문 그대로 노출, ${{ vars.NAME }}
Variables는 리전명이나 플래그 같은 비민감 데이터용이다. 토큰을 여기 넣으면 GitHub UI에서 값이 그대로 보인다.
environment: Production 추가하고 다시 돌렸다. 이번엔 토큰이 잘 들어갔다 (NPM_TOKEN: ***). 근데 새로운 에러:
E403 403 Forbidden - PUT https://registry.npmjs.org/@in-gabia%2ffont
Two-factor authentication or granular access token with bypass 2fa enabled
is required to publish packages.npm 계정에 2FA를 켜놓으면, CI에서 publish할 때 토큰 타입이 중요해진다.
npm 토큰은 두 종류가 있다:
Classic Token: 예전 방식. 2FA 계정에서는 publish 차단됨.
Granular Access Token: 세분화된 권한 설정 가능. 2FA bypass 옵션이 있어서 CI에서 사용 가능.
나는 Classic Token (Automation 타입)을 발급해서 넣었는데, 2FA가 걸린 계정이라 E403이 떴다.
npmjs.org → Access Tokens → Generate New Token → Granular Access Token
Packages and scopes에서 @in-gabia 스코프에 Read and write 권한
생성된 토큰으로 GitHub Environment secrets의 NPM_TOKEN 교체
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
jobs:
release:
if: "!contains(github.event.head_commit.message, '[skip ci]')"
runs-on: ubuntu-latest
environment: Production
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
registry-url: "https://registry.npmjs.org"
- run: pnpm install --frozen-lockfile
- run: pnpm --filter './packages/*' build
- uses: changesets/action@v1
with:
publish: pnpm changeset publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
# .npmrc
@in-gabia:registry=https://registry.npmjs.org
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
auto-install-peers=true세 가지를 배웠다.
CI 로그에서 환경변수 값을 먼저 확인하자. NPM_TOKEN: ***이면 토큰이 잘 들어간 거고, NPM_TOKEN:처럼 빈 값이면 시크릿 접근 자체에 문제가 있는 거다. 이 한 줄 차이로 디버깅 방향이 완전히 달라진다.
GitHub Secrets의 스코프를 구분하자. Repository secrets와 Environment secrets는 접근 문법이 같아서 헷갈린다. Environment secrets를 쓰면 배포 보호 규칙(승인자 지정, 브랜치 제한)을 걸 수 있는 장점이 있지만, environment: 지정을 빠뜨리면 에러 없이 빈 값이 들어간다.
npm 토큰 타입을 확인하자. 2FA 계정에서 CI publish를 하려면 Granular Access Token이 필수다. Classic Token은 설정을 아무리 맞춰도 E403으로 차단된다.