diff --git a/.schema/latest.json b/.schema/latest.json index 8aa3e3e045..48bf0791de 100644 --- a/.schema/latest.json +++ b/.schema/latest.json @@ -53,6 +53,7 @@ "publish_no_verify": null, "publish_timeout": null, "release": null, + "release_commits": null, "repo_url": null, "semver_check": null }, @@ -569,6 +570,14 @@ "null" ] }, + "release_commits": { + "title": "Release Commits", + "description": "Prepare release only if at least one commit respects this regex.", + "type": [ + "string", + "null" + ] + }, "repo_url": { "title": "Repo URL", "description": "GitHub/Gitea repository url where your project is hosted. It is used to generate the changelog release link. It defaults to the url of the default remote.", diff --git a/crates/release_plz/src/args/update.rs b/crates/release_plz/src/args/update.rs index 571ff80d54..8b30603256 100644 --- a/crates/release_plz/src/args/update.rs +++ b/crates/release_plz/src/args/update.rs @@ -161,6 +161,9 @@ impl Update { if let Some(registry) = &self.registry { update = update.with_registry(registry.clone()); } + if let Some(release_commits) = config.workspace.release_commits { + update = update.with_release_commits(release_commits.clone())?; + } Ok(update) } diff --git a/crates/release_plz/src/config.rs b/crates/release_plz/src/config.rs index 9f0c45819c..4d435e03a9 100644 --- a/crates/release_plz/src/config.rs +++ b/crates/release_plz/src/config.rs @@ -123,6 +123,9 @@ pub struct Workspace { /// It is used to generate the changelog release link. /// It defaults to the url of the default remote. pub repo_url: Option, + /// # Release Commits + /// Prepare release only if at least one commit respects this regex. + pub release_commits: Option, } impl Workspace { @@ -361,6 +364,7 @@ mod tests { git_release_type = "prod" git_release_draft = false publish_timeout = "10m" + release_commits = "^feat:" "#; const BASE_PACKAGE_CONFIG: &str = r#" @@ -387,6 +391,7 @@ mod tests { pr_draft: false, pr_labels: vec![], publish_timeout: Some("10m".to_string()), + release_commits: Some("^feat:".to_string()), }, package: [].into(), } @@ -504,6 +509,7 @@ mod tests { ..Default::default() }, publish_timeout: Some("10m".to_string()), + release_commits: Some("^feat:".to_string()), }, package: [PackageSpecificConfigWithName { name: "crate1".to_string(), @@ -536,6 +542,7 @@ mod tests { pr_labels = ["label1"] publish_timeout = "10m" repo_url = "https://github.com/MarcoIeni/release-plz" + release_commits = "^feat:" [changelog] diff --git a/crates/release_plz/tests/all/changelog.rs b/crates/release_plz/tests/all/changelog.rs index 71e7b91976..a6ed930410 100644 --- a/crates/release_plz/tests/all/changelog.rs +++ b/crates/release_plz/tests/all/changelog.rs @@ -1,5 +1,32 @@ use crate::helpers::{test_context::TestContext, TEST_REGISTRY}; +#[tokio::test] +#[cfg_attr(not(feature = "docker-tests"), ignore)] +async fn release_plz_does_not_open_release_pr_if_there_are_no_release_commits() { + let context = TestContext::new().await; + + let config = r#" + [workspace] + release_commits = "^feat:" + "#; + context.write_release_plz_toml(config); + + context.run_release_pr().success(); + + let opened_prs = context.opened_release_prs().await; + // no features are present in the commits, so release-plz doesn't open the release PR + assert_eq!(opened_prs.len(), 0); + + std::fs::write(context.repo_dir().join("new.rs"), "// hi").unwrap(); + context.repo.add_all_and_commit("feat: new file").unwrap(); + + context.run_release_pr().success(); + + // we added a feature, so release-plz opened the release PR + let opened_prs = context.opened_release_prs().await; + assert_eq!(opened_prs.len(), 1); +} + #[tokio::test] #[cfg_attr(not(feature = "docker-tests"), ignore)] async fn release_plz_adds_changelog_on_new_project() { diff --git a/crates/release_plz_core/src/diff.rs b/crates/release_plz_core/src/diff.rs index 40f4b0e83a..0fe173cec6 100644 --- a/crates/release_plz_core/src/diff.rs +++ b/crates/release_plz_core/src/diff.rs @@ -1,4 +1,5 @@ use git_cliff_core::commit::Commit; +use regex::Regex; use crate::semver_check::SemverCheck; @@ -45,4 +46,41 @@ impl<'a> Diff<'a> { } } } + + /// Return `true` if any commit message matches the given pattern. + pub fn any_commit_matches(&self, pattern: &Regex) -> bool { + self.commits + .iter() + .any(|commit| pattern.is_match(&commit.message)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + pub fn create_diff() -> Diff<'static> { + let mut diff = Diff::new(false); + diff.add_commits(&vec![Commit::new( + "1e6903d".to_string(), + "feature release".to_string(), + )]); + diff + } + + #[test] + fn test_is_commit_message_matched() { + let diff = create_diff(); + let pattern = Regex::new(r"^feat").unwrap(); + let present = diff.any_commit_matches(&pattern); + assert!(present); + } + + #[test] + fn test_is_commit_message_not_matched() { + let diff = create_diff(); + let pattern = Regex::new(r"mismatch").unwrap(); + let present = diff.any_commit_matches(&pattern); + assert!(!present); + } } diff --git a/crates/release_plz_core/src/next_ver.rs b/crates/release_plz_core/src/next_ver.rs index 83daf986f2..2ab3f8c2b1 100644 --- a/crates/release_plz_core/src/next_ver.rs +++ b/crates/release_plz_core/src/next_ver.rs @@ -75,6 +75,9 @@ pub struct UpdateRequest { repo_url: Option, /// Package-specific configurations. packages_config: PackagesConfig, + /// Release Commits + /// Prepare release only if at least one commit respects a regex. + release_commits: Option, } #[derive(Debug, Clone, Default)] @@ -213,6 +216,7 @@ impl UpdateRequest { allow_dirty: false, repo_url: None, packages_config: PackagesConfig::default(), + release_commits: None, }) } @@ -296,6 +300,16 @@ impl UpdateRequest { } } + pub fn with_release_commits(self, release_commits: String) -> anyhow::Result { + let regex = + Regex::new(&release_commits).context("invalid release_commits regex pattern")?; + + Ok(Self { + release_commits: Some(regex), + ..self + }) + } + pub fn local_manifest_dir(&self) -> anyhow::Result<&Path> { self.local_manifest .parent() @@ -649,6 +663,11 @@ impl Updater<'_> { } for (p, diff) in packages_diffs { + if let Some(ref release_commits_regex) = self.req.release_commits { + if !diff.any_commit_matches(release_commits_regex) { + continue; + }; + } // Calculate next version without taking into account workspace version let next_version = if let Some(max_workspace_version) = &new_workspace_version { if workspace_version_pkgs.contains(p.name.as_str()) { diff --git a/website/docs/config.md b/website/docs/config.md index 4de8aaab9a..4b283b714f 100644 --- a/website/docs/config.md +++ b/website/docs/config.md @@ -25,6 +25,7 @@ pr_labels = ["release"] # add the `release` label to the release Pull Request publish_allow_dirty = true # add `--allow-dirty` to `cargo publish` semver_check = false # disable API breaking changes checks publish_timeout = "10m" # set a timeout for `cargo publish` +release_commits = "^feat:" # prepare release only if at least one commit matches a regex [[package]] # the double square brackets define a TOML table array name = "package_a" @@ -70,6 +71,7 @@ the following sections: - [`publish_no_verify`](#the-publish_no_verify-field) — Don't verify package build. - [`publish_timeout`](#the-publish_timeout-field) — `cargo publish` timeout. - [`release`](#the-release-field) - Enable the processing of the packages. + - [`release_commits`](#the-release_commits-field) - Customize which commits trigger a release. - [`repo_url`](#the-repo_url-field) — Repository URL. - [`semver_check`](#the-semver_check-field) — Run [cargo-semver-checks]. - [`[[package]]`](#the-package-section) — Package-specific configurations. @@ -332,6 +334,25 @@ Example: release = false ``` +#### The `release_commits` field + +In `release-plz update` and `release-plz release-pr`, `release-plz` bumps the version and updates the changelog +of the package only if at least one of the commits matches the `release_commits` regex. + +You can use this if you think it is too noisy to raise PRs on every commit. + +Examples: + +- With `release_commits = "^feat:"`, release-plz will update the package only if there's a new feature. +- With `release_commits = "^(feat:|docs:)"`, release-plz will update the package only if there's a new feature or a documentation change. + +By default, release-plz updates the package on every commit. + +:::warning +The filtered commits are still included in the changelog. +To exclude certain commits from the changelog, use the [commit_parsers](#the-commit_parsers-field) field. +::: + #### The `repo_url` field GitHub/Gitea repository URL where your project is hosted.