Skip to content

feat(remediate): --mark-compromised + shipper fix-forward (#98 PR 3)#134

Merged
EffortlessSteven merged 2 commits into
mainfrom
feat/98-fix-forward
Apr 18, 2026
Merged

feat(remediate): --mark-compromised + shipper fix-forward (#98 PR 3)#134
EffortlessSteven merged 2 commits into
mainfrom
feat/98-fix-forward

Conversation

@EffortlessSteven

Copy link
Copy Markdown
Member

Reopened after #132 merge. Original PR #131 auto-closed when base branch was deleted.

PR 3 of the #98 Remediate staged rollout:

  • shipper yank --mark-compromised amends the receipt's compromise fields
  • shipper fix-forward --from-receipt prints topologically-ordered supersession plan
  • 4 new tests for fix_forward plan building
  • docs/how-to/remediate-a-compromised-release.md — operator walkthrough tying yank + plan-yank + fix-forward together

Completes the Remediate rollout alongside the now-merged #121 and #132.

@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@EffortlessSteven has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 36 minutes and 3 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 36 minutes and 3 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e19c008e-daad-43db-a1b7-f4812deaea66

📥 Commits

Reviewing files that changed from the base of the PR and between abb51b1 and cc64e72.

⛔ Files ignored due to path filters (5)
  • crates/shipper-cli/tests/snapshots/cli_snapshots__help_text.snap is excluded by !**/*.snap
  • crates/shipper-cli/tests/snapshots/cli_snapshots__no_subcommand_error.snap is excluded by !**/*.snap
  • crates/shipper-cli/tests/snapshots/e2e_expanded__help_fix_forward.snap is excluded by !**/*.snap
  • crates/shipper-cli/tests/snapshots/e2e_expanded__help_root.snap is excluded by !**/*.snap
  • crates/shipper-cli/tests/snapshots/e2e_expanded__help_yank.snap is excluded by !**/*.snap
📒 Files selected for processing (6)
  • crates/shipper-cli/src/main.rs
  • crates/shipper-cli/tests/e2e_expanded.rs
  • crates/shipper/src/engine/fix_forward.rs
  • crates/shipper/src/engine/mod.rs
  • docs/README.md
  • docs/how-to/remediate-a-compromised-release.md
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/98-fix-forward

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Close the remediation staged rollout: yank (PR 1) + plan-yank (PR 2) +
**fix-forward planning (this PR)** give operators the three building
blocks for handling a compromised release without improvising.

## What's new

### \`shipper yank --mark-compromised\`

New opt-in flag on the existing yank primitive. When set, after a
successful \`cargo yank\`, shipper also amends the matching
\`PackageReceipt\` in \`<state_dir>/receipt.json\` with:

- \`compromised_at = Utc::now()\`
- \`compromised_by = <--reason>\`

These are the fields added (additively) in PR 2 (#129). The amendment
lets downstream commands find compromised packages without scanning
events.jsonl.

Behaviour is tolerant:
- No receipt → warn + proceed (yank still succeeds)
- No matching package in receipt → warn + proceed
- Load/write errors → warn + proceed (yank already happened; a
  receipt-write failure doesn't retroactively unyank)

### \`shipper fix-forward --from-receipt <path>\`

New planning command. Reads a receipt, finds packages flagged
\`compromised_at = Some(_)\`, and prints an ordered supersession plan:

- Topological order (dependencies first, dependents last) — opposite
  of plan-yank, because fix-forward *publishes* replacements while
  plan-yank *removes* reachability.
- Suggested successor versions use a placeholder format (\`<old>-next\`)
  so the operator picks the real bump (patch / minor / major).
- Instructional preamble walks through the full remediation loop:
  bump Cargo.toml → commit → \`shipper publish\` → optional
  \`shipper plan-yank --compromised-only\` for containment.

Like plan-yank, fix-forward is **planning only**. It does not edit
Cargo.toml or invoke publish — those are operator steps.

## Tests

4 new tests in \`engine::fix_forward::tests\`:

- only compromised+Published packages produce steps (Failed-but-
  compromised packages are silently dropped — they were never shipped)
- topological order preserved from receipt.packages
- empty plan (no compromised packages) renders with guidance toward
  \`--mark-compromised\`
- text render enumerates steps with reason and points toward the
  publish + plan-yank follow-ups

Plus CLI snapshots for \`yank --help\` (gains \`--mark-compromised\`) and
\`fix-forward --help\`.

## Scope — explicitly NOT in this PR

- **Automated Cargo.toml bump.** Workspace-edit territory; needs its
  own PR with --dry-run, --apply, and git-cleanliness guards.
- **Publishing successors.** \`shipper publish\` already does this; fix-
  forward just tells the operator when to run it.
- **Chaining \`superseded_by\` back to the original receipt.** Requires
  post-publish receipt amendment from the successor run. Follow-on.
- **\`release_compromised\` top-level state.** The receipt-level marker.
  For now the per-package \`compromised_at\` fields cover the
  planning-level need.
Stitch the Remediate pillar together with a step-by-step operator
guide that walks through yank --mark-compromised → fix-forward plan →
publish → optional plan-yank finalize. Tied to docs/README.md's how-to
index so operators can find it without folklore.

Covers:
- 4-step remediation flow with concrete commands
- Worked example: single-CVE in a multi-crate workspace
- Explicit statement of what yank does NOT do (lockfile invalidation,
  version bumping, consumer notification)

No code changes. This only references commands added in this branch
stack (#121 yank, #129 plan-yank, #131 mark-compromised + fix-forward).
@EffortlessSteven EffortlessSteven merged commit a42b025 into main Apr 18, 2026
2 of 3 checks passed
@EffortlessSteven EffortlessSteven deleted the feat/98-fix-forward branch April 18, 2026 00:13

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 46b3a2b6b3

ℹ️ 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".

}

fn is_compromised(p: &PackageReceipt) -> bool {
p.compromised_at.is_some() && matches!(p.state, PackageState::Published)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Treat already-published skipped entries as fix-forward inputs

is_compromised currently requires PackageState::Published, but receipt generation can store successfully published versions as PackageState::Skipped { reason: "already published" } during resume flows (see crates/shipper/src/engine/mod.rs:746-753). If an operator marks one of those entries compromised, fix-forward silently drops it from the plan, which can leave resumed releases with an incomplete remediation plan.

Useful? React with 👍 / 👎.

Comment on lines +177 to +178
let receipt: Receipt = serde_json::from_str(&raw)
.with_context(|| format!("failed to parse receipt at {}", path.display()))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Load fix-forward receipts with schema migration support

This path deserializes JSON straight into Receipt, bypassing the version/migration logic used by state::execution_state::load_receipt and migrate_receipt. As a result, older but still-supported receipts (e.g., v1 missing newer fields) will fail in shipper fix-forward --from-receipt ..., even though the codebase otherwise supports upgrading those receipts.

Useful? React with 👍 / 👎.

@codecov

codecov Bot commented Apr 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.22807% with 77 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/shipper-cli/src/main.rs 0.00% 70 Missing ⚠️
crates/shipper/src/engine/fix_forward.rs 95.56% 7 Missing ⚠️

📢 Thoughts on this report? Let us know!

EffortlessSteven added a commit that referenced this pull request Apr 18, 2026
Three-column reconciliation of what's actually merged vs each issue's
acceptance checklist. Single source of truth for what closes each
pillar issue.

**Findings:**

- **#90 Recover** — honestly closable. Code side is done (#124 + #130);
  operator-side real rehearsal is an ops action, not a code gap.

- **#97 Prove tier 2** — 85% done. Rehearsal + visibility + hard gate +
  plan_id binding all landed (#127 + #133). Missing: install/smoke
  check (cargo install against the rehearsal registry / consumer
  build). One narrow follow-up PR closes it.

- **#98 Remediate** — 60% done. Receipt schema + plan-yank (from-receipt)
  + yank primitive + fix-forward planning all landed (#121 + #132 +
  #134). Missing: plan-yank's --starting-crate graph mode, plan
  execution for yank + fix-forward. Two narrow follow-ups.

Also captures the two review concerns on #122 (Trusted Publishing)
that were addressed in a follow-up commit to that PR.

Recommended next merge order and follow-up PRs spelled out at bottom.
EffortlessSteven added a commit that referenced this pull request Apr 18, 2026
Three-column reconciliation of what's actually merged vs each issue's
acceptance checklist. Single source of truth for what closes each
pillar issue.

**Findings:**

- **#90 Recover** — honestly closable. Code side is done (#124 + #130);
  operator-side real rehearsal is an ops action, not a code gap.

- **#97 Prove tier 2** — 85% done. Rehearsal + visibility + hard gate +
  plan_id binding all landed (#127 + #133). Missing: install/smoke
  check (cargo install against the rehearsal registry / consumer
  build). One narrow follow-up PR closes it.

- **#98 Remediate** — 60% done. Receipt schema + plan-yank (from-receipt)
  + yank primitive + fix-forward planning all landed (#121 + #132 +
  #134). Missing: plan-yank's --starting-crate graph mode, plan
  execution for yank + fix-forward. Two narrow follow-ups.

Also captures the two review concerns on #122 (Trusted Publishing)
that were addressed in a follow-up commit to that PR.

Recommended next merge order and follow-up PRs spelled out at bottom.
EffortlessSteven added a commit that referenced this pull request Apr 18, 2026
Closes the remaining plan-execution gap in #98. \`shipper plan-yank
--format json\` emits a YankPlan; \`shipper yank --plan <PATH>\` walks
that plan and invokes \`cargo yank\` per entry.

## What's new

- \`YankEntry\` / \`YankPlan\` now derive \`Deserialize\` so plans
  round-trip via \`--format json\` → file → \`--plan\`. The \`filter\`
  field moves from \`&'static str\` to \`Cow<'static, str>\` so owned
  strings coming off disk don't require allocation in the construction
  path.
- \`plan_yank::load_plan_from_path\` — thin helper mirroring
  \`load_receipt_from_path\`.
- CLI: \`shipper yank --plan <PATH>\`. Mutually exclusive with
  \`--crate\` / \`--version\` / \`--reason\` (clap \`conflicts_with\`);
  those become optional in plan mode.
- Handler: iterates entries in order, emits \`PackageYanked\` event
  per invocation, halts on first non-zero cargo exit (reverse-topo
  plan means every downstream entry would be failing to yank a
  dependent of a crate we just failed on — continuing is damage).

## Scope — what this PR does and doesn't

**Does:**
- Execute yank plans end-to-end.
- Correct halt-on-failure semantics.
- Events-as-truth: every invocation writes a PackageYanked event
  (with real exit code) to events.jsonl, including failures.

**Does not:**
- Resume interrupted plan runs. First yank is destructive on the
  registry; re-running the plan with the same reason is idempotent
  (cargo yank of an already-yanked version is a no-op), so the
  simplest fix is to just re-run the plan, and operator discipline
  picks up the rest.
- Add a \`fix-forward\` executor. That requires Cargo.toml version
  bumps which are workspace-edit territory — deserves its own PR
  with --dry-run / --apply / git-cleanliness guards.

## Tests

3 new unit tests (8 total in \`plan_yank::tests\`):
- YankPlan JSON round-trips: serialize, write, load → entries + order
  preserved, per-entry reason preserved.
- \`load_plan_from_path\` errors cleanly on missing file.
- \`load_plan_from_path\` errors cleanly on malformed JSON.

Regenerated \`yank --help\` snapshot.

## Closes the rest of #98's open gap

With PR #138 (starting-crate) and this one, #98's acceptance checklist is fulfilled:

- [x] Receipt fields (compromised_at/compromised_by/superseded_by) — PR #132
- [x] plan-yank --from-receipt + --starting-crate + --reason — PR #132 + #138
- [x] shipper yank executes a plan — **this PR**
- [ ] shipper fix-forward --plan executes a plan — follow-on (needs Cargo.toml edit tooling)
- [x] Docs distinguish yank semantics — PR #134

The fix-forward executor is the last remaining #98 piece. It's cleanly deferred because it adds workspace-edit scope; this PR keeps the execution story for yank self-contained.
EffortlessSteven added a commit that referenced this pull request Apr 18, 2026
Closes the remaining plan-execution gap in #98. \`shipper plan-yank
--format json\` emits a YankPlan; \`shipper yank --plan <PATH>\` walks
that plan and invokes \`cargo yank\` per entry.

- \`YankEntry\` / \`YankPlan\` now derive \`Deserialize\` so plans
  round-trip via \`--format json\` → file → \`--plan\`. The \`filter\`
  field moves from \`&'static str\` to \`Cow<'static, str>\` so owned
  strings coming off disk don't require allocation in the construction
  path.
- \`plan_yank::load_plan_from_path\` — thin helper mirroring
  \`load_receipt_from_path\`.
- CLI: \`shipper yank --plan <PATH>\`. Mutually exclusive with
  \`--crate\` / \`--version\` / \`--reason\` (clap \`conflicts_with\`);
  those become optional in plan mode.
- Handler: iterates entries in order, emits \`PackageYanked\` event
  per invocation, halts on first non-zero cargo exit (reverse-topo
  plan means every downstream entry would be failing to yank a
  dependent of a crate we just failed on — continuing is damage).

**Does:**
- Execute yank plans end-to-end.
- Correct halt-on-failure semantics.
- Events-as-truth: every invocation writes a PackageYanked event
  (with real exit code) to events.jsonl, including failures.

**Does not:**
- Resume interrupted plan runs. First yank is destructive on the
  registry; re-running the plan with the same reason is idempotent
  (cargo yank of an already-yanked version is a no-op), so the
  simplest fix is to just re-run the plan, and operator discipline
  picks up the rest.
- Add a \`fix-forward\` executor. That requires Cargo.toml version
  bumps which are workspace-edit territory — deserves its own PR
  with --dry-run / --apply / git-cleanliness guards.

3 new unit tests (8 total in \`plan_yank::tests\`):
- YankPlan JSON round-trips: serialize, write, load → entries + order
  preserved, per-entry reason preserved.
- \`load_plan_from_path\` errors cleanly on missing file.
- \`load_plan_from_path\` errors cleanly on malformed JSON.

Regenerated \`yank --help\` snapshot.

With PR #138 (starting-crate) and this one, #98's acceptance checklist is fulfilled:

- [x] Receipt fields (compromised_at/compromised_by/superseded_by) — PR #132
- [x] plan-yank --from-receipt + --starting-crate + --reason — PR #132 + #138
- [x] shipper yank executes a plan — **this PR**
- [ ] shipper fix-forward --plan executes a plan — follow-on (needs Cargo.toml edit tooling)
- [x] Docs distinguish yank semantics — PR #134

The fix-forward executor is the last remaining #98 piece. It's cleanly deferred because it adds workspace-edit scope; this PR keeps the execution story for yank self-contained.
EffortlessSteven added a commit that referenced this pull request Apr 18, 2026
…#139)

Closes the remaining plan-execution gap in #98. \`shipper plan-yank
--format json\` emits a YankPlan; \`shipper yank --plan <PATH>\` walks
that plan and invokes \`cargo yank\` per entry.

- \`YankEntry\` / \`YankPlan\` now derive \`Deserialize\` so plans
  round-trip via \`--format json\` → file → \`--plan\`. The \`filter\`
  field moves from \`&'static str\` to \`Cow<'static, str>\` so owned
  strings coming off disk don't require allocation in the construction
  path.
- \`plan_yank::load_plan_from_path\` — thin helper mirroring
  \`load_receipt_from_path\`.
- CLI: \`shipper yank --plan <PATH>\`. Mutually exclusive with
  \`--crate\` / \`--version\` / \`--reason\` (clap \`conflicts_with\`);
  those become optional in plan mode.
- Handler: iterates entries in order, emits \`PackageYanked\` event
  per invocation, halts on first non-zero cargo exit (reverse-topo
  plan means every downstream entry would be failing to yank a
  dependent of a crate we just failed on — continuing is damage).

**Does:**
- Execute yank plans end-to-end.
- Correct halt-on-failure semantics.
- Events-as-truth: every invocation writes a PackageYanked event
  (with real exit code) to events.jsonl, including failures.

**Does not:**
- Resume interrupted plan runs. First yank is destructive on the
  registry; re-running the plan with the same reason is idempotent
  (cargo yank of an already-yanked version is a no-op), so the
  simplest fix is to just re-run the plan, and operator discipline
  picks up the rest.
- Add a \`fix-forward\` executor. That requires Cargo.toml version
  bumps which are workspace-edit territory — deserves its own PR
  with --dry-run / --apply / git-cleanliness guards.

3 new unit tests (8 total in \`plan_yank::tests\`):
- YankPlan JSON round-trips: serialize, write, load → entries + order
  preserved, per-entry reason preserved.
- \`load_plan_from_path\` errors cleanly on missing file.
- \`load_plan_from_path\` errors cleanly on malformed JSON.

Regenerated \`yank --help\` snapshot.

With PR #138 (starting-crate) and this one, #98's acceptance checklist is fulfilled:

- [x] Receipt fields (compromised_at/compromised_by/superseded_by) — PR #132
- [x] plan-yank --from-receipt + --starting-crate + --reason — PR #132 + #138
- [x] shipper yank executes a plan — **this PR**
- [ ] shipper fix-forward --plan executes a plan — follow-on (needs Cargo.toml edit tooling)
- [x] Docs distinguish yank semantics — PR #134

The fix-forward executor is the last remaining #98 piece. It's cleanly deferred because it adds workspace-edit scope; this PR keeps the execution story for yank self-contained.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant