1use crate::config::PackageConfig;
10use crate::publishers::{PublishCtx, PublishState, publisher_for};
11
12#[derive(Debug, Clone)]
15pub enum PublishOutcome {
16 NotConfigured { path: String },
18 AlreadyPublished { path: String, publisher: String },
20 Succeeded { path: String, publisher: String },
22 Failed {
24 path: String,
25 publisher: String,
26 message: String,
27 },
28}
29
30impl PublishOutcome {
31 pub fn path(&self) -> &str {
32 match self {
33 Self::NotConfigured { path }
34 | Self::AlreadyPublished { path, .. }
35 | Self::Succeeded { path, .. }
36 | Self::Failed { path, .. } => path,
37 }
38 }
39
40 pub fn is_failure(&self) -> bool {
41 matches!(self, Self::Failed { .. })
42 }
43}
44
45pub fn run_package_publish(
51 package: &PackageConfig,
52 version: &str,
53 tag: &str,
54 dry_run: bool,
55 env: &[(&str, &str)],
56) -> PublishOutcome {
57 let Some(cfg) = package.publish.as_ref() else {
58 return PublishOutcome::NotConfigured {
59 path: package.path.clone(),
60 };
61 };
62
63 let publisher = publisher_for(cfg);
64 let pub_name = publisher.name().to_string();
65 let ctx = PublishCtx {
66 package,
67 version,
68 tag,
69 dry_run,
70 env,
71 };
72
73 match publisher.check(&ctx) {
74 Ok(PublishState::Completed) => {
75 eprintln!(
76 "{pub_name} ({}): already at {version} — skipping",
77 package.path
78 );
79 return PublishOutcome::AlreadyPublished {
80 path: package.path.clone(),
81 publisher: pub_name,
82 };
83 }
84 Ok(PublishState::Needed) => {}
85 Ok(PublishState::Unknown(reason)) => {
86 eprintln!(
87 "{pub_name} ({}): state check inconclusive ({reason}) — attempting publish",
88 package.path
89 );
90 }
91 Err(e) => {
92 return PublishOutcome::Failed {
93 path: package.path.clone(),
94 publisher: pub_name,
95 message: format!("check failed: {e}"),
96 };
97 }
98 }
99
100 match publisher.run(&ctx) {
101 Ok(()) => PublishOutcome::Succeeded {
102 path: package.path.clone(),
103 publisher: pub_name,
104 },
105 Err(e) => PublishOutcome::Failed {
106 path: package.path.clone(),
107 publisher: pub_name,
108 message: e.to_string(),
109 },
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::config::{PackageConfig, PublishConfig};
117
118 #[test]
119 fn not_configured_when_publish_is_none() {
120 let pkg = PackageConfig {
121 path: ".".into(),
122 ..Default::default()
123 };
124 let outcome = run_package_publish(&pkg, "1.0.0", "v1.0.0", false, &[]);
125 assert!(matches!(outcome, PublishOutcome::NotConfigured { .. }));
126 }
127
128 #[test]
129 fn custom_run_succeeds() {
130 let pkg = PackageConfig {
131 path: ".".into(),
132 publish: Some(PublishConfig::Custom {
133 command: "true".into(),
134 check: None,
135 cwd: Some(".".into()),
136 }),
137 ..Default::default()
138 };
139 let outcome = run_package_publish(&pkg, "1.0.0", "v1.0.0", false, &[]);
140 assert!(matches!(outcome, PublishOutcome::Succeeded { .. }));
141 }
142
143 #[test]
144 fn custom_run_failure_captured() {
145 let pkg = PackageConfig {
146 path: ".".into(),
147 publish: Some(PublishConfig::Custom {
148 command: "false".into(),
149 check: None,
150 cwd: Some(".".into()),
151 }),
152 ..Default::default()
153 };
154 let outcome = run_package_publish(&pkg, "1.0.0", "v1.0.0", false, &[]);
155 assert!(matches!(outcome, PublishOutcome::Failed { .. }));
156 }
157
158 #[test]
159 fn custom_check_completed_short_circuits_run() {
160 let pkg = PackageConfig {
162 path: ".".into(),
163 publish: Some(PublishConfig::Custom {
164 command: "false".into(),
165 check: Some("true".into()),
166 cwd: Some(".".into()),
167 }),
168 ..Default::default()
169 };
170 let outcome = run_package_publish(&pkg, "1.0.0", "v1.0.0", false, &[]);
171 assert!(matches!(outcome, PublishOutcome::AlreadyPublished { .. }));
172 }
173
174 #[test]
175 fn go_always_completed() {
176 let pkg = PackageConfig {
177 path: ".".into(),
178 publish: Some(PublishConfig::Go),
179 ..Default::default()
180 };
181 let outcome = run_package_publish(&pkg, "1.0.0", "v1.0.0", false, &[]);
182 assert!(matches!(outcome, PublishOutcome::AlreadyPublished { .. }));
183 }
184
185 #[test]
186 fn unknown_check_still_runs() {
187 let pkg = PackageConfig {
193 path: ".".into(),
194 publish: Some(PublishConfig::Custom {
195 command: "true".into(),
196 check: None,
197 cwd: Some(".".into()),
198 }),
199 ..Default::default()
200 };
201 let outcome = run_package_publish(&pkg, "1.0.0", "v1.0.0", false, &[]);
202 assert!(
203 matches!(outcome, PublishOutcome::Succeeded { .. }),
204 "Unknown state should proceed to run, got {outcome:?}"
205 );
206 }
207
208 #[test]
209 fn dry_run_skips_execution() {
210 let pkg = PackageConfig {
212 path: ".".into(),
213 publish: Some(PublishConfig::Custom {
214 command: "false".into(),
215 check: None,
216 cwd: Some(".".into()),
217 }),
218 ..Default::default()
219 };
220 let outcome = run_package_publish(&pkg, "1.0.0", "v1.0.0", true, &[]);
221 assert!(matches!(outcome, PublishOutcome::Succeeded { .. }));
223 }
224}