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