Skip to content

feat(preflight): --preflight-only flag (#100)#153

Merged
EffortlessSteven merged 5 commits into
mainfrom
feat/100-preflight-only
Apr 21, 2026
Merged

feat(preflight): --preflight-only flag (#100)#153
EffortlessSteven merged 5 commits into
mainfrom
feat/100-preflight-only

Conversation

@EffortlessSteven

Copy link
Copy Markdown
Member

Summary

Closes the last open gap in the Prove competency (#100). Adds shipper preflight --preflight-only, a session-isolated re-audit that lets operators get a fresh Finishability (Proven / NotProven / Failed) signal without disturbing any in-flight publish state.

  • CLI: new --preflight-only flag, scoped to the preflight subcommand only (clap rejects it on publish, resume, etc.). Defaults to false for full back-compat.
  • Engine: new engine::run_preflight_with_options() entry point taking PreflightRunOptions { fresh_audit: bool }. The historical run_preflight() is preserved as a back-compat wrapper that delegates with defaults. When fresh_audit = true:
    • Does NOT read or append events.jsonl (events-as-truth invariant for the publish flow stays intact).
    • Does NOT write state.json (no publish state side-effect).
    • Writes its trace to a session-scoped sidecar at <state_dir>/preflight-only.events.jsonl, truncated on first flush and appended thereafter (one fresh trace per invocation, not accumulated).
  • Events: new EventLog::write_to_file_truncating() helper plus a preflight_only_events_path() resolver and a PREFLIGHT_ONLY_EVENTS_FILE constant.

Acceptance criteria (from the plan scout's #100 comment)

  • --preflight-only parses and is scoped to preflight (rejected on incompatible commands by clap).
  • Fresh audit ignores prior state.json and prior events.jsonl.
  • Finishability reflects current filesystem + registry state, not cached.
  • Three new tests covering CLI parsing, prior-state isolation, and current-reality reflection across repeated runs.
  • No changes to public engine signatures; back-compat preserved via the run_preflight wrapper.

Tests

Three new tests added, all passing:

  • preflight_only_flag_parses_and_defaults_to_false (shipper-cli) — pins clap surface and back-compat default.
  • run_preflight_fresh_audit_ignores_prior_state (shipper-core) — seeds sentinel events.jsonl + state.json, asserts both are byte-identical after a fresh audit and that the sidecar carries the full trace.
  • run_preflight_with_dirty_git_fresh_audit (shipper-core) — runs fresh audit twice against differing registry realities (NotProven then Proven), asserts authoritative log stays absent and sidecar truncates between runs (exactly one PreflightStarted).

Test plan

  • cargo fmt --all -- --check clean
  • cargo clippy --workspace --all-targets --all-features -- -D warnings clean
  • cargo test -p shipper-cli -p shipper-core (the only flakes I observed are the pre-existing preflight_command_finishability_proven / preflight_command_snapshot tests in cli_e2e.rs, which fight over target/package scratch space when run in parallel; both pass with --test-threads=1 and neither test was touched by this PR)
  • Manual smoke: cargo run -p shipper -- preflight --preflight-only against a workspace with a stale .shipper/ and confirm events.jsonl is unchanged while preflight-only.events.jsonl is fresh

Out of scope

Per the plan scout's comment: multi-registry fresh-audit semantics, automated re-preflight after edits, and CI hook integration are deferred. Recommend closing #100 on merge.

@coderabbitai

coderabbitai Bot commented Apr 19, 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 48 minutes and 8 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 48 minutes and 8 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: 1bda9c8f-f182-447f-b4a8-6d1efdefa455

📥 Commits

Reviewing files that changed from the base of the PR and between bc2bb0d and 74f0645.

⛔ Files ignored due to path filters (7)
  • crates/shipper-cli/tests/snapshots/cli_snapshots__clean_help.snap is excluded by !**/*.snap
  • crates/shipper-cli/tests/snapshots/cli_snapshots__help_text.snap is excluded by !**/*.snap
  • crates/shipper-cli/tests/snapshots/cli_snapshots__preflight_help.snap is excluded by !**/*.snap
  • crates/shipper-cli/tests/snapshots/e2e_expanded__help_clean.snap is excluded by !**/*.snap
  • crates/shipper-cli/tests/snapshots/e2e_expanded__help_preflight.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__inspect_events_empty.snap is excluded by !**/*.snap
📒 Files selected for processing (5)
  • crates/shipper-cli/src/lib.rs
  • crates/shipper-cli/tests/bdd_workflow.rs
  • crates/shipper-core/src/engine/mod.rs
  • crates/shipper-core/src/state/events/mod.rs
  • crates/shipper-core/src/state/events/tests.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/100-preflight-only

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 a --preflight-only flag to the preflight command, enabling session-isolated audits that do not interfere with the authoritative events.jsonl log or state.json. The implementation includes a new run_preflight_with_options entry point in the engine and a sidecar event log (preflight-only.events.jsonl) that is truncated upon each new session. Feedback suggests simplifying the internal flush_events closure by capturing the path from the outer scope and optimizing event serialization by using serde_json::to_writer to avoid unnecessary string allocations.

Comment thread crates/shipper-core/src/engine/mod.rs Outdated
Comment on lines +142 to +149
let mut flush_events = |log: &events::EventLog, path: &Path| -> Result<()> {
if fresh_audit_first_flush {
fresh_audit_first_flush = false;
log.write_to_file_truncating(path)
} else {
log.write_to_file(path)
}
};

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 path parameter in the flush_events closure is redundant as it always receives &events_path. Capturing events_path directly from the outer scope would simplify the closure definition and its call sites.

Suggested change
let mut flush_events = |log: &events::EventLog, path: &Path| -> Result<()> {
if fresh_audit_first_flush {
fresh_audit_first_flush = false;
log.write_to_file_truncating(path)
} else {
log.write_to_file(path)
}
};
let mut flush_events = |log: &events::EventLog| -> Result<()> {
if fresh_audit_first_flush {
fresh_audit_first_flush = false;
log.write_to_file_truncating(&events_path)
} else {
log.write_to_file(&events_path)
}
};

package: "all".to_string(),
});
event_log.write_to_file(&events_path)?;
flush_events(&event_log, &events_path)?;

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

Update call site to match the simplified flush_events closure signature.

Suggested change
flush_events(&event_log, &events_path)?;
flush_events(&event_log)?;

package: "all".to_string(),
});
event_log.write_to_file(&events_path)?;
flush_events(&event_log, &events_path)?;

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

Update call site to match the simplified flush_events closure signature.

Suggested change
flush_events(&event_log, &events_path)?;
flush_events(&event_log)?;

package: "all".to_string(),
});
event_log.write_to_file(&events_path)?;
flush_events(&event_log, &events_path)?;

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

Update call site to match the simplified flush_events closure signature.

Suggested change
flush_events(&event_log, &events_path)?;
flush_events(&event_log)?;

Comment on lines +159 to +162
for event in &self.events {
let line = serde_json::to_string(event).context("failed to serialize event to JSON")?;
writeln!(writer, "{}", line).context("failed to write event line")?;
}

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

Using serde_json::to_writer is more efficient than serde_json::to_string followed by writeln! as it avoids allocating an intermediate string for each event. This is particularly beneficial if the event log contains many entries or large payloads (e.g., captured command output).

Suggested change
for event in &self.events {
let line = serde_json::to_string(event).context("failed to serialize event to JSON")?;
writeln!(writer, "{}", line).context("failed to write event line")?;
}
for event in &self.events {
serde_json::to_writer(&mut writer, event).context("failed to serialize event to JSON")?;
writeln!(writer).context("failed to write event line")?;
}

@codecov

codecov Bot commented Apr 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.26962% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/shipper-cli/src/lib.rs 92.20% 6 Missing ⚠️
crates/shipper-core/src/engine/mod.rs 99.43% 1 Missing ⚠️
crates/shipper-core/src/state/events/mod.rs 97.36% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@EffortlessSteven EffortlessSteven force-pushed the feat/100-preflight-only branch 2 times, most recently from 3b885ac to 3623957 Compare April 21, 2026 13:30
Closes the last gap in the Prove competency (#100): operators can now
re-run a finishability assessment against the current workspace without
disturbing any in-flight publish state. `shipper preflight --preflight-only`
performs a session-isolated audit — it neither reads nor appends to the
authoritative `events.jsonl`, never writes `state.json`, and instead
emits its trace to a sidecar at `.shipper/preflight-only.events.jsonl`
(truncated each invocation). The CLI flag is scoped to the `preflight`
subcommand and threads through a new `engine::PreflightRunOptions` with
`fresh_audit: bool`; the historical `run_preflight()` entry point is
preserved as a back-compat wrapper. Three tests pin the behavior: CLI
flag parses and defaults to `false`, fresh-audit ignores prior
state.json + events.jsonl seeds, and repeated fresh audits reflect
current registry reality (truncate-on-first-write, not accumulated).
@EffortlessSteven EffortlessSteven force-pushed the feat/100-preflight-only branch from 3623957 to 74f0645 Compare April 21, 2026 14:11
@EffortlessSteven EffortlessSteven merged commit eb0edf0 into main Apr 21, 2026
20 checks passed
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