feat(rehearse): hard gate — run_publish requires passing rehearsal (#97 PR 3)#128
feat(rehearse): hard gate — run_publish requires passing rehearsal (#97 PR 3)#128EffortlessSteven wants to merge 2 commits into
Conversation
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
61b8fa3 to
2b22bf7
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 61b8fa32d6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if receipt.plan_id != ws.plan.plan_id { | ||
| bail!( | ||
| "rehearsal receipt is stale: rehearsal ran for plan_id {} but the current plan_id is {}. \ | ||
| The workspace changed between rehearse and publish; re-run `shipper rehearse` against the current plan.", | ||
| receipt.plan_id, |
There was a problem hiding this comment.
Reject receipts from a different rehearsal registry
The hard gate accepts any receipt that matches plan_id and passed, but it never verifies that the receipt was produced against the currently configured rehearsal_registry. If the configured rehearsal target changes (or is mistyped), an older passing receipt from a different registry can still authorize live publish, which bypasses the intended guarantee that publish is gated on rehearsal against the configured sandbox.
Useful? React with 👍 / 👎.
| fs::rename(&tmp, &path) | ||
| .with_context(|| format!("failed to rename {} -> {}", tmp.display(), path.display()))?; | ||
| Ok(()) |
There was a problem hiding this comment.
Fsync parent directory after writing rehearsal receipt
This write path fsyncs the temp file and renames it, but does not fsync the parent directory after the rename. After a crash/power loss, the renamed rehearsal.json entry can be lost even though rehearsal reported success, which can cause a later publish to fail the gate as if no rehearsal happened. The state persistence path already does a parent-dir fsync for this reason, so this path should match that durability behavior.
Useful? React with 👍 / 👎.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
827ca17 to
87c5604
Compare
… PR 3) Close the rehearsal loop. PR 2 gave us \`shipper rehearse\` that publishes to an alt registry and emits RehearsalComplete. This PR makes live publish refuse to run unless a fresh, passing rehearsal receipt exists for the current plan_id. ## What's new - **\`state::rehearsal\`** module: a \`rehearsal.json\` sidecar next to \`state.json\` / \`events.jsonl\`. Schema versioned (\`shipper.rehearsal.v1\`), atomic write (\`.tmp\` + rename + fsync), crash-safe. Small enough to be grep-readable. - **Event schema additive**: \`RehearsalStarted\` and \`RehearsalComplete\` now carry \`plan_id\`. The events.jsonl audit trail now has enough information to replay the gate's decision after the fact. This is additive on variants that only shipped with PR 2 (#127), so no migration needed. - **\`engine::enforce_rehearsal_gate\`** + call site at the top of \`run_publish\`. Decision tree: 1. \`opts.rehearsal_registry = None\` → dormant, publish proceeds. Unchanged behavior for every caller who hasn't opted in. 2. \`opts.rehearsal_skip = true\` → proceed with loud warning. No fake-passing receipt is synthesized; auditors see "no rehearsal ran for this plan_id" in events.jsonl. 3. No \`rehearsal.json\` → refuse with fix-suggesting error. 4. Receipt's \`plan_id\` != current plan_id → refuse (stale). 5. \`passed: false\` → refuse with reason. 6. Fresh passing receipt → proceed, log a confirmation info line. - **\`run_rehearsal\` persists the sidecar** on completion. Best-effort: if the sidecar write fails, the events log is still authoritative and the gate will (correctly) refuse, blocking publish — the safe default. ## Tests 13 new tests. All green locally, clippy clean, fmt clean: - 3 roundtrip/persistence tests in \`state::rehearsal\` - 6 gate-helper tests (dormant / skip / missing / stale / failing / fresh-pass) - 1 e2e test that \`run_publish\` actually calls the gate and bails when rehearsal is required but missing Total rehearsal+gate coverage: 17 unit tests. ## Scope — not in this PR - **Install / smoke check** against the rehearsal registry (PR 4 or follow-on). The current gate trusts the rehearsal's publish + visibility verification. - **CI recipe** for kellnr or equivalent sidecar (separate PR; docs work, not code).
Stitch the Prove pillar tier 2 together with a step-by-step operator guide that walks through rehearsal-registry configuration, shipper rehearse, the hard gate, and a kellnr-sidecar CI recipe. Covers: - When to use rehearsal (first-publish, post-refactor) - Config via .shipper.toml or CLI flag - Event emissions + rehearsal.json sidecar - Hard-gate decision tree - Complete GitHub Actions example with kellnr sidecar - Troubleshooting for the 3 common gate errors - What rehearsal does NOT cover yet (install/smoke, consumer-build) Tied to docs/README.md so operators can find it. No code changes.
60c87d2 to
3549bd5
Compare
Summary
Close the rehearsal loop. PR 2 (#127) gave us `shipper rehearse` that publishes to an alt registry and emits `RehearsalComplete`. This PR makes live publish refuse to run unless a fresh, passing rehearsal receipt exists for the current `plan_id`.
Gate decision tree
Evaluated at the top of `run_publish`:
Persistence
New sidecar: `state_dir/rehearsal.json` (`shipper.rehearsal.v1` schema). Atomic write via `.tmp` + rename + fsync, crash-safe. Small enough to `cat` and grok. Shape:
```json
{
"schema_version": "shipper.rehearsal.v1",
"plan_id": "...",
"registry": "...",
"passed": true,
"packages_attempted": 3,
"packages_published": 3,
"summary": "...",
"started_at": "...",
"completed_at": "..."
}
```
Event schema additive
`RehearsalStarted` and `RehearsalComplete` now carry `plan_id`. Lets an auditor replay the gate's decision from `events.jsonl` alone. Additive on events that only shipped with PR 2 (#127), so no migration needed for landed state.
Tests
13 new unit tests (total rehearsal+gate coverage: 17):
Scope — explicitly NOT in this PR
Test plan