fix(release): chain the tag check, re-prompt an empty token, make the rerun command cwd-proof

Third fix-up for the crate rerun blocks (8b92463a2c, 9ded7ce896,
3fd44b5501) after re-review. The verdict was push, with two low and two nit
findings. All are fixed here, plus one hazard found while probing the fixes
(Refs CW-7445).

What
- Token check in the founder's shell. In the `cargo owner --list` block, the
  trailing `# prints the owner, e.g. `Hmbown (Hunter Bown)`` was not a
  comment in interactive zsh, where interactivecomments is off by default.
  It became five extra arguments and a backtick command substitution, and
  cargo failed with `unexpected argument '#'` before it checked any token.
  The example moves into the sentence before the block.
- Tag guard. Step 5 now reads
  `./scripts/release/require-release-tag-checkout.sh X.Y.Z && {`. Before,
  a failed check still prompted for the token, created a root and started
  publish-crates.sh, and only the script's inner guard stopped it.
- Empty token. Both prompts loop with
  `token=; while [ -z "${token}" ]; do ... read -rs token || break; ... done`.
  Without bracketed paste (macOS /bin/bash 3.2), a stray blank line pasted
  after the block was read as an empty token. The block then did nothing,
  and a token typed at the next shell prompt would run as a command and land
  in history. End of input still stops the block.
- Refusal command. It prints `cd "<repo root>" && mkdir -p ... && mkdir ...
  && ... python3 "<abs>/build-lock.py" ... -- "<abs>/publish-crates.sh"
  <mode>`. Pasted from another directory, the old `$0` form failed with
  FileNotFoundError, and an absolute script path alone still failed at
  `cargo metadata` (no Cargo.toml in cwd).
- Wording. An interrupted run from the blocks leaves package/ under
  `${root}/target/package` with any cargo. build-lock.py serializes only
  with dev-cargo.sh and other build-lock.py users, not with a cargo wrapper
  that keeps its own build slots (such as cargowhale on this Mac).

Verification (source: 41ef8e0eb4 plus this diff; blocks extracted from
the doc; a cargo shim exits 99 on publish, or prints argc; no token, no
network)
- Step 5 with X.Y.Z=0.10.2 (no tag), under bash -s and zsh -f -s: printed
  "::error::Local release tag v0.10.2 does not exist.", gave no prompt and
  created no root.
- Step 5 with the guard replaced by `true` and publish-crates.sh by an env
  printer:
  - token line: "child token=set";
  - two blank lines then a token: three prompts, then "child token=set";
  - end of input: one prompt, nothing run;
  - a second run in the same second stopped at "mkdir: ... File exists".
  Each was run under bash -s and zsh -f -s.
- Owner block: blank line then token under bash -s and zsh -f -s, and
  token under `zsh -f -i` (interactivecomments off): "ARGSHIM token=set
  argc=3".
- Real-state refusal (shim, default ~/.cargo, repo ./target): the printed
  command, run from a scratch directory outside the repo, reached
  "Verifying all 29 release package tarballs" (shim exit 99,
  CODEWHALE_BUILD_LOCK_HELD=1, target and home under the new root). Run
  again with the same printed root, it stopped at "File exists".
- publish-crates.test.py under build-lock.py: OK (6 tests).
  validate-crate-publish-order.test.sh: passed.
  .github/scripts/release-workflows.test.js: exit 0. `bash -n`: OK.
  node web/scripts/check-facts.mjs: OK. RELEASE_RUNBOOK.md is not in
  agent-docs.generated.json, so nothing was regenerated.
- npm test and check:web were last run on a5326d10db
  (101/19/420(413+7)/7, vitest 772/772, check:web exit 0). This change
  touches only docs/RELEASE_RUNBOOK.md and one echo line in
  publish-crates.sh.

Not verified: an interactive tty paste with bracketed paste in zsh 5.9,
which is the founder's shell. A real token and the publish itself are the
founder's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-16 10:50:36 -07:00
1 parent 41ef8e0eb4
commit 3cf53b6cb7
2 files changed
+21 -15

No files matched your search

+19 -13
View File
@@ -165,7 +165,9 @@ That run was a cold build of about 19 GB and 962 compile steps, so check
`scripts/dev-cache.sh`. `CARGO_TARGET_DIR` moves the build directory with
plain Cargo and with `scripts/dev-cargo.sh`, which honors an inherited target
directory, and `scripts/build-lock.py` holds the machine build lock whichever
`cargo` is on `PATH`. The 0.10.2 receipt run set `CODEWHALE_CACHE_ROOT`
`cargo` is on `PATH`. That lock serializes only with `scripts/dev-cargo.sh`
and other `build-lock.py` users; a `cargo` wrapper with its own build slots
does not take it. The 0.10.2 receipt run set `CODEWHALE_CACHE_ROOT`
instead, which only `scripts/dev-cargo.sh` reads, and kept its lock inside the
new root. `mkdir` without `-p` refuses a root that already exists, and the
`&&` chain stops the run there. Every run leaves state that the next run at
@@ -186,9 +188,11 @@ does not use.
If a dry-run is interrupted (Ctrl-C, or a shell that gets killed), inspect the
half-packed `package/` directory in Cargo's build directory before retrying and
coordinate with active builds. With plain Cargo that is `target/package`;
`scripts/dev-cargo.sh` moves it under `${CODEWHALE_CACHE_ROOT}/build/`, and
`cargo metadata --format-version 1 --no-deps` reports it as `build_directory`.
coordinate with active builds. For a run from the blocks above that is
`${root}/target/package` with any `cargo`. Otherwise plain Cargo uses
`target/package` and `scripts/dev-cargo.sh` moves it under
`${CODEWHALE_CACHE_ROOT}/build/`; `cargo metadata --format-version 1
--no-deps` reports it as `build_directory`.
Cargo can resume against it and every unpacked tarball may then fail with
`error: Current directory is invalid:
No such file or directory (os error 2)` — which looks like a compiler or
@@ -396,13 +400,12 @@ and fails branch-only release sources before assets are published.
`main`, with `HEAD` still matching the immutable remote release tag:
```bash
./scripts/release/require-release-tag-checkout.sh X.Y.Z
{
./scripts/release/require-release-tag-checkout.sh X.Y.Z && {
mode=publish
cache="${CODEWHALE_CACHE_ROOT:-${XDG_CACHE_HOME:-$HOME/.cache}/codewhale}"
lock="${CODEWHALE_BUILD_LOCK_FILE:-${cache}/build.lock}"
root="${cache}/release-verify-$(git rev-parse --short=10 HEAD)-${mode}-$(date -u +%Y%m%dT%H%M%SZ)"
printf 'crates.io token: ' >&2; read -rs token; echo >&2
token=; while [ -z "${token}" ]; do printf 'crates.io token: ' >&2; read -rs token || break; echo >&2; done
[ -n "${token}" ] && mkdir -p "${cache}" && mkdir "${root}" &&
CARGO_REGISTRY_TOKEN="${token}" CARGO_HOME="${root}/cargo-home" \
CARGO_TARGET_DIR="${root}/target" \
@@ -420,9 +423,12 @@ and fails branch-only release sources before assets are published.
empty `CARGO_HOME` and build directory of its own, as the package preflight
above describes; the dry run's state makes it refuse otherwise. The braces
make the shell read the whole block before `read` runs, so a pasted or
piped block never takes its own next line as the token. `read -rs` keeps
the token out of shell history, an empty token stops the block, and the
environment variable keeps the token out of `credentials.toml`.
piped block never takes its own next line as the token, and the prompt
repeats on an empty line, so a stray blank line from a paste is not
taken as the token. `read -rs` keeps the token out of shell history,
end of input stops the block, and the environment variable keeps the
token out of `credentials.toml`. The tag check is chained with `&&`, so a
failed check asks for no token.
If shared `main` advances or the checkout becomes dirty, stop and coordinate
a new candidate or separately authorized recovery with Hunter. Do not switch
branches, create another checkout, force-fetch a conflicting tag or bypass
@@ -619,12 +625,12 @@ for that version — bump to the next patch instead.
`403 {"errors":[{"detail":"this action can only be performed on the
crates.io website"}]}` even for a good token (checked 2026-09-21 with a
token that `cargo owner` accepts), so a 403 there proves nothing about the
credential.
credential. A good token prints the owner, e.g. `Hmbown (Hunter Bown)`:
```bash
{
printf 'crates.io token: ' >&2; read -rs token; echo >&2
[ -n "${token}" ] && CARGO_REGISTRY_TOKEN="${token}" cargo owner --list codewhale-tui # prints the owner, e.g. `Hmbown (Hunter Bown)`
token=; while [ -z "${token}" ]; do printf 'crates.io token: ' >&2; read -rs token || break; echo >&2; done
[ -n "${token}" ] && CARGO_REGISTRY_TOKEN="${token}" cargo owner --list codewhale-tui
unset token
}
```
+2 -2
View File
@@ -91,8 +91,8 @@ if (( ${#reused[@]} > 0 )); then
printf ' %s\n' "${reused[@]}"
echo "Cargo treats crates served from its temporary package registry as immutable by name and version. It skips extraction once .cargo-ok exists and keeps compiled units that do not track their sources, so this run would verify old code. Nothing was deleted."
echo "Rerun from a new, empty Cargo home and build directory. Every run leaves state the next run at this version refuses, so publish and each partial-publish rerun need their own root:"
echo " mkdir -p \"${cache_root}\" && mkdir \"${verify_root}\" &&"
echo " CARGO_HOME=\"${verify_root}/cargo-home\" CARGO_TARGET_DIR=\"${verify_root}/target\" python3 \"$(dirname "${script_dir}")/build-lock.py\" \"${CODEWHALE_BUILD_LOCK_FILE:-${cache_root}/build.lock}\" -- $0 ${mode}"
echo " cd \"$(cd "${script_dir}/../.." && pwd)\" && mkdir -p \"${cache_root}\" && mkdir \"${verify_root}\" &&"
echo " CARGO_HOME=\"${verify_root}/cargo-home\" CARGO_TARGET_DIR=\"${verify_root}/target\" python3 \"$(dirname "${script_dir}")/build-lock.py\" \"${CODEWHALE_BUILD_LOCK_FILE:-${cache_root}/build.lock}\" -- \"${script_dir}/publish-crates.sh\" ${mode}"
echo "CARGO_TARGET_DIR moves the build directory with plain cargo and with scripts/dev-cargo.sh, and build-lock.py holds the machine build lock whichever cargo is on PATH."
echo "For publish mode, pass the crates.io token as CARGO_REGISTRY_TOKEN for that one command, so Cargo writes no credentials.toml under the new root. After cargo login with that CARGO_HOME instead, run cargo logout with it once the publish finishes. See docs/RELEASE_RUNBOOK.md."
} >&2