cargo_release/steps/
hook.rs

1use std::ffi::OsStr;
2use std::path::Path;
3
4use crate::error::CliError;
5use crate::ops::cmd;
6use crate::ops::git;
7use crate::ops::replace::{NOW, Template};
8use crate::steps::plan;
9
10/// Run pre-release hooks
11#[derive(Debug, Clone, clap::Args)]
12pub struct HookStep {
13    #[command(flatten)]
14    manifest: clap_cargo::Manifest,
15
16    #[command(flatten)]
17    workspace: clap_cargo::Workspace,
18
19    /// Process all packages whose current version is unpublished
20    #[arg(long)]
21    unpublished: bool,
22
23    /// Custom config file
24    #[arg(short, long = "config", value_name = "PATH")]
25    custom_config: Option<std::path::PathBuf>,
26
27    /// Ignore implicit configuration files.
28    #[arg(long)]
29    isolated: bool,
30
31    /// Unstable options
32    #[arg(short = 'Z', value_name = "FEATURE")]
33    z: Vec<crate::config::UnstableValues>,
34
35    /// Comma-separated globs of branch names a release can happen from
36    #[arg(long, value_delimiter = ',')]
37    allow_branch: Option<Vec<String>>,
38
39    /// Actually perform a release. Dry-run mode is the default
40    #[arg(short = 'x', long)]
41    execute: bool,
42
43    #[arg(short = 'n', long, conflicts_with = "execute", hide = true)]
44    dry_run: bool,
45
46    /// Skip release confirmation and version preview
47    #[arg(long)]
48    no_confirm: bool,
49}
50
51impl HookStep {
52    pub fn run(&self) -> Result<(), CliError> {
53        git::git_version()?;
54        let mut index = crate::ops::index::CratesIoIndex::new();
55
56        if self.dry_run {
57            let _ =
58                crate::ops::shell::warn("`--dry-run` is superfluous, dry-run is done by default");
59        }
60
61        let ws_meta = self
62            .manifest
63            .metadata()
64            // When evaluating dependency ordering, we need to consider optional dependencies
65            .features(cargo_metadata::CargoOpt::AllFeatures)
66            .exec()?;
67        let config = self.to_config();
68        let ws_config = crate::config::load_workspace_config(&config, &ws_meta)?;
69        let mut pkgs = plan::load(&config, &ws_meta)?;
70
71        let (_selected_pkgs, excluded_pkgs) = self.workspace.partition_packages(&ws_meta);
72        for excluded_pkg in excluded_pkgs {
73            let Some(pkg) = pkgs.get_mut(&excluded_pkg.id) else {
74                // Either not in workspace or marked as `release = false`.
75                continue;
76            };
77            if !pkg.config.release() {
78                continue;
79            }
80
81            let crate_name = pkg.meta.name.as_str();
82            let explicitly_excluded = self.workspace.exclude.contains(&excluded_pkg.name);
83            // 1. Don't show this message if already not releasing in config
84            // 2. Still respect `--exclude`
85            if pkg.config.release()
86                && pkg.config.publish()
87                && self.unpublished
88                && !explicitly_excluded
89            {
90                let version = &pkg.initial_version;
91                if !crate::ops::cargo::is_published(
92                    &mut index,
93                    pkg.config.registry(),
94                    crate_name,
95                    &version.full_version_string,
96                    pkg.config.certs_source(),
97                ) {
98                    log::debug!(
99                        "enabled {}, v{} is unpublished",
100                        crate_name,
101                        version.full_version_string
102                    );
103                    continue;
104                }
105            }
106
107            pkg.config.pre_release_replacements = Some(vec![]);
108            pkg.config.release = Some(false);
109        }
110
111        let pkgs = plan::plan(pkgs)?;
112
113        let (selected_pkgs, _excluded_pkgs): (Vec<_>, Vec<_>) = pkgs
114            .into_iter()
115            .map(|(_, pkg)| pkg)
116            .partition(|p| p.config.release());
117        if selected_pkgs.is_empty() {
118            let _ = crate::ops::shell::error("no packages selected");
119            return Err(2.into());
120        }
121
122        let dry_run = !self.execute;
123        let mut failed = false;
124
125        // STEP 0: Help the user make the right decisions.
126        failed |= !super::verify_git_is_clean(
127            ws_meta.workspace_root.as_std_path(),
128            dry_run,
129            log::Level::Warn,
130        )?;
131
132        super::warn_changed(&ws_meta, &selected_pkgs)?;
133
134        failed |= !super::verify_git_branch(
135            ws_meta.workspace_root.as_std_path(),
136            &ws_config,
137            dry_run,
138            log::Level::Warn,
139        )?;
140
141        failed |= !super::verify_if_behind(
142            ws_meta.workspace_root.as_std_path(),
143            &ws_config,
144            dry_run,
145            log::Level::Warn,
146        )?;
147
148        // STEP 1: Release Confirmation
149        super::confirm("Bump", &selected_pkgs, self.no_confirm, dry_run)?;
150
151        // STEP 2: update current version, save and commit
152        for pkg in &selected_pkgs {
153            hook(&ws_meta, pkg, dry_run)?;
154        }
155
156        super::finish(failed, dry_run)
157    }
158
159    fn to_config(&self) -> crate::config::ConfigArgs {
160        crate::config::ConfigArgs {
161            custom_config: self.custom_config.clone(),
162            isolated: self.isolated,
163            z: self.z.clone(),
164            allow_branch: self.allow_branch.clone(),
165            ..Default::default()
166        }
167    }
168}
169
170pub fn hook(
171    ws_meta: &cargo_metadata::Metadata,
172    pkg: &plan::PackageRelease,
173    dry_run: bool,
174) -> Result<(), CliError> {
175    if let Some(pre_rel_hook) = pkg.config.pre_release_hook() {
176        let cwd = &pkg.package_root;
177        let crate_name = pkg.meta.name.as_str();
178        let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
179        let prev_version_var = pkg.initial_version.bare_version_string.as_str();
180        let prev_metadata_var = pkg.initial_version.full_version.build.as_str();
181        let version_var = version.bare_version_string.as_str();
182        let metadata_var = version.full_version.build.as_str();
183        let template = Template {
184            prev_version: Some(prev_version_var),
185            prev_metadata: Some(prev_metadata_var),
186            version: Some(version_var),
187            metadata: Some(metadata_var),
188            crate_name: Some(crate_name),
189            date: Some(NOW.as_str()),
190            tag_name: pkg.planned_tag.as_deref(),
191            ..Default::default()
192        };
193        let pre_rel_hook = pre_rel_hook
194            .args()
195            .into_iter()
196            .map(|arg| template.render(arg))
197            .collect::<Vec<_>>();
198        log::debug!("calling pre-release hook: {:?}", pre_rel_hook);
199        let envs = maplit::btreemap! {
200            OsStr::new("PREV_VERSION") => prev_version_var.as_ref(),
201            OsStr::new("PREV_METADATA") => prev_metadata_var.as_ref(),
202            OsStr::new("NEW_VERSION") => version_var.as_ref(),
203            OsStr::new("NEW_METADATA") => metadata_var.as_ref(),
204            OsStr::new("DRY_RUN") => OsStr::new(if dry_run { "true" } else { "false" }),
205            OsStr::new("CRATE_NAME") => OsStr::new(crate_name),
206            OsStr::new("WORKSPACE_ROOT") => ws_meta.workspace_root.as_os_str(),
207            OsStr::new("CRATE_ROOT") => pkg.manifest_path.parent().unwrap_or_else(|| Path::new(".")).as_os_str(),
208        };
209        // we use dry_run environmental variable to run the script
210        // so here we set dry_run=false and always execute the command.
211        if !cmd::call_with_env(pre_rel_hook, envs, cwd, false)? {
212            let _ = crate::ops::shell::error(format!(
213                "release of {crate_name} aborted by non-zero return of prerelease hook."
214            ));
215            return Err(101.into());
216        }
217    }
218
219    Ok(())
220}