Skip to content

feat(yank): shipper yank <crate>@<version> (#98 PR 1)#121

Merged
EffortlessSteven merged 2 commits into
mainfrom
feat/98-yank-command
Apr 18, 2026
Merged

feat(yank): shipper yank <crate>@<version> (#98 PR 1)#121
EffortlessSteven merged 2 commits into
mainfrom
feat/98-yank-command

Conversation

@EffortlessSteven

Copy link
Copy Markdown
Member

Summary

Opens the Remediate pillar (#98) with its first primitive: a single
shipper yank <crate>@<version> command. Containment, not undo
existing lockfile pins stay pinned; the yank only prevents NEW resolves
against that version. Downstream consumers get told to cargo update -p <name>.

  • ops/cargo: new cargo_yank(workspace, name, version, registry, lines, timeout),
    mirrors cargo_publish invariants (non-default registry passthrough,
    timeout polling, output redaction + tailing via shipper-output-sanitizer).
  • CLI: shipper yank --crate <NAME> --version <VERSION> --reason <REASON>
    • Emits PackageYanked { crate_name, version, reason, exit_code } to
      <state_dir>/events.jsonl — the reason is operator-supplied
      (e.g. "CVE-2026-0001 disclosed; containing while patch released")
      and enters the event log alongside publish events.
    • Loud operator warning about containment semantics + fix-forward
      guidance (cargo update -p <NAME>).
  • types: EventType::PackageYanked variant.
  • tests:
    • 3 fake-cargo unit tests in ops/cargo (args passthrough,
      crates-io registry omission, non-zero exit propagation)
    • yank --help snapshot in e2e_expanded

Scope

Intentionally tight. Follow-on PRs under #98 compose this primitive into:

  • shipper plan-yank — reverse-topological containment plan
  • shipper fix-forward — patch, publish, update-pin

Receipt augmentation (compromised_at/by/superseded_by) is not in
this PR — it was explored and pulled to tighten scope; will land on the
plan-yank PR where the schema change pays for itself.

Test plan

  • cargo test -p shipper --lib ops::cargo::tests::cargo_yank — 3/3 pass
  • cargo test -p shipper-cli --test e2e_expanded help_yank_snapshot — green
  • cargo fmt --all — clean
  • cargo clippy -p shipper -p shipper-cli --all-targets --all-features -- -D warnings — no issues
  • CI multi-OS green

@coderabbitai

coderabbitai Bot commented Apr 17, 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 16 minutes and 58 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 16 minutes and 58 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: b251232b-6668-4451-b553-6b1ed8c5ad68

📥 Commits

Reviewing files that changed from the base of the PR and between cc0dc68 and bdb84eb.

⛔ Files ignored due to path filters (4)
  • 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_root.snap is excluded by !**/*.snap
  • crates/shipper-cli/tests/snapshots/e2e_expanded__help_yank.snap is excluded by !**/*.snap
📒 Files selected for processing (4)
  • crates/shipper-cli/src/main.rs
  • crates/shipper-cli/tests/e2e_expanded.rs
  • crates/shipper-types/src/lib.rs
  • crates/shipper/src/ops/cargo/mod.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/98-yank-command

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.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces the yank command to the shipper-cli, enabling users to mark specific crate versions as non-installable in the registry. The changes include a new cargo_yank wrapper in the library, an updated EventType for event logging, and comprehensive E2E and unit tests. Feedback highlights the need for consistent registry handling in multi-registry setups, optimization of string allocations by moving variables instead of cloning, and stricter adherence to the project's 'events-as-truth' invariant by treating event log write failures as fatal errors.

Comment on lines +621 to +625
let registry_name = opts
.registries
.first()
.map(|r| r.name.clone())
.unwrap_or_else(|| "crates-io".to_string());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The logic for selecting the registry for the yank command could lead to unexpected behavior in a multi-registry configuration. It currently selects the first registry from opts.registries. If a user has multiple registries configured (e.g., via .shipper.toml), the yank command will silently only operate on the first one in the list. This is inconsistent with other commands like publish and status which iterate over all configured registries.

To avoid this ambiguity, I suggest one of two approaches:

  1. Explicitly iterate over all registries in opts.registries for the yank command, similar to how other commands behave.
  2. If yank is intended to only work on a single registry at a time, consider getting the registry name more directly from the --registry flag, and perhaps error out if opts.registries contains more than one entry and --registry is not specified. This would make the behavior more explicit for the user.

Comment on lines +640 to +642
crate_name: crate_name.clone(),
version: version.clone(),
reason: reason.clone(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The crate_name, version, and reason strings are being cloned here, but since they are owned by the Yank command scope and are not used after being passed to EventType::PackageYanked, they can be moved instead. This avoids unnecessary allocations.

Suggested change
crate_name: crate_name.clone(),
version: version.clone(),
reason: reason.clone(),
crate_name,
version,
reason,

Comment on lines +648 to +653
if let Err(err) = log.write_to_file(&events_file) {
reporter.warn(&format!(
"failed to append PackageYanked event to {}: {err:#}",
events_file.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.

medium

A failure to write the PackageYanked event to events.jsonl is currently handled as a warning. According to the project's invariants (docs/INVARIANTS.md), events.jsonl is the authoritative source of truth. If a yank operation succeeds but is not recorded, the system's state becomes inconsistent with its history. This could cause issues with auditing and future tooling that relies on the event log.

Consider making this a hard error by using reporter.error() and returning an Err result, to ensure that any successful yank is always recorded.

Suggested change
if let Err(err) = log.write_to_file(&events_file) {
reporter.warn(&format!(
"failed to append PackageYanked event to {}: {err:#}",
events_file.display()
));
}
if let Err(err) = log.write_to_file(&events_file) {
reporter.error(&format!(
"failed to append PackageYanked event to {}: {err:#}",
events_file.display()
));
anyhow::bail!("failed to record yank event");
}

@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: 76ccb80434

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

Comment on lines +621 to +625
let registry_name = opts
.registries
.first()
.map(|r| r.name.clone())
.unwrap_or_else(|| "crates-io".to_string());

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 Use selected registry when invoking cargo yank

This picks registry_name from opts.registries, but that list is empty in normal single-registry flows (including --registry <name>), so the command silently falls back to crates-io. As a result, shipper yank --registry private-reg ... still targets crates.io instead of the requested registry unless multi-registry flags are used, which can yank the wrong destination.

Useful? React with 👍 / 👎.

Comment on lines +619 to +620
let workspace_root =
std::env::current_dir().context("failed to resolve current dir for cargo yank")?;

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 Run cargo yank from the resolved workspace root

The yank command ignores the resolved workspace/manifest location and always uses the process current directory as Cargo's working directory. When --manifest-path points to a different workspace, Cargo will evaluate config relative to the wrong tree, so registry mappings or auth settings in the target workspace's .cargo/config.toml may not apply and yank can fail unexpectedly.

Useful? React with 👍 / 👎.

First primitive of the Remediate pillar (#98). Wraps `cargo yank` as
containment, not undo — existing lockfile pins stay pinned; the yank
only prevents NEW resolves against that version.

- ops/cargo: cargo_yank(workspace, name, version, registry, lines, timeout)
  mirrors the cargo_publish invariants (non-default registry passthrough,
  timeout polling, output redaction + tailing via output-sanitizer).
- CLI: `shipper yank --crate <name> --version <ver> --reason <why>` emits
  a PackageYanked event to <state_dir>/events.jsonl capturing the reason
  and the cargo exit code; surfaces a loud operator warning about
  containment semantics and the `cargo update -p <name>` remediation step
  for downstream consumers.
- types: PackageYanked event variant (crate_name, version, reason, exit_code).
- tests: three fake-cargo unit tests in ops/cargo (args passthrough,
  crates-io registry omission, non-zero exit propagation) + a yank
  --help snapshot in e2e_expanded.

Scope-limited on purpose: follow-on PRs under #98 compose this into
`shipper plan-yank` (reverse-topological containment plan) and
`shipper fix-forward` (patch-and-republish).
@codecov

codecov Bot commented Apr 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.87117% with 54 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/shipper-cli/src/main.rs 0.00% 54 Missing ⚠️

📢 Thoughts on this report? Let us know!

EffortlessSteven added a commit that referenced this pull request Apr 17, 2026
…ds (#98 PR 2)

Next step on the Remediate pillar. PR 1 (#121) landed the single-crate
\`shipper yank\` primitive; this PR composes it into a reverse-topological
containment plan built from a prior publish receipt.

## What's new

- **Receipt schema** (additive): PackageReceipt gains three Option
  fields, all \`#[serde(default, skip_serializing_if = \"Option::is_none\")]\`
  so existing receipts read back unchanged:
  - \`compromised_at: Option<DateTime<Utc>>\` — when this version was
    marked compromised
  - \`compromised_by: Option<String>\` — operator-supplied reason (CVE,
    incident, free-form tag)
  - \`superseded_by: Option<String>\` — populated by \`shipper
    fix-forward\` (#98 PR 3)

- **\`shipper::engine::plan_yank\`** module:
  - \`PlanYankFilter::{AllPublished, CompromisedOnly}\` selector
  - \`YankPlan\`, \`YankEntry\` value types
  - \`build_plan(receipt, filter)\` — filter receipt.packages, reverse
    to get dependents-first order, emit entries
  - \`render_text(plan)\` — produces copy-pasteable \`shipper yank ...\`
    command lines, one per entry, with \`--reason <REASON>\` placeholders
    for the operator to fill in
  - \`load_receipt_from_path\` — receipt loader for arbitrary paths
    (default is \`<state_dir>/receipt.json\`)

- **\`shipper plan-yank\` CLI subcommand**:
  \`\`\`
  shipper plan-yank [--from-receipt PATH] [--compromised-only]
  \`\`\`
  Honors the global \`--format\` flag: \`text\` (default) prints the
  copy-pasteable yank-command list; \`json\` prints structured
  YankPlan for scripting.

## Scope — explicitly NOT in this PR

- **Plan execution** (wrapping \`shipper yank\` to run each entry) →
  #98 PR 3 along with fix-forward.
- **\`--mark-compromised\`** (populating the new receipt fields
  programmatically) → #98 PR 3. For now, operators can mark packages
  compromised by hand-editing receipt.json or via downstream tooling.
- **release_compromised state** (higher-level "this release is a
  write-off") → #98 PR 3.

## Why reverse topological, specifically

For workspace A → B → C (A is a leaf, B depends on A, C depends on B):

- Publish order: A, B, C
- **Yank order: C, B, A** — dependents first. New resolves for C will
  still pull B (if B is not yet yanked), but at least C itself is
  unresolvable. Yanking A first would be fine semantically but can
  mislead operators who read \`cargo search\` output.

Plus: reverse-topo matches the containment mental model users bring
from incident response — "pull the thing furthest from your reach first,
work back toward the root."

## Tests

5 unit tests in \`engine::plan_yank::tests\` (all green):

- reverses publish order for \`AllPublished\`
- excludes Failed/Skipped packages (only Published gets a yank entry)
- \`CompromisedOnly\` filter drops healthy packages, keeps only flagged
- empty receipt → empty plan with a helpful comment
- text-render ordering invariant: dependents appear before dependencies

Plus a CLI \`plan-yank --help\` snapshot in \`e2e_expanded\`.

## Companion churn

Mechanical: every PackageReceipt construction site (engine + state tests
+ integration tests) now sets the three new Option fields to None. The
schema is additive; no runtime semantics changed for existing code paths.
EffortlessSteven added a commit that referenced this pull request Apr 17, 2026
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 6e95b83 into main Apr 18, 2026
20 of 21 checks passed
@EffortlessSteven EffortlessSteven deleted the feat/98-yank-command branch April 18, 2026 00:04
EffortlessSteven added a commit that referenced this pull request Apr 18, 2026
…ds (#98 PR 2)

Next step on the Remediate pillar. PR 1 (#121) landed the single-crate
\`shipper yank\` primitive; this PR composes it into a reverse-topological
containment plan built from a prior publish receipt.

## What's new

- **Receipt schema** (additive): PackageReceipt gains three Option
  fields, all \`#[serde(default, skip_serializing_if = \"Option::is_none\")]\`
  so existing receipts read back unchanged:
  - \`compromised_at: Option<DateTime<Utc>>\` — when this version was
    marked compromised
  - \`compromised_by: Option<String>\` — operator-supplied reason (CVE,
    incident, free-form tag)
  - \`superseded_by: Option<String>\` — populated by \`shipper
    fix-forward\` (#98 PR 3)

- **\`shipper::engine::plan_yank\`** module:
  - \`PlanYankFilter::{AllPublished, CompromisedOnly}\` selector
  - \`YankPlan\`, \`YankEntry\` value types
  - \`build_plan(receipt, filter)\` — filter receipt.packages, reverse
    to get dependents-first order, emit entries
  - \`render_text(plan)\` — produces copy-pasteable \`shipper yank ...\`
    command lines, one per entry, with \`--reason <REASON>\` placeholders
    for the operator to fill in
  - \`load_receipt_from_path\` — receipt loader for arbitrary paths
    (default is \`<state_dir>/receipt.json\`)

- **\`shipper plan-yank\` CLI subcommand**:
  \`\`\`
  shipper plan-yank [--from-receipt PATH] [--compromised-only]
  \`\`\`
  Honors the global \`--format\` flag: \`text\` (default) prints the
  copy-pasteable yank-command list; \`json\` prints structured
  YankPlan for scripting.

## Scope — explicitly NOT in this PR

- **Plan execution** (wrapping \`shipper yank\` to run each entry) →
  #98 PR 3 along with fix-forward.
- **\`--mark-compromised\`** (populating the new receipt fields
  programmatically) → #98 PR 3. For now, operators can mark packages
  compromised by hand-editing receipt.json or via downstream tooling.
- **release_compromised state** (higher-level "this release is a
  write-off") → #98 PR 3.

## Why reverse topological, specifically

For workspace A → B → C (A is a leaf, B depends on A, C depends on B):

- Publish order: A, B, C
- **Yank order: C, B, A** — dependents first. New resolves for C will
  still pull B (if B is not yet yanked), but at least C itself is
  unresolvable. Yanking A first would be fine semantically but can
  mislead operators who read \`cargo search\` output.

Plus: reverse-topo matches the containment mental model users bring
from incident response — "pull the thing furthest from your reach first,
work back toward the root."

## Tests

5 unit tests in \`engine::plan_yank::tests\` (all green):

- reverses publish order for \`AllPublished\`
- excludes Failed/Skipped packages (only Published gets a yank entry)
- \`CompromisedOnly\` filter drops healthy packages, keeps only flagged
- empty receipt → empty plan with a helpful comment
- text-render ordering invariant: dependents appear before dependencies

Plus a CLI \`plan-yank --help\` snapshot in \`e2e_expanded\`.

## Companion churn

Mechanical: every PackageReceipt construction site (engine + state tests
+ integration tests) now sets the three new Option fields to None. The
schema is additive; no runtime semantics changed for existing code paths.
EffortlessSteven added a commit that referenced this pull request Apr 18, 2026
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 added a commit that referenced this pull request Apr 18, 2026
…PR 2) (#132)

* feat(remediate): plan-yank command + compromised-release receipt fields (#98 PR 2)

Next step on the Remediate pillar. PR 1 (#121) landed the single-crate
\`shipper yank\` primitive; this PR composes it into a reverse-topological
containment plan built from a prior publish receipt.

## What's new

- **Receipt schema** (additive): PackageReceipt gains three Option
  fields, all \`#[serde(default, skip_serializing_if = \"Option::is_none\")]\`
  so existing receipts read back unchanged:
  - \`compromised_at: Option<DateTime<Utc>>\` — when this version was
    marked compromised
  - \`compromised_by: Option<String>\` — operator-supplied reason (CVE,
    incident, free-form tag)
  - \`superseded_by: Option<String>\` — populated by \`shipper
    fix-forward\` (#98 PR 3)

- **\`shipper::engine::plan_yank\`** module:
  - \`PlanYankFilter::{AllPublished, CompromisedOnly}\` selector
  - \`YankPlan\`, \`YankEntry\` value types
  - \`build_plan(receipt, filter)\` — filter receipt.packages, reverse
    to get dependents-first order, emit entries
  - \`render_text(plan)\` — produces copy-pasteable \`shipper yank ...\`
    command lines, one per entry, with \`--reason <REASON>\` placeholders
    for the operator to fill in
  - \`load_receipt_from_path\` — receipt loader for arbitrary paths
    (default is \`<state_dir>/receipt.json\`)

- **\`shipper plan-yank\` CLI subcommand**:
  \`\`\`
  shipper plan-yank [--from-receipt PATH] [--compromised-only]
  \`\`\`
  Honors the global \`--format\` flag: \`text\` (default) prints the
  copy-pasteable yank-command list; \`json\` prints structured
  YankPlan for scripting.

## Scope — explicitly NOT in this PR

- **Plan execution** (wrapping \`shipper yank\` to run each entry) →
  #98 PR 3 along with fix-forward.
- **\`--mark-compromised\`** (populating the new receipt fields
  programmatically) → #98 PR 3. For now, operators can mark packages
  compromised by hand-editing receipt.json or via downstream tooling.
- **release_compromised state** (higher-level "this release is a
  write-off") → #98 PR 3.

## Why reverse topological, specifically

For workspace A → B → C (A is a leaf, B depends on A, C depends on B):

- Publish order: A, B, C
- **Yank order: C, B, A** — dependents first. New resolves for C will
  still pull B (if B is not yet yanked), but at least C itself is
  unresolvable. Yanking A first would be fine semantically but can
  mislead operators who read \`cargo search\` output.

Plus: reverse-topo matches the containment mental model users bring
from incident response — "pull the thing furthest from your reach first,
work back toward the root."

## Tests

5 unit tests in \`engine::plan_yank::tests\` (all green):

- reverses publish order for \`AllPublished\`
- excludes Failed/Skipped packages (only Published gets a yank entry)
- \`CompromisedOnly\` filter drops healthy packages, keeps only flagged
- empty receipt → empty plan with a helpful comment
- text-render ordering invariant: dependents appear before dependencies

Plus a CLI \`plan-yank --help\` snapshot in \`e2e_expanded\`.

## Companion churn

Mechanical: every PackageReceipt construction site (engine + state tests
+ integration tests) now sets the three new Option fields to None. The
schema is additive; no runtime semantics changed for existing code paths.

* fix(plan-yank): appease Linux rustfmt + drop broken intra-doc link
EffortlessSteven added a commit that referenced this pull request Apr 18, 2026
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 added a commit that referenced this pull request Apr 18, 2026
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 added a commit that referenced this pull request Apr 18, 2026
…134)

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

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.

* docs: how-to for remediating a compromised release (#98)

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 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.
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