Skip to main content

crate_seq_core/pipeline/
dry_run.rs

1//! Dry-run publish pipeline: checkout, validate, package — no network writes.
2
3use std::path::{Path, PathBuf};
4use std::process::Command;
5
6use crate_seq_ledger::{detect_tag_pattern, load};
7
8use crate::pipeline::source::resolve_source;
9use crate::validate::{run_cargo_check, validate_no_path_deps};
10use crate::Error;
11
12/// Outcome of a dry-run for a single version.
13#[derive(Debug)]
14pub enum DryRunOutcome {
15    /// `cargo package --allow-dirty` succeeded.
16    Pass,
17    /// `cargo package` failed; contains stderr.
18    PackageFailed(String),
19    /// Path dependencies were found; lists dep names.
20    PathDepsFound(Vec<String>),
21    /// `cargo check` failed; contains stderr.
22    CargoCheckFailed(String),
23}
24
25/// Result of a dry-run for a single version.
26#[derive(Debug)]
27pub struct DryRunVersionResult {
28    /// The semantic version that was tested.
29    pub version: semver::Version,
30    /// The tag ref used for the checkout.
31    pub tag_ref: String,
32    /// The outcome of the dry-run for this version.
33    pub outcome: DryRunOutcome,
34}
35
36/// Result of the dry-run for all pending versions.
37#[derive(Debug)]
38pub struct DryRunReport {
39    /// Crate name from the ledger.
40    pub crate_name: String,
41    /// Per-version results in ascending version order.
42    pub results: Vec<DryRunVersionResult>,
43}
44
45/// Resolves the tag pattern from ledger settings or auto-detection.
46fn resolve_tag_pattern(settings: &crate_seq_ledger::LedgerSettings, crate_name: &str) -> String {
47    settings
48        .tag_pattern
49        .clone()
50        .unwrap_or_else(|| detect_tag_pattern(crate_name, false))
51}
52
53/// Runs `cargo package --allow-dirty` for the manifest at `manifest_path`.
54///
55/// Returns `Pass` on success or `PackageFailed(stderr)` on failure.
56fn run_cargo_package(manifest_path: &Path) -> Result<DryRunOutcome, Error> {
57    let output = Command::new("cargo")
58        .args(["package", "--allow-dirty", "--manifest-path"])
59        .arg(manifest_path)
60        .output()
61        .map_err(|e| Error::Subprocess(e.to_string()))?;
62
63    if output.status.success() {
64        Ok(DryRunOutcome::Pass)
65    } else {
66        let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
67        Ok(DryRunOutcome::PackageFailed(stderr))
68    }
69}
70
71/// Maps path-dep or cargo-check errors to soft outcomes; propagates infrastructure errors.
72///
73/// Returns `None` if both validations pass (pipeline should continue).
74fn run_validations(manifest_path: &Path) -> Result<Option<DryRunOutcome>, Error> {
75    match validate_no_path_deps(manifest_path) {
76        Ok(()) => {}
77        Err(Error::PathDependencies { deps, .. }) => {
78            return Ok(Some(DryRunOutcome::PathDepsFound(deps)));
79        }
80        Err(e) => return Err(e),
81    }
82
83    match run_cargo_check(manifest_path) {
84        Ok(()) => {}
85        Err(Error::CargoCheck { stderr }) => {
86            return Ok(Some(DryRunOutcome::CargoCheckFailed(stderr)));
87        }
88        Err(e) => return Err(e),
89    }
90
91    Ok(None)
92}
93
94/// Processes a single pending entry: resolve source → validate → rewrite → package.
95fn process_entry(
96    entry: &crate_seq_ledger::LedgerEntry,
97    repo_path: &Path,
98    crate_rel_path: &Path,
99    snapshot_store: &Path,
100) -> Result<DryRunVersionResult, Error> {
101    let tag_ref = entry.ref_.clone();
102    let checkout = resolve_source(entry, repo_path, snapshot_store)?;
103    let manifest_path = checkout.path().join(crate_rel_path).join("Cargo.toml");
104
105    if let Some(failed_outcome) = run_validations(&manifest_path)? {
106        return Ok(DryRunVersionResult {
107            version: entry.version.clone(),
108            tag_ref,
109            outcome: failed_outcome,
110        });
111    }
112
113    crate_seq_manifest::rewrite_version(&manifest_path, &entry.version)?;
114    let outcome = run_cargo_package(&manifest_path)?;
115
116    Ok(DryRunVersionResult {
117        version: entry.version.clone(),
118        tag_ref,
119        outcome,
120    })
121}
122
123/// Returns the default snapshot store adjacent to the ledger file.
124fn default_snapshot_store(ledger_path: &Path) -> PathBuf {
125    ledger_path
126        .parent()
127        .unwrap_or_else(|| Path::new("."))
128        .join(".crate-seq-snapshots")
129}
130
131/// Returns the crate directory's path relative to `repo_path`.
132///
133/// Strips `repo_path` from `ledger_path.parent()`. Falls back to an empty
134/// relative path when the ledger sits directly at the repo root (single-crate repos).
135fn crate_rel_path(ledger_path: &Path, repo_path: &Path) -> PathBuf {
136    let crate_dir = ledger_path.parent().unwrap_or(repo_path);
137    crate_dir
138        .strip_prefix(repo_path)
139        .unwrap_or(std::path::Path::new(""))
140        .to_owned()
141}
142
143/// Dry-run: resolve each pending version, rewrite `Cargo.toml`, run `cargo package`.
144///
145/// `snapshot_store` overrides the default `.crate-seq-snapshots/` directory for
146/// entries with `source = snapshot`. Git-tag entries ignore `snapshot_store`.
147/// No network writes are performed. Does not modify the ledger.
148///
149/// # Errors
150///
151/// Returns [`Error::Ledger`] if the ledger cannot be loaded, [`Error::Git`] on
152/// checkout failure, [`Error::SnapshotNotFound`] if a snapshot tarball is missing,
153/// [`Error::Manifest`] on rewrite failure, or [`Error::Subprocess`] if `cargo
154/// package` cannot be spawned.
155pub fn publish_dry_run(
156    ledger_path: &Path,
157    repo_path: &Path,
158    snapshot_store: Option<PathBuf>,
159) -> Result<DryRunReport, Error> {
160    let ledger = load(ledger_path)?;
161    let crate_name = ledger.crate_config.name.clone();
162    let _tag_pattern = resolve_tag_pattern(&ledger.settings, &crate_name);
163
164    let store = snapshot_store.unwrap_or_else(|| default_snapshot_store(ledger_path));
165    let rel_path = crate_rel_path(ledger_path, repo_path);
166
167    let pending: Vec<crate_seq_ledger::LedgerEntry> =
168        ledger.pending_versions().into_iter().cloned().collect();
169
170    let mut results = Vec::with_capacity(pending.len());
171    for entry in &pending {
172        results.push(process_entry(entry, repo_path, &rel_path, &store)?);
173    }
174
175    Ok(DryRunReport {
176        crate_name,
177        results,
178    })
179}