1#[cfg(test)]
4#[path = "execute_tests.rs"]
5mod tests;
6
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10use crate_seq_ledger::{load, save};
11use crate_seq_registry::{backoff_publish, BackoffConfig, CratesIoClient, PublishOutcome};
12
13use crate::auth::require_token;
14use crate::pipeline::patch::inject_workspace_patches;
15use crate::pipeline::source::resolve_source;
16use crate::validate::{run_cargo_check, validate_no_path_deps};
17use crate::Error;
18
19#[derive(Debug)]
21pub enum VersionPublishOutcome {
22 Published,
24 AlreadyPublished,
26 Skipped,
28 Failed(String),
30}
31
32#[derive(Debug)]
34pub struct PublishVersionResult {
35 pub version: semver::Version,
37 pub tag_ref: String,
39 pub outcome: VersionPublishOutcome,
41}
42
43#[derive(Debug)]
45pub struct PublishReport {
46 pub crate_name: String,
48 pub results: Vec<PublishVersionResult>,
50}
51
52fn run_package(manifest_path: &Path) -> Result<Option<String>, Error> {
54 let output = Command::new("cargo")
55 .args(["package", "--allow-dirty", "--manifest-path"])
56 .arg(manifest_path)
57 .output()
58 .map_err(|e| Error::Subprocess(e.to_string()))?;
59
60 if output.status.success() {
61 Ok(None)
62 } else {
63 let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
64 Ok(Some(stderr))
65 }
66}
67
68fn persist(ledger_path: &Path, ledger: &crate_seq_ledger::CrateSeqLedger) -> Result<(), Error> {
70 save(ledger_path, ledger)?;
71 Ok(())
72}
73
74fn validate_manifest(manifest_path: &Path) -> Result<(), Error> {
76 validate_no_path_deps(manifest_path)?;
77 run_cargo_check(manifest_path)
78}
79
80fn default_snapshot_store(ledger_path: &Path) -> PathBuf {
82 ledger_path
83 .parent()
84 .unwrap_or_else(|| Path::new("."))
85 .join(".crate-seq-snapshots")
86}
87
88fn crate_rel_path(ledger_path: &Path, repo_path: &Path) -> PathBuf {
93 let crate_dir = ledger_path.parent().unwrap_or(repo_path);
94 crate_dir
95 .strip_prefix(repo_path)
96 .unwrap_or(std::path::Path::new(""))
97 .to_owned()
98}
99
100#[allow(clippy::too_many_arguments)]
102fn process_entry(
103 entry: &crate_seq_ledger::LedgerEntry,
104 ledger_path: &Path,
105 ledger: &mut crate_seq_ledger::CrateSeqLedger,
106 repo_path: &Path,
107 crate_rel_path: &Path,
108 snapshot_store: &Path,
109 client: &CratesIoClient,
110 token: Option<&str>,
111 backoff: &BackoffConfig,
112 crate_name: &str,
113 results: &mut Vec<PublishVersionResult>,
114) -> Result<bool, Error> {
115 let tag_ref = entry.ref_.clone();
116 let version = entry.version.clone();
117
118 if client.check_version_exists(crate_name, &version)? {
119 ledger.mark_published(&version)?;
120 persist(ledger_path, ledger)?;
121 results.push(PublishVersionResult {
122 version,
123 tag_ref,
124 outcome: VersionPublishOutcome::AlreadyPublished,
125 });
126 return Ok(true);
127 }
128
129 let checkout = resolve_source(entry, repo_path, snapshot_store)?;
130 inject_workspace_patches(checkout.path())?; let manifest_path = checkout.path().join(crate_rel_path).join("Cargo.toml");
132
133 validate_manifest(&manifest_path)?;
134
135 crate_seq_manifest::rewrite_version(&manifest_path, &version)?;
136
137 if let Some(stderr) = run_package(&manifest_path)? {
138 results.push(PublishVersionResult {
139 version,
140 tag_ref,
141 outcome: VersionPublishOutcome::Failed(stderr),
142 });
143 persist(ledger_path, ledger)?;
144 return Ok(false);
145 }
146
147 match backoff_publish(&checkout.path().join(crate_rel_path), token, backoff)? {
148 PublishOutcome::Success | PublishOutcome::AlreadyPublished => {
149 ledger.mark_published(&version)?;
150 persist(ledger_path, ledger)?;
151 results.push(PublishVersionResult {
152 version,
153 tag_ref,
154 outcome: VersionPublishOutcome::Published,
155 });
156 Ok(true)
157 }
158 PublishOutcome::RateLimited => {
159 results.push(PublishVersionResult {
160 version,
161 tag_ref,
162 outcome: VersionPublishOutcome::Failed(
163 "rate limited — retries exhausted".to_owned(),
164 ),
165 });
166 persist(ledger_path, ledger)?;
167 Ok(false)
168 }
169 PublishOutcome::Failed(msg) => {
170 results.push(PublishVersionResult {
171 version,
172 tag_ref,
173 outcome: VersionPublishOutcome::Failed(msg),
174 });
175 persist(ledger_path, ledger)?;
176 Ok(false)
177 }
178 }
179}
180
181pub fn publish_execute(
204 ledger_path: &Path,
205 repo_path: &Path,
206 token: Option<&str>,
207 backoff_config: &BackoffConfig,
208 crate_seq_version: &str,
209 snapshot_store: Option<PathBuf>,
210) -> Result<PublishReport, Error> {
211 let client = CratesIoClient::new(crate_seq_version)?;
212 publish_execute_with_client(
213 ledger_path,
214 repo_path,
215 token,
216 backoff_config,
217 snapshot_store,
218 &client,
219 )
220}
221
222pub(crate) fn publish_execute_with_client(
227 ledger_path: &Path,
228 repo_path: &Path,
229 token: Option<&str>,
230 backoff_config: &BackoffConfig,
231 snapshot_store: Option<PathBuf>,
232 client: &CratesIoClient,
233) -> Result<PublishReport, Error> {
234 let mut ledger = load(ledger_path)?;
235 let crate_name = ledger.crate_config.name.clone();
236
237 let resolved_token = require_token(token, &ledger.auth)?;
238 let token_ref = resolved_token.as_deref();
239
240 let store = snapshot_store.unwrap_or_else(|| default_snapshot_store(ledger_path));
241 let rel_path = crate_rel_path(ledger_path, repo_path);
242
243 let pending: Vec<crate_seq_ledger::LedgerEntry> =
244 ledger.pending_versions().into_iter().cloned().collect();
245
246 let mut results = Vec::with_capacity(pending.len());
247
248 for entry in &pending {
249 let ok = process_entry(
250 entry,
251 ledger_path,
252 &mut ledger,
253 repo_path,
254 &rel_path,
255 &store,
256 client,
257 token_ref,
258 backoff_config,
259 &crate_name,
260 &mut results,
261 )?;
262 if !ok {
263 break;
264 }
265 }
266
267 Ok(PublishReport {
268 crate_name,
269 results,
270 })
271}