diff --git a/.github/workflows/release-node.yml b/.github/workflows/release-node.yml index 4a6ec33..13da16f 100644 --- a/.github/workflows/release-node.yml +++ b/.github/workflows/release-node.yml @@ -12,6 +12,7 @@ on: permissions: contents: write + id-token: write # required for npm Trusted Publisher (OIDC tokenless publish) jobs: build-native: @@ -158,11 +159,9 @@ jobs: --repo "${{ github.repository }}" --clobber - name: Publish platform packages - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | for dir in sdks/node/npm/*/; do - OUTPUT=$((cd "$dir" && npm publish --access public) 2>&1) && echo "$OUTPUT" || { + OUTPUT=$((cd "$dir" && npm publish --provenance --access public) 2>&1) && echo "$OUTPUT" || { echo "$OUTPUT" if echo "$OUTPUT" | grep -Eq "cannot publish over the previously published versions|You cannot publish over the previously published version"; then echo "::warning::Package already published for $dir — skipping." @@ -175,11 +174,9 @@ jobs: done - name: Publish edgeparse (main package) - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | cd sdks/node - OUTPUT=$(npm publish --access public 2>&1) && echo "$OUTPUT" || { + OUTPUT=$(npm publish --provenance --access public 2>&1) && echo "$OUTPUT" || { echo "$OUTPUT" if echo "$OUTPUT" | grep -Eq "cannot publish over the previously published versions|You cannot publish over the previously published version"; then echo "edgeparse already published at this version — skipping." diff --git a/.github/workflows/release-wasm.yml b/.github/workflows/release-wasm.yml index d539b30..3fcfea7 100644 --- a/.github/workflows/release-wasm.yml +++ b/.github/workflows/release-wasm.yml @@ -13,6 +13,7 @@ on: permissions: contents: write # upload GitHub Release assets packages: write # publish to GitHub Packages npm registry + id-token: write # required for npm Trusted Publisher (OIDC tokenless publish) jobs: publish-wasm: @@ -96,6 +97,11 @@ jobs: 'README.md', 'LICENSE' ]; + pkg.publishConfig = { + access: 'public', + registry: 'https://registry.npmjs.org', + provenance: true + }; fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); console.log('Metadata synced to version: ' + version); " @@ -115,7 +121,7 @@ jobs: const pkgPath = 'crates/edgeparse-wasm/pkg/package.json'; const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); pkg.name = 'edgeparse-wasm'; - pkg.publishConfig = { access: 'public', registry: 'https://registry.npmjs.org' }; + pkg.publishConfig = { access: 'public', registry: 'https://registry.npmjs.org', provenance: true }; fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); " cd crates/edgeparse-wasm/pkg @@ -128,25 +134,27 @@ jobs: retention-days: 30 # ───────────────────────────────────────────────────────────────────── - # 5a. Publish to npm (primary registry — enables jsDelivr & unpkg CDNs). - # Uses NPM_TOKEN Classic Automation token stored as a repository - # secret. "already-published" is treated as idempotent. + # 5a. Publish to npm via Trusted Publisher (OIDC — no access token). + # Requires id-token:write permission (set at top of this file) and + # the package configured at: + # https://www.npmjs.com/package/edgeparse-wasm + # → Settings → Trusted Publisher → workflow: release-wasm.yml + # "already-published" is treated as idempotent. # ───────────────────────────────────────────────────────────────────── - name: Publish to npm registry - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | node -e " const fs = require('fs'); const pkgPath = 'crates/edgeparse-wasm/pkg/package.json'; const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); pkg.name = 'edgeparse-wasm'; - pkg.publishConfig = { access: 'public', registry: 'https://registry.npmjs.org' }; + pkg.publishConfig = { access: 'public', registry: 'https://registry.npmjs.org', provenance: true }; fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); " cd crates/edgeparse-wasm/pkg - npm publish --access public --registry https://registry.npmjs.org \ - || { CODE=$?; [ "$CODE" -eq 1 ] && npm info edgeparse-wasm@${{ env.VERSION }} >/dev/null 2>&1 && echo "Already published — skipping." || exit $CODE; } + npm publish --provenance --access public --registry https://registry.npmjs.org \ + || { CODE=$?; npm info edgeparse-wasm@${{ env.VERSION }} version >/dev/null 2>&1 \ + && echo "Already published — skipping." || exit $CODE; } # ───────────────────────────────────────────────────────────────────── # 5b. Publish to GitHub Packages (secondary registry — useful for diff --git a/docs/07-cicd-publishing.md b/docs/07-cicd-publishing.md index b756361..8ae1e0c 100644 --- a/docs/07-cicd-publishing.md +++ b/docs/07-cicd-publishing.md @@ -90,7 +90,7 @@ Each GitHub Release includes: | Secret | Used by | Purpose | |--------|---------|---------| | `CARGO_REGISTRY_TOKEN` | `release-rust.yml` | Publish crates to crates.io | -| `NPM_TOKEN` | `release-node.yml`, `release-wasm.yml` | Publish Node.js and WASM packages to npm | +| `NPM_TOKEN` | `release-node.yml`, `release-wasm.yml` | Fallback token — **not needed** when Trusted Publisher (OIDC) is configured for the package | | `DOCKERHUB_TOKEN` | `release-docker.yml` | Push Docker images to Docker Hub | | `HOMEBREW_TAP_TOKEN` | `release-cli.yml` | Push `edgeparse.rb` to the Homebrew tap | @@ -104,11 +104,30 @@ Each GitHub Release includes: ### External setup - crates.io: create a token with `publish-new` and `publish-update` -- npm: use a Classic Automation token so the main package, platform packages, and - WASM package (`edgeparse-wasm`) can publish from CI. Store it as the `NPM_TOKEN` - repository secret. Granular tokens often miss package names — use Classic Automation. -- PyPI: configure Trusted Publishing for `release-python.yml` in environment - `pypi` +- **npm (Trusted Publisher — recommended):** configure via OIDC so CI publishes + without any token: + 1. Go to the package settings page on npmjs.com + 2. Under **Trusted Publisher**, set Publisher = GitHub Actions, + org/user = `raphaelmansuy`, repository = `edgeparse`, + workflow filename = the publishing workflow (see table below), environment = (leave blank) + 3. Add `id-token: write` permission to the publishing job (already done in both + `release-node.yml` and `release-wasm.yml`) + 4. Use `npm publish --provenance --access public` — no `NODE_AUTH_TOKEN` env var needed + + | npm package | Workflow filename | + |-------------|-------------------| + | `edgeparse` | `release-node.yml` | + | `edgeparse-wasm` | `release-wasm.yml` | + + > For `edgeparse-wasm`: the package must exist on npm before the Trusted Publisher + > entry can be saved. Publish the first version manually with `--otp`, then + > configure the Trusted Publisher entry — all subsequent releases use OIDC. + +- **npm (Classic token — fallback):** if Trusted Publisher is not yet configured, + create a Classic Automation token at + and store it as `NPM_TOKEN`. + Granular Access Tokens cannot create new packages and will fail with `E404`. +- PyPI: configure Trusted Publishing for `release-python.yml` in environment `pypi` - Docker Hub: create a read/write access token for account `rmansuy` - Homebrew tap: create a PAT with `contents: write` on `raphaelmansuy/homebrew-edgeparse` @@ -230,39 +249,49 @@ fast on mismatches. - Builds native `.node` binaries for five targets - Syncs the package version from the tag -- Publishes five platform packages and the main `edgeparse` package +- Publishes five platform packages and the main `edgeparse` package using **npm Trusted Publisher (OIDC)** — no `NPM_TOKEN` needed +- Adds provenance attestation (`--provenance`) to every published package - Treats "already published" as idempotent rather than fatal ### `release-wasm.yml` - Builds the browser-targeted WASM package with `wasm-pack` -- Syncs the npm package metadata (version, exports, files) from the tag -- Publishes `edgeparse-wasm` to npm (primary) using `NPM_TOKEN` +- Syncs the npm package metadata (version, exports, files, provenance) from the tag +- Publishes `edgeparse-wasm` to npm using **npm Trusted Publisher (OIDC)** — no `NPM_TOKEN` needed +- Adds provenance attestation (`--provenance`) so the package appears as verified on npmjs.com - Publishes `@raphaelmansuy/edgeparse-wasm` to GitHub Packages (secondary) using the built-in `GITHUB_TOKEN` — no extra secret required - Uploads the tarball to the GitHub Release (`--clobber` for idempotent re-runs) - Both publish steps treat "already published" as non-fatal -#### Required secrets / permissions +#### Required permissions (already in workflow) -| What | Where | Notes | -|------|-------|-------| -| `NPM_TOKEN` | Repository secret | Classic Automation token; set scoped access to `edgeparse-wasm` or use an account-level token | -| `packages: write` | Workflow permission (already in `release-wasm.yml`) | Allows push to GitHub Packages | -| `contents: write` | Workflow permission (already in `release-wasm.yml`) | Allows creating / updating GitHub Releases | +| Permission | Purpose | +|------------|---------| +| `id-token: write` | Mint OIDC token for npm Trusted Publisher | +| `packages: write` | Push to GitHub Packages | +| `contents: write` | Create / update GitHub Releases | -#### Configuring `NPM_TOKEN` +#### Configuring npm Trusted Publisher for `edgeparse-wasm` + +> **One-time setup** — after the first manual publish creates the package on npm: + +1. Go to → **Settings** → **Trusted Publisher** +2. Publisher: `GitHub Actions` +3. Organization or user: `raphaelmansuy` +4. Repository: `edgeparse` +5. Workflow filename: `release-wasm.yml` +6. Environment name: *(leave blank)* +7. Click **Save changes** + +All future tag releases will publish via OIDC with no token required. + +After updating, re-trigger without retagging: ```bash -# 1. Go to https://www.npmjs.com/settings//tokens -# 2. Generate → Classic Token → Automation -# 3. Add to the repository: -gh secret set NPM_TOKEN --body "" --repo raphaelmansuy/edgeparse +gh workflow run release-wasm.yml --repo raphaelmansuy/edgeparse \ + --field tag_name=v0.2.4 ``` -The `npm` GitHub environment (`environment: npm` in the workflow) can optionally -be configured with required reviewers or deployment protection rules under -**Settings → Environments** if you want a manual approval gate before publish. - ### `release-cli.yml` - Builds five CLI archives @@ -308,6 +337,28 @@ Crates.io versions are immutable. Bump the version and retag. Use a Classic Automation token for `NPM_TOKEN`. Granular tokens often miss one or more package names and produce `E403 Forbidden`. +### `edgeparse-wasm` npm publish fails with `E404 Not Found` + +This happens when either: + +1. **Wrong token type** — A Granular Access Token was used instead of a Classic + Automation token. Granular tokens cannot create brand-new package names on npm. + Solution: replace `NPM_TOKEN` with a Classic Automation token (see + [Configuring NPM_TOKEN](#configuring-npm_token-step-by-step) above). + +2. **Package name not yet claimed** — `edgeparse-wasm` has never been published + before, so npm has no record of the package. This is normal on first release; + a Classic Automation token will create it automatically. + +After fixing the secret, re-trigger the workflow without retagging: + +```bash +gh workflow run release-wasm.yml --repo raphaelmansuy/edgeparse \ + --field tag_name=v0.2.4 +``` + +The workflow treats "already published" as idempotent, so re-running is safe. + ### PyPI publish fails with `invalid-publisher` The PyPI Trusted Publisher entry must match: