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                if let Some(level_or_version) = &self.level_or_version {
73                    pkg.bump(level_or_version, self.metadata.as_deref())?;
74                }
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) = self.workspace.partition_packages(&ws_meta);
87        for excluded_pkg in &excluded_pkgs {
88            let Some(pkg) = pkgs.get_mut(&excluded_pkg.id) else {
89                // Either not in workspace or marked as `release = false`.
90                continue;
91            };
92            if !pkg.config.release() {
93                continue;
94            }
95
96            let crate_name = pkg.meta.name.as_str();
97            let explicitly_excluded = self.workspace.exclude.contains(&excluded_pkg.name);
98            // 1. Don't show this message if already not releasing in config
99            // 2. Still respect `--exclude`
100            if pkg.config.release()
101                && pkg.config.publish()
102                && self.unpublished
103                && !explicitly_excluded
104            {
105                let version = &pkg.initial_version;
106                if !cargo::is_published(
107                    &mut index,
108                    pkg.config.registry(),
109                    crate_name,
110                    &version.full_version_string,
111                    pkg.config.certs_source(),
112                ) {
113                    log::debug!(
114                        "enabled {}, v{} is unpublished",
115                        crate_name,
116                        version.full_version_string
117                    );
118                    continue;
119                }
120            }
121
122            pkg.planned_version = None;
123            pkg.config.release = Some(false);
124
125            if let Some(prior_tag_name) = &pkg.prior_tag {
126                if let Some(changed) =
127                    crate::steps::version::changed_since(&ws_meta, pkg, prior_tag_name)
128                {
129                    if !changed.is_empty() {
130                        let _ = crate::ops::shell::warn(format!(
131                            "disabled by user, skipping {crate_name} which has files changed since {prior_tag_name}: {changed:#?}"
132                        ));
133                    } else {
134                        log::trace!(
135                            "disabled by user, skipping {} (no changes since {})",
136                            crate_name,
137                            prior_tag_name
138                        );
139                    }
140                } else {
141                    log::debug!(
142                        "disabled by user, skipping {} (no {} tag)",
143                        crate_name,
144                        prior_tag_name
145                    );
146                }
147            } else {
148                log::debug!("disabled by user, skipping {} (no tag found)", crate_name,);
149            }
150        }
151
152        let pkgs = plan::plan(pkgs)?;
153
154        for excluded_pkg in &excluded_pkgs {
155            let Some(pkg) = pkgs.get(&excluded_pkg.id) else {
156                // Either not in workspace or marked as `release = false`.
157                continue;
158            };
159
160            // HACK: `index` only supports default registry
161            if pkg.config.publish() && pkg.config.registry().is_none() {
162                let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
163                let crate_name = pkg.meta.name.as_str();
164                if !cargo::is_published(
165                    &mut index,
166                    pkg.config.registry(),
167                    crate_name,
168                    &version.full_version_string,
169                    pkg.config.certs_source(),
170                ) {
171                    let _ = crate::ops::shell::warn(format!(
172                        "disabled by user, skipping {} v{} despite being unpublished",
173                        crate_name, version.full_version_string,
174                    ));
175                }
176            }
177        }
178
179        let (selected_pkgs, excluded_pkgs): (Vec<_>, Vec<_>) = pkgs
180            .into_iter()
181            .map(|(_, pkg)| pkg)
182            .partition(|p| p.config.release());
183        if selected_pkgs.is_empty() {
184            let _ = crate::ops::shell::error("no packages selected");
185            return Err(2.into());
186        }
187
188        let dry_run = !self.execute;
189        let mut failed = false;
190
191        let consolidate_commits = super::consolidate_commits(&selected_pkgs, &excluded_pkgs)?;
192        ws_config.consolidate_commits = Some(consolidate_commits);
193
194        // STEP 0: Help the user make the right decisions.
195        failed |= !super::verify_git_is_clean(
196            ws_meta.workspace_root.as_std_path(),
197            dry_run,
198            log::Level::Error,
199        )?;
200
201        failed |= !super::verify_tags_missing(&selected_pkgs, dry_run, log::Level::Error)?;
202
203        failed |=
204            !super::verify_monotonically_increasing(&selected_pkgs, dry_run, log::Level::Error)?;
205
206        let mut double_publish = false;
207        for pkg in &selected_pkgs {
208            if !pkg.config.publish() {
209                continue;
210            }
211            let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
212            let crate_name = pkg.meta.name.as_str();
213            if cargo::is_published(
214                &mut index,
215                pkg.config.registry(),
216                crate_name,
217                &version.full_version_string,
218                pkg.config.certs_source(),
219            ) {
220                let _ = crate::ops::shell::error(format!(
221                    "{} {} is already published",
222                    crate_name, version.full_version_string
223                ));
224                double_publish = true;
225            }
226        }
227        if double_publish {
228            failed = true;
229            if !dry_run {
230                return Err(101.into());
231            }
232        }
233
234        super::warn_changed(&ws_meta, &selected_pkgs)?;
235
236        failed |= !super::verify_git_branch(
237            ws_meta.workspace_root.as_std_path(),
238            &ws_config,
239            dry_run,
240            log::Level::Error,
241        )?;
242
243        failed |= !super::verify_if_behind(
244            ws_meta.workspace_root.as_std_path(),
245            &ws_config,
246            dry_run,
247            log::Level::Warn,
248        )?;
249
250        failed |= !super::verify_metadata(&selected_pkgs, dry_run, log::Level::Error)?;
251        failed |= !super::verify_rate_limit(
252            &selected_pkgs,
253            &mut index,
254            &ws_config.rate_limit,
255            dry_run,
256            log::Level::Error,
257        )?;
258
259        // STEP 1: Release Confirmation
260        super::confirm("Release", &selected_pkgs, self.no_confirm, dry_run)?;
261
262        // STEP 2: update current version, save and commit
263        if consolidate_commits {
264            let update_lock =
265                super::version::update_versions(&ws_meta, &selected_pkgs, &excluded_pkgs, dry_run)?;
266            if update_lock {
267                log::debug!("updating lock file");
268                if !dry_run {
269                    let workspace_path = ws_meta.workspace_root.as_std_path().join("Cargo.toml");
270                    cargo::update_lock(&workspace_path)?;
271                }
272            }
273
274            for pkg in &selected_pkgs {
275                super::replace::replace(pkg, dry_run)?;
276
277                // pre-release hook
278                super::hook::hook(&ws_meta, pkg, dry_run)?;
279            }
280
281            super::commit::workspace_commit(&ws_meta, &ws_config, &selected_pkgs, dry_run)?;
282        } else {
283            for pkg in &selected_pkgs {
284                if let Some(version) = pkg.planned_version.as_ref() {
285                    let crate_name = pkg.meta.name.as_str();
286                    let _ = crate::ops::shell::status(
287                        "Upgrading",
288                        format!(
289                            "{} from {} to {}",
290                            crate_name,
291                            pkg.initial_version.full_version_string,
292                            version.full_version_string
293                        ),
294                    );
295                    cargo::set_package_version(
296                        &pkg.manifest_path,
297                        version.full_version_string.as_str(),
298                        dry_run,
299                    )?;
300                    crate::steps::version::update_dependent_versions(
301                        &ws_meta, pkg, version, dry_run,
302                    )?;
303                    if dry_run {
304                        log::debug!("updating lock file");
305                    } else {
306                        cargo::update_lock(&pkg.manifest_path)?;
307                    }
308                }
309
310                super::replace::replace(pkg, dry_run)?;
311
312                // pre-release hook
313                super::hook::hook(&ws_meta, pkg, dry_run)?;
314
315                super::commit::pkg_commit(pkg, dry_run)?;
316            }
317        }
318
319        // STEP 3: cargo publish
320        super::publish::publish(&selected_pkgs, dry_run, &ws_config.unstable)?;
321        super::owner::ensure_owners(&selected_pkgs, dry_run)?;
322
323        // STEP 5: Tag
324        super::tag::tag(&selected_pkgs, dry_run)?;
325
326        // STEP 6: git push
327        super::push::push(&ws_config, &ws_meta, &selected_pkgs, dry_run)?;
328
329        super::finish(failed, dry_run)
330    }
331}