decrating: dogfood Shipper for the release workflow#80
Conversation
Rewrite .github/workflows/release.yml to use shipper plan / preflight / publish / resume instead of raw cargo publish. - Release runs shipper publish as the train executor - .shipper/ state persisted as artifact throughout (before plan, before preflight, after publish, on any failure) - GitHub Release only finalized AFTER crates.io publishes succeed - workflow_dispatch "release-rehearse" job runs plan+preflight dry-run for safety verification - workflow_dispatch "release-resume" job downloads .shipper/ artifact from a failed run and continues Per docs/release-v0.3.0-rc.1-manifest.md and docs/decrating-plan.md §6 Phase 8 follow-up.
There was a problem hiding this comment.
Code Review
This pull request transitions the release process to an automated GitHub Actions workflow that dogfoods the shipper tool. The documentation is updated with instructions for release rehearsals, automated publishing, and failure recovery. Review feedback highlights several technical inaccuracies in the documentation regarding the shipper CLI, specifically that the plan command does not persist state or support JSON output, and the binary name is shipper-cli. It also suggests using cargo install --dry-run for more accurate post-publish verification.
| `shipper plan` → upload `.shipper/` → `shipper preflight` | ||
| → upload `.shipper/` → `shipper publish` → `cargo search` verification |
There was a problem hiding this comment.
The sequence shipper plan → upload .shipper/ implies that the plan command populates the .shipper/ directory with state artifacts. However, based on the implementation in crates/shipper-cli/src/main.rs, the plan command only prints to stdout and does not write to the state directory. The first command that persists execution state to disk is shipper preflight. The checklist should be updated to reflect that the .shipper/ directory will not contain execution state (like state.json) until after preflight or publish has run.
| 2. `cargo build --release -p shipper-cli`, then install `target/release/shipper` | ||
| to `/usr/local/bin/shipper` (the binary that will drive the train is the | ||
| one we just built from this tag). |
There was a problem hiding this comment.
The command cargo build --release -p shipper-cli produces a binary named shipper-cli by default. However, the documentation refers to installing target/release/shipper. Unless the binary is explicitly renamed in the shipper-cli crate's Cargo.toml (e.g., via [[bin]] name = "shipper"), the installation step to /usr/local/bin/shipper will fail because the source file will not be found. It is safer to use cargo install --path crates/shipper-cli --bin shipper if the binary name is intended to be shipper.
| 2. `cargo build --release -p shipper-cli`, then install `target/release/shipper` | ||
| to `/usr/local/bin/shipper` (the binary that will drive the train is the | ||
| one we just built from this tag). | ||
| 3. `shipper plan --registry crates-io --state-dir .shipper --format json` |
There was a problem hiding this comment.
| writes the plan summary into `.shipper/plan.txt` (and the engine also | ||
| persists its internal plan artefacts under `.shipper/`). |
There was a problem hiding this comment.
The shipper plan command is a diagnostic tool that analyzes the workspace and prints the result to stdout; it does not persist "internal plan artefacts" to the .shipper/ directory. State persistence (writing to state.json) typically begins during the preflight or publish phases which utilize the engine. If the workflow expects the .shipper/ directory to contain state information after only running plan, it will likely find it empty (except for any manually redirected output like plan.txt).
| 7. On success, `cargo search <crate>` is run against every published crate | ||
| to confirm visibility from a fresh resolver. |
There was a problem hiding this comment.
While cargo search is a useful sanity check, it queries the registry's web API and search index rather than the resolver logic used during a build or install (which depends on the registry index or sparse index). Consequently, it doesn't strictly confirm visibility "from a fresh resolver". A more robust verification of resolver visibility would be running cargo install --dry-run <crate>.
Summary
Rewrites
.github/workflows/release.ymlto drive the crates.io publishtrain with Shipper itself (
shipper plan->shipper preflight->shipper publish) instead of rawcargo publish. Closes the credibilitygap between the product's promise and the repo's release path, and is the
Phase 8 follow-up called out in
docs/release-v0.3.0-rc.1-manifest.mdanddocs/decrating-plan.md.Workflow shape
Triggered on
v*.*.*tag push:msrv-gate—cargo check --workspaceagainst Rust 1.92 before anything publishes.build-binaries— cross-platform binary artifacts (Linux/macOS/Windows). Uploaded but NOT attached to a release yet.publish-crates-io— builds and installsshipper, then runs:shipper plan --format json(upload.shipper/asshipper-state-plan)shipper preflight(upload.shipper/asshipper-state-preflight; fail fast on error)shipper publishwith--policy safe --verify-mode package --readiness-method both --readiness-timeout 15m --verify-timeout 10m --max-attempts 12 --base-delay 10s --max-delay 15m --retry-strategy exponentialcargo searcheach of the 12 crates to confirm registry visibility.shipper/asshipper-state-final(90-day retention) — always, even on failure.create-release— runs ONLY afterpublish-crates-iosucceeds. Creates the GitHub Release and attaches the platform binaries plus a tarball of the final.shipper/state as publish evidence.Triggered via
workflow_dispatch:release-rehearse— plan + preflight against a chosen ref, no publishing. Use this before cutting a tag.release-resume— downloads a prior failed run's.shipper/artifact (artifact_run_idinput) and runsshipper resume. The plan-ID check inside shipper is the guardrail against workspace drift between runs.First-publish rate-limit handling
12 brand-new crates; crates.io allows a 5-crate burst, then 1 new crate per 10 minutes. The workflow does NOT hard-code tier batching or sleeps —
shipper publish's retry loop handles HTTP 429 as retryable with exponential backoff (--max-delay 15mis larger than the 10-minute window), and--readiness-method bothblocks the next publish on sparse-index AND API visibility of the previous one. Full train ~70-90 minutes; job timeout is 180 minutes; on overrun,release-resumepicks up from.shipper/state.json.Preserved vs rewritten
build-binariesjob and platform matrix, softprops/action-gh-release usage, environment-gatedreleasejob.publish-crates-iojob now drives shipper, notcargo publish. Thecreate-releasejob now depends on successful publish and bundles the.shipper/state as release evidence.ci.yml,architecture-guard.yml,fuzz.yml,mutation.yml) are untouched.Docs updates
RELEASE_CHECKLIST_v0.3.0.md— replaces the hand-rolledcargo publish --dry-runlist with workflow-driven steps (rehearse first, then tag, then resume if needed).docs/release-v0.3.0-rc.1-manifest.md— new section explaining how the workflow executes the train.docs/decrating-plan.md— Phase 8 note marking the release-workflow work complete.Test plan
cargo check --workspacepasses locally (verified)cargo build -p shipper-cli --releaseproduces a working binary (verified)python3 -c "import yaml; yaml.safe_load(...)")uses:action pins match the rest of the repo (verified — same versions asci.yml)release-rehearsevia workflow_dispatch against this branch (manual step after merge) and confirm plan + preflight complete against crates.io without publishingrelease-resumejob if interrupted