Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ jobs:
test -n "$package_version"
test "$RELEASE_TAG" = "v$package_version"

- name: Require npm Trusted Publishing readiness
- name: Require npm token publishing readiness
env:
NPM_PUBLISH_ENABLED: ${{ vars.XGENY_NPM_PUBLISH_ENABLED }}
shell: bash
Expand Down Expand Up @@ -856,7 +856,7 @@ jobs:
}

publish-npm:
name: Publish npm packages with OIDC
name: Publish npm packages with token authentication and provenance
needs: publish
runs-on: ubuntu-24.04
permissions:
Expand All @@ -875,6 +875,7 @@ jobs:
with:
node-version: 24.20.0
package-manager-cache: false
registry-url: https://registry.npmjs.org/

- name: Recheck immutable GitHub Release binding
env:
Expand Down Expand Up @@ -955,10 +956,12 @@ jobs:

- name: Publish platform packages then launcher
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
RELEASE_TAG: ${{ github.event.workflow_run.head_branch }}
shell: bash
run: |
set -euo pipefail
test -n "$NODE_AUTH_TOKEN"
node npm/scripts/publish.mjs \
--directory release \
--tag "$RELEASE_TAG"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ binary에 포함되어 있어 network나 별도 파일 없이 `xgeny licenses`
- [ADR-0031: npm은 네이티브 XGENy의 무스크립트 배포 계층](docs/adr/0031-npm-native-distribution.md)
- [ADR-0032: 모델 프로필과 OS 보안 저장소 분리](docs/adr/0032-model-profiles-and-secure-credential-boundary.md)
- [ADR-0033: 대화형 REPL과 durable progress/cancellation 경계](docs/adr/0033-interactive-repl-durable-progress.md)
- [ADR-0034: npm granular token 게시와 provenance 분리](docs/adr/0034-npm-token-publishing-policy.md)

## 개발 및 검증

Expand Down
22 changes: 9 additions & 13 deletions docs/adr/0031-npm-native-distribution.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0031: npm은 네이티브 XGENy의 무스크립트 배포 계층이다

- 상태: Accepted
- 상태: Accepted (`npm` 게시 인증 결정은 [ADR-0034](0034-npm-token-publishing-policy.md)가 대체)
- 날짜: 2026-09-01
- 적용 범위: npm package, GitHub Actions release, 사용자 설치

Expand Down Expand Up @@ -47,20 +47,16 @@ license/provenance notice와 binary hash를 게시 전에 다시 검증한다. P
마지막에 게시한다.

GitHub Release를 만드는 workflow의 `GITHUB_TOKEN`이 별도 `release` event workflow를 시작한다고
가정하지 않는다. 같은 `.github/workflows/release.yml`의 후속 `publish-npm` job이 npm Trusted Publisher
OIDC를 사용한다. 이 job만 `id-token: write`를 가지며 npm password나 장기 access token은 저장하지 않는다.
가정하지 않는다. 같은 `.github/workflows/release.yml`의 후속 `publish-npm` job이 npm을 게시한다. 게시
인증과 provenance credential의 분리 정책은 ADR-0034를 따른다.

### 4. 최초 package 생성만 사람의 2FA 승인을 요구한다
### 4. 최초 package 생성도 별도 실행 파일을 포함하지 않는다

npm은 아직 존재하지 않는 package에 Trusted Publisher를 미리 연결할 수 없다. 그래서 실행 파일이 없는
`0.0.0-bootstrap.0` tarball 여섯 개를 사람이 검토한 뒤 npm 2FA로 한 번 게시한다. 그 다음 각 package의
Trusted Publisher를 repository `PlateerLab/xgeny-cli`, workflow `release.yml`, environment 없음,
allowed action `npm publish`에 연결한다. Publishing access는 2FA를 요구하고 token publish를 금지한 뒤
`XGENY_NPM_PUBLISH_ENABLED=true`를 설정한다. Scope 또는 package 소유권을 확인할 수 없으면 release하지
않고 package 이름 계약부터 다시 결정한다.
npm package 이름을 최초 생성할 때는 실행 파일이 없는 `0.0.0-bootstrap.0` tarball 여섯 개를 검토한 뒤
게시한다. Bootstrap package는 launcher, native binary와 lifecycle script를 포함하지 않는다. Scope 또는
package 소유권을 확인할 수 없으면 release하지 않고 package 이름 계약부터 다시 결정한다.

Bootstrap package는 launcher, native binary와 lifecycle script를 포함하지 않는다. 계정 비밀번호,
recovery code, OTP와 npm token은 repository, 명령 기록 또는 GitHub secret에 넣지 않는다.
Bootstrap과 실제 release의 게시 인증은 ADR-0034의 granular token 경계를 동일하게 적용한다.

### 5. 부분 실패는 동일 bytes만 재개한다

Expand All @@ -83,7 +79,7 @@ job은 같은 immutable GitHub Release로 재실행할 수 있지만, bytes가
- 다섯 OS/architecture에서 package pack과 loopback registry global-install smoke
- GitHub Release raw asset과 tar member의 streaming SHA-256 parity
- 게시 전 `npm publish --dry-run` file allow-list
- 장기 token marker, OIDC 권한, 검증-before-publish와 launcher-last workflow 정적 검사
- token secret 격리, provenance OIDC 권한, 검증-before-publish와 launcher-last workflow 정적 검사
- 게시 뒤 다섯 target에서 exact npm version 설치, platform package 존재, `--version`, `licenses`, state
미생성 확인

Expand Down
68 changes: 68 additions & 0 deletions docs/adr/0034-npm-token-publishing-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# ADR-0034: npm은 granular token으로 게시하고 provenance는 OIDC로 증명한다

- 상태: Accepted
- 날짜: 2026-09-02
- 적용 범위: npm package bootstrap, GitHub Actions release, npm credential 운영
- 대체 결정: ADR-0031의 npm Trusted Publisher와 계정 2FA 요구

## 배경

ADR-0031은 장기 credential을 없애기 위해 npm Trusted Publisher를 채택했다. 그러나 package가 존재하기
전에는 Trusted Publisher를 연결할 수 없고, 최초 package 생성과 trust 설정에 npm 계정 2FA가 필요하다.
운영자는 이 사람 개입을 유지하지 않고 `bypass 2FA` granular access token으로 release를 단순화하기로
결정했다.

이 결정은 token 탈취 시 npm publish 권한이 token 만료 또는 폐기까지 유지되는 위험을 수용한다. 대신
native artifact의 checksum, GitHub artifact attestation, exact version/SRI, launcher-last 게시와 npm
provenance 검증은 유지한다.

## 결정

### 1. npm 인증은 repository secret 하나로 제한한다

Package read/write와 `bypass 2FA` 권한이 있는 granular access token을 GitHub Actions repository secret
`NPM_TOKEN`에 저장한다. Token 값은 source, workflow literal, release artifact, npm package와 명령 출력에
넣지 않는다. 가능하면 `@xgen` package 범위와 만료 시점을 제한하며, 만료나 권한 변경 시 secret을
교체한다.

Release workflow는 token을 `publish-npm` job의 최종 publish step에만 `NODE_AUTH_TOKEN`으로 주입한다.
Checkout, GitHub Release 다운로드, checksum과 package 검증은 secret 없이 먼저 끝나야 한다.
`actions/setup-node`가 만드는 npmrc에는 환경변수 참조만 있고 실제 token 값은 기록하지 않는다.

### 2. OIDC는 인증이 아니라 provenance에만 사용한다

Publish job의 `id-token: write`는 npm registry 로그인에 쓰지 않는다. `npm publish --provenance`가 public
GitHub repository와 release commit을 증명하는 SLSA provenance를 생성하는 데만 사용한다. 게시 뒤 모든
package의 registry metadata에서 provenance predicate, local tarball SRI와 dist-tag를 다시 확인한다.

### 3. bootstrap과 release가 같은 인증 경계를 사용한다

실행 파일 없는 `0.0.0-bootstrap.0` 여섯 package도 같은 granular token으로 platform 우선, launcher
마지막 순서로 게시한다. 실제 `0.1.0-rc.3`은 immutable GitHub Release bundle을 유일한 입력으로 사용한다.
이미 존재하는 version은 SRI, dist-tag와 provenance가 모두 같을 때만 재실행에서 건너뛴다.

Repository variable `XGENY_NPM_PUBLISH_ENABLED=true`는 token의 scope, write 권한과 `bypass 2FA`를 관리자가
확인했다는 acknowledgement다. Variable이 없거나 secret이 비어 있으면 release는 fail-closed한다.

## 결과

- npm 계정 2FA와 Trusted Publisher 설정 없이 자동 release할 수 있다.
- GitHub Actions secret 관리 권한과 token 수명이 새로운 공급망 경계가 된다.
- Token이 유출되면 허용 범위의 package를 공격자가 게시할 수 있으므로 repository secret 접근과 workflow
변경 권한을 제한해야 한다.
- npm provenance와 GitHub artifact attestation은 유지되지만, token-free Trusted Publishing보다 인증
보안 수준은 낮다.

## 대안

Trusted Publisher는 token-free 인증과 더 좁은 workflow identity를 제공하지만 계정 2FA와 최초 설정이
필요해 운영 편의 요구와 맞지 않아 대체했다. Staged publishing도 최종 승인에 2FA가 필요하므로 채택하지
않는다. Token을 source나 repository npmrc에 넣는 방식은 credential 유출 범위가 커서 채택하지 않는다.

## 검증

- workflow에는 `${{ secrets.NPM_TOKEN }}` 참조가 정확히 한 번만 존재한다.
- `NODE_AUTH_TOKEN`은 publish 단일 step에서만 non-empty 확인 후 사용한다.
- token 주입 전에 GitHub Release binding, checksum과 npm bundle 검증을 완료한다.
- publisher는 `--provenance`, launcher-last, SRI·dist-tag·provenance 사후검증을 유지한다.
- workflow와 문서에 literal `npm_...` token 값이나 `_authToken` 설정을 허용하지 않는다.
71 changes: 24 additions & 47 deletions docs/development/npm-distribution.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# npm 배포와 Trusted Publishing 운영
# npm granular token 게시와 provenance 운영

`@xgen/cli`는 native XGENy를 npm으로 설치하기 위한 선택형 배포 계층이다. 직접 GitHub installer를 쓰는
사용자는 Node.js가 필요 없고, npm 경로를 고른 사용자는 Node.js 22.14 이상이 필요하다. 두 경로는 같은
Expand All @@ -25,13 +25,14 @@ compile은 없다. Launcher는 현재 platform package가 없으면 종료한다

## 최초 1회 운영 준비

이 절차는 release tag를 만들기 전에 npm scope 관리자 한 명이 대화형으로 수행한다.
이 절차는 release tag를 만들기 전에 npm scope 관리자 한 명이 수행한다.

1. npm 계정의 2FA와 recovery 수단을 확인한다. 공유되었거나 노출 가능성이 있는 비밀번호는 먼저
교체한다. 비밀번호, OTP, recovery code와 token을 repository나 shell script에 기록하지 않는다.
2. 해당 계정 또는 npm organization이 `@xgen` scope 여섯 package를 public으로 게시할 권한이 있는지
1. 해당 계정 또는 npm organization이 `@xgen` scope 여섯 package를 public으로 게시할 권한이 있는지
확인한다. 권한이나 scope 확보 여부가 불명확하면 `@xgen/cli`를 게시하지 말고 naming ADR을 먼저
갱신한다.
2. npm에서 package read/write 권한과 `bypass 2FA`가 있는 granular access token을 준비한다. 가능하면
`@xgen` package 범위와 만료 시점을 제한한다. Token 값은 repository, release artifact, 문서와 명령
출력에 넣지 않고 GitHub Actions repository secret `NPM_TOKEN`에만 저장한다.
3. clean `main` checkout에서 bootstrap tarball을 만든다.

```bash
Expand All @@ -43,41 +44,15 @@ node npm/scripts/bootstrap.mjs --output-dir "$bootstrap_dir"
4. `npm-bootstrap-manifest.json`의 package name, version, integrity를 확인하고 각 tarball에
`package.json`, `README.md`, `LICENSE`만 있는지
`npm publish --dry-run --ignore-scripts --tag bootstrap --json TARBALL`로 검토한다.
5. npm web login/2FA가 활성화된 현재 session에서 여섯 tarball을 `--access public --tag bootstrap`으로
한 번 게시한다. Bootstrap에는 GitHub OIDC provenance 설정이 없으며 이 단계는 자동화하지 않는다.
OTP를 명령행 인자로 남기지 않는다.
6. npm package 여섯 개 각각에 GitHub Actions Trusted Publisher를 설정한다.
- organization/user: `PlateerLab`
- repository: `xgeny-cli`
- workflow filename: `release.yml`
- environment: 사용하지 않음
- allowed actions: `npm publish`만 선택하고 `npm stage publish`는 선택하지 않음
2026-05-20 이후 생성하는 Trusted Publisher는 allowed action을 하나 이상 명시해야 한다. 현재 release
workflow는 직접 `npm publish`를 호출하므로 여섯 package 모두 위 선택이 같아야 한다.

npm CLI 11.5.1 이상을 쓰면 npmjs.com의 동일 설정을 아래처럼 적용하고 즉시 다시 읽어 확인할 수 있다.
이전 CLI를 유지해야 하면 package settings 화면에서 같은 값을 직접 설정한다.

```bash
trusted_packages='@xgen/cli-linux-x64-musl @xgen/cli-linux-arm64-musl @xgen/cli-darwin-x64 @xgen/cli-darwin-arm64 @xgen/cli-win32-x64 @xgen/cli'
for package in $trusted_packages; do
npm trust github "$package" \
--repository PlateerLab/xgeny-cli \
--file release.yml \
--allow-publish
npm trust list "$package" --json
done
```

7. Package settings의 Publishing access를 `Require two-factor authentication and disallow tokens`로
설정하고 write token을 추가하지 않았는지 확인한 뒤 bootstrap version을 deprecated 처리한다.
이 설정은 GitHub OIDC Trusted Publisher를 막지 않는다.
Package 자체는 unpublish하지 않는다.
8. GitHub repository variable `XGENY_NPM_PUBLISH_ENABLED=true`를 설정한다. 이 값은 여섯 package의
소유권과 Trusted Publisher 설정을 사람이 확인했다는 fail-closed acknowledgement다.

Trusted Publisher는 이미 존재하는 package에만 설정할 수 있기 때문에 bootstrap이 필요하다. Bootstrap
version은 실행 파일과 `bin` entry가 없어 사용 가능한 XGENy release가 아니다.
5. 동일 token을 현재 process에만 secret-injection하고 여섯 tarball을 platform 우선, launcher 마지막
순서로 `--access public --tag bootstrap` 게시한다. Token 값을 shell script나 npmrc에 literal로 쓰지
않는다. Bootstrap에는 실행 파일과 `bin` entry가 없고 GitHub provenance가 없는 package-name 예약용
version이다.
6. GitHub repository secret `NPM_TOKEN`이 설정된 것을 확인한 뒤 repository variable
`XGENY_NPM_PUBLISH_ENABLED=true`를 설정한다. 이 값은 token의 scope, write 권한과 `bypass 2FA`를 사람이
확인했다는 fail-closed acknowledgement다.
7. RC3 게시와 검증이 끝나면 bootstrap version을 deprecated 처리한다. Package 자체는 unpublish하지
않는다.

## Release 동작

Expand All @@ -93,10 +68,12 @@ quality + 5-platform native build
-> 5-platform public registry install smoke
```

npm publish job은 GitHub-hosted Ubuntu runner, Node.js 24.20.0/npm 11.19.0과 OIDC
`id-token: write`를 사용한다. `NODE_AUTH_TOKEN`, `NPM_TOKEN`과 npm password는 사용하지 않는다.
Prerelease는 `next`, stable은 `latest` dist-tag로 게시한다. 모든 package에는 npm provenance가 있어야
하며 registry metadata에서 SLSA provenance predicate가 확인되지 않으면 job이 실패한다.
npm publish job은 GitHub-hosted Ubuntu runner와 Node.js 24.20.0/npm 11.19.0을 사용한다. Granular token은
GitHub secret `NPM_TOKEN`에서 publish 단일 step의 `NODE_AUTH_TOKEN`으로만 주입되고, checkout·bundle
검증과 다른 job에는 노출되지 않는다. `id-token: write`는 npm 인증이 아니라 `--provenance`의 GitHub OIDC
증명에만 사용한다. Prerelease는 `next`, stable은 `latest` dist-tag로 게시한다. 모든 package에는 npm
provenance가 있어야 하며 registry metadata에서 SLSA provenance predicate가 확인되지 않으면 job이
실패한다.

## 부분 실패와 재실행

Expand All @@ -105,7 +82,7 @@ Platform package를 launcher보다 먼저 게시하므로 중간 실패가 가
tarball과 모두 같을 때만 skip한다. 하나라도 다르면 자동 수정, unpublish 또는 overwrite하지 않는다.

- 동일 bytes인 일부 게시: 실패 job 재실행
- Trusted Publisher/권한 오설정: 설정을 바로잡은 뒤 실패 job 재실행
- Token 만료·scope·write 또는 `bypass 2FA` 오설정: secret을 바로잡은 뒤 실패 job 재실행
- registry bytes 또는 package name 소유권 불일치: 중단하고 새 version PR과 tag 사용
- GitHub Release 전 실패: 원인을 main에서 수정하고 더 높은 version/tag 사용

Expand All @@ -123,5 +100,5 @@ sh scripts/check-npm-distribution-workflow.sh
각 platform CI는 release binary를 platform tarball에 넣고 loopback registry에서 `npm install -g`,
대화형 `/status`/`/exit`, 동일 version 재설치와 `npm uninstall -g`를 수행한다. Release assemble은 여섯
tarball의 file allow-list, 고지와 raw binary byte parity를 확인한다.
로컬이나 PR에서는 실제 npm publish, package bootstrap, Trusted Publisher 변경과 release tag 생성을
수행하지 않는다.
로컬이나 PR에서는 실제 npm publish, package bootstrap, GitHub secret 변경과 release tag 생성을 수행하지
않는다.
4 changes: 2 additions & 2 deletions docs/development/rc3-release-candidate.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ install smoke를 수행한다.

## 게시 경계

이 문서를 merge하는 것만으로 release를 게시하지 않는다. Merge 뒤 npm scope/bootstrap/Trusted
Publisher 준비와 `XGENY_NPM_PUBLISH_ENABLED=true`를 확인하고, 현재 `origin/main` head와 package version이
이 문서를 merge하는 것만으로 release를 게시하지 않는다. Merge 뒤 npm scope/bootstrap, repository secret
`NPM_TOKEN`과 `XGENY_NPM_PUBLISH_ENABLED=true`를 확인하고, 현재 `origin/main` head와 package version이
정확히 일치할 때 별도 보호 tag `v0.1.0-rc.3`을 만들면 release workflow가 모든 release gate를 다시
실행한다. GitHub Release 전 실패한 tag나 asset은 이동·교체·재사용하지 않고 더 높은 새 version으로
수정한다. Immutable GitHub Release 뒤 동일 npm bundle의 부분 실패만 SRI 검증 아래 재실행한다.
Loading