1use itertools::Itertools;
2
3use crate::error::CliError;
4use crate::ops::git;
5use crate::steps::plan;
6
7#[derive(Debug, Clone, clap::Args)]
11pub struct PublishStep {
12 #[command(flatten)]
13 manifest: clap_cargo::Manifest,
14
15 #[command(flatten)]
16 workspace: clap_cargo::Workspace,
17
18 #[arg(short, long = "config", value_name = "PATH")]
20 custom_config: Option<std::path::PathBuf>,
21
22 #[arg(long)]
24 isolated: bool,
25
26 #[arg(short = 'Z', value_name = "FEATURE")]
28 z: Vec<crate::config::UnstableValues>,
29
30 #[arg(long, value_delimiter = ',')]
32 allow_branch: Option<Vec<String>>,
33
34 #[arg(short = 'x', long)]
36 execute: bool,
37
38 #[arg(short = 'n', long, conflicts_with = "execute", hide = true)]
39 dry_run: bool,
40
41 #[arg(long)]
43 no_confirm: bool,
44
45 #[command(flatten)]
46 publish: crate::config::PublishArgs,
47}
48
49impl PublishStep {
50 pub fn run(&self) -> Result<(), CliError> {
51 git::git_version()?;
52
53 if self.dry_run {
54 let _ =
55 crate::ops::shell::warn("`--dry-run` is superfluous, dry-run is done by default");
56 }
57
58 let ws_meta = self
59 .manifest
60 .metadata()
61 .features(cargo_metadata::CargoOpt::AllFeatures)
63 .exec()?;
64 let config = self.to_config();
65 let ws_config = crate::config::load_workspace_config(&config, &ws_meta)?;
66 let mut pkgs = plan::load(&config, &ws_meta)?;
67
68 let (_selected_pkgs, excluded_pkgs) = self.workspace.partition_packages(&ws_meta);
69 for excluded_pkg in excluded_pkgs {
70 let Some(pkg) = pkgs.get_mut(&excluded_pkg.id) else {
71 continue;
73 };
74 if !pkg.config.release() {
75 continue;
76 }
77
78 pkg.config.publish = Some(false);
79 pkg.config.release = Some(false);
80
81 let crate_name = pkg.meta.name.as_str();
82 log::debug!("disabled by user, skipping {crate_name}",);
83 }
84
85 let mut pkgs = plan::plan(pkgs)?;
86
87 let mut index = crate::ops::index::CratesIoIndex::new();
88 for pkg in pkgs.values_mut() {
89 if pkg.config.release() {
90 let crate_name = pkg.meta.name.as_str();
91 let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
92 if crate::ops::cargo::is_published(
93 &mut index,
94 pkg.config.registry(),
95 crate_name,
96 &version.full_version_string,
97 pkg.config.certs_source(),
98 ) {
99 let _ = crate::ops::shell::warn(format!(
100 "disabled due to previous publish ({}), skipping {}",
101 version.full_version_string, crate_name
102 ));
103 pkg.config.publish = Some(false);
104 pkg.config.release = Some(false);
105 }
106 }
107 }
108
109 let (selected_pkgs, _excluded_pkgs): (Vec<_>, Vec<_>) = pkgs
110 .into_iter()
111 .map(|(_, pkg)| pkg)
112 .partition(|p| p.config.release());
113 if selected_pkgs.is_empty() {
114 let _ = crate::ops::shell::error("no packages selected");
115 return Err(2.into());
116 }
117
118 let dry_run = !self.execute;
119 let mut failed = false;
120
121 failed |= !super::verify_git_is_clean(
123 ws_meta.workspace_root.as_std_path(),
124 dry_run,
125 log::Level::Error,
126 )?;
127
128 failed |= !super::verify_git_branch(
129 ws_meta.workspace_root.as_std_path(),
130 &ws_config,
131 dry_run,
132 log::Level::Error,
133 )?;
134
135 failed |= !super::verify_if_behind(
136 ws_meta.workspace_root.as_std_path(),
137 &ws_config,
138 dry_run,
139 log::Level::Warn,
140 )?;
141
142 failed |= !super::verify_metadata(&selected_pkgs, dry_run, log::Level::Error)?;
143 failed |= !super::verify_rate_limit(
144 &selected_pkgs,
145 &mut index,
146 &ws_config.rate_limit,
147 dry_run,
148 log::Level::Error,
149 )?;
150
151 super::confirm("Publish", &selected_pkgs, self.no_confirm, dry_run)?;
153
154 publish(&selected_pkgs, dry_run, &ws_config.unstable)?;
156
157 super::finish(failed, dry_run)
158 }
159
160 fn to_config(&self) -> crate::config::ConfigArgs {
161 crate::config::ConfigArgs {
162 custom_config: self.custom_config.clone(),
163 isolated: self.isolated,
164 z: self.z.clone(),
165 allow_branch: self.allow_branch.clone(),
166 publish: self.publish.clone(),
167 ..Default::default()
168 }
169 }
170}
171
172pub fn publish(
173 pkgs: &[plan::PackageRelease],
174 dry_run: bool,
175 unstable: &crate::config::Unstable,
176) -> Result<(), CliError> {
177 if pkgs.is_empty() {
178 Ok(())
179 } else if unstable.workspace_publish() {
180 let first_pkg = pkgs.first().unwrap();
181 let registry = first_pkg.config.registry();
182 let target = first_pkg.config.target.as_deref();
183 if pkgs
184 .iter()
185 .all(|p| p.config.registry() == registry && p.config.target.as_deref() == target)
186 {
187 let manifest_path = &first_pkg.manifest_path;
188 workspace_publish(manifest_path, pkgs, registry, target, dry_run)
189 } else {
190 serial_publish(pkgs, dry_run)
191 }
192 } else {
193 serial_publish(pkgs, dry_run)
194 }
195}
196
197fn workspace_publish(
198 manifest_path: &std::path::Path,
199 pkgs: &[plan::PackageRelease],
200 registry: Option<&str>,
201 target: Option<&str>,
202 dry_run: bool,
203) -> Result<(), CliError> {
204 let crate_names = pkgs.iter().map(|p| p.meta.name.as_str()).join(", ");
205 let _ = crate::ops::shell::status("Publishing", crate_names);
206
207 let verify = pkgs.iter().all(|p| p.config.verify());
208 let features = pkgs.iter().map(|p| &p.features).collect::<Vec<_>>();
209 let pkgids = pkgs
215 .iter()
216 .filter(|p| p.config.publish())
217 .map(|p| p.meta.name.as_str())
218 .collect::<Vec<_>>();
219 if !crate::ops::cargo::publish(
220 dry_run,
221 verify,
222 manifest_path,
223 &pkgids,
224 &features,
225 registry,
226 target,
227 )? {
228 return Err(101.into());
229 }
230
231 if !dry_run {
234 let publish_grace_sleep = std::env::var("PUBLISH_GRACE_SLEEP")
235 .unwrap_or_else(|_| Default::default())
236 .parse()
237 .unwrap_or(0);
238 if 0 < publish_grace_sleep {
239 log::debug!(
240 "waiting an additional {} seconds for {} to update its indices...",
241 publish_grace_sleep,
242 registry.unwrap_or("crates.io")
243 );
244 std::thread::sleep(std::time::Duration::from_secs(publish_grace_sleep));
245 }
246 }
247
248 Ok(())
249}
250
251fn serial_publish(pkgs: &[plan::PackageRelease], dry_run: bool) -> Result<(), CliError> {
252 for pkg in pkgs {
253 if !pkg.config.publish() {
254 continue;
255 }
256
257 let crate_name = pkg.meta.name.as_str();
258 let _ = crate::ops::shell::status("Publishing", crate_name);
259
260 let verify = if !pkg.config.verify() {
261 false
262 } else if dry_run && pkgs.len() != 1 {
263 log::debug!("skipping verification to avoid unpublished dependencies from dry-run");
264 false
265 } else {
266 true
267 };
268 let features = &[&pkg.features];
270 let pkgid = &[crate_name];
276 if !crate::ops::cargo::publish(
277 dry_run,
278 verify,
279 &pkg.manifest_path,
280 pkgid,
281 features,
282 pkg.config.registry(),
283 pkg.config.target.as_ref().map(AsRef::as_ref),
284 )? {
285 return Err(101.into());
286 }
287
288 if !dry_run {
291 let publish_grace_sleep = std::env::var("PUBLISH_GRACE_SLEEP")
292 .unwrap_or_else(|_| Default::default())
293 .parse()
294 .unwrap_or(0);
295 if 0 < publish_grace_sleep {
296 log::debug!(
297 "waiting an additional {} seconds for {} to update its indices...",
298 publish_grace_sleep,
299 pkg.config.registry().unwrap_or("crates.io")
300 );
301 std::thread::sleep(std::time::Duration::from_secs(publish_grace_sleep));
302 }
303 }
304 }
305
306 Ok(())
307}