diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a239d46..cafbaee 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -4,6 +4,19 @@ name: Release # (npm run ci + build + audit:openclaw + pack:check) and then publishes the # package to the public npm registry with provenance. # +# AUTH: npm TRUSTED PUBLISHING (OIDC) — no NPM_TOKEN. The publish authenticates +# tokenlessly: GitHub Actions presents a short-lived OIDC identity (via the +# `id-token: write` permission below) that npm verifies against the trusted +# publisher configured on the package (org + repo `honeycomb` + this workflow +# filename `release.yaml`). There is NO long-lived automation token anywhere in +# this workflow or in repo secrets. Provenance stays ON (the same OIDC identity +# also signs the supply-chain attestation). Trusted Publishing REQUIRES npm +# >= 11.5.1, which Node 22 does NOT ship (it bundles npm 10.x) — see the "Upgrade +# npm" step below, which is mandatory for OIDC to engage. The trusted publisher +# must be configured on npm BEFORE the first CI publish, and that config requires +# the package to already exist, so the FIRST publish is a one-time manual publish +# (2FA); every subsequent CI publish is tokenless. See RELEASING.md / PRD-048a. +# # DRAFT STATUS (read RELEASING.md before going public): the package is still # `private: true` and named `honeycomb` (a name already owned by someone else on # the public registry). This workflow is wired so a maintainer can go public with @@ -11,9 +24,10 @@ name: Release # * the publishability preflight aborts if `private` is still true OR if the # name is the taken unscoped `honeycomb` — so a stray tag push cannot publish # a broken or duplicate package by accident. -# * a real `npm publish` only happens when NPM_TOKEN is set AND this is not a -# dry run; otherwise the job runs the gate + `npm publish --dry-run` and stays -# green (mirrors ci.yaml's gated-integration pattern: skip, don't fail). +# * a real `npm publish` only happens on a tag push AND when this is not a dry +# run; a workflow_dispatch always rehearses (gate + `npm publish --dry-run`) +# and stays green (mirrors ci.yaml's gated-integration pattern: skip, don't +# fail). The publish intent is the TRIGGER (a tag = go live), not a token. # # Two ways in: # * push a version tag `vX.Y.Z` -> a real publish attempt (still gated as above). @@ -41,10 +55,13 @@ concurrency: # Least privilege (mirrors ci.yaml principle #7), scoped UP only for what a # publish needs: # * contents: write — ONLY to create the GitHub Release for the pushed tag. -# * id-token: write — the OIDC token npm provenance signs against, so the -# published package carries a verifiable supply-chain -# attestation (`npm publish --provenance`). Without this -# scope provenance generation fails. +# * id-token: write — the short-lived OIDC identity, which now does DOUBLE duty: +# (1) it is the publish AUTH for npm Trusted Publishing +# (npm verifies it against the package's trusted publisher; +# this replaces NPM_TOKEN), AND (2) npm provenance signs +# against it so the published package carries a verifiable +# supply-chain attestation (`npm publish --provenance`). +# Without this scope BOTH tokenless auth and provenance fail. permissions: contents: write id-token: write @@ -57,16 +74,28 @@ jobs: - name: Checkout uses: actions/checkout@v4.2.2 - # registry-url is what wires `npm publish` (and provenance) to the public - # npm registry and makes setup-node write the NODE_AUTH_TOKEN line into - # .npmrc. One pinned Node major — the publish surface does not need the - # full engine matrix (that canary lives on ci.yaml's quality-gate). + # registry-url points `npm publish` (and provenance) at the public npm registry. + # With Trusted Publishing there is NO NODE_AUTH_TOKEN: registry-url may scaffold a + # ~/.npmrc, but we supply no token VALUE — auth is the OIDC identity, not a token. + # Dependency caching is deliberately OFF on this publish job: a lower-privilege + # workflow could poison an npm cache that this high-privilege release build then + # executes (zizmor cache-poisoning). `npm ci` installs from the committed lockfile. + # One pinned Node major — the publish surface does not need the full engine matrix + # (that canary lives on ci.yaml's quality-gate). - name: Setup Node uses: actions/setup-node@v6.4.0 with: node-version: '22.x' registry-url: 'https://registry.npmjs.org' - cache: npm + + # Node 22 bundles npm 10.x, but npm Trusted Publishing (OIDC) requires + # npm >= 11.5.1 — without it the OIDC handshake never engages and `npm + # publish` falls back to token auth (which we no longer provide) and fails. + # Upgrade BEFORE npm ci / build / publish so every npm invocation in this job + # is OIDC-capable. Pinned to an EXACT version (not a `^` range) so a release + # never runs an unreviewed npm; bump it deliberately (must stay >= 11.5.1). + - name: Upgrade npm (Trusted Publishing needs npm >= 11.5.1) + run: npm install -g npm@11.6.2 - name: Install (npm ci) run: npm ci @@ -150,38 +179,31 @@ jobs: fi echo "OK — package is publishable (not private, scoped name)." - # ── Token gate. ────────────────────────────────────────────────────────── - # secrets are not allowed in a job/step `if:`, so read NPM_TOKEN in a run - # step and emit a boolean. Empty token (before the secret is set, or on a - # fork) -> publish=false -> the publish step does a --dry-run and the job - # stays GREEN (skip, don't fail — same pattern as ci.yaml's secret gate). - - name: Token gate - id: token - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: | - if [ -n "$NPM_TOKEN" ]; then - echo "has_token=true" >> "$GITHUB_OUTPUT" - else - echo "has_token=false" >> "$GITHUB_OUTPUT" - fi - - # Decide dry-run vs real publish in one place. A real publish requires ALL: - # * a token is present, AND - # * this is not a workflow_dispatch dry_run. - # On a tag push `inputs.dry_run` is empty, so the only gate is the token. + # ── Resolve publish mode — gated on the TRIGGER, not a token. ───────────── + # Trusted Publishing needs no NPM_TOKEN, so the publish intent is expressed + # by HOW the workflow was triggered. A real publish requires ALL THREE: + # * this is a `push` event (`github.event_name == 'push'`) — NOT a manual + # dispatch (a workflow_dispatch CAN be run against a tag ref, so the + # event-name check is what actually keeps the manual button a rehearsal), AND + # * the pushed ref is a tag (`github.ref_type == 'tag'`) — pushing `vX.Y.Z` + # is the deliberate "go live" signal, AND + # * it is not a dispatch dry_run (`inputs.dry_run != 'true'`; empty on a push). + # The manual dispatch button always rehearses (it is not a `push`), even if a + # maintainer points it at a tag and unchecks dry_run. Real publishes only ever + # come from a pushed `vX.Y.Z` tag. - name: Resolve publish mode id: mode env: - HAS_TOKEN: ${{ steps.token.outputs.has_token }} + EVENT_NAME: ${{ github.event_name }} + REF_TYPE: ${{ github.ref_type }} DRY_RUN_INPUT: ${{ github.event.inputs.dry_run }} run: | - if [ "$HAS_TOKEN" = "true" ] && [ "$DRY_RUN_INPUT" != "true" ]; then + if [ "$EVENT_NAME" = "push" ] && [ "$REF_TYPE" = "tag" ] && [ "$DRY_RUN_INPUT" != "true" ]; then echo "publish=true" >> "$GITHUB_OUTPUT" - echo "Mode: REAL publish (token present, not a dry run)." + echo "Mode: REAL publish (pushed tag, not a dry run) — authenticating via OIDC Trusted Publishing." else echo "publish=false" >> "$GITHUB_OUTPUT" - echo "Mode: DRY-RUN (token=$HAS_TOKEN, dry_run_input=$DRY_RUN_INPUT) — running npm publish --dry-run, not publishing." + echo "Mode: DRY-RUN (event=$EVENT_NAME, ref_type=$REF_TYPE, dry_run_input=$DRY_RUN_INPUT) — running npm publish --dry-run, not publishing." fi # ── Publish. ───────────────────────────────────────────────────────────── @@ -192,27 +214,26 @@ jobs: # --access public: scoped packages default to restricted; this publishes # them public. (Also set via publishConfig in # package.json when going public — belt and suspenders.) - # NODE_AUTH_TOKEN is the .npmrc auth var setup-node wired to registry-url. + # Auth is TOKENLESS: npm (>= 11.5.1, ensured above) performs the OIDC + # handshake using the id-token: write identity and verifies it against the + # package's trusted publisher. There is NO NODE_AUTH_TOKEN / .npmrc auth line. - name: Publish (real) if: ${{ steps.mode.outputs.publish == 'true' }} run: npm publish --provenance --access public env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # `prepack` rebuilds the bundle here, so the published tarball needs the # telemetry define present on THIS step too (see Build step comment). HONEYCOMB_POSTHOG_KEY: ${{ secrets.HONEYCOMB_POSTHOG_KEY }} HONEYCOMB_REF_DEFAULT: ${{ vars.HONEYCOMB_REF_DEFAULT }} # Rehearse: same command minus the real upload. Runs whenever we are NOT in - # real-publish mode (dry_run input, or no token yet) so the pipeline is - # exercised end-to-end and stays green before secrets exist. + # real-publish mode (any workflow_dispatch, or a non-tag ref) so the pipeline + # is exercised end-to-end and stays green. `--dry-run` does no OIDC handshake + # and no upload, so it is tokenless by nature — nothing to wire here. - name: Publish (dry-run rehearsal) if: ${{ steps.mode.outputs.publish != 'true' }} run: npm publish --provenance --access public --dry-run env: - # Harmless when empty; present so the dry-run exercises the same auth - # path the real publish takes. - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # The dry-run's `prepack` rebuilds too — keep the define present so the # rehearsal exercises the same build the real publish takes. HONEYCOMB_POSTHOG_KEY: ${{ secrets.HONEYCOMB_POSTHOG_KEY }} diff --git a/RELEASING.md b/RELEASING.md index d5be42b..412bc80 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -9,9 +9,17 @@ this document is the order to flip them in. > **TL;DR for the impatient maintainer.** You must (1) pick + provision a > **scoped** npm name (the unscoped `honeycomb` is already taken — see below), > (2) add `publishConfig` for public access + provenance, (3) remove -> `private: true`, and (4) set the `NPM_TOKEN` repo secret. Until all four are -> done, `release.yaml` runs the full gate + a `--dry-run` and stays green -> without ever publishing. +> `private: true`, and (4) **configure GitHub Actions as the package's trusted +> publisher on npm** (tokenless OIDC — no `NPM_TOKEN`). Until the three in-repo +> switches are flipped, `release.yaml` runs the full gate + a `--dry-run` and +> stays green without ever publishing. +> +> **Auth is npm Trusted Publishing (OIDC), not a token.** CI authenticates with a +> short-lived GitHub OIDC identity that npm verifies against the trusted publisher +> you configure on the package — there is **no `NPM_TOKEN` secret** anywhere. +> Provenance stays on (same identity). One catch: a trusted publisher can only be +> set on a package that already exists, so the **first** publish is a one-time +> manual 2FA publish; every CI publish after that is tokenless. --- @@ -24,7 +32,9 @@ use a **scoped** name under an npm org/scope you control: - `@legioncodeinc/honeycomb` (matches the GitHub org `legioncodeinc`), or - `@legioncode/honeycomb`, or any scope whose npm org exists and lists the - publishing identity (you, or the CI automation token) as a member. + publishing identity (you, as an org member) with publish rights. CI itself + publishes tokenlessly via the trusted publisher (switch (d)), not via a member + account. The release workflow has a **fail-closed preflight** that aborts the publish if `package.json` `name` is still the unscoped `honeycomb` OR if `private: true` is @@ -38,9 +48,10 @@ these switches are flipped. ### (a) Choose + provision the scoped name 1. Decide the scope (e.g. `@legioncodeinc`). The corresponding **npm org/scope - must already exist** on npmjs.com, and the publishing identity (your npm - account, or the automation token in step (d)) **must be a member** with - publish rights. Create the org at npmjs.com if it does not exist. + must already exist** on npmjs.com, and your npm account **must be a member** + with publish rights (for the bootstrap publish and break-glass; CI publishes + via the trusted publisher in step (d), not an account). Create the org at + npmjs.com if it does not exist. 2. Set it in `package.json`: ```jsonc "name": "@legioncodeinc/honeycomb", @@ -78,16 +89,36 @@ run at all (npm itself blocks it), which is why the draft is safe. > and do a local-install dogfood (below) **without** removing `private` — leave > it set until you genuinely intend to go live. -### (d) Set the `NPM_TOKEN` repo secret - -1. On npmjs.com, create an **automation** access token (type: Automation, so it - bypasses 2FA in CI) scoped to publish for the package/scope. -2. In GitHub → repo **Settings → Secrets and variables → Actions → New - repository secret**, add it as `NPM_TOKEN`. - -Until this secret exists, `release.yaml` does the gate + `npm publish --dry-run` -and stays green (it will not hard-fail on a missing token) — same skip-don't-fail -pattern as `ci.yaml`'s gated integration job. +### (d) Configure the trusted publisher on npm (tokenless OIDC — no `NPM_TOKEN`) + +CI authenticates via **npm Trusted Publishing**: GitHub Actions presents a +short-lived OIDC identity that npm verifies against a trusted publisher you +configure on the package. There is **no `NPM_TOKEN` secret** — nothing +long-lived to leak or rotate. + +1. **First publish is a one-time manual bootstrap.** A trusted publisher can only + be attached to a package that already exists on npm. So the very first release + is a manual, interactive (2FA) `npm publish` by an org member with publish + rights — this creates `@legioncodeinc/honeycomb` on the registry. (See "Cut + the release" below for the bootstrap-vs-subsequent split.) +2. **Attach the trusted publisher.** On npmjs.com → the package → **Settings → + Trusted Publishers → GitHub Actions**, add: + - **Organization / user:** `legioncodeinc` + - **Repository:** `honeycomb` + - **Workflow filename:** `release.yaml` + - **Environment:** optional — leave blank, or set one and add a matching + `environment:` to the publish job in `release.yaml` for an extra approval gate. +3. **Do NOT set an `NPM_TOKEN` secret.** The workflow does not read one. The only + release secret is `HONEYCOMB_POSTHOG_KEY` (PRD-050e telemetry), set separately. + +> **npm version floor.** Trusted Publishing requires **npm >= 11.5.1**. Node 22 +> bundles npm 10.x, so `release.yaml` upgrades npm (`npm i -g npm@^11.5.1`) before +> publish — without it the OIDC handshake never engages and the publish fails. + +Until the package exists + the trusted publisher is attached, a real CI publish +cannot succeed; before then, `release.yaml` on a `workflow_dispatch` does the gate ++ `npm publish --dry-run` and stays green (the `--dry-run` does no OIDC handshake) +— same skip-don't-fail pattern as `ci.yaml`'s gated integration job. --- @@ -133,15 +164,22 @@ Once (a)–(d) are flipped and a dry-run rehearsal is green: > commit. CI's tag-vs-`package.json` guard checks the root version against > the tag; sync-versions keeps the harness manifests honest with it. + > **Bootstrap (first release only).** The trusted publisher cannot be attached + > until the package exists on npm, so the FIRST publish is a one-time manual + > publish by an org member: `npm publish --access public` (interactive, 2FA), + > then attach the trusted publisher per switch (d). Every release after the + > first is the tokenless CI flow below. + 2. **Push the tag.** ```sh git push --follow-tags ``` - The `vX.Y.Z` tag triggers `release.yaml`. With `NPM_TOKEN` set and this being - a tag push (not a dry-run dispatch), the workflow runs the full gate, the - tag-vs-version guard, the publishability preflight, then - `npm publish --provenance --access public`, and finally creates the GitHub - Release for the tag. + The `vX.Y.Z` tag triggers `release.yaml`. Because this is a tag push (not a + dry-run dispatch), the workflow upgrades npm (>= 11.5.1), runs the full gate, + the tag-vs-version guard, the publishability preflight, then + `npm publish --provenance --access public` — authenticating **tokenlessly via + OIDC** against the trusted publisher (no `NPM_TOKEN`) — and finally creates the + GitHub Release for the tag. --- @@ -168,18 +206,21 @@ Once (a)–(d) are flipped and a dry-run rehearsal is green: `.github/workflows/release.yaml` triggers on a `v*` tag push or a manual `workflow_dispatch` (`dry_run` defaults true). It: -1. checks out, sets up Node 22 against `registry.npmjs.org`, `npm ci`; +1. checks out, sets up Node 22 against `registry.npmjs.org`, **upgrades npm to + >= 11.5.1** (required for Trusted Publishing OIDC), `npm ci`; 2. runs the **full gate** — `npm run ci` + `npm run build` + `npm run audit:openclaw` + `npm run pack:check` (same recipe as `ci.yaml`); 3. **tag-vs-`package.json` guard** — the pushed `vX.Y.Z` must equal `package.json` version (skipped on dispatch); 4. **publishability preflight (fail-closed)** — aborts if `private: true` or if `name` is the taken unscoped `honeycomb`; -5. **publishes** with `--provenance --access public` — but ONLY when `NPM_TOKEN` - is set AND it is not a dry run; otherwise it does `npm publish --dry-run` and - stays green; +5. **publishes** with `--provenance --access public`, **authenticating tokenlessly + via OIDC** against the trusted publisher (no `NPM_TOKEN`) — but ONLY on a tag + push that is not a dry run; otherwise it does `npm publish --dry-run` and stays + green; 6. creates the **GitHub Release** for the tag (real publishes only). `permissions` are least-privilege: `contents: write` only to create the GitHub -Release, `id-token: write` only so npm provenance can sign an OIDC supply-chain -attestation. +Release, `id-token: write` doing double duty — it is both the **publish auth** for +npm Trusted Publishing (replacing `NPM_TOKEN`) and the identity npm provenance +signs its OIDC supply-chain attestation against. diff --git a/library/requirements/backlog/prd-048-npm-publishing-pipeline/prd-048-npm-publishing-pipeline-index.md b/library/requirements/backlog/prd-048-npm-publishing-pipeline/prd-048-npm-publishing-pipeline-index.md index 549583a..2f3766a 100644 --- a/library/requirements/backlog/prd-048-npm-publishing-pipeline/prd-048-npm-publishing-pipeline-index.md +++ b/library/requirements/backlog/prd-048-npm-publishing-pipeline/prd-048-npm-publishing-pipeline-index.md @@ -17,16 +17,19 @@ a switch-flipping PRD, not a build-it-from-scratch PRD. A grounded read of the r - **The pipeline exists and fails closed.** `release.yaml` runs the full CI gate + build + audits + pack-check, a tag-vs-`package.json` version guard, a **publishability preflight that aborts if - `private: true` or the name is the unscoped `honeycomb`**, a token gate (no `NPM_TOKEN` → `--dry-run`, - stays green), provenance via OIDC (`id-token: write`), and a GitHub Release step. Nothing is missing - from the *machinery*. + `private: true` or the name is the unscoped `honeycomb`**, a trigger-gated publish mode (only a tag + push, not a dry-run dispatch, publishes for real — otherwise `--dry-run`, stays green), tokenless OIDC + auth via npm Trusted Publishing + provenance (both off the same `id-token: write` identity), and a + GitHub Release step. Nothing is missing from the *machinery*. - **The package ships deliberately un-publishable.** `package.json` carries `"private": true`, the unscoped name `honeycomb` (owned by a third party on npm — `honeycomb@0.1.4`), and a **commented-out** `publishConfig` block. This is intentional: a stray tag push cannot leak a broken or duplicate package. -- **Going public is a documented sequence of four conscious switches** (RELEASING.md): (a) scoped name, - (b) `publishConfig` public+provenance, (c) remove `private`, (d) set the `NPM_TOKEN` repo secret — - plus the off-repo prerequisite that the **npm org/scope must exist** with our identity as a publishing - member. +- **Going public is a documented sequence of conscious switches** (RELEASING.md): (a) scoped name, + (b) `publishConfig` public+provenance, (c) remove `private`, (d) **configure the trusted publisher on + npm** (GitHub Actions as the trusted publisher for the package — no `NPM_TOKEN` secret) — plus the + off-repo prerequisite that the **npm org/scope must exist** with our identity as a publishing member. + *(Auth model superseded — see D-2′: CI now publishes via npm Trusted Publishing (OIDC), not an + automation token.)* - **RELEASING.md flags one real gap.** `npm version` runs npm's `version` lifecycle but **not** `prebuild`, so `scripts/sync-versions.mjs` does not auto-run on a version bump. Without a wired `"version"` npm script, the propagated harness-manifest versions can drift out of the tagged commit — @@ -40,6 +43,16 @@ This PRD spends the existing infrastructure: it does the off-repo provisioning, infra was built to wait for, closes the version-sync gap, fixes the now-incorrect install docs, and proves it all green WITHOUT crossing the go-live line. +> **Auth amendment (operator decision, supersedes D-2):** CI publishing switched from an org **Automation +> token** (`NPM_TOKEN` secret) to **npm Trusted Publishing (OIDC)** — tokenless. GitHub Actions presents a +> short-lived OIDC identity that npm verifies against a trusted publisher configured on the package; no +> long-lived token is stored anywhere. Provenance stays ON (same `id-token: write` identity). This affects +> 048a (configure the trusted publisher instead of minting a token), the go-public switch list (switch (d) +> is now "configure the trusted publisher", not "set `NPM_TOKEN`"), and `release.yaml` (the publish mode is +> gated on the trigger, not a token; npm is upgraded to >= 11.5.1 so OIDC engages). One-time bootstrap: a +> trusted publisher can only be configured on a package that already exists, so the FIRST publish is a +> manual 2FA publish; all subsequent CI publishes are tokenless. + ## Decisions (made before authoring) - **D-0 — Scoped name is `@legioncodeinc/honeycomb`.** Matches the GitHub org `legioncodeinc` and RELEASING.md's primary recommendation. The `bin` stays `honeycomb` (independent of package name — @@ -56,12 +69,12 @@ chain last: | Sub-PRD | Wave | Deliverable | Confidence | |---|---|---|---| -| **048a** | W0 | **npm org `@legioncodeinc` provisioning** — org exists, publishing identity is a member, automation token minted, `NPM_TOKEN` repo secret set | high (off-repo, manual) | +| **048a** | W0 | **npm org `@legioncodeinc` provisioning + trusted publisher** — org exists, publishing identity is a member, GitHub Actions configured as the package's trusted publisher (OIDC; no `NPM_TOKEN`) | high (off-repo, manual) | | **048b** | W0 | **Go-public switch-flips + name reconciliation** — scoped name, `publishConfig`, remove `private`, fix README/SDK `@honeycomb/sdk` → `@legioncodeinc/honeycomb` | high | | **048c** | W1 | **Version-sync lifecycle hardening** — wire the `"version"` npm script so `sync-versions` + `git add` run inside `npm version`, closing the manifest-drift gap | high | | **048d** | W1 | **Rehearsal + verification** — CI dry-run green with switches flipped, local `pack:check` + `npm pack` + scratch-dir install dogfood; NO tag pushed | high | -> **Cross-PRD note (feeds [PRD-050e](../../completed/prd-050-quick-install-and-guided-setup/prd-050e-quick-install-and-guided-setup-operator-adoption-telemetry.md)):** `release.yaml` now sets `HONEYCOMB_POSTHOG_KEY` (secret) + optional `HONEYCOMB_REF_DEFAULT` (var) as **step-scoped `env:` on just the build + publish steps** (least privilege — `npm ci`/gate/audits don't see it), where esbuild `define`-bakes them into the daemon bundle for PRD-050e adoption telemetry. It is **release-only by design** (never `ci.yaml`) and **fail-soft** (unset key → `""` → telemetry disabled), so it does not affect this PRD's dry-run/rehearsal green path. The only added go-live action is populating the `HONEYCOMB_POSTHOG_KEY` secret (PostHog project "Honeycomb" 485287) alongside `NPM_TOKEN` in 048a — both are release secrets set the same way. +> **Cross-PRD note (feeds [PRD-050e](../../completed/prd-050-quick-install-and-guided-setup/prd-050e-quick-install-and-guided-setup-operator-adoption-telemetry.md)):** `release.yaml` now sets `HONEYCOMB_POSTHOG_KEY` (secret) + optional `HONEYCOMB_REF_DEFAULT` (var) as **step-scoped `env:` on just the build + publish steps** (least privilege — `npm ci`/gate/audits don't see it), where esbuild `define`-bakes them into the daemon bundle for PRD-050e adoption telemetry. It is **release-only by design** (never `ci.yaml`) and **fail-soft** (unset key → `""` → telemetry disabled), so it does not affect this PRD's dry-run/rehearsal green path. The only added go-live secret is `HONEYCOMB_POSTHOG_KEY` (PostHog project "Honeycomb" 485287), set in 048a — note that with the move to Trusted Publishing (D-2, superseded), `HONEYCOMB_POSTHOG_KEY` is now the **only** release secret; there is no longer an `NPM_TOKEN` to set alongside it (npm auth is tokenless OIDC). ## Design alternatives + recommendation (per sub-PRD) @@ -70,9 +83,15 @@ chain last: one human. - **(b) An org automation token** (type: Automation, bypasses 2FA in CI) as the CI publishing identity, with humans as org members for break-glass local publishes. -**RECOMMENDED: (b) for CI** (`release.yaml` already reads `NPM_TOKEN` and expects an automation token — -RELEASING.md step (d)), with the maintainer's personal account as an org **member** for rehearsal/manual -fallback. The token is the CI identity; the human is the owner-of-last-resort. +- **(c) npm Trusted Publishing (OIDC)** — no token at all. GitHub Actions presents a short-lived OIDC + identity that npm verifies against a trusted publisher configured on the package; humans stay org + members for break-glass local publishes. +**RECOMMENDED: ~~(b) for CI~~ → (c) (operator decision, supersedes the original (b) recommendation and +D-2).** Trusted Publishing removes the long-lived `NPM_TOKEN` entirely — the largest standing +supply-chain liability in option (b) (a token that can leak, must be scoped, and must be rotated). CI +authenticates tokenlessly via OIDC; provenance is unchanged (same `id-token: write` identity). The +maintainer's personal account stays an org **member** for the one-time bootstrap publish and any +break-glass/manual fallback. (Requires npm >= 11.5.1 in CI — `release.yaml` upgrades npm before publish.) ### 048b — how aggressively to reconcile the `@honeycomb/sdk` placeholder - **(a) Minimal:** flip the four switches; leave README/SDK docs saying `@honeycomb/sdk`. @@ -101,8 +120,19 @@ Both are safe (RELEASING.md: `private: true` blocks `npm publish` but NOT `npm p together they are the strongest go-live confidence available short of publishing. ## Decisions -- **D-2 — CI publishes via an org Automation token (048a); humans are org members for break-glass - (048a).** The `NPM_TOKEN` secret is the CI identity; no release depends on a single human's 2FA. +- **D-2 — ~~CI publishes via an org Automation token (048a); humans are org members for break-glass + (048a). The `NPM_TOKEN` secret is the CI identity; no release depends on a single human's 2FA.~~** + **SUPERSEDED (operator decision, 2026-06-25) by D-2′ below.** *(Original text retained, struck, for + the decision record.)* +- **D-2′ — CI publishes via npm Trusted Publishing (OIDC); no `NPM_TOKEN`.** GitHub Actions is configured + as the package's trusted publisher (org + repo `honeycomb` + workflow `release.yaml`, optionally pinned + to an environment). `release.yaml` authenticates tokenlessly: the `id-token: write` OIDC identity is the + publish auth (and still signs provenance). No long-lived automation token is minted or stored — removing + the token-scope/expiry/leak liability entirely. Requires npm >= 11.5.1 in CI (Node 22 ships npm 10.x, so + `release.yaml` upgrades npm before publish). Humans remain org members for the one-time bootstrap publish + and break-glass. **Bootstrap nuance:** a trusted publisher can only be configured on a package that + already exists on npm, so the FIRST publish is a one-time manual publish (2FA); every subsequent CI + publish is tokenless OIDC. - **D-3 — The scoped name + `publishConfig` + `private` removal land together in ONE switch-flip commit (048b),** so the repo is never in a half-flipped state where the preflight's intent is ambiguous. - **D-4 — README + SDK docs are reconciled to `@legioncodeinc/honeycomb` in the same change as the rename @@ -118,14 +148,17 @@ together they are the strongest go-live confidence available short of publishing ## Acceptance criteria - **AC-1 — The npm org is real and publish-capable (048a).** `@legioncodeinc` exists on npmjs.com; the - CI automation identity and the maintainer are members with publish rights to the scope; an Automation - token is minted and stored as the `NPM_TOKEN` GitHub Actions repo secret. Verified by `npm org ls - legioncodeinc` (or the npmjs UI) and a token-present `release.yaml` run reaching the publish step in - `--dry-run` mode. -- **AC-2 — The four go-public switches are flipped (048b).** `package.json` has `name: + maintainer is a member with publish rights to the scope; GitHub Actions is configured as the package's + **trusted publisher** (org + repo `honeycomb` + workflow `release.yaml`) so CI publishes tokenlessly via + OIDC — **no `NPM_TOKEN` secret**. Verified by `npm org ls legioncodeinc` (or the npmjs UI), the trusted + publisher appearing on the package's npm settings, and a `release.yaml` dispatch run reaching the publish + step in `--dry-run` mode. (Bootstrap: the trusted publisher can only be set once the package exists, so + the first real publish is a one-time manual 2FA publish — out of scope here per D-1.) +- **AC-2 — The in-repo go-public switches are flipped (048b).** `package.json` has `name: "@legioncodeinc/honeycomb"`, an uncommented `publishConfig` with `access: "public"` + `provenance: true`, and **no** `private` key; the release preflight's two abort conditions are both false. Proven by - the preflight step passing in a dispatch run. + the preflight step passing in a dispatch run. (The former fourth switch — "set `NPM_TOKEN`" — is + superseded by the off-repo trusted-publisher configuration in AC-1; there is no token switch.) - **AC-3 — Name references are consistent (048b).** No shipped artifact advertises the non-existent `@honeycomb/sdk`: README and `src/sdk/CONVENTIONS.md` + the `src/sdk/index.ts` barrel comment reference `@legioncodeinc/honeycomb` (+ its `/react`,`/vercel`,`/openai` subpaths). `grep -rn "@honeycomb/sdk"` @@ -136,8 +169,10 @@ together they are the strongest go-live confidence available short of publishing (or a throwaway bump) showing all harness manifests updated and staged, then reverted. - **AC-5 — The pipeline is green end-to-end in dry-run (048d).** A `workflow_dispatch` run of `release.yaml` (with the switches flipped on the branch) clears the full gate, build, audits, - pack-check, the tag-vs-version guard (or its dispatch-skip), the **now-passing** preflight, and reaches - `npm publish --provenance --access public --dry-run` green. + pack-check, the tag-vs-version guard (or its dispatch-skip), the **now-passing** preflight, the npm + upgrade (>= 11.5.1) and trigger-gated publish-mode resolution (dispatch → dry-run), and reaches + `npm publish --provenance --access public --dry-run` green. (The `--dry-run` does no OIDC handshake, so + it is green without the trusted publisher configured — that is exercised only by a real tag-push publish.) - **AC-6 — The runtime tarball actually works (048d).** `npm run pack:check` passes (required files present, no forbidden files/secrets), and `npm pack` + `npm install ./*.tgz` into a scratch dir yields a working `honeycomb` CLI and a dashboard whose assets (CSS, fonts, logo) load. @@ -146,21 +181,29 @@ together they are the strongest go-live confidence available short of publishing PRD close. The go-live runbook (RELEASING.md "Cut the release") is confirmed accurate against the flipped state and is the only remaining step. - **AC-8 — Gates stay green.** `npm run ci` / `build` / `audit:openclaw` / `audit:sql` / `pack:check` - stay green across every sub-PRD; no secret/token is committed (the `NPM_TOKEN` lives only in GitHub - Actions secrets, never in the repo — grep-proven). + stay green across every sub-PRD; no secret/token is committed. With Trusted Publishing there is no + `NPM_TOKEN` at all; the only release secret is `HONEYCOMB_POSTHOG_KEY`, which lives only in GitHub + Actions secrets, never in the repo — grep-proven. ## Risks / Out of scope - **Risk — flipping `private`/name DISARMS the only hard safety catch.** Once 048b lands, the preflight no - longer fails closed, so an accidental tag push or a `dry_run=false` dispatch with the token set WOULD - publish. Mitigated by D-1/D-6 (no tag this PRD), by 048d documenting that "no tag + no real dispatch" is - now the operative guard, and by the token-gate still requiring `NPM_TOKEN` to be present for a real - publish. The go-live tag remains a deliberate human action. + longer fails closed, so an accidental tag push WOULD publish. Note the auth switch (D-2′) REMOVES the old + token-gate backstop: there is no `NPM_TOKEN` whose absence used to force a dry-run, so a real publish is + now gated purely on the TRIGGER — only a tag push publishes (a `workflow_dispatch` always dry-runs because + it is not a tag ref). Mitigated by D-1/D-6 (no tag this PRD) and by 048d documenting that "no `vX.Y.Z` tag + pushed" is now the operative guard. The go-live tag remains a deliberate human action; the trusted + publisher only authorizes publishes from this repo's `release.yaml`, so no other workflow can publish. - **Risk — the `@legioncodeinc` npm org name is itself taken/unavailable.** Mitigated in 048a: verify scope availability FIRST; if unavailable, escalate the naming decision (the alternatives `@legioncode` / `@olliebot` were on the table) before any in-repo rename in 048b. -- **Risk — automation token scope/expiry.** An over-broad or short-lived token is a supply-chain liability. - Mitigated by 048a: a granular Automation token scoped to the package/scope, with a documented rotation - owner. +- **Risk — Trusted Publishing config + bootstrap + npm version floor (replaces the old automation-token + scope/expiry risk — no token now).** Three operational gotchas: (1) the trusted publisher must be + configured on npm with the exact org + repo `honeycomb` + workflow filename `release.yaml` (a mismatch + silently denies the OIDC publish); (2) a trusted publisher can only be set on a package that already + exists, so the FIRST publish is a one-time manual 2FA publish (bootstrap) before CI can publish + tokenlessly; (3) CI must run npm >= 11.5.1 or OIDC never engages — `release.yaml` upgrades npm before + publish. Mitigated by 048a (configure the publisher, do the bootstrap publish) and the workflow's npm + upgrade step. Removing the long-lived token eliminates the leak/rotation liability entirely. - **Risk — first publish is irreversible-ish.** npm unpublish is heavily restricted within 72h and blocked after. Mitigated by D-1 (this PRD does not publish) + the local pack-install dogfood (048d) catching artifact defects before the human ever cuts the real tag. @@ -175,9 +218,10 @@ together they are the strongest go-live confidence available short of publishing GitHub Release uses `generate_release_notes` for now. ## Dependencies -- **The release pipeline (the machinery).** `.github/workflows/release.yaml` (gate + preflight + token gate - + provenance + Release), `RELEASING.md` (the canonical go-public runbook this PRD executes and verifies), - `scripts/pack-check.mjs` (the tarball guard 048d runs). +- **The release pipeline (the machinery).** `.github/workflows/release.yaml` (gate + preflight + + trigger-gated publish mode + tokenless OIDC Trusted Publishing + provenance + Release), `RELEASING.md` + (the canonical go-public runbook this PRD executes and verifies), `scripts/pack-check.mjs` (the tarball + guard 048d runs). - **`package.json`** — the `name`, `private`, commented `publishConfig`, `files` allowlist, `bin`, and the `scripts` block where 048c wires the `"version"` lifecycle hook. - **`scripts/sync-versions.mjs`** — the version single-source propagator 048c wires into `npm version`. diff --git a/library/requirements/backlog/prd-048-npm-publishing-pipeline/prd-048a-npm-org-provisioning.md b/library/requirements/backlog/prd-048-npm-publishing-pipeline/prd-048a-npm-org-provisioning.md index 5cf78dc..0444c5e 100644 --- a/library/requirements/backlog/prd-048-npm-publishing-pipeline/prd-048a-npm-org-provisioning.md +++ b/library/requirements/backlog/prd-048-npm-publishing-pipeline/prd-048a-npm-org-provisioning.md @@ -1,51 +1,87 @@ -# PRD-048a — npm org `@legioncodeinc` provisioning + automation token +# PRD-048a — npm org `@legioncodeinc` provisioning + trusted publisher > Status: backlog · Parent: PRD-048 · Wave: W0 · Type: S (off-repo, manual) -> Goal: make the scope `@legioncodeinc` a real, publish-capable npm org with the CI automation identity -> and the maintainer as members, and wire its Automation token into GitHub Actions as `NPM_TOKEN` — the -> hard off-repo prerequisite every later sub-PRD's publish step depends on. +> Goal: make the scope `@legioncodeinc` a real, publish-capable npm org with the maintainer as a member, +> and configure **GitHub Actions as the package's trusted publisher** so CI publishes tokenlessly via OIDC +> (npm Trusted Publishing) — the hard off-repo prerequisite every later sub-PRD's publish step depends on. +> **No `NPM_TOKEN` is minted or stored** (supersedes the original automation-token approach — see PRD-048 +> D-2′). ## Why -RELEASING.md step (a)/(d): a scoped publish requires the **npm org/scope to already exist** with the -publishing identity as a member, and `release.yaml`'s token gate reads a `NPM_TOKEN` secret (an -**Automation** token, so it bypasses 2FA in CI). None of this lives in the repo — it is account-side -provisioning that must be done by hand, with the maintainer's npmjs account, before the in-repo rename -(048b) means anything. Doing it first also de-risks the naming decision: if `@legioncodeinc` is somehow -unavailable, we learn it here, before any in-repo churn. +RELEASING.md step (a): a scoped publish requires the **npm org/scope to already exist** with the +publishing identity as a member. The auth mechanism, however, has been **amended (PRD-048 D-2′, operator +decision)** from an org Automation token to **npm Trusted Publishing (OIDC)**: instead of a long-lived +`NPM_TOKEN` secret, `release.yaml` presents a short-lived GitHub OIDC identity (`id-token: write`) that +npm verifies against a **trusted publisher** configured on the package. None of this lives in the repo — +it is account-side provisioning that must be done by hand with the maintainer's npmjs account before the +in-repo rename (048b) means anything. Doing it first also de-risks the naming decision: if `@legioncodeinc` +is somehow unavailable, we learn it here, before any in-repo churn. + +**Bootstrap nuance (the one real wrinkle of Trusted Publishing).** A trusted publisher can only be +configured on a package that **already exists** on npm. So the very first publish cannot be tokenless — it +is a one-time **manual publish** (interactive, 2FA) by a maintainer/org member, which creates the package +on the registry. Only after the package exists can the trusted publisher be attached, after which every +subsequent CI publish from `release.yaml` is tokenless OIDC. (That first manual publish is the deliberate +go-live step PRD-048 D-1 keeps out of scope; this sub-PRD provisions everything up to it.) ## What (scope) - **Verify scope availability.** Confirm `@legioncodeinc` is free / already owned by us on npmjs.com. If taken by a third party, STOP and escalate the naming decision (alternatives `@legioncode`, `@olliebot`) to the PRD-048 owner before 048b renames anything. - **Create the org** `@legioncodeinc` on npmjs.com (if it does not exist) under the maintainer's account. -- **Add members + rights.** Maintainer = org member with publish rights (break-glass / local manual - publishes). Mint a granular **Automation** access token scoped to publish for `@legioncodeinc/*` (or the - single package) — this is the CI identity. -- **Wire the secret.** GitHub → repo Settings → Secrets and variables → Actions → new repository secret - `NPM_TOKEN` = the Automation token. -- **Document the rotation owner** (who rotates the token, on what cadence) in RELEASING.md or a short note, - so the credential is not orphaned. +- **Add members + rights.** Maintainer = org member with publish rights — needed for the one-time + bootstrap publish and any break-glass / local manual publishes. +- **Configure the trusted publisher (the CI identity — replaces the automation token).** On the package's + npm settings (after the bootstrap publish has created the package), add a **GitHub Actions trusted + publisher** with: + - **Organization / user:** `legioncodeinc` + - **Repository:** `honeycomb` + - **Workflow filename:** `release.yaml` + - **Environment:** optional — leave blank, or set one (and add a matching `environment:` to the publish + job in `release.yaml`) if an extra approval gate is wanted. + No token is generated, scoped, or stored anywhere. CI auth is the OIDC handshake; the human identity is + only the owner-of-last-resort for break-glass. +- **No `NPM_TOKEN` secret.** Do NOT mint an Automation token and do NOT set an `NPM_TOKEN` GitHub Actions + secret — the workflow no longer reads one. (The only release secret is `HONEYCOMB_POSTHOG_KEY` for + PRD-050e telemetry, set the same way; see the PRD-048 cross-PRD note.) +- **Note the npm-version floor.** Trusted Publishing requires **npm >= 11.5.1** in CI; `release.yaml` + upgrades npm before publish (Node 22 ships npm 10.x). No account-side action — recorded here so the + provisioning owner understands why the workflow upgrades npm. ## Acceptance criteria - **a-AC-1 — Org exists + is ours.** `@legioncodeinc` exists on npmjs.com and the maintainer's account is a member with publish rights. Verified via `npm org ls legioncodeinc` or the npmjs org page. -- **a-AC-2 — Automation token minted + scoped.** An Automation-type token (2FA-bypassing) scoped to the - package/scope exists. Its value is NEVER committed to the repo or pasted into any tracked file. -- **a-AC-3 — `NPM_TOKEN` secret set.** The token is stored as the GitHub Actions repository secret - `NPM_TOKEN`. Verified by `release.yaml`'s token gate resolving `has_token=true` on a dispatch run. -- **a-AC-4 — Rotation owner recorded.** A one-line note names who owns token rotation and the cadence. +- **a-AC-2 — Trusted publisher configured (no token).** A GitHub Actions trusted publisher is configured on + the package — org `legioncodeinc`, repo `honeycomb`, workflow `release.yaml` (optional environment) — so + CI publishes tokenlessly via OIDC. **No Automation token is minted and no `NPM_TOKEN` secret exists** in + the repo or GitHub Actions. (Because the trusted publisher requires the package to exist, this AC is + satisfied once the one-time bootstrap publish has run and the publisher is attached — see a-AC-3.) +- **a-AC-3 — Bootstrap path documented.** RELEASING.md (or a short note) records that the FIRST publish is a + one-time manual 2FA publish by an org member to create the package, after which the trusted publisher is + attached and all subsequent CI publishes are tokenless. (The bootstrap publish itself is the PRD-048 D-1 + go-live step — out of scope here; only the documented path is in scope.) +- **a-AC-4 — npm-version floor noted.** It is recorded (here and/or RELEASING.md) that CI must run + npm >= 11.5.1 for OIDC to engage, and that `release.yaml` upgrades npm before publish — so the provisioner + does not mistake a tokenless-but-old-npm run for a misconfigured publisher. ## Risks / Out of scope - **Risk — scope unavailable.** Handle by verifying FIRST (a-AC-1 blocks 048b); escalate naming, do not silently pick a different scope. -- **Risk — token too broad / never rotated.** Mitigated by a granular Automation token + a recorded - rotation owner (a-AC-2/a-AC-4). +- **Risk — trusted-publisher misconfig silently denies the publish.** A mismatch in org/repo/workflow + filename causes npm to reject the OIDC publish. Mitigated by a-AC-2's exact triple + (`legioncodeinc` / `honeycomb` / `release.yaml`) and a dry-run rehearsal (048d) before the real tag. +- **Risk — bootstrap chicken-and-egg.** Trusted publishing cannot be configured before the package exists; + forgetting this makes the first CI publish fail. Mitigated by a-AC-3 documenting the one-time manual + bootstrap publish. - **Out of scope — the in-repo rename / switch-flips (048b).** This sub-PRD only provisions the account side; it changes no `package.json` field. -- **Out of scope — actually publishing.** Provisioning makes a real publish possible; PRD-048 D-1 keeps it - out of scope. +- **Out of scope — actually publishing (incl. the bootstrap publish).** Provisioning makes a real publish + possible; PRD-048 D-1 keeps the go-live publish out of scope. ## Dependencies - npmjs.com account (the maintainer's — confirmed: the user has an NPMjs account). -- GitHub repo admin access to set the Actions secret. -- `release.yaml`'s token gate (the consumer of `NPM_TOKEN`) — already wired; this only feeds it. +- GitHub repo admin access to configure the trusted publisher on npm (and, if an environment is used, to + create it in repo settings). No GitHub Actions secret is set for npm auth. +- `release.yaml`'s OIDC publish path (`id-token: write` + the npm-upgrade step) only **consumes** the + OIDC trust — it does not configure the publisher. This sub-PRD (048a) configures the npm-side trusted + publisher that publish path authenticates against. diff --git a/library/requirements/backlog/prd-048-npm-publishing-pipeline/prd-048d-rehearsal-verification.md b/library/requirements/backlog/prd-048-npm-publishing-pipeline/prd-048d-rehearsal-verification.md index 49273af..f139d1e 100644 --- a/library/requirements/backlog/prd-048-npm-publishing-pipeline/prd-048d-rehearsal-verification.md +++ b/library/requirements/backlog/prd-048-npm-publishing-pipeline/prd-048d-rehearsal-verification.md @@ -12,30 +12,36 @@ the live dogfood before declaring a wiring PRD done — a CI dry-run that never while the tarball is missing a runtime asset (CSS, fonts, the logo, a bin) that `pack-check` allow-lists but a real install would expose. RELEASING.md confirms both rehearsals are safe: `private: true` blocks `npm publish` but NOT `npm pack` / local install, and the `workflow_dispatch` `dry_run` defaults to true. -After 048b removes `private`, the catch that matters becomes "no tag pushed + no real dispatch" — this -sub-PRD makes that the explicit, documented operative guard and verifies the pipeline stops exactly one -deliberate human action short of live. +After 048b removes `private` and the auth switches to trigger-gated OIDC (PRD-048 D-2′), the catch that +matters becomes "no `vX.Y.Z` tag pushed" — a `workflow_dispatch` always dry-runs (it is not a tag ref), so +only a tag triggers a real publish. This sub-PRD makes that the explicit, documented operative guard and +verifies the pipeline stops exactly one deliberate human action short of live. ## What (scope) - **CI dry-run rehearsal.** On the branch with 048b's switches flipped, run `release.yaml` via Actions → Release → Run workflow with `dry_run` checked (default). Confirm it clears the full gate, - build, audits, pack-check, the dispatch-skipped tag guard, the now-passing preflight, the token gate, and - reaches `npm publish --provenance --access public --dry-run` GREEN. + build, audits, pack-check, the dispatch-skipped tag guard, the now-passing preflight, the npm upgrade + (>= 11.5.1) and the trigger-gated publish-mode step (a dispatch resolves to dry-run because it is not a + tag ref — no token involved), and reaches `npm publish --provenance --access public --dry-run` GREEN. + (The `--dry-run` does no OIDC handshake, so it is green even before the trusted publisher is attached.) - **Local pack-check.** `npm run pack:check` (runs `prepack` → fresh build, then scans the tarball for forbidden/secret files and asserts required runtime files present). - **Local install dogfood.** `npm pack`, then `npm install ./legioncodeinc-honeycomb-*.tgz` into a scratch dir; run `honeycomb --help` and launch the dashboard; confirm assets (CSS, fonts, logo) load — the runtime files pack-check allow-lists. -- **Document the operative guard.** Record (in RELEASING.md or 048's report) that, post-048b, the safety - catch is no longer the preflight but "no `vX.Y.Z` tag pushed + no `dry_run=false` dispatch", and that the - real go-live is RELEASING.md "Cut the release" — a separate, deliberate step. +- **Document the operative guard.** Record (in RELEASING.md or 048's report) that, post-048b + the OIDC + auth switch (D-2′), the safety catch is no longer the preflight nor a token, but the **trigger**: only a + `vX.Y.Z` tag push publishes (a `workflow_dispatch` always dry-runs, even with `dry_run=false`, because it + is not a tag ref). So the operative guard is "**no `vX.Y.Z` tag pushed**", and the real go-live is + RELEASING.md "Cut the release" — a separate, deliberate step. - **Confirm nothing published.** `npm view @legioncodeinc/honeycomb` still 404s / shows nothing we published, at PRD close. ## Acceptance criteria - **d-AC-1 — CI dry-run green to the publish step.** A `workflow_dispatch` `dry_run=true` run of `release.yaml` on the switch-flipped branch reaches `npm publish … --dry-run` green; the preflight passes - (not fails closed); the token gate resolves correctly. Run URL recorded in the report. + (not fails closed); the publish-mode step resolves to dry-run (a dispatch is not a tag ref — no token + gate exists any more, auth is tokenless OIDC). Run URL recorded in the report. - **d-AC-2 — pack-check passes.** `npm run pack:check` is green: required runtime files present, no forbidden files, no secrets/tokens in the tarball. - **d-AC-3 — Installed tarball works.** `npm pack` + install into a scratch dir yields a working @@ -44,13 +50,18 @@ deliberate human action short of live. - **d-AC-4 — No go-live occurred.** No `vX.Y.Z` tag pushed; no real publish; `npm view @legioncodeinc/honeycomb` shows nothing published by us. (PRD-048 AC-7.) - **d-AC-5 — Operative guard + go-live runbook documented.** RELEASING.md (or the 048 report) states that - the preflight is now disarmed, names "no tag + no real dispatch" as the live guard, and confirms "Cut the - release" is accurate against the flipped state. + the preflight is now disarmed and auth is tokenless OIDC, names "**no `vX.Y.Z` tag pushed**" as the live + guard (a dispatch always dry-runs, so only a tag triggers a real publish), and confirms "Cut the release" + is accurate against the flipped state (including the one-time manual bootstrap publish). ## Risks / Out of scope -- **Risk — an accidental real publish during rehearsal.** A `dry_run=false` dispatch (or a stray tag) with - the token set WOULD publish now that the preflight is disarmed. Mitigated by always leaving `dry_run` - checked, pushing NO tag, and d-AC-5's explicit guard documentation. +- **Risk — an accidental real publish during rehearsal.** Now that the preflight is disarmed (048b) and + auth is trigger-gated OIDC (D-2′), a real publish is gated purely on a **tag push** — a `workflow_dispatch` + (even `dry_run=false`, even if pointed at a tag ref) resolves to dry-run because the mode step requires + `github.event_name == 'push'` — a `workflow_dispatch` never sets `publish=true`. So the stray-tag path is + the live risk: a pushed `vX.Y.Z` tag would publish. Mitigated by pushing NO tag this PRD, the trusted + publisher only authorizing publishes from this repo's `release.yaml`, and d-AC-5's explicit guard + documentation. - **Risk — eventual-consistency / flaky live reads in any smoke.** Project memory: poll to convergence, never a single immediate read, in any live-backed smoke this dogfood touches. - **Risk — scratch-dir install pulls the heavy optional embed dep.** The `@huggingface/transformers`