Skip to main content

cargo_release/steps/
release.rs

1use crate::config;
2use crate::error::CliError;
3use crate::ops::cargo;
4use crate::ops::git;
5use crate::steps::plan;
6
7#[derive(Debug, Clone, clap::Args)]
8pub struct ReleaseStep {
9    #[command(flatten)]
10    manifest: clap_cargo::Manifest,
11
12    #[command(flatten)]
13    workspace: clap_cargo::Workspace,
14
15    /// Process all packages whose current version is unpublished
16    #[arg(long, conflicts_with = "level_or_version")]
17    unpublished: bool,
18
19    /// Either bump by LEVEL or set the VERSION for all selected packages
20    #[arg(value_name = "LEVEL|VERSION")]
21    level_or_version: Option<super::TargetVersion>,
22
23    /// Semver metadata
24    #[arg(short, long, requires = "level_or_version")]
25    metadata: Option<String>,
26
27    /// Actually perform a release. Dry-run mode is the default
28    #[arg(short = 'x', long)]
29    execute: bool,
30
31    #[arg(short = 'n', long, conflicts_with = "execute", hide = true)]
32    dry_run: bool,
33
34    /// Skip release confirmation and version preview
35    #[arg(long)]
36    no_confirm: bool,
37
38    /// The name of tag for the previous release.
39    #[arg(long, value_name = "NAME")]
40    prev_tag_name: Option<String>,
41
42    #[command(flatten)]
43    config: config::ConfigArgs,
44}
45
46impl ReleaseStep {
47    pub fn run(&self) -> Result<(), CliError> {
48        git::git_version()?;
49        let mut index = crate::ops::index::CratesIoIndex::new();
50
51        if self.dry_run {
52            let _ =
53                crate::ops::shell::warn("`--dry-run` is superfluous, dry-run is done by default");
54        }
55
56        let ws_meta = self
57            .manifest
58            .metadata()
59            // When evaluating dependency ordering, we need to consider optional dependencies
60            .features(cargo_metadata::CargoOpt::AllFeatures)
61            .exec()?;
62        let mut ws_config = config::load_workspace_config(&self.config, &ws_meta)?;
63        let mut pkgs = plan::load(&self.config, &ws_meta)?;
64
65        for pkg in pkgs.values_mut() {
66            if let Some(prev_tag) = self.prev_tag_name.as_ref() {
67                // Trust the user that the tag passed in is the latest tag for the workspace and that
68                // they don't care about any changes from before this tag.
69                pkg.set_prior_tag(prev_tag.to_owned());
70            }
71            if pkg.config.release()
72                && let Some(level_or_version) = &self.level_or_version
73            {
74                pkg.bump(level_or_version, self.metadata.as_deref())?;
75            }
76            if index.has_krate(
77                pkg.config.registry(),
78                &pkg.meta.name,
79                pkg.config.certs_source(),
80            )? {
81                // Already published, skip it.  Use `cargo release owner` for one-time updates
82                pkg.ensure_owners = false;
83            }
84        }
85
86        let (_selected_pkgs, excluded_pkgs) =
87            if self.unpublished && self.workspace == clap_cargo::Workspace::default() {
88                ws_meta.packages.iter().partition(|_| false)
89            } else {
90                self.workspace.partition_packages(&ws_meta)
91            };
92        for excluded_pkg in &excluded_pkgs {
93            let Some(pkg) = pkgs.get_mut(&excluded_pkg.id) else {
94                // Either not in workspace or marked as `release = false`.
95                continue;
96            };
97            if !pkg.config.release() {
98                continue;
99            }
100
101            let crate_name = pkg.meta.name.as_str();
102            let explicitly_excluded = self.workspace.exclude.contains(&excluded_pkg.name);
103            // 1. Don't show this message if already not releasing in config
104            // 2. Still respect `--exclude`
105            if pkg.config.release()
106                && pkg.config.publish()
107                && self.unpublished
108                && !explicitly_excluded
109            {
110                let version = &pkg.initial_version;
111                if !cargo::is_published(
112                    &mut index,
113                    pkg.config.registry(),
114                    crate_name,
115                    &version.full_version_string,
116                    pkg.config.certs_source(),
117                ) {
118                    log::debug!(
119                        "enabled {}, v{} is unpublished",
120                        crate_name,
121                        version.full_version_string
122                    );
123                    continue;
124                }
125            }
126
127            pkg.planned_version = None;
128            pkg.config.release = Some(false);
129
130            if let Some(prior_tag_name) = &pkg.prior_tag {
131                if let Some(changed) =
132                    crate::steps::version::changed_since(&ws_meta, pkg, prior_tag_name)
133                {
134                    if !changed.is_empty() {
135                        let _ = crate::ops::shell::warn(format!(
136                            "disabled by user, skipping {crate_name} which has files changed since {prior_tag_name}: {changed:#?}"
137                        ));
138                    } else {
139                        log::trace!(
140                            "disabled by user, skipping {crate_name} (no changes since {prior_tag_name})"
141                        );
142                    }
143                } else {
144                    log::debug!(
145                        "disabled by user, skipping {crate_name} (no {prior_tag_name} tag)"
146                    );
147                }
148            } else {
149                log::debug!("disabled by user, skipping {crate_name} (no tag found)",);
150            }
151        }
152
153        let pkgs = plan::plan(pkgs)?;
154
155        for excluded_pkg in &excluded_pkgs {
156            let Some(pkg) = pkgs.get(&excluded_pkg.id) else {
157                // Either not in workspace or marked as `release = false`.
158                continue;
159            };
160
161            // HACK: `index` only supports default registry
162            if pkg.config.publish() && pkg.config.registry().is_none() {
163                let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
164                let crate_name = pkg.meta.name.as_str();
165                if !cargo::is_published(
166                    &mut index,
167                    pkg.config.registry(),
168                    crate_name,
169                    &version.full_version_string,
170                    pkg.config.certs_source(),
171                ) {
172                    let _ = crate::ops::shell::warn(format!(
173                        "disabled by user, skipping {} v{} despite being unpublished",
174                        crate_name, version.full_version_string,
175                    ));
176                }
177            }
178        }
179
180        let (selected_pkgs, excluded_pkgs): (Vec<_>, Vec<_>) = pkgs
181            .into_iter()
182            .map(|(_, pkg)| pkg)
183            .partition(|p| p.config.release());
184        if selected_pkgs.is_empty() {
185            let _ = crate::ops::shell::error("no packages selected");
186            return Err(2.into());
187        }
188
189        let dry_run = !self.execute;
190        let mut failed = false;
191
192        let consolidate_commits = super::consolidate_commits(&selected_pkgs, &excluded_pkgs)?;
193        ws_config.consolidate_commits = Some(consolidate_commits);
194
195        // STEP 0: Help the user make the right decisions.
196        failed |= !super::verify_dependencies(
197            &selected_pkgs,
198            &excluded_pkgs,
199            &mut index,
200            dry_run,
201            log::Level::Error,
202        )?;
203
204        failed |= !super::verify_git_is_clean(
205            ws_meta.workspace_root.as_std_path(),
206            dry_run,
207            log::Level::Error,
208        )?;
209
210        failed |= !super::verify_tags_missing(&selected_pkgs, dry_run, log::Level::Error)?;
211
212        failed |=
213            !super::verify_monotonically_increasing(&selected_pkgs, dry_run, log::Level::Error)?;
214
215        let mut double_publish = false;
216        for pkg in &selected_pkgs {
217            if !pkg.config.publish() {
218                continue;
219            }
220            let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
221            let crate_name = pkg.meta.name.as_str();
222            if cargo::is_published(
223                &mut index,
224                pkg.config.registry(),
225                crate_name,
226                &version.full_version_string,
227                pkg.config.certs_source(),
228            ) {
229                let registry = pkg.config.registry().unwrap_or("crates.io");
230                let _ = crate::ops::shell::error(format!(
231                    "{} {} is already published to {}",
232                    crate_name, version.full_version_string, registry
233                ));
234                double_publish = true;
235            }
236        }
237        if double_publish {
238            failed = true;
239            if !dry_run {
240                return Err(101.into());
241            }
242        }
243
244        super::warn_changed(&ws_meta, &selected_pkgs)?;
245
246        failed |= !super::verify_git_branch(
247            ws_meta.workspace_root.as_std_path(),
248            &ws_config,
249            dry_run,
250            log::Level::Error,
251        )?;
252
253        failed |= !super::verify_if_behind(
254            ws_meta.workspace_root.as_std_path(),
255            &ws_config,
256            dry_run,
257            log::Level::Warn,
258        )?;
259
260        failed |= !super::verify_metadata(&selected_pkgs, dry_run, log::Level::Error)?;
261        failed |= !super::verify_rate_limit(
262            &selected_pkgs,
263            &mut index,
264            &ws_config.rate_limit,
265            dry_run,
266            log::Level::Error,
267        )?;
268
269        // STEP 1: Release Confirmation
270        super::confirm("Release", &selected_pkgs, self.no_confirm, dry_run)?;
271
272        // STEP 2: update current version, save and commit
273        if consolidate_commits {
274            let update_lock =
275                super::version::update_versions(&ws_meta, &selected_pkgs, &excluded_pkgs, dry_run)?;
276            if update_lock {
277                log::debug!("updating lock file");
278                if !dry_run {
279                    let workspace_path = ws_meta.workspace_root.as_std_path().join("Cargo.toml");
280                    cargo::update_lock(&workspace_path)?;
281                }
282            }
283
284            for pkg in &selected_pkgs {
285                super::replace::replace(pkg, dry_run)?;
286
287                // pre-release hook
288                super::hook::hook(&ws_meta, pkg, dry_run)?;
289            }
290
291            super::commit::workspace_commit(&ws_meta, &ws_config, &selected_pkgs, dry_run)?;
292        } else {
293            for pkg in &selected_pkgs {
294                if let Some(version) = pkg.planned_version.as_ref() {
295                    let crate_name = pkg.meta.name.as_str();
296                    let _ = crate::ops::shell::status(
297                        "Upgrading",
298                        format!(
299                            "{} from {} to {}",
300                            crate_name,
301                            pkg.initial_version.full_version_string,
302                            version.full_version_string
303                        ),
304                    );
305                    cargo::set_package_version(
306                        &pkg.manifest_path,
307                        version.full_version_string.as_str(),
308                        dry_run,
309                    )?;
310                    crate::steps::version::update_dependent_versions(
311                        &ws_meta, pkg, version, dry_run,
312                    )?;
313                    if dry_run {
314                        log::debug!("updating lock file");
315                    } else {
316                        cargo::update_lock(&pkg.manifest_path)?;
317                    }
318                }
319
320                super::replace::replace(pkg, dry_run)?;
321
322                // pre-release hook
323                super::hook::hook(&ws_meta, pkg, dry_run)?;
324
325                super::commit::pkg_commit(pkg, dry_run)?;
326            }
327        }
328
329        // STEP 3: cargo publish
330        super::publish::publish(&selected_pkgs, dry_run)?;
331        super::owner::ensure_owners(&selected_pkgs, dry_run)?;
332
333        // STEP 5: Tag
334        super::tag::tag(&selected_pkgs, dry_run)?;
335
336        // STEP 6: git push
337        super::push::push(&ws_config, &ws_meta, &selected_pkgs, dry_run)?;
338
339        super::finish(failed, dry_run)
340    }
341}