cargo_release/steps/
publish.rs

1use itertools::Itertools;
2
3use crate::error::CliError;
4use crate::ops::git;
5use crate::steps::plan;
6
7/// Publish the specified packages
8///
9/// Will automatically skip published versions
10#[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    /// Custom config file
19    #[arg(short, long = "config", value_name = "PATH")]
20    custom_config: Option<std::path::PathBuf>,
21
22    /// Ignore implicit configuration files.
23    #[arg(long)]
24    isolated: bool,
25
26    /// Unstable options
27    #[arg(short = 'Z', value_name = "FEATURE")]
28    z: Vec<crate::config::UnstableValues>,
29
30    /// Comma-separated globs of branch names a release can happen from
31    #[arg(long, value_delimiter = ',')]
32    allow_branch: Option<Vec<String>>,
33
34    /// Actually perform a release. Dry-run mode is the default
35    #[arg(short = 'x', long)]
36    execute: bool,
37
38    #[arg(short = 'n', long, conflicts_with = "execute", hide = true)]
39    dry_run: bool,
40
41    /// Skip release confirmation and version preview
42    #[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            // When evaluating dependency ordering, we need to consider optional dependencies
62            .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                // Either not in workspace or marked as `release = false`.
72                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        // STEP 0: Help the user make the right decisions.
122        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        // STEP 1: Release Confirmation
152        super::confirm("Publish", &selected_pkgs, self.no_confirm, dry_run)?;
153
154        // STEP 3: cargo publish
155        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    // HACK: Ignoring the more precise `pkg.meta.id`.  While it has been stabilized,
210    // the version won't match after we do a version bump and it seems too messy to bother
211    // trying to specify it.
212    // atm at least Cargo doesn't seem to mind if `crate_name` is also a transitive dep, unlike
213    // other cargo commands
214    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    // HACK: This is a fallback in case users can't or don't want to rely on cargo waiting for
232    // them
233    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        // feature list to release
269        let features = &[&pkg.features];
270        // HACK: Ignoring the more precise `pkg.meta.id`.  While it has been stabilized,
271        // the version won't match after we do a version bump and it seems too messy to bother
272        // trying to specify it.
273        // atm at least Cargo doesn't seem to mind if `crate_name` is also a transitive dep, unlike
274        // other cargo commands
275        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        // HACK: This is a fallback in case users can't or don't want to rely on cargo waiting for
289        // them
290        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}