diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 131be96..026cff6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 @@ -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: @@ -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: @@ -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" diff --git a/README.md b/README.md index 4dc7b7b..c285b8c 100644 --- a/README.md +++ b/README.md @@ -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) ## 개발 및 검증 diff --git a/docs/adr/0031-npm-native-distribution.md b/docs/adr/0031-npm-native-distribution.md index 5becf27..fdfe0fd 100644 --- a/docs/adr/0031-npm-native-distribution.md +++ b/docs/adr/0031-npm-native-distribution.md @@ -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, 사용자 설치 @@ -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만 재개한다 @@ -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 미생성 확인 diff --git a/docs/adr/0034-npm-token-publishing-policy.md b/docs/adr/0034-npm-token-publishing-policy.md new file mode 100644 index 0000000..5c2fa24 --- /dev/null +++ b/docs/adr/0034-npm-token-publishing-policy.md @@ -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` 설정을 허용하지 않는다. diff --git a/docs/development/npm-distribution.md b/docs/development/npm-distribution.md index a5d2fb9..b8acea6 100644 --- a/docs/development/npm-distribution.md +++ b/docs/development/npm-distribution.md @@ -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 이상이 필요하다. 두 경로는 같은 @@ -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 @@ -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 동작 @@ -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이 +실패한다. ## 부분 실패와 재실행 @@ -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 사용 @@ -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 생성을 수행하지 +않는다. diff --git a/docs/development/rc3-release-candidate.md b/docs/development/rc3-release-candidate.md index df2b8cb..ab456d8 100644 --- a/docs/development/rc3-release-candidate.md +++ b/docs/development/rc3-release-candidate.md @@ -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 검증 아래 재실행한다. diff --git a/docs/development/releasing.md b/docs/development/releasing.md index d309664..ba7bddc 100644 --- a/docs/development/releasing.md +++ b/docs/development/releasing.md @@ -29,15 +29,12 @@ binary를 감싸는 선택형 설치 채널이며 제품 runtime을 다시 구 `XGENY_MAIN_PROTECTION_ENABLED=true`를 둔다. - Bypass actor가 없음을 관리자가 확인한 뒤 Actions repository variable `XGENY_RELEASE_RULESET_NO_BYPASS=true`를 설정한다. Read-only ruleset API는 bypass 목록을 숨길 수 있다. -- npm의 `@xgen` scope에 여섯 package를 게시할 권한, 계정 2FA와 recovery 수단을 확인한다. 실행 파일이 - 없는 bootstrap version을 먼저 게시한 뒤 각 package의 Trusted Publisher를 repository - `PlateerLab/xgeny-cli`, workflow `release.yml`, environment 없음, allowed action `npm publish`로 설정한다. - Publishing access는 `Require two-factor authentication and disallow tokens`로 제한한다. 장기 npm - token은 만들거나 GitHub secret에 저장하지 않는다. 전체 절차는 - [npm 배포와 Trusted Publishing](npm-distribution.md)을 따른다. -- 여섯 package의 Trusted Publisher 설정을 확인한 뒤 repository variable - `XGENY_NPM_PUBLISH_ENABLED=true`를 둔다. 값이 없거나 exact `true`가 아니면 native build 전에 release가 - fail-closed한다. +- npm의 `@xgen` scope에 여섯 package를 게시할 owner 권한을 확인한다. Package read/write와 `bypass 2FA` + 권한이 있는 granular token을 repository secret `NPM_TOKEN`에 저장하고 source, artifact와 명령 출력에는 + 넣지 않는다. 실행 파일이 없는 bootstrap version을 같은 정책으로 먼저 게시한다. 전체 절차는 + [npm granular token 게시와 provenance](npm-distribution.md)를 따른다. +- token의 scope와 권한을 확인한 뒤 repository variable `XGENY_NPM_PUBLISH_ENABLED=true`를 둔다. 값이 + 없거나 exact `true`가 아니면 native build 전에 release가 fail-closed한다. - macOS notarization과 Windows Authenticode가 없는 build는 SemVer와 별개로 prototype이라고 명시하고 OS 경고 가능성을 안내한다. @@ -97,7 +94,7 @@ Publish 이전에는 다음을 다시 확인한다. 9. 각 target native binary를 담은 exact-version npm package pack, loopback registry global-install smoke, lifecycle script 부재와 raw release asset byte parity 10. 최종 asset allow-list, `sha256sum -c`, 조립된 Linux fixture installer smoke와 GitHub artifact attestation -11. GitHub 게시 뒤 npm OIDC Trusted Publishing으로 platform package 다섯 개와 launcher 순차 게시, +11. GitHub 게시 뒤 granular token으로 platform package 다섯 개와 launcher 순차 게시, GitHub OIDC provenance·SRI·dist-tag 검증 12. 게시 뒤 다섯 target에서 GitHub public exact-tag 및 exact npm version 설치, 대화형 `/status`/`/exit`, 동일 version 재설치·제거, stable release의 `releases/latest/download` bootstrap과 내부 `latest` @@ -123,8 +120,9 @@ latest로 지정한다. 게시 직후에는 일반 release 조회로 exact tag, Quality, build와 assemble job은 repository read 권한만 가진다. 모든 target과 assembly가 통과한 뒤 GitHub publish job에만 `contents: write`, `id-token: write`, `attestations: write`를 부여한다. npm -publish job은 GitHub Release 게시 뒤 별도로 `contents: read`, `id-token: write`만 받는다. 모든 checkout은 -credential persistence를 끈다. 외부 GitHub Action은 mutable tag가 아니라 full commit SHA로 고정한다. +publish job은 GitHub Release 게시 뒤 별도로 `contents: read`, `id-token: write`만 받고, `NPM_TOKEN`은 +검증이 끝난 publish 단일 step에만 주입한다. 모든 checkout은 credential persistence를 끈다. 외부 GitHub +Action은 mutable tag가 아니라 full commit SHA로 고정한다. ## 게시 artifact diff --git a/npm/scripts/bootstrap.mjs b/npm/scripts/bootstrap.mjs index fb272f4..d8c4e93 100644 --- a/npm/scripts/bootstrap.mjs +++ b/npm/scripts/bootstrap.mjs @@ -31,7 +31,7 @@ function manifest(name, specification) { return { name, version: bootstrapVersion, - description: 'Non-executable package-name bootstrap for the XGENy npm trusted publisher', + description: 'Non-executable package-name bootstrap for the XGENy npm distribution', license: 'Apache-2.0', repository: { type: 'git', url: repositoryUrl }, homepage: 'https://github.com/PlateerLab/xgeny-cli#readme', @@ -66,8 +66,8 @@ async function pack(outputDirectory, name, specification, outputName) { await writeFile( path.join(stage, 'README.md'), `# \`${name}\` bootstrap\n\n` + - 'This non-executable version only creates the public package name before configuring ' + - 'the npm Trusted Publisher. Install a later Developer Preview version instead.\n', + 'This non-executable version only creates the public package name before the first ' + + 'XGENy npm release. Install a later Developer Preview version instead.\n', 'utf8', ); await copyFile(path.join(repoRoot, 'LICENSE'), path.join(stage, 'LICENSE')); diff --git a/scripts/check-npm-distribution-workflow.sh b/scripts/check-npm-distribution-workflow.sh index 8ef6c5e..bca0b44 100755 --- a/scripts/check-npm-distribution-workflow.sh +++ b/scripts/check-npm-distribution-workflow.sh @@ -31,9 +31,12 @@ def fail(message: str) -> None: raise SystemExit(f"{workflow}: {message}") -for secret_marker in ("NODE_AUTH_TOKEN", "NPM_TOKEN", "_authToken", "npm_password"): +for secret_marker in ("_authToken", "npm_password"): if secret_marker.lower() in text.lower(): - fail(f"long-lived npm credential marker is forbidden: {secret_marker}") + fail(f"literal npm credential configuration is forbidden: {secret_marker}") + +if re.search(r"npm_[A-Za-z0-9]{20,}", text): + fail("literal npm token value is forbidden") job_starts = [index for index, line in enumerate(lines) if line == " publish-npm:"] if len(job_starts) != 1: @@ -57,6 +60,7 @@ required_job_fragments = ( "test \"$NPM_PUBLISH_ENABLED\" = \"true\"", "node-version: 24.20.0", "actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38", + "registry-url: https://registry.npmjs.org/", "node npm/scripts/verify-release.mjs", "node npm/scripts/publish.mjs", ) @@ -71,7 +75,28 @@ if job.count("node npm/scripts/publish.mjs") != 1: if job.index("node npm/scripts/verify-release.mjs") > job.index("node npm/scripts/publish.mjs"): fail("npm release bundle verification must happen before publication") -gate_step = text.find("- name: Require npm Trusted Publishing readiness") +secret_expression = "${{ secrets.NPM_TOKEN }}" +if text.count(secret_expression) != 1: + fail("NPM_TOKEN secret must be referenced exactly once") +publish_step_marker = "- name: Publish platform packages then launcher" +if publish_step_marker not in job: + fail("npm publish step is missing") +publish_step = job[job.index(publish_step_marker) :] +for fragment in ( + f"NODE_AUTH_TOKEN: {secret_expression}", + 'test -n "$NODE_AUTH_TOKEN"', + "node npm/scripts/publish.mjs", +): + if fragment not in publish_step: + fail(f"npm publish step is missing token isolation contract: {fragment}") +if job[: job.index(publish_step_marker)].count("NODE_AUTH_TOKEN") != 0: + fail("npm token must not be exposed before the publish step") +if job.count("NODE_AUTH_TOKEN") != 2: + fail("npm token must be limited to one env binding and one non-empty check") +if job.index("node npm/scripts/verify-release.mjs") > job.index(secret_expression): + fail("npm token must not be exposed until after release bundle verification") + +gate_step = text.find("- name: Require npm token publishing readiness") build_job = text.find("\n build:") if gate_step < 0 or build_job < 0 or gate_step > build_job: fail("npm readiness must fail closed in the release quality gate before native builds") diff --git a/scripts/check-rc3-public-docs.sh b/scripts/check-rc3-public-docs.sh index a529524..8bba18a 100644 --- a/scripts/check-rc3-public-docs.sh +++ b/scripts/check-rc3-public-docs.sh @@ -85,16 +85,24 @@ require( require( npm_distribution, ( - "allowed actions", - "`npm publish`만 선택", - "`npm stage publish`는 선택하지 않음", - "npm trust github", - "--allow-publish", - "npm trust list", - "Require two-factor authentication and disallow tokens", + "granular access token", + "`bypass 2FA`", + "GitHub Actions repository secret `NPM_TOKEN`", + "publish 단일 step의 `NODE_AUTH_TOKEN`", + "`--provenance`", + "npm 인증이 아니라", ), "npm-distribution", ) +for stale_fragment in ( + "Require two-factor authentication and disallow tokens", + "npm trust github", + "npm trust list", +): + if stale_fragment in npm_distribution: + raise SystemExit( + f"{npm_distribution_path}: stale Trusted Publishing instruction remains: {stale_fragment}" + ) print("RC3 public documentation contract: PASS") PY