返回 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 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
437 lines MARKDOWN