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 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 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 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 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 continue;
158 };
159
160 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 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 super::confirm("Release", &selected_pkgs, self.no_confirm, dry_run)?;
261
262 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 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 super::hook::hook(&ws_meta, pkg, dry_run)?;
314
315 super::commit::pkg_commit(pkg, dry_run)?;
316 }
317 }
318
319 super::publish::publish(&selected_pkgs, dry_run, &ws_config.unstable)?;
321 super::owner::ensure_owners(&selected_pkgs, dry_run)?;
322
323 super::tag::tag(&selected_pkgs, dry_run)?;
325
326 super::push::push(&ws_config, &ws_meta, &selected_pkgs, dry_run)?;
328
329 super::finish(failed, dry_run)
330 }
331}