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)?;
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(pkgs: &[plan::PackageRelease], dry_run: bool) -> Result<(), CliError> {
173 if pkgs.is_empty() {
174 Ok(())
175 } else {
176 let first_pkg = pkgs.first().unwrap();
177 let registry = first_pkg.config.registry();
178 let target = first_pkg.config.target.as_deref();
179 let publish_grace_sleep = publish_grace_sleep();
180 if publish_grace_sleep.is_none()
181 && pkgs
182 .iter()
183 .all(|p| p.config.registry() == registry && p.config.target.as_deref() == target)
184 {
185 let manifest_path = &first_pkg.manifest_path;
186 workspace_publish(manifest_path, pkgs, registry, target, dry_run)
187 } else {
188 serial_publish(pkgs, publish_grace_sleep, dry_run)
189 }
190 }
191}
192
193fn workspace_publish(
194 manifest_path: &std::path::Path,
195 pkgs: &[plan::PackageRelease],
196 registry: Option<&str>,
197 target: Option<&str>,
198 dry_run: bool,
199) -> Result<(), CliError> {
200 let crate_names = pkgs.iter().map(|p| p.meta.name.as_str()).join(", ");
201 let _ = crate::ops::shell::status("Publishing", crate_names);
202
203 let verify = pkgs.iter().all(|p| p.config.verify());
204 let features = pkgs.iter().map(|p| &p.features).collect::<Vec<_>>();
205 let pkgids = pkgs
211 .iter()
212 .filter(|p| p.config.publish())
213 .map(|p| p.meta.name.as_str())
214 .collect::<Vec<_>>();
215 if !crate::ops::cargo::publish(
216 dry_run,
217 verify,
218 manifest_path,
219 &pkgids,
220 &features,
221 registry,
222 target,
223 )? {
224 return Err(101.into());
225 }
226
227 Ok(())
228}
229
230fn serial_publish(
231 pkgs: &[plan::PackageRelease],
232 publish_grace_sleep: Option<u64>,
233 dry_run: bool,
234) -> Result<(), CliError> {
235 for pkg in pkgs {
236 if !pkg.config.publish() {
237 continue;
238 }
239
240 let crate_name = pkg.meta.name.as_str();
241 let _ = crate::ops::shell::status("Publishing", crate_name);
242
243 let verify = if !pkg.config.verify() {
244 false
245 } else if dry_run && pkgs.len() != 1 {
246 log::debug!("skipping verification to avoid unpublished dependencies from dry-run");
247 false
248 } else {
249 true
250 };
251 let features = &[&pkg.features];
253 let pkgid = &[crate_name];
259 if !crate::ops::cargo::publish(
260 dry_run,
261 verify,
262 &pkg.manifest_path,
263 pkgid,
264 features,
265 pkg.config.registry(),
266 pkg.config.target.as_ref().map(AsRef::as_ref),
267 )? {
268 return Err(101.into());
269 }
270
271 if !dry_run && let Some(publish_grace_sleep) = publish_grace_sleep {
274 log::debug!(
275 "waiting an additional {} seconds for {} to update its indices...",
276 publish_grace_sleep,
277 pkg.config.registry().unwrap_or("crates.io")
278 );
279 std::thread::sleep(std::time::Duration::from_secs(publish_grace_sleep));
280 }
281 }
282
283 Ok(())
284}
285
286fn publish_grace_sleep() -> Option<u64> {
287 let publish_grace_sleep = std::env::var("PUBLISH_GRACE_SLEEP")
288 .unwrap_or_else(|_| Default::default())
289 .parse()
290 .unwrap_or(0);
291 if publish_grace_sleep == 0 {
292 None
293 } else {
294 Some(publish_grace_sleep)
295 }
296}