mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(release): npm staged publishing + pre-publish boot-smoke (WS1.3) (#7092)
* feat(release): npm staged publishing + pre-publish boot-smoke (WS1.3) v3.8.47 shipped an npm tarball that crashed on every boot and had to be deprecated — the publish path had no runtime gate and the owner's 2FA happened BEFORE any proof. Two changes to npm-publish.yml: - check:pack-boot runs right before any publish (dist/ is already assembled by build:cli in the same job) — a non-booting tarball now fails the workflow before anything reaches the registry. - npm publish becomes 'npm stage publish' (staged publishing, GA 2026-05-22, npm >= 11.15 ensured in-job): the exact bytes are parked on the registry but NOT installable until the owner runs 'npm stage approve <id>' with 2FA. The workflow summary prints the approve/verify/reject flow; RELEASE_CHECKLIST documents the owner flow, the one-time Trusted Publisher stage-only config, and the deprecate-first rollback playbook. publish_mode=direct (workflow_dispatch) is the emergency fallback to the legacy immediate publish. First real-registry exercise happens on the next release with the fallback one dispatch away (D2 decision, v3.8.49 plan). GitHub Packages secondary publish unchanged. YAML parse validated. * docs(release): reference upcoming verifier without file paths (docs-all strict) * fix(release): pin npm 11.15.0 in the staged-publish version guard (no @latest in the publish job)
This commit is contained in:
committed by
GitHub
parent
5ab63203aa
commit
a96e4b58f8
64
.github/workflows/npm-publish.yml
vendored
64
.github/workflows/npm-publish.yml
vendored
@@ -22,6 +22,14 @@ on:
|
||||
- latest
|
||||
- next
|
||||
- historic
|
||||
publish_mode:
|
||||
description: "staged = npm stage publish (owner approves with 2FA after the staged boot-verify); direct = legacy immediate publish (emergency fallback only)"
|
||||
required: false
|
||||
default: "staged"
|
||||
type: choice
|
||||
options:
|
||||
- staged
|
||||
- direct
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
@@ -166,8 +174,34 @@ jobs:
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: gh release upload "$TAG" sbom-npm.cdx.json --clobber
|
||||
|
||||
- name: Publish to npm
|
||||
# WS1.2/WS1.3 (#7065 class): the artifact that is about to be published must
|
||||
# BOOT. build:cli already assembled dist/ above; this packs+installs+boots the
|
||||
# real tarball and fails the publish before anything reaches the registry.
|
||||
- name: Boot-smoke the tarball before ANY publish
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
run: npm run check:pack-boot
|
||||
|
||||
# WS1.3 (D2, v3.8.49 plan): STAGED publishing by default — `npm stage publish`
|
||||
# parks the exact bytes on the registry WITHOUT making them installable; the
|
||||
# owner then verifies and approves with 2FA (`npm stage approve`), moving the
|
||||
# human gate to AFTER the proof instead of before it. Requires npm >= 11.15
|
||||
# (staged publishing GA 2026-05-22). publish_mode=direct is the emergency
|
||||
# fallback (legacy immediate publish) via workflow_dispatch.
|
||||
- name: Ensure npm supports staged publishing
|
||||
if: steps.resolve.outputs.skip != 'true' && (github.event_name != 'workflow_dispatch' || inputs.publish_mode != 'direct')
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CUR=$(npm --version)
|
||||
if ! node -e "const [a,b]='$(npm --version)'.split('.').map(Number); process.exit(a>11||(a===11&&b>=15)?0:1)"; then
|
||||
# Pinned exact version (supply-chain: never float @latest in the publish
|
||||
# job); bump deliberately when a newer npm is required.
|
||||
echo "npm $CUR < 11.15 — installing pinned npm 11.15.0 for staged publishing"
|
||||
npm install -g --ignore-scripts npm@11.15.0
|
||||
fi
|
||||
npm --version
|
||||
|
||||
- name: Publish to npm (staged — owner approves with 2FA)
|
||||
if: steps.resolve.outputs.skip != 'true' && (github.event_name != 'workflow_dispatch' || inputs.publish_mode != 'direct')
|
||||
env:
|
||||
VERSION: ${{ steps.resolve.outputs.version }}
|
||||
TAG: ${{ steps.resolve.outputs.tag }}
|
||||
@@ -175,10 +209,32 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Always pass --tag explicitly. Defense in depth: even if VERSION is
|
||||
# accidentally an older release, `npm publish --tag historic` will
|
||||
# NOT promote it to `@latest`.
|
||||
# accidentally an older release, the historic tag will NOT claim `@latest`.
|
||||
npm stage publish --provenance --access public --tag "$TAG"
|
||||
{
|
||||
echo "## 📦 omniroute@$VERSION STAGED (not yet installable)"
|
||||
echo ""
|
||||
echo "The exact bytes are parked on the registry. To release them:"
|
||||
echo '```'
|
||||
echo "npm stage list omniroute # find the stage id"
|
||||
echo "npm stage approve <id> # owner 2FA — THE publish"
|
||||
echo '```'
|
||||
echo "To verify the staged bytes first: npm stage download <id> → run"
|
||||
echo "scripts/check/check-pack-boot.mjs against them (see RELEASE_CHECKLIST)."
|
||||
echo "To discard: npm stage reject <id>."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "✅ Staged omniroute@$VERSION (dist-tag=$TAG) — awaiting owner 'npm stage approve'"
|
||||
|
||||
- name: Publish to npm (DIRECT — emergency fallback)
|
||||
if: steps.resolve.outputs.skip != 'true' && github.event_name == 'workflow_dispatch' && inputs.publish_mode == 'direct'
|
||||
env:
|
||||
VERSION: ${{ steps.resolve.outputs.version }}
|
||||
TAG: ${{ steps.resolve.outputs.tag }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm publish --provenance --access public --tag "$TAG"
|
||||
echo "✅ Published omniroute@$VERSION (dist-tag=$TAG)"
|
||||
echo "✅ Published omniroute@$VERSION (dist-tag=$TAG) [DIRECT mode]"
|
||||
|
||||
- name: Publish to GitHub Packages
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
|
||||
1
changelog.d/maintenance/npm-staged-publishing.md
Normal file
1
changelog.d/maintenance/npm-staged-publishing.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Release**: npm publishing is now STAGED by default — the workflow boots the packed tarball (`check:pack-boot`) and runs `npm stage publish` (bytes parked on the registry, not installable); the owner verifies the staged bytes and releases them with `npm stage approve` + 2FA, moving the human gate to after the proof (the structural fix for the #7065 broken-tarball class); `publish_mode=direct` remains as a documented emergency fallback
|
||||
@@ -37,6 +37,34 @@ npm run test:e2e # optional but recommended
|
||||
/capture-release-evidences-cc
|
||||
```
|
||||
|
||||
## npm Staged Publishing (default since v3.8.49 — WS1.3/D2)
|
||||
|
||||
The npm-publish workflow no longer publishes directly: it boots the packed tarball
|
||||
(`check:pack-boot`) and then runs `npm stage publish` — the exact bytes are parked on
|
||||
the registry, **not installable** until the owner approves. The human 2FA gate moved
|
||||
to AFTER the proof, not before it.
|
||||
|
||||
**Owner flow after the workflow goes green:**
|
||||
|
||||
1. `npm stage list omniroute` — find the stage id (also printed in the workflow summary).
|
||||
2. Verify the staged bytes (recommended): `npm stage download <id>`, then install the
|
||||
downloaded tarball into a temp prefix and boot it (`npm run check:pack-boot` automates
|
||||
the same pack→install→boot verdict in CI).
|
||||
3. `npm stage approve <id>` — the 2FA prompt IS the publish. `npm stage reject <id>` discards.
|
||||
4. Post-publish net: the post-publish verifier (WS1.4 of the v3.8.49 plan) installs the
|
||||
published version from the public registry in a clean container and boots it.
|
||||
|
||||
**Emergency fallback:** `workflow_dispatch` with `publish_mode=direct` restores the
|
||||
legacy immediate `npm publish` (use only if staging itself misbehaves; record why).
|
||||
|
||||
**One-time hardening (owner, npmjs.com):** configure the Trusted Publisher for
|
||||
`omniroute` in stage-only mode so a leaked long-lived token cannot `npm publish`
|
||||
directly from anywhere — CI can only stage; only the owner's 2FA releases.
|
||||
|
||||
**Broken-artifact playbook (unchanged):** `npm deprecate omniroute@<bad> "<reason> — use <fixed>"`
|
||||
as the default reflex (minutes, reversible); `npm unpublish` only inside the 72h/no-dependents
|
||||
window and never as the first move. Docker: never rewrite a version tag — rollback is
|
||||
repointing `latest` to the last good digest.
|
||||
## Hotfix Fast-Lane (label `hotfix`)
|
||||
|
||||
A PR labeled `hotfix` skips the heavy CI matrix (9-shard E2E, coverage ratchet,
|
||||
|
||||
Reference in New Issue
Block a user