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_dependencies(
123 &selected_pkgs,
124 &excluded_pkgs,
125 &mut index,
126 dry_run,
127 log::Level::Error,
128 )?;
129
130 failed |= !super::verify_git_is_clean(
131 ws_meta.workspace_root.as_std_path(),
132 dry_run,
133 log::Level::Error,
134 )?;
135
136 failed |= !super::verify_git_branch(
137 ws_meta.workspace_root.as_std_path(),
138 &ws_config,
139 dry_run,
140 log::Level::Error,
141 )?;
142
143 failed |= !super::verify_if_behind(
144 ws_meta.workspace_root.as_std_path(),
145 &ws_config,
146 dry_run,
147 log::Level::Warn,
148 )?;
149
150 failed |= !super::verify_metadata(&selected_pkgs, dry_run, log::Level::Error)?;
151 failed |= !super::verify_rate_limit(
152 &selected_pkgs,
153 &mut index,
154 &ws_config.rate_limit,
155 dry_run,
156 log::Level::Error,
157 )?;
158
159 super::confirm("Publish", &selected_pkgs, self.no_confirm, dry_run)?;
161
162 publish(&selected_pkgs, dry_run)?;
164
165 super::finish(failed, dry_run)
166 }
167
168 fn to_config(&self) -> crate::config::ConfigArgs {
169 crate::config::ConfigArgs {
170 custom_config: self.custom_config.clone(),
171 isolated: self.isolated,
172 z: self.z.clone(),
173 allow_branch: self.allow_branch.clone(),
174 publish: self.publish.clone(),
175 ..Default::default()
176 }
177 }
178}
179
180pub fn publish(pkgs: &[plan::PackageRelease], dry_run: bool) -> Result<(), CliError> {
181 if pkgs.is_empty() {
182 Ok(())
183 } else {
184 let first_pkg = pkgs.first().unwrap();
185 let registry = first_pkg.config.registry();
186 let target = first_pkg.config.target.as_deref();
187 let publish_grace_sleep = publish_grace_sleep();
188 if publish_grace_sleep.is_none()
189 && pkgs
190 .iter()
191 .all(|p| p.config.registry() == registry && p.config.target.as_deref() == target)
192 {
193 let manifest_path = &first_pkg.manifest_path;
194 workspace_publish(manifest_path, pkgs, registry, target, dry_run)
195 } else {
196 serial_publish(pkgs, publish_grace_sleep, dry_run)
197 }
198 }
199}
200
201fn workspace_publish(
202 manifest_path: &std::path::Path,
203 pkgs: &[plan::PackageRelease],
204 registry: Option<&str>,
205 target: Option<&str>,
206 dry_run: bool,
207) -> Result<(), CliError> {
208 let crate_names = pkgs.iter().map(|p| p.meta.name.as_str()).join(", ");
209 let _ = crate::ops::shell::status("Publishing", crate_names);
210
211 let verify = pkgs.iter().all(|p| p.config.verify());
212 let features = pkgs.iter().map(|p| &p.features).collect::<Vec<_>>();
213 let pkgids = pkgs
219 .iter()
220 .filter(|p| p.config.publish())
221 .map(|p| p.meta.name.as_str())
222 .collect::<Vec<_>>();
223 if !crate::ops::cargo::publish(
224 dry_run,
225 verify,
226 manifest_path,
227 &pkgids,
228 &features,
229 registry,
230 target,
231 )? {
232 return Err(101.into());
233 }
234
235 Ok(())
236}
237
238fn serial_publish(
239 pkgs: &[plan::PackageRelease],
240 publish_grace_sleep: Option<u64>,
241 dry_run: bool,
242) -> Result<(), CliError> {
243 for pkg in pkgs {
244 if !pkg.config.publish() {
245 continue;
246 }
247
248 let crate_name = pkg.meta.name.as_str();
249 let _ = crate::ops::shell::status("Publishing", crate_name);
250
251 let verify = if !pkg.config.verify() {
252 false
253 } else if dry_run && pkgs.len() != 1 {
254 log::debug!("skipping verification to avoid unpublished dependencies from dry-run");
255 false
256 } else {
257 true
258 };
259 let features = &[&pkg.features];
261 let pkgid = &[crate_name];
267 if !crate::ops::cargo::publish(
268 dry_run,
269 verify,
270 &pkg.manifest_path,
271 pkgid,
272 features,
273 pkg.config.registry(),
274 pkg.config.target.as_ref().map(AsRef::as_ref),
275 )? {
276 return Err(101.into());
277 }
278
279 if !dry_run && let Some(publish_grace_sleep) = publish_grace_sleep {
282 log::debug!(
283 "waiting an additional {} seconds for {} to update its indices...",
284 publish_grace_sleep,
285 pkg.config.registry().unwrap_or("crates.io")
286 );
287 std::thread::sleep(std::time::Duration::from_secs(publish_grace_sleep));
288 }
289 }
290
291 Ok(())
292}
293
294fn publish_grace_sleep() -> Option<u64> {
295 let publish_grace_sleep = std::env::var("PUBLISH_GRACE_SLEEP")
296 .unwrap_or_else(|_| Default::default())
297 .parse()
298 .unwrap_or(0);
299 if publish_grace_sleep == 0 {
300 None
301 } else {
302 Some(publish_grace_sleep)
303 }
304}