| 1 | # CodeWhale Release Runbook |
| 2 | |
| 3 | This runbook is the source of truth for shipping Rust crates, GitHub release assets, |
| 4 | and the `codewhale` npm wrapper. |
| 5 | |
| 6 | Current packaging note: |
| 7 | - `codewhale-tui` is the live runtime crate shipped to users today. |
| 8 | - `codewhale-app-server` is a supporting library crate. The shipped entrypoint |
| 9 | is `codewhale app-server`; do not add or publish a standalone app-server binary. |
| 10 | |
| 11 | ## Canonical Publish Targets |
| 12 | |
| 13 | - End-user crates: |
| 14 | - `codewhale-tui` |
| 15 | - `codewhale-cli` |
| 16 | - Supporting crates published from this workspace: |
| 17 | - `codewhale-build-support` |
| 18 | - `codewhale-mcp` |
| 19 | - `codewhale-paths` |
| 20 | - `codewhale-protocol` |
| 21 | - `codewhale-release` |
| 22 | - `codewhale-secrets` |
| 23 | - `codewhale-state` |
| 24 | - `codewhale-workflow` |
| 25 | - `codewhale-workflow-js` |
| 26 | - `codewhale-execpolicy` |
| 27 | - `codewhale-hooks` |
| 28 | - `codewhale-tools` |
| 29 | - `codewhale-config` |
| 30 | - `codewhale-lane` |
| 31 | - `codewhale-agent` |
| 32 | - `codewhale-core` |
| 33 | - `codewhale-app-server` |
| 34 | |
| 35 | ## Version Coordination |
| 36 | |
| 37 | - Rust crates inherit the shared workspace version from [Cargo.toml](../Cargo.toml). |
| 38 | - Internal path dependency versions should match the shared workspace version; stale older pins are release blockers once the workspace version moves. |
| 39 | - The npm wrapper version lives in [npm/codewhale/package.json](../npm/codewhale/package.json). |
| 40 | - `codewhaleBinaryVersion` controls which GitHub release binaries the npm wrapper downloads. |
| 41 | - Packaging-only npm releases are allowed: |
| 42 | - bump the npm package version |
| 43 | - leave `codewhaleBinaryVersion` pinned to the previously released Rust binaries |
| 44 | - rerun `npm pack` smoke checks before `npm publish` |
| 45 | |
| 46 | ## Release Source Timing |
| 47 | |
| 48 | Freeze the source before creating a public `vX.Y.Z` tag. The version bump is |
| 49 | not the release; it is the last source-prep commit before the tag. Do not keep |
| 50 | merging same-version feature/fix PRs after `vX.Y.Z` exists and assume the |
| 51 | release workflow will pick them up. It will not: the tag is the release anchor. |
| 52 | |
| 53 | Before tagging, verify the live queue and existing anchors: |
| 54 | |
| 55 | ```bash |
| 56 | gh issue list --repo Hmbown/CodeWhale --milestone "vX.Y.Z" --state open |
| 57 | gh pr list --repo Hmbown/CodeWhale --state open --limit 100 |
| 58 | git ls-remote origin refs/heads/main refs/tags/vX.Y.Z |
| 59 | gh release view vX.Y.Z --repo Hmbown/CodeWhale |
| 60 | ./scripts/release/check-published.sh X.Y.Z |
| 61 | ``` |
| 62 | |
| 63 | If a same-version tag already exists but there is no GitHub Release and nothing |
| 64 | is published, stop and choose deliberately: |
| 65 | |
| 66 | - publish exactly the tagged SHA, leaving later commits for the next patch; |
| 67 | - bump the later work to the next patch version and tag that later SHA; or |
| 68 | - with explicit maintainer approval only, delete/recreate the unpublished tag |
| 69 | after confirming no package, GitHub Release, mirror, or installer consumer has |
| 70 | treated it as public. |
| 71 | |
| 72 | Do not delete, move, or recreate a release tag implicitly as part of ordinary |
| 73 | PR merge or milestone cleanup work. |
| 74 | |
| 75 | ## Preflight |
| 76 | |
| 77 | Run these from the repository root before cutting a tag: |
| 78 | |
| 79 | ```bash |
| 80 | ./scripts/release/check-versions.sh # version drift between workspace, npm, lockfile |
| 81 | cargo fmt --all -- --check |
| 82 | cargo check --workspace --all-targets --locked |
| 83 | cargo clippy --workspace --all-targets --all-features --locked -- -D warnings |
| 84 | cargo test --workspace --all-features --locked |
| 85 | ./scripts/release/publish-crates.sh dry-run |
| 86 | ``` |
| 87 | |
| 88 | `check-versions.sh` also runs in CI on every push/PR (the `versions` job in |
| 89 | `.github/workflows/ci.yml`), so drift between `Cargo.toml`, the per-crate |
| 90 | manifests, `npm/codewhale/package.json`, and `Cargo.lock` is caught before |
| 91 | release time rather than at it. |
| 92 | |
| 93 | The source-controlled CNB pipeline mirrors the heavy Linux version/fmt/check/ |
| 94 | clippy/test/npm-smoke gates for `fix/*`, `rebrand/*`, `work/v*`, and `main`. |
| 95 | GitHub Actions keeps the cheap drift/fmt statuses plus macOS and Windows |
| 96 | coverage, while CNB carries the Linux work. |
| 97 | |
| 98 | `publish-crates.sh dry-run` performs a full `cargo publish --dry-run` for crates |
| 99 | without unpublished workspace dependencies and a packaging preflight for dependent |
| 100 | workspace crates. That avoids false negatives from crates.io not yet containing the |
| 101 | new workspace version while still validating package contents before publish. |
| 102 | |
| 103 | For npm wrapper verification, build the three shipped entrypoints and run the |
| 104 | cross-platform smoke harness. This packs the npm wrapper, installs it into a |
| 105 | clean temporary project, serves local release assets over HTTP, and checks the |
| 106 | dispatcher-to-TUI path (`codewhale doctor --help`), the installed native shortcut |
| 107 | (`codew --version`), and the direct TUI entrypoint (`codewhale-tui --help`). |
| 108 | |
| 109 | ```bash |
| 110 | cargo build --release --locked -p codewhale-cli -p codewhale-tui |
| 111 | node scripts/release/npm-wrapper-smoke.js |
| 112 | ``` |
| 113 | |
| 114 | Set `DEEPSEEK_TUI_KEEP_SMOKE_DIR=1` to keep the temporary pack/install |
| 115 | directory for inspection. |
| 116 | |
| 117 | ## Exact-head GitHub proof before publication |
| 118 | |
| 119 | Two manual workflows provide exact-head evidence without crossing the public |
| 120 | release boundary. Run them only after the intended source is on a named ref |
| 121 | (normally the frozen `main`), and pass the full commit SHA as an independent |
| 122 | guard against that ref moving between inspection and dispatch: |
| 123 | |
| 124 | ```bash |
| 125 | git fetch origin main |
| 126 | candidate_sha="$(git rev-parse origin/main)" |
| 127 | |
| 128 | gh workflow run ci.yml --ref main \ |
| 129 | -f expected_sha="${candidate_sha}" |
| 130 | gh workflow run release-candidate.yml --ref main \ |
| 131 | -f expected_sha="${candidate_sha}" |
| 132 | ``` |
| 133 | |
| 134 | The manual `ci.yml` path verifies that the dispatch resolved to |
| 135 | `expected_sha`, disables light-change shortcuts, and forces the heavy Rust, |
| 136 | workflow, mobile, Actions, Linux, macOS, Windows, npm-wrapper, and documentation |
| 137 | gates. A mismatch fails before those gates start; it never silently tests a |
| 138 | different head. |
| 139 | |
| 140 | `release-candidate.yml` also fails unless the selected ref resolves to the |
| 141 | exact requested SHA. It invokes the same reusable artifact workflow as the |
| 142 | public release, building all seven targets (including Android arm64 and native |
| 143 | Windows arm64), staging `codewhale`, `codew`, and `codewhale-tui`, building the |
| 144 | NSIS installer and nine platform archives, and validating the authoritative |
| 145 | 34-file inventory from `npm/codewhale/scripts/artifacts.js`. It then installs |
| 146 | the packed npm wrapper against those assembled local assets and exercises its |
| 147 | delegated entrypoints. The resulting `codewhale-release-assets` bundle is a |
| 148 | short-lived GitHub Actions artifact only. |
| 149 | |
| 150 | This candidate workflow does not create a tag or GitHub Release, publish a |
| 151 | crate or npm package, push a container, update Homebrew, deploy anything, or |
| 152 | write repository contents. Its green result is evidence, not publication |
| 153 | authorization. The stop line remains explicit Hunter approval: do not create |
| 154 | the `vX.Y.Z` tag, dispatch `release.yml`, or run any registry publication step |
| 155 | until that approval is given. |
| 156 | |
| 157 | The Android target is cross-built and included in the checksum/bundle gates, |
| 158 | but GitHub's Linux runner cannot execute the Android binary as a real Termux |
| 159 | user. Keep the real-device limitation in the release packet unless separate |
| 160 | device evidence exists. |
| 161 | |
| 162 | To exercise `npm run release:check` locally as well, regenerate the local asset |
| 163 | directory with a full asset matrix fixture before starting the server: |
| 164 | |
| 165 | ```bash |
| 166 | DEEPSEEK_TUI_PREPARE_ALL_ASSETS=1 node scripts/release/prepare-local-release-assets.js |
| 167 | cd npm/codewhale |
| 168 | DEEPSEEK_TUI_VERSION=X.Y.Z DEEPSEEK_TUI_RELEASE_BASE_URL=http://127.0.0.1:8123/ npm run release:check |
| 169 | ``` |
| 170 | |
| 171 | Set `DEEPSEEK_TUI_VERSION` to the npm package version you are verifying for that local run. |
| 172 | |
| 173 | The CNB workflow runs the Linux tarball install + delegated-entrypoint smoke |
| 174 | test; GitHub Actions keeps macOS and Windows smoke coverage. |
| 175 | |
| 176 | After publishing, prove the release is visible in both registries: |
| 177 | |
| 178 | ```bash |
| 179 | ./scripts/release/check-published.sh X.Y.Z |
| 180 | ``` |
| 181 | |
| 182 | Do not mark a Rust release complete until that command sees `codewhale@X.Y.Z` |
| 183 | on npm and every `codewhale-*` crate at `X.Y.Z` on crates.io. For a rare |
| 184 | npm packaging-only release, run with `--allow-npm-binary-mismatch` and keep the |
| 185 | release notes explicit that no new Rust binary version shipped. |
| 186 | |
| 187 | ## Post-Merge Branch Hygiene |
| 188 | |
| 189 | After a release or scratch integration branch lands, run the branch hygiene |
| 190 | helper before pruning anything: |
| 191 | |
| 192 | ```bash |
| 193 | ./scripts/release/branch-hygiene.sh --release-branch codex/vX.Y.Z |
| 194 | ``` |
| 195 | |
| 196 | The default mode is a dry run. It reports the current checkout branch, main ref, |
| 197 | local and remote release tips, safe local or remote branch deletes, branches |
| 198 | kept for contributor work, and branches that still need a human decision. Review |
| 199 | that report before running `--prune --yes`, and add `--prune-remote` only when |
| 200 | you have confirmed the remote branches are safe to delete. |
| 201 | |
| 202 | Use `--remote upstream` when you are working from a fork and the canonical |
| 203 | release refs live on the upstream remote instead of `origin`. |
| 204 | |
| 205 | Verify the helper itself after changing it: |
| 206 | |
| 207 | ```bash |
| 208 | bash scripts/release/branch-hygiene.test.sh |
| 209 | bash scripts/release/ensure-release-on-main.test.sh |
| 210 | ``` |
| 211 | |
| 212 | Those scripts are pinned to LF line endings so the same command works from a |
| 213 | Windows checkout under Bash. |
| 214 | |
| 215 | ## Rust Crates Release |
| 216 | |
| 217 | Crate publishing to crates.io is **manual** — there is no automated |
| 218 | `crates-publish` GitHub workflow. Operators run the helpers in |
| 219 | `scripts/release/` from a developer workstation that has `cargo login` |
| 220 | configured. |
| 221 | |
| 222 | Release commits must land on `main` before any `vX.Y.Z` tag is pushed. Do not |
| 223 | tag a release-only branch. Open the release PR against `main`, let required |
| 224 | review and CI finish, merge it, then explicitly tag the final source commit |
| 225 | that is reachable from `main`. This is what lets GitHub process `Closes #N` |
| 226 | lines automatically and show the release PR as merged. The tag release workflow runs |
| 227 | `scripts/release/ensure-release-on-main.sh` for tag pushes and manual dispatches, |
| 228 | and fails branch-only release sources before assets are published. |
| 229 | |
| 230 | 1. Write the CHANGELOG entry, then run |
| 231 | `./scripts/release/prepare-release.sh X.Y.Z` — it bumps every |
| 232 | version-bearing file (workspace + crate pins + npm wrapper + README |
| 233 | install tags), refreshes the lockfile and generated files, and runs |
| 234 | the version and OHOS gates. It is safe to rerun after the workspace already |
| 235 | equals `X.Y.Z`: the second run skips replacements, refreshes the packaged |
| 236 | changelog and web facts, and reruns both gates. |
| 237 | 2. Run `./scripts/release/publish-crates.sh dry-run` locally; it must be clean. |
| 238 | 3. Merge the release PR into `main` before tagging. After the same-version |
| 239 | queue is frozen and `main` is at the intended source SHA, create `vX.Y.Z` |
| 240 | from `main` with the manual **Create release tag** workflow or with a signed |
| 241 | local tag push from a developer machine. |
| 242 | - If `RELEASE_TAG_PAT` is configured, the tag push starts `release.yml`. |
| 243 | - If no Release run appears and the tag already exists, first confirm that |
| 244 | no tag-triggered run is queued or active, then dispatch the exact tag: |
| 245 | `gh workflow run release.yml --ref vX.Y.Z -f version=X.Y.Z`. |
| 246 | - Never dispatch from `main`, and do not start a duplicate while the |
| 247 | tag-triggered run is merely delayed. The workflow serializes runs for the |
| 248 | same tag. It also refuses to start release work when that tag already owns |
| 249 | any GitHub Release asset, rechecks immediately before upload, and disables |
| 250 | the release action's overwrite behavior. A normal rerun must never replace |
| 251 | public bytes. |
| 252 | 4. Wait for the GitHub Release workflow and all public assets to finish, then |
| 253 | fetch the release tag and run the public asset gate. Do not publish any |
| 254 | Cargo or npm package until it passes: |
| 255 | |
| 256 | ```bash |
| 257 | git fetch --force origin +refs/tags/vX.Y.Z:refs/tags/vX.Y.Z |
| 258 | ./scripts/release/verify-release-assets.sh X.Y.Z |
| 259 | ``` |
| 260 | |
| 261 | 5. Create a clean detached checkout of the immutable release tag, then publish |
| 262 | the Rust crates from that checkout only: |
| 263 | |
| 264 | ```bash |
| 265 | git worktree add --detach ../codewhale-release-vX.Y.Z vX.Y.Z |
| 266 | cd ../codewhale-release-vX.Y.Z |
| 267 | ./scripts/release/require-release-tag-checkout.sh X.Y.Z |
| 268 | ./scripts/release/publish-crates.sh publish |
| 269 | ``` |
| 270 | |
| 271 | Both Cargo and npm publication fail closed unless `HEAD`, the clean local |
| 272 | checkout, and the remote `vX.Y.Z` tag still agree. The authoritative 18-crate |
| 273 | dependency order lives in `scripts/release/crates.sh`; do not maintain a |
| 274 | second handwritten order in this runbook. The helper waits for each new |
| 275 | version to appear on crates.io before moving to dependents and safely skips |
| 276 | versions that are already public on a rerun. |
| 277 | |
| 278 | The publish helper is idempotent for reruns: already-published crate versions are skipped. |
| 279 | |
| 280 | ## GitHub Release Assets |
| 281 | |
| 282 | `.github/workflows/release.yml` builds these binaries: |
| 283 | |
| 284 | - `codewhale-*` CLI binaries for Linux x64/arm64, Android arm64, macOS |
| 285 | x64/arm64, and Windows x64/arm64 |
| 286 | - `codewhale-tui-*` TUI binaries for the same target matrix |
| 287 | - `codew-*` shortcut binaries for the same target matrix |
| 288 | - `codewhale.bat` for the Windows npm launcher |
| 289 | - platform `.tar.gz` / `.zip` archives and `CodeWhaleSetup.exe` |
| 290 | |
| 291 | The release job also uploads `codewhale-artifacts-sha256.txt` and |
| 292 | `codewhale-bundles-sha256.txt`. The npm installer and release verification |
| 293 | script depend on those manifests. The authoritative release asset list lives in |
| 294 | `npm/codewhale/scripts/artifacts.js`. |
| 295 | |
| 296 | Before any Cargo or npm publish, prove that the public GitHub Release assets |
| 297 | belong to the tag commit you are publishing: |
| 298 | |
| 299 | ```bash |
| 300 | ./scripts/release/verify-release-assets.sh X.Y.Z |
| 301 | ``` |
| 302 | |
| 303 | That gate compares the local and remote `vX.Y.Z` tag SHAs, confirms a |
| 304 | successful `Release` workflow run used that SHA, then runs the npm wrapper's |
| 305 | release check against the public GitHub asset URLs. The npm check fails if the |
| 306 | release is missing a required binary, archive, installer, or manifest; either |
| 307 | manifest omits a required row; or the assets predate the matching release |
| 308 | workflow run. If the command fails, rerun or repair `release.yml`; do not |
| 309 | publish Cargo or npm against stale assets. |
| 310 | |
| 311 | ## npm Wrapper Release |
| 312 | |
| 313 | **The npm publish step is manual.** `release.yml` no longer runs `npm publish` |
| 314 | because the npm account requires 2FA OTP on every publish, and an automation |
| 315 | token that bypasses 2FA has not been provisioned. The GitHub Release flow |
| 316 | remains fully automated; only the npm wrapper publish requires a developer |
| 317 | on a workstation with `npm login` and an authenticator app. |
| 318 | |
| 319 | ### Steps |
| 320 | |
| 321 | 1. Set the npm package version in [npm/codewhale/package.json](../npm/codewhale/package.json) to match the workspace `Cargo.toml`. CI's version-drift guard will catch mismatches before tag. |
| 322 | 2. Set `codewhaleBinaryVersion` to the GitHub release tag that should supply binaries. |
| 323 | 3. Push the version bump to `main`. After the release source is frozen, create |
| 324 | the matching `vX.Y.Z` tag from `main`; `release.yml` then builds the binary |
| 325 | matrix and drafts the GitHub Release. |
| 326 | 4. **Wait for the GitHub Release to finalize** with the full binary and archive |
| 327 | matrix, Windows installer, and both checksum manifests. The npm |
| 328 | `prepublishOnly` hook (`scripts/verify-release-assets.js`) requires every |
| 329 | asset to be present. |
| 330 | 5. Run the public asset freshness gate from the repo root: |
| 331 | |
| 332 | ```bash |
| 333 | ./scripts/release/verify-release-assets.sh X.Y.Z |
| 334 | ``` |
| 335 | |
| 336 | For a rare packaging-only npm release where the npm package version intentionally |
| 337 | points at older Rust binaries, add `--allow-npm-binary-mismatch` and keep the |
| 338 | release notes explicit that no new binary version shipped. Carry the same |
| 339 | explicit exception through the publish command with |
| 340 | `CODEWHALE_ALLOW_NPM_BINARY_MISMATCH=1 npm publish --access public`; the exact |
| 341 | tag/clean-checkout guard still applies. |
| 342 | |
| 343 | 6. From the same clean detached `vX.Y.Z` worktree used above (or a newly |
| 344 | created one), confirm npm auth and publish the wrapper manually. The |
| 345 | `prepublishOnly` hook rejects a branch-ahead or dirty checkout before the |
| 346 | registry write: |
| 347 | |
| 348 | ```bash |
| 349 | npm whoami |
| 350 | cd npm/codewhale |
| 351 | npm publish --access public |
| 352 | # (you will be prompted for the npm OTP from your authenticator) |
| 353 | npm view codewhale@X.Y.Z version codewhaleBinaryVersion --json |
| 354 | cd ../.. |
| 355 | ./scripts/release/check-published.sh X.Y.Z |
| 356 | ``` |
| 357 | |
| 358 | If `npm whoami` or `npm publish` reports `E401`, `ENEEDAUTH`, or an OTP/login |
| 359 | failure, do not edit package contents. Run: |
| 360 | |
| 361 | ```bash |
| 362 | npm login |
| 363 | npm whoami |
| 364 | cd npm/codewhale |
| 365 | npm publish --access public |
| 366 | ``` |
| 367 | |
| 368 | Rerun the same `npm publish --access public` command after completing the login |
| 369 | or OTP prompt. The package's `prepublishOnly` hook reruns the release-asset |
| 370 | gate before each publish attempt, so an auth failure cannot accidentally skip |
| 371 | asset verification on retry. |
| 372 | |
| 373 | Do not publish `npm/deepseek-tui`; it is deprecated compatibility metadata only. |
| 374 | |
| 375 | ### Why not automated? |
| 376 | |
| 377 | - `release.yml`'s old `publish-npm` job used `secrets.NPM_TOKEN`, but npm's 2FA-by-default policy means a publish token must be either an automation token with "Bypass 2FA for token authentication" enabled OR an account-level 2FA-disabled state. We don't have either configured. |
| 378 | - The standalone `publish-npm.yml` and `crates-publish.yml` workflows have been removed; no inert automation plumbing remains. A future move to npm Trusted Publishing (OIDC) would re-introduce a dedicated workflow at that point. |
| 379 | |
| 380 | ### If you fix the token later |
| 381 | |
| 382 | To re-enable automated publish: provision an npm automation token with "Bypass 2FA for token authentication" enabled (or set up npm Trusted Publishing via OIDC), store the corresponding secret on the repo, and re-add a `publish-npm` job to `release.yml` (or a dedicated workflow) along with reverting this section's "manual" framing. |
| 383 | |
| 384 | ## CNB Cool mirror |
| 385 | |
| 386 | Every push to `main`, `fix/*`, `rebrand/*`, `work/v*`, and every `v*` tag is mirrored to |
| 387 | `cnb.cool/codewhale.net/codewhale` via the `Sync to CNB` workflow |
| 388 | so users behind GitHub-blocking networks can fetch the source and so CNB can |
| 389 | run the heavy Linux CI lane. After a release tag, **verify the mirror caught |
| 390 | it** before declaring the release shipped: |
| 391 | |
| 392 | ```bash |
| 393 | git ls-remote https://cnb.cool/codewhale.net/codewhale.git refs/tags/vX.Y.Z |
| 394 | ``` |
| 395 | |
| 396 | If the workflow failed for the release tag, use the exact-tag rerun or |
| 397 | `workflow_dispatch --ref vX.Y.Z` recovery documented in |
| 398 | [docs/CNB_MIRROR.md](CNB_MIRROR.md#manual-fallback). |
| 399 | |
| 400 | ## Recovery and Rollback |
| 401 | |
| 402 | - User-facing rollback: |
| 403 | - npm: `npm install -g codewhale@X.Y.Z` |
| 404 | - Cargo: `cargo install codewhale-cli --version X.Y.Z --locked --force` |
| 405 | and `cargo install codewhale-tui --version X.Y.Z --locked --force` |
| 406 | - manual assets: download binaries or the platform archive plus the matching |
| 407 | `codewhale-artifacts-sha256.txt` or `codewhale-bundles-sha256.txt` |
| 408 | manifest from `https://github.com/Hmbown/CodeWhale/releases/tag/vX.Y.Z` |
| 409 | - workspace files: use `/restore list [N]` and `/restore <N>` for side-git |
| 410 | snapshots; this does not change the installed binary version or rewrite |
| 411 | conversation history |
| 412 | - keep [docs/INSTALL.md](INSTALL.md#roll-back-to-a-previous-release) in sync |
| 413 | with these commands |
| 414 | - Crates publish partially: |
| 415 | - rerun `./scripts/release/publish-crates.sh publish` |
| 416 | - already-published crate versions will be skipped |
| 417 | - GitHub assets missing or checksum manifest incomplete: |
| 418 | - fix `.github/workflows/release.yml`, but do not rerun it over an existing |
| 419 | asset set and do not delete assets merely to make the guard pass |
| 420 | - if any asset may have been public or consumed, cut a new patch version |
| 421 | - only after explicit maintainer approval and proof that no downstream |
| 422 | publication or consumer treated the failed asset set as public may a |
| 423 | deliberately scoped recovery remove the failed release before an exact-tag |
| 424 | rerun; record that exception in the release packet |
| 425 | - npm packaging-only problem: |
| 426 | - bump only the npm package version |
| 427 | - keep `codewhaleBinaryVersion` on the last known-good Rust release |
| 428 | - repack and republish the wrapper |
| 429 | - A bad npm publish cannot be overwritten: |
| 430 | - publish a new npm version with corrected metadata or install logic |
| 431 | - CNB mirror failed for the release tag: |
| 432 | - check the run via `gh run list --workflow=sync-cnb.yml` |
| 433 | - rerun the failed tag run, or dispatch |
| 434 | `gh workflow run sync-cnb.yml --ref vX.Y.Z`; never omit the tag ref |
| 435 | - follow the proof steps in |
| 436 | [docs/CNB_MIRROR.md](CNB_MIRROR.md#manual-fallback) |
| 437 |