返回 CodeWhale
RELEASE_RUNBOOK.md
根目录 / docs / RELEASE_RUNBOOK.md
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 linked into the installed
8 `codewhale`/`codew` commands; it is not a third installed command.
9 - `codewhale-app-server` is a supporting library crate. The shipped entrypoint
10 is `codewhale app-server`; do not add or publish a standalone app-server binary.
11
12 ## Canonical Publish Targets
13
14 - End-user crates:
15 - `codewhale-tui`
16 - `codewhale-cli`
17 - Supporting crates published from this workspace:
18 - `codewhale-build-support`
19 - `codewhale-mcp`
20 - `codewhale-paths`
21 - `codewhale-protocol`
22 - `codewhale-release`
23 - `codewhale-secrets`
24 - `codewhale-state`
25 - `codewhale-telemetry`
26 - `codewhale-workflow`
27 - `codewhale-workflow-js`
28 - `codewhale-execpolicy`
29 - `codewhale-hooks`
30 - `codewhale-tools`
31 - `codewhale-config`
32 - `codewhale-cloud-facts`
33 - `codewhale-lane`
34 - `codewhale-agent`
35 - `codewhale-core`
36 - `codewhale-command-contract`
37 - `codewhale-app-server`
38
39 ## Version Coordination
40
41 - Rust crates inherit the shared workspace version from [Cargo.toml](../Cargo.toml).
42 - Internal path dependency versions should match the shared workspace version; stale older pins are release blockers once the workspace version moves.
43 - The npm wrapper version lives in [npm/codewhale/package.json](../npm/codewhale/package.json).
44 - `codewhaleBinaryVersion` controls which GitHub release binaries the npm wrapper downloads.
45 - Packaging-only npm releases are allowed:
46 - bump the npm package version
47 - leave `codewhaleBinaryVersion` pinned to the previously released Rust binaries
48 - rerun `npm pack` smoke checks before `npm publish`
49
50 ## Release Source Timing
51
52 Freeze the source before creating a public `vX.Y.Z` tag. The version bump is
53 not the release; it is the last source-prep commit before the tag. Do not keep
54 merging same-version feature/fix PRs after `vX.Y.Z` exists and assume the
55 release workflow will pick them up. It will not: the tag is the release anchor.
56
57 Before tagging, verify the live queue and existing anchors:
58
59 ```bash
60 gh issue list --repo Hmbown/CodeWhale --milestone "vX.Y.Z" --state open
61 gh pr list --repo Hmbown/CodeWhale --state open --limit 100
62 git ls-remote origin refs/heads/main refs/tags/vX.Y.Z
63 gh release view vX.Y.Z --repo Hmbown/CodeWhale
64 ./scripts/release/check-published.sh X.Y.Z
65 ```
66
67 If a same-version tag already exists but there is no GitHub Release and nothing
68 is published, stop and choose deliberately:
69
70 - publish exactly the tagged SHA, leaving later commits for the next patch;
71 - bump the later work to the next patch version and tag that later SHA; or
72 - with explicit maintainer approval only, delete/recreate the unpublished tag
73 after confirming no package, GitHub Release, mirror, or installer consumer has
74 treated it as public.
75
76 Do not delete, move, or recreate a release tag implicitly as part of ordinary
77 PR merge or milestone cleanup work.
78
79 ## Preflight
80
81 Run these from the repository root before cutting a tag:
82
83 ```bash
84 ./scripts/release/check-versions.sh # workspace/npm/SDK/VS Code/generated-fact/lock drift
85 cargo fmt --all -- --check
86 cargo check --workspace --all-targets --locked
87 cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
88 cargo test --workspace --all-features --locked
89 ./scripts/release/publish-crates.sh dry-run
90 ```
91
92 `check-versions.sh` also runs in CI on every push/PR (the `versions` job in
93 `.github/workflows/ci.yml`), so drift between `Cargo.toml`, the per-crate
94 manifests, the npm wrapper and Runtime SDK, the VS Code extension and lock,
95 generated web facts, and `Cargo.lock` is caught before release time rather than
96 at it.
97
98 The source-controlled CNB pipeline mirrors the heavy Linux version/fmt/check/
99 clippy/test/npm-smoke gates for `fix/*`, `rebrand/*`, `work/v*`, and `main`.
100 GitHub Actions keeps the cheap drift/fmt statuses plus macOS and Windows
101 coverage, while CNB carries the Linux work.
102
103 `publish-crates.sh` requires Cargo 1.90 or newer for multi-package verification;
104 this release-tool requirement is separate from the runtime's Rust 1.88 MSRV.
105 Use an up-to-date stable toolchain (`rustup update stable`) for release work.
106
107 Both modes validate publication order against the locked workspace graph, then
108 run one `cargo publish --dry-run --locked --registry crates-io` covering all
109 release crates listed in `scripts/release/crates.sh`. Cargo resolves unpublished
110 workspace dependencies through a
111 temporary local registry, builds every unpacked tarball, and checks publication
112 metadata before any upload. Dry-run mode permits source edits and stops there.
113 Publish mode requires the approved release checkout and assets, then skips
114 versions already on crates.io and uploads the remaining crates in dependency
115 order. Resuming still verifies the complete source release; it never weakens
116 the artifact gate merely because an earlier crate was already uploaded.
117 Registry-side acceptance and credentials are still checked during real upload;
118 a successful preflight cannot guarantee that every later upload will succeed.
119
120 For npm wrapper verification, build the single runtime and run the
121 cross-platform smoke harness. This packs the npm wrapper, installs it into a
122 clean temporary project, serves local release assets over HTTP, and checks both
123 published commands against that runtime: `codewhale doctor --help` and
124 `codew --version`.
125
126 ```bash
127 cargo build --release --locked -p codewhale-cli -p codewhale-tui
128 node scripts/release/npm-wrapper-smoke.js
129 ```
130
131 Set `DEEPSEEK_TUI_KEEP_SMOKE_DIR=1` to keep the temporary pack/install
132 directory for inspection.
133
134 ## Exact-head GitHub proof before publication
135
136 Two manual workflows provide exact-head evidence without crossing the public
137 release boundary. Run them only after the intended source is on a named ref
138 (normally the frozen `main`), and pass the full commit SHA as an independent
139 guard against that ref moving between inspection and dispatch:
140
141 ```bash
142 git fetch origin main
143 candidate_sha="$(git rev-parse origin/main)"
144
145 gh workflow run ci.yml --ref main \
146 -f expected_sha="${candidate_sha}"
147 gh workflow run release-candidate.yml --ref main \
148 -f expected_sha="${candidate_sha}"
149 ```
150
151 The manual `ci.yml` path verifies that the dispatch resolved to
152 `expected_sha`, disables light-change shortcuts, and forces the heavy Rust,
153 workflow, mobile, Actions, Linux, macOS, Windows, npm-wrapper, and documentation
154 gates. A mismatch fails before those gates start; it never silently tests a
155 different head.
156
157 `release-candidate.yml` also fails unless the selected ref resolves to the
158 exact requested SHA. It invokes the same reusable artifact workflow as the
159 public release, building all seven targets (including Android arm64 and native
160 Windows arm64), staging `codewhale` and `codew` (single binary), building the
161 NSIS installer and nine platform archives, and validating the authoritative
162 34-file inventory from `npm/codewhale/scripts/artifacts.js` (27 current
163 artifacts and manifests plus seven compatibility-only `codewhale-tui-*`
164 filenames containing the same compiled `codewhale` bytes for v0.9.4 update
165 clients). It then installs
166 the packed npm wrapper against those assembled local assets and exercises its
167 delegated entrypoints. The resulting `codewhale-release-assets` bundle is a
168 short-lived GitHub Actions artifact only.
169
170 This candidate workflow does not create a tag or GitHub Release, publish a
171 crate or npm package, push a container, update Homebrew, deploy anything, or
172 write repository contents. Its green result is evidence, not publication
173 authorization. The stop line remains explicit Hunter approval: do not create
174 the `vX.Y.Z` tag, dispatch `release.yml`, or run any registry publication step
175 until that approval is given.
176
177 The Android target is cross-built and included in the checksum/bundle gates,
178 but GitHub's Linux runner cannot execute the Android binary as a real Termux
179 user. Keep the real-device limitation in the release packet unless separate
180 device evidence exists.
181
182 To exercise `npm run release:check` locally as well, regenerate the local asset
183 directory with a full asset matrix fixture before starting the server:
184
185 ```bash
186 DEEPSEEK_TUI_PREPARE_ALL_ASSETS=1 node scripts/release/prepare-local-release-assets.js
187 cd npm/codewhale
188 DEEPSEEK_TUI_VERSION=X.Y.Z DEEPSEEK_TUI_RELEASE_BASE_URL=http://127.0.0.1:8123/ npm run release:check
189 ```
190
191 Set `DEEPSEEK_TUI_VERSION` to the npm package version you are verifying for that local run.
192
193 The CNB workflow runs the Linux tarball install + delegated-entrypoint smoke
194 test; GitHub Actions keeps macOS and Windows smoke coverage.
195
196 After publishing, prove the release is visible in both registries:
197
198 ```bash
199 ./scripts/release/check-published.sh X.Y.Z
200 ```
201
202 Do not mark a Rust release complete until that command sees `codewhale@X.Y.Z`
203 on npm and every `codewhale-*` crate at `X.Y.Z` on crates.io. For a rare
204 npm packaging-only release, run with `--allow-npm-binary-mismatch` and keep the
205 release notes explicit that no new Rust binary version shipped.
206
207 ## Post-Merge Branch Hygiene
208
209 After a release or scratch integration branch lands, run the branch hygiene
210 helper before pruning anything:
211
212 ```bash
213 ./scripts/release/branch-hygiene.sh --release-branch codex/vX.Y.Z
214 ```
215
216 The default mode is a dry run. It reports the current checkout branch, main ref,
217 local and remote release tips, safe local or remote branch deletes, branches
218 kept for contributor work, and branches that still need a human decision. Review
219 that report before running `--prune --yes`, and add `--prune-remote` only when
220 you have confirmed the remote branches are safe to delete.
221
222 Use `--remote upstream` when you are working from a fork and the canonical
223 release refs live on the upstream remote instead of `origin`.
224
225 Verify the helper itself after changing it:
226
227 ```bash
228 bash scripts/release/branch-hygiene.test.sh
229 bash scripts/release/ensure-release-on-main.test.sh
230 ```
231
232 Those scripts are pinned to LF line endings so the same command works from a
233 Windows checkout under Bash.
234
235 ## Rust Crates Release
236
237 Crate publishing to crates.io is **manual** — there is no automated
238 `crates-publish` GitHub workflow. Operators run the helpers in
239 `scripts/release/` from a developer workstation that has `cargo login`
240 configured.
241
242 Release commits must land on `main` before any `vX.Y.Z` tag is pushed. Do not
243 tag a release-only branch. Open the release PR against `main`, let required
244 review and CI finish, merge it, then explicitly tag the final source commit
245 that is reachable from `main`. This is what lets GitHub process `Closes #N`
246 lines automatically and show the release PR as merged. The tag release workflow runs
247 `scripts/release/ensure-release-on-main.sh` for tag pushes and manual dispatches,
248 and fails branch-only release sources before assets are published.
249
250 1. Write the CHANGELOG entry, then run
251 `./scripts/release/prepare-release.sh X.Y.Z` — it bumps every
252 version-bearing file (workspace + crate pins + npm wrapper + Runtime SDK +
253 VS Code extension/lock + remote-smoke default + public source-candidate
254 facts + README install tags), refreshes the Cargo/npm locks and generated
255 files, and runs
256 the version and OHOS gates. It is safe to rerun after the workspace already
257 equals `X.Y.Z`: the second run skips replacements, refreshes the packaged
258 changelog and web facts, and reruns both gates.
259 2. Run `./scripts/release/publish-crates.sh dry-run` locally; it must be clean.
260 3. Merge the release PR into `main` before tagging. After the same-version
261 queue is frozen and `main` is at the intended source SHA, create `vX.Y.Z`
262 from `main` with the manual **Create release tag** workflow or with a signed
263 local tag push from a developer machine.
264 - If `RELEASE_TAG_PAT` is configured, the tag push starts `release.yml`.
265 - If no Release run appears and the tag already exists, first confirm that
266 no tag-triggered run is queued or active, then dispatch the exact tag:
267 `gh workflow run release.yml --ref vX.Y.Z -f version=X.Y.Z`.
268 - Never dispatch from `main`, and do not start a duplicate while the
269 tag-triggered run is merely delayed. The workflow serializes runs for the
270 same tag. It also refuses to start release work when that tag already owns
271 any GitHub Release asset, rechecks immediately before upload, and disables
272 the release action's overwrite behavior. A normal rerun must never replace
273 public bytes.
274 4. Wait for the GitHub Release workflow and all public assets to finish, then
275 fetch the release tag and run the public asset gate. Do not publish any
276 Cargo or npm package until it passes:
277
278 ```bash
279 git fetch --force origin +refs/tags/vX.Y.Z:refs/tags/vX.Y.Z
280 ./scripts/release/verify-release-assets.sh X.Y.Z
281 ```
282
283 5. Create a clean detached checkout of the immutable release tag, then publish
284 the Rust crates from that checkout only:
285
286 ```bash
287 git worktree add --detach ../codewhale-release-vX.Y.Z vX.Y.Z
288 cd ../codewhale-release-vX.Y.Z
289 ./scripts/release/require-release-tag-checkout.sh X.Y.Z
290 ./scripts/release/publish-crates.sh publish
291 ```
292
293 Both Cargo and npm publication fail closed unless `HEAD`, the clean local
294 checkout, and the remote `vX.Y.Z` tag still agree. The authoritative crate
295 dependency order lives in `scripts/release/crates.sh`; do not maintain a
296 second handwritten order in this runbook. The helper waits for each new
297 version to appear on crates.io before moving to dependents and safely skips
298 versions that are already public on a rerun.
299
300 The publish helper is idempotent for reruns: already-published crate versions are skipped.
301
302 ## GitHub Release Assets
303
304 `.github/workflows/release.yml` builds and stages these artifacts:
305
306 - one `codewhale-*` runtime binary for Linux x64/arm64, Android arm64, macOS
307 x64/arm64, and Windows x64/arm64
308 - byte-identical `codew-*` command assets copied from that runtime
309 - byte-identical `codewhale-tui-*` compatibility filenames so installed v0.9.4
310 clients can discover and complete the one-runtime upgrade; current installers
311 never expose those filenames as a third command
312 - `codewhale.bat` for the Windows npm/GitHub x64 launcher, and the same
313 filename inside Windows zip archives and the NSIS install (those copies
314 launch `codewhale.exe` and prefer Windows Terminal)
315 - platform `.tar.gz` / `.zip` archives and `CodeWhaleSetup.exe`
316
317 The release job also uploads `codewhale-artifacts-sha256.txt` and
318 `codewhale-bundles-sha256.txt`. The npm installer and release verification
319 script depend on those manifests. The authoritative release asset list lives in
320 `npm/codewhale/scripts/artifacts.js`.
321
322 Before any Cargo or npm publish, prove that the public GitHub Release assets
323 belong to the tag commit you are publishing:
324
325 ```bash
326 ./scripts/release/verify-release-assets.sh X.Y.Z
327 ```
328
329 That gate compares the local and remote `vX.Y.Z` tag SHAs, confirms a
330 successful `Release` workflow run used that SHA, then runs the npm wrapper's
331 release check against the public GitHub asset URLs. The npm check fails if the
332 release is missing a required binary, archive, installer, or manifest; either
333 manifest omits a required row; or the assets predate the matching release
334 workflow run. If the command fails, rerun or repair `release.yml`; do not
335 publish Cargo or npm against stale assets.
336
337 ## AUR / Omarchy Package
338
339 `codewhale-bin` is a downstream package of the same Linux release, not a new
340 Codewhale semantic version. After the public asset gate above passes, render
341 its AUR metadata from the verified release directory:
342
343 ```bash
344 ./packaging/aur/render.sh /path/to/release-assets /tmp/codewhale-bin
345 ```
346
347 The renderer reads the workspace version and extracts the x64/arm64 archive
348 hashes only after both release checksum manifests agree with the actual files.
349 It emits no `SKIP` checksums or source-controlled per-release values. Follow
350 [`packaging/aur/README.md`](../packaging/aur/README.md) for the clean Arch build,
351 `.SRCINFO` comparison, and package-content checks.
352
353 The GitHub release workflows only verify that the AUR metadata can be rendered
354 from their candidate assets. They do not publish to AUR. AUR publication is a
355 separate, explicitly authorized maintainer action after the matching tag and
356 assets are public.
357
358 ## npm Wrapper Release
359
360 `release.yml` publishes `codewhale` through npm Trusted Publishing after the
361 exact-SHA GitHub Release job succeeds. The job has only `contents: read` and
362 `id-token: write`; it does not use `NPM_TOKEN`, `NODE_AUTH_TOKEN`, or a
363 long-lived bypass-2FA credential.
364
365 Before the first automated publish, configure the `codewhale` package's npm
366 Trusted Publisher with these exact values:
367
368 - organization or user: `Hmbown`
369 - repository: `CodeWhale`
370 - workflow filename: `release.yml`
371 - environment: leave blank (the workflow does not claim a GitHub environment)
372
373 That npm-side binding is an external release gate. If it is missing or differs
374 in case, repository, workflow filename, or environment, the publish job must
375 fail; do not add a token fallback to make it pass.
376
377 ### Steps
378
379 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.
380 2. Set `codewhaleBinaryVersion` to the GitHub release tag that should supply binaries.
381 3. Push the version bump to `main`. After the release source is frozen, create
382 the matching `vX.Y.Z` tag from `main`; `release.yml` then builds the binary
383 matrix and publishes the GitHub Release after its artifact and container gates.
384 The tag also syncs to `cnb.cool/codewhale.net/codewhale`, whose pipeline
385 independently publishes a Linux x64 release and marks it latest. Include
386 that destination in publication approval; it does not wait for GitHub Release.
387 4. **Wait for the GitHub Release to finalize** with the full binary and archive
388 matrix, Windows installer, and both checksum manifests. The dependent `npm`
389 job checks the remote tag again, runs the public asset freshness gate and
390 package tests, then publishes with OIDC. The package's `prepublishOnly` hook
391 repeats the clean exact-tag and public-asset checks immediately before the
392 registry write.
393 5. Confirm the `npm` job succeeded, then prove the published package and binary
394 version are visible:
395
396 ```bash
397 npm view codewhale@X.Y.Z version codewhaleBinaryVersion --json
398 ./scripts/release/check-published.sh X.Y.Z
399 ```
400
401 For a rare packaging-only npm release where the npm package version intentionally
402 points at older Rust binaries, add `--allow-npm-binary-mismatch` and keep the
403 release notes explicit that no new binary version shipped. That exception is a
404 separate manual release path: the normal trusted-publishing job deliberately
405 does not set `CODEWHALE_ALLOW_NPM_BINARY_MISMATCH`.
406
407 Do not publish `npm/deepseek-tui`; it is deprecated compatibility metadata only.
408
409 ### Manual recovery
410
411 If GitHub OIDC is unavailable after the GitHub Release and public-asset gate are
412 green, use a clean detached checkout of the immutable tag. Authenticate
413 interactively with npm's normal WebAuthn/2FA flow; never create a long-lived
414 bypass-2FA token:
415
416 ```bash
417 ./scripts/release/require-release-tag-checkout.sh X.Y.Z
418 ./scripts/release/verify-release-assets.sh X.Y.Z
419 npm login
420 npm whoami
421 cd npm/codewhale
422 npm publish --access public
423 ```
424
425 The same `prepublishOnly` gates rerun on every attempt. An OIDC or login failure
426 is not permission to edit the tagged package, move the tag, or skip asset
427 verification.
428
429 ## CNB Cool mirror
430
431 Every push to `main`, `fix/*`, `rebrand/*`, `work/v*`, and every `v*` tag is mirrored to
432 `cnb.cool/codewhale.net/codewhale` via the `Sync to CNB` workflow
433 so users behind GitHub-blocking networks can fetch the source and so CNB can
434 run the heavy Linux CI lane. After a release tag, **verify the mirror caught
435 it** before declaring the release shipped:
436
437 ```bash
438 git ls-remote https://cnb.cool/codewhale.net/codewhale.git refs/tags/vX.Y.Z
439 ```
440
441 If the workflow failed for the release tag, use the exact-tag rerun or
442 `workflow_dispatch --ref vX.Y.Z` recovery documented in
443 [docs/CNB_MIRROR.md](CNB_MIRROR.md#manual-fallback).
444
445 ## Recovery and Rollback
446
447 ### Re-tagging an UNPUBLISHED release (pulled before npm/crates)
448
449 If `vX.Y.Z` was tagged and a GitHub Release was created, but **no package was
450 published** (npm still on the prior version, `check-published.sh` shows the
451 crates unpublished), the tag is still recoverable — a bug found after tagging
452 can be fixed and the same version recut. Confirm first that nothing consumed it:
453 `npm view codewhale version` and crates.io both show the PRIOR version, no
454 Homebrew/Winget/mirror points at it, and the GitHub Release download counts are
455 only the release pipeline's own verification passes. Then, with explicit
456 maintainer approval:
457
458 ```bash
459 # 1. land the fix on main (normal PR + required CI)
460 # 2. delete the premature Release + tag
461 gh release delete vX.Y.Z --repo Hmbown/CodeWhale --yes --cleanup-tag
462 git push origin :refs/tags/vX.Y.Z # belt-and-suspenders
463 git tag -d vX.Y.Z # local
464 # 3. recut at the fixed HEAD (workspace version unchanged)
465 gh workflow run auto-tag.yml --repo Hmbown/CodeWhale --ref main
466 # 4. release.yml rebuilds assets; rebuild + reinstall locally from the new tag
467 ```
468
469 This is the sanctioned path from "do not delete/move/recreate a release tag
470 implicitly": it is explicit, approved, and only for a tag no registry consumer
471 has treated as public. Never do it once a crate or the npm wrapper is published
472 for that version — bump to the next patch instead.
473
474 ### External publish gates (not code defects)
475
476 - **crates.io:** publishing needs a valid `cargo login` token on the operator
477 machine (`curl -H "Authorization: <token>" https://crates.io/api/v1/me`
478 returning 200). A 403 means the token is missing/expired — `cargo login`,
479 then `./scripts/release/publish-crates.sh publish`.
480 - **npm:** the OIDC job publishes only if the npmjs.com Trusted Publisher for
481 `Hmbown` / `CodeWhale` / workflow `release.yml` / blank environment is
482 configured. Missing config → the `npm` job fails `E404 No match found`. Fix
483 the binding (or use the manual WebAuthn recovery above), then re-run the
484 failed `npm` job: `gh run rerun <release-run-id> --failed`.
485
486 - User-facing rollback:
487 - npm: `npm install -g codewhale@X.Y.Z`
488 - Cargo: `cargo install codewhale-cli --version X.Y.Z --locked --force`;
489 add an optional `codew` alias as documented in
490 [docs/INSTALL.md](INSTALL.md#7-build-from-source)
491 - manual assets: download binaries or the platform archive plus the matching
492 `codewhale-artifacts-sha256.txt` or `codewhale-bundles-sha256.txt`
493 manifest from `https://github.com/Hmbown/CodeWhale/releases/tag/vX.Y.Z`
494 - workspace files: use `/restore list [N]` and `/restore <N>` for side-git
495 snapshots; this does not change the installed binary version or rewrite
496 conversation history
497 - keep [docs/INSTALL.md](INSTALL.md#roll-back-to-a-previous-release) in sync
498 with these commands
499 - Crates publish partially:
500 - rerun `./scripts/release/publish-crates.sh publish`
501 - already-published crate versions will be skipped
502 - GitHub assets missing or checksum manifest incomplete:
503 - fix `.github/workflows/release.yml`, but do not rerun it over an existing
504 asset set and do not delete assets merely to make the guard pass
505 - if any asset may have been public or consumed, cut a new patch version
506 - only after explicit maintainer approval and proof that no downstream
507 publication or consumer treated the failed asset set as public may a
508 deliberately scoped recovery remove the failed release before an exact-tag
509 rerun; record that exception in the release packet
510 - npm packaging-only problem:
511 - bump only the npm package version
512 - keep `codewhaleBinaryVersion` on the last known-good Rust release
513 - repack and republish the wrapper
514 - A bad npm publish cannot be overwritten:
515 - publish a new npm version with corrected metadata or install logic
516 - CNB mirror failed for the release tag:
517 - check the run via `gh run list --workflow=sync-cnb.yml`
518 - rerun the failed tag run, or dispatch
519 `gh workflow run sync-cnb.yml --ref vX.Y.Z`; never omit the tag ref
520 - follow the proof steps in
521 [docs/CNB_MIRROR.md](CNB_MIRROR.md#manual-fallback)
522 - Workflow runner failure or hung release job:
523 - every release-lane job carries an explicit `timeout-minutes` to contain
524 unattended runs, but timeouts are containment rather than immediate recovery
525 - if a workflow job sits `in_progress` with 404 logs (or produces no useful
526 log output for 20 minutes), cancel the run and rerun failed jobs / dispatch
527 an exact-ref rerun rather than waiting out the full job timeout
528 - check the last-useful-log timestamp before cancelling to distinguish an
529 infrastructure failure (runner dropped / HTTP 404 on log stream) from a
530 legitimate long build step (e.g. Windows artifact compilation)
531
531 lines MARKDOWN