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 #[arg(long, conflicts_with = "level_or_version")]
17 unpublished: bool,
18
19 #[arg(value_name = "LEVEL|VERSION")]
21 level_or_version: Option<super::TargetVersion>,
22
23 #[arg(short, long, requires = "level_or_version")]
25 metadata: Option<String>,
26
27 #[arg(short = 'x', long)]
29 execute: bool,
30
31 #[arg(short = 'n', long, conflicts_with = "execute", hide = true)]
32 dry_run: bool,
33
34 #[arg(long)]
36 no_confirm: bool,
37
38 #[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 .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 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 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 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 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 continue;
159 };
160
161 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 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 super::confirm("Release", &selected_pkgs, self.no_confirm, dry_run)?;
271
272 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 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 super::hook::hook(&ws_meta, pkg, dry_run)?;
324
325 super::commit::pkg_commit(pkg, dry_run)?;
326 }
327 }
328
329 super::publish::publish(&selected_pkgs, dry_run)?;
331 super::owner::ensure_owners(&selected_pkgs, dry_run)?;
332
333 super::tag::tag(&selected_pkgs, dry_run)?;
335
336 super::push::push(&ws_config, &ws_meta, &selected_pkgs, dry_run)?;
338
339 super::finish(failed, dry_run)
340 }
341}