use release_plz_core::fs_utils::Utf8TempDir;
use crate::helpers::{test_context::TestContext, today};
#[tokio::test]
#[cfg_attr(not(feature = "docker-tests"), ignore)]
async fn git_only_with_default_tag_name() {
let context = TestContext::new().await;
let config = r#"
[workspace]
git_only = true
"#;
context.write_release_plz_toml(config);
context.repo.tag("v0.1.0", "Release v0.1.0").unwrap();
let readme = context.repo_dir().join("README.md");
fs_err::write(&readme, "# Updated README").unwrap();
context.push_all_changes("fix: update readme");
context.run_release_pr().success();
let opened_prs = context.opened_release_prs().await;
assert_eq!(opened_prs.len(), 1);
assert_eq!(opened_prs[0].title, "chore: release v0.1.1");
}
#[tokio::test]
#[cfg_attr(not(feature = "docker-tests"), ignore)]
async fn git_only_with_custom_tag_name() {
let context = TestContext::new().await;
let config = r#"
[workspace]
git_only = true
git_tag_name = "release-{{ version }}-prod"
"#;
context.write_release_plz_toml(config);
context
.repo
.tag("release-0.1.0-prod", "Release 0.1.0 production")
.unwrap();
let new_file = context.repo_dir().join("src").join("new.rs");
fs_err::write(&new_file, "// New feature").unwrap();
context.push_all_changes("feat: add new module");
context.run_release_pr().success();
let opened_prs = context.opened_release_prs().await;
assert_eq!(opened_prs.len(), 1);
assert_eq!(opened_prs[0].title, "chore: release v0.1.1");
}
#[tokio::test]
#[cfg_attr(not(feature = "docker-tests"), ignore)]
async fn git_only_finds_highest_version_tag() {
let context = TestContext::new().await;
let config = r#"
[workspace]
git_only = true
"#;
context.write_release_plz_toml(config);
use cargo_metadata::semver::Version;
context.repo.tag("v0.1.0", "Release v0.1.0").unwrap();
context.set_package_version(&context.gitea.repo, &Version::parse("0.1.5").unwrap());
context.push_all_changes("chore: bump version to 0.1.5");
context.repo.tag("v0.1.5", "Release v0.1.5").unwrap();
context.set_package_version(&context.gitea.repo, &Version::parse("0.2.0").unwrap());
context.push_all_changes("chore: bump version to 0.2.0");
context.repo.tag("v0.2.0", "Release v0.2.0").unwrap();
let readme = context.repo_dir().join("README.md");
fs_err::write(&readme, "# Updated README").unwrap();
context.push_all_changes("fix: update readme");
context.run_release_pr().success();
let opened_prs = context.opened_release_prs().await;
assert_eq!(opened_prs.len(), 1);
assert_eq!(opened_prs[0].title, "chore: release v0.2.1");
}
#[tokio::test]
#[cfg_attr(not(feature = "docker-tests"), ignore)]
async fn git_only_ignores_non_matching_tags() {
let context = TestContext::new().await;
let config = r#"
[workspace]
git_only = true
"#;
context.write_release_plz_toml(config);
context.repo.tag("v0.1.0", "Release v0.1.0").unwrap();
context.repo.tag("release-0.2.0", "Release 0.2.0").unwrap();
context.repo.tag("beta-0.3.0", "Beta 0.3.0").unwrap();
let readme = context.repo_dir().join("README.md");
fs_err::write(&readme, "# Updated README").unwrap();
context.push_all_changes("fix: update readme");
context.run_release_pr().success();
let opened_prs = context.opened_release_prs().await;
assert_eq!(opened_prs.len(), 1);
assert_eq!(opened_prs[0].title, "chore: release v0.1.1");
}
#[tokio::test]
#[cfg_attr(not(feature = "docker-tests"), ignore)]
async fn git_only_no_matching_tag_creates_initial_release() {
let context = TestContext::new().await;
let config = r#"
[workspace]
git_only = true
"#;
context.write_release_plz_toml(config);
context.repo.tag("release-0.1.0", "Release 0.1.0").unwrap();
let readme = context.repo_dir().join("README.md");
fs_err::write(&readme, "# Updated README").unwrap();
context.push_all_changes("fix: update readme");
let _outcome = context.run_release_pr().success();
let opened_prs = context.opened_release_prs().await;
assert_eq!(
opened_prs.len(),
1,
"Expected PR for initial release when no matching tag exists"
);
let pr = &opened_prs[0];
assert_eq!(pr.title, "chore: release v0.1.0");
}
#[tokio::test]
#[cfg_attr(not(feature = "docker-tests"), ignore)]
async fn git_only_no_tags_creates_initial_release() {
let context = TestContext::new().await;
let config = r#"
[workspace]
git_only = true
"#;
context.write_release_plz_toml(config);
let readme = context.repo_dir().join("README.md");
fs_err::write(&readme, "# Updated README").unwrap();
context.push_all_changes("fix: update readme");
let _outcome = context.run_release_pr().success();
let opened_prs = context.opened_release_prs().await;
assert_eq!(
opened_prs.len(),
1,
"Expected PR for initial release when no tags exist"
);
let pr = &opened_prs[0];
assert_eq!(pr.title, "chore: release v0.1.0");
}
#[tokio::test]
#[cfg_attr(not(feature = "docker-tests"), ignore)]
async fn git_only_feat_commit_minor_bump() {
let context = TestContext::new().await;
let config = r#"
[workspace]
git_only = true
features_always_increment_minor = true
"#;
context.write_release_plz_toml(config);
context.repo.tag("v0.1.0", "Release v0.1.0").unwrap();
let readme = context.repo_dir().join("README.md");
fs_err::write(&readme, "# Fix 1").unwrap();
context.push_all_changes("fix: first fix");
let new_file = context.repo_dir().join("src").join("feature.rs");
fs_err::write(&new_file, "// New feature").unwrap();
context.push_all_changes("feat: add new feature");
context.run_release_pr().success();
let opened_prs = context.opened_release_prs().await;
assert_eq!(opened_prs.len(), 1);
assert_eq!(opened_prs[0].title, "chore: release v0.2.0");
}
#[tokio::test]
#[cfg_attr(not(feature = "docker-tests"), ignore)]
async fn git_only_respects_release_commits_regex() {
let context = TestContext::new().await;
let config = r#"
[workspace]
git_only = true
release_commits = "^feat:"
"#;
context.write_release_plz_toml(config);
context.repo.tag("v0.1.0", "Release v0.1.0").unwrap();
let readme = context.repo_dir().join("README.md");
fs_err::write(&readme, "# Fixed README").unwrap();
context.push_all_changes("fix: correct readme");
let outcome = context.run_release_pr().success();
outcome.stdout("{\"prs\":[]}\n");
let opened_prs = context.opened_release_prs().await;
assert_eq!(opened_prs.len(), 0);
let new_file = context.repo_dir().join("src").join("feature.rs");
fs_err::write(&new_file, "// New feature").unwrap();
context.push_all_changes("feat: add new feature");
context.run_release_pr().success();
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 git_only_workspace_level_applies_to_all() {
let context = TestContext::new_workspace(&["lib1", "lib2"]).await;
let config = r#"
[workspace]
git_only = true
git_tag_name = "{{ package }}-v{{ version }}"
"#;
context.write_release_plz_toml(config);
context
.repo
.tag("lib1-v0.1.0", "Release lib1 v0.1.0")
.unwrap();
context
.repo
.tag("lib2-v0.1.0", "Release lib2 v0.1.0")
.unwrap();
let lib1_file = context.package_path("lib1").join("src").join("main.rs");
fs_err::write(&lib1_file, "fn main() { println!(\"updated\"); }").unwrap();
context.push_all_changes("feat: update lib1");
context.run_release_pr().success();
let opened_prs = context.opened_release_prs().await;
assert_eq!(opened_prs.len(), 1);
let pr_body = opened_prs[0].body.as_ref().unwrap();
let today = today();
let username = context.gitea.user.username();
let repo = &context.gitea.repo;
assert_eq!(
format!(
r"
## 🤖 New release
* `lib1`: 0.1.0 -> 0.1.1
<details><summary><i><b>Changelog</b></i></summary><p>
<blockquote>
## [0.1.1](https://localhost/{username}/{repo}/compare/lib1-v0.1.0...lib1-v0.1.1) - {today}
### Added
- update lib1
</blockquote>
</p></details>
---
This PR was generated with [release-plz](https://github.com/release-plz/release-plz/)."
)
.trim(),
pr_body.trim()
);
}
#[tokio::test]
#[cfg_attr(not(feature = "docker-tests"), ignore)]
async fn git_only_per_package_prefix() {
let context = TestContext::new_workspace(&["api", "core"]).await;
let config = r#"
[workspace]
git_only = true
[[package]]
name = "api"
git_tag_name = "api-v{{ version }}"
"#;
context.write_release_plz_toml(config);
context
.repo
.tag("api-v0.1.0", "Release api v0.1.0")
.unwrap();
context
.repo
.tag("core-v0.1.0", "Release core v0.1.0")
.unwrap();
let api_file = context.package_path("api").join("src").join("lib.rs");
fs_err::write(&api_file, "pub fn api_updated() {}").unwrap();
context.push_all_changes("feat: update api");
context.run_release_pr().success();
let opened_prs = context.opened_release_prs().await;
assert_eq!(opened_prs.len(), 1);
let pr_body = opened_prs[0].body.as_ref().expect("PR should have body");
let today = today();
let username = context.gitea.user.username();
let repo = &context.gitea.repo;
assert_eq!(
format!(
r"
## 🤖 New release
* `api`: 0.1.0 -> 0.1.1 (✓ API compatible changes)
<details><summary><i><b>Changelog</b></i></summary><p>
<blockquote>
## [0.1.1](https://localhost/{username}/{repo}/compare/api-v0.1.0...api-v0.1.1) - {today}
### Added
- update api
</blockquote>
</p></details>
---
This PR was generated with [release-plz](https://github.com/release-plz/release-plz/)."
)
.trim(),
pr_body.trim()
);
}
#[tokio::test]
#[cfg_attr(not(feature = "docker-tests"), ignore)]
async fn git_only_with_lightweight_tags() {
let context = TestContext::new().await;
let config = r#"
[workspace]
git_only = true
"#;
context.write_release_plz_toml(config);
context.repo.tag_lightweight("v0.1.0").unwrap();
let readme = context.repo_dir().join("README.md");
fs_err::write(&readme, "# Updated README").unwrap();
context.push_all_changes("fix: update readme");
context.run_release_pr().success();
let opened_prs = context.opened_release_prs().await;
assert_eq!(opened_prs.len(), 1);
assert_eq!(opened_prs[0].title, "chore: release v0.1.1");
}
#[tokio::test]
#[cfg_attr(not(feature = "docker-tests"), ignore)]
async fn git_only_mixed_annotated_and_lightweight_tags() {
use cargo_metadata::semver::Version;
let context = TestContext::new().await;
let config = r#"
[workspace]
git_only = true
"#;
context.write_release_plz_toml(config);
context.repo.tag_lightweight("v0.1.0").unwrap();
context.set_package_version(&context.gitea.repo, &Version::parse("0.1.5").unwrap());
context.push_all_changes("chore: bump version to 0.1.5");
context.repo.tag("v0.1.5", "Release v0.1.5").unwrap();
let readme = context.repo_dir().join("README.md");
fs_err::write(&readme, "# Updated README").unwrap();
context.push_all_changes("fix: update readme");
context.run_release_pr().success();
let opened_prs = context.opened_release_prs().await;
assert_eq!(opened_prs.len(), 1);
assert_eq!(opened_prs[0].title, "chore: release v0.1.6");
}
#[tokio::test]
#[cfg_attr(not(feature = "docker-tests"), ignore)]
async fn git_only_invalid_tag_format() {
let context = TestContext::new().await;
let config = r#"
[workspace]
git_only = true
"#;
context.write_release_plz_toml(config);
context.repo.tag("v0.1.0", "Release v0.1.0").unwrap();
context.repo.tag("v1.2.invalid", "Invalid version").unwrap();
context.repo.tag("vNOT_A_VERSION", "Not a version").unwrap();
let readme = context.repo_dir().join("README.md");
fs_err::write(&readme, "# Updated README").unwrap();
context.push_all_changes("fix: update readme");
context.run_release_pr().success();
let opened_prs = context.opened_release_prs().await;
assert_eq!(opened_prs.len(), 1);
assert_eq!(opened_prs[0].title, "chore: release v0.1.1");
}
#[tokio::test]
#[cfg_attr(not(feature = "docker-tests"), ignore)]
async fn git_only_breaking_change() {
let context = TestContext::new().await;
let config = r#"
[workspace]
git_only = true
features_always_increment_minor = true
"#;
context.write_release_plz_toml(config);
context.repo.tag("v0.1.0", "Release v0.1.0").unwrap();
let readme = context.repo_dir().join("README.md");
fs_err::write(&readme, "# Breaking change").unwrap();
context.push_all_changes("feat!: breaking API change");
context.run_release_pr().success();
let opened_prs = context.opened_release_prs().await;
assert_eq!(opened_prs.len(), 1);
assert_eq!(opened_prs[0].title, "chore: release v0.2.0");
}
#[tokio::test]
#[cfg_attr(not(feature = "docker-tests"), ignore)]
async fn git_only_multiple_packages_changed_workspace() {
let context = TestContext::new_workspace(&["pkg1", "pkg2", "pkg3"]).await;
let config = r#"
[workspace]
git_only = true
git_tag_name = "{{ package }}-v{{ version }}"
"#;
context.write_release_plz_toml(config);
context
.repo
.tag("pkg1-v0.1.0", "Release pkg1 v0.1.0")
.unwrap();
context
.repo
.tag("pkg2-v0.1.0", "Release pkg2 v0.1.0")
.unwrap();
context
.repo
.tag("pkg3-v0.1.0", "Release pkg3 v0.1.0")
.unwrap();
let pkg1_file = context.package_path("pkg1").join("src").join("main.rs");
fs_err::write(&pkg1_file, "fn main() { println!(\"pkg1 updated\"); }").unwrap();
let pkg2_file = context.package_path("pkg2").join("src").join("main.rs");
fs_err::write(&pkg2_file, "fn main() { println!(\"pkg2 updated\"); }").unwrap();
context.push_all_changes("feat: update pkg1 and pkg2");
context.run_release_pr().success();
let opened_prs = context.opened_release_prs().await;
assert_eq!(opened_prs.len(), 1);
let today = today();
let username = context.gitea.user.username();
let repo = &context.gitea.repo;
let pr_body = opened_prs[0].body.as_ref().unwrap();
assert_eq!(
format!(
"
## 🤖 New release
* `pkg1`: 0.1.0 -> 0.1.1
* `pkg2`: 0.1.0 -> 0.1.1
<details><summary><i><b>Changelog</b></i></summary><p>
## `pkg1`
<blockquote>
## [0.1.1](https://localhost/{username}/{repo}/compare/pkg1-v0.1.0...pkg1-v0.1.1) - {today}
### Added
- update pkg1 and pkg2
</blockquote>
## `pkg2`
<blockquote>
## [0.1.1](https://localhost/{username}/{repo}/compare/pkg2-v0.1.0...pkg2-v0.1.1) - {today}
### Added
- update pkg1 and pkg2
</blockquote>
</p></details>
---
This PR was generated with [release-plz](https://github.com/release-plz/release-plz/)."
)
.trim(),
pr_body.trim()
);
}
#[tokio::test]
#[cfg_attr(not(feature = "docker-tests"), ignore)]
async fn git_only_with_publish_enabled_fails_validation() {
let context = TestContext::new().await;
let config = r#"
[workspace]
git_only = true
publish = true
"#;
context.write_release_plz_toml(config);
context.repo.tag("v0.1.0", "Release v0.1.0").unwrap();
let readme = context.repo_dir().join("README.md");
fs_err::write(&readme, "# Updated README").unwrap();
context.push_all_changes("fix: update readme");
let error = context.run_release_pr().failure().to_string();
assert!(
error.contains("git_only")
&& error.contains("publish")
&& error.contains("mutually exclusive"),
"Expected validation error about git_only and publish being mutually exclusive, got: {error}"
);
}
#[tokio::test]
#[cfg_attr(not(feature = "docker-tests"), ignore)]
async fn git_only_with_publish_disabled_succeeds() {
let context = TestContext::new().await;
let config = r#"
[workspace]
git_only = true
publish = false
"#;
context.write_release_plz_toml(config);
context.repo.tag("v0.1.0", "Release v0.1.0").unwrap();
let readme = context.repo_dir().join("README.md");
fs_err::write(&readme, "# Updated README").unwrap();
context.push_all_changes("fix: update readme");
context.run_release_pr().success();
let opened_prs = context.opened_release_prs().await;
assert_eq!(opened_prs.len(), 1);
assert_eq!(opened_prs[0].title, "chore: release v0.1.1");
}
#[tokio::test]
#[cfg_attr(not(feature = "docker-tests"), ignore)]
async fn git_only_workspace_with_package_publish_enabled_fails() {
let context = TestContext::new_workspace(&["pkg-a", "pkg-b"]).await;
let config = r#"
[workspace]
git_only = true
[[package]]
name = "pkg-a"
publish = true
"#;
context.write_release_plz_toml(config);
context.repo.tag("v0.1.0", "Release v0.1.0").unwrap();
let readme = context.package_path("pkg-a").join("README.md");
fs_err::write(&readme, "# Updated README").unwrap();
context.push_all_changes("fix: update pkg-a readme");
let error = context.run_release_pr().failure().to_string();
assert!(
error.contains("git_only")
&& error.contains("publish")
&& error.contains("mutually exclusive"),
"Expected validation error about git_only and publish being mutually exclusive, got: {error}",
);
}
#[tokio::test]
#[cfg_attr(not(feature = "docker-tests"), ignore)]
async fn git_only_with_publish_enabled_fails_validation_release_cmd() {
let context = TestContext::new().await;
let config = r#"
[workspace]
git_only = true
publish = true
"#;
context.write_release_plz_toml(config);
context.repo.tag("v0.1.0", "Release v0.1.0").unwrap();
let readme = context.repo_dir().join("README.md");
fs_err::write(&readme, "# Updated README").unwrap();
context.push_all_changes("fix: update readme");
let error = context.run_release().failure().to_string();
assert!(
error.contains("git_only")
&& error.contains("publish")
&& error.contains("mutually exclusive"),
"Expected validation error about git_only and publish being mutually exclusive, got: {error}"
);
}
#[tokio::test]
#[cfg_attr(not(feature = "docker-tests"), ignore)]
async fn git_only_release_creates_tag() {
use cargo_metadata::semver::Version;
let context = TestContext::new().await;
let config = r#"
[workspace]
git_only = true
publish = false
"#;
context.write_release_plz_toml(config);
context.repo.tag("v0.1.0", "Release v0.1.0").unwrap();
context.set_package_version(&context.gitea.repo, &Version::parse("0.1.1").unwrap());
context.run_cargo_check();
context.push_all_changes("fix: bug fix");
let crate_name = &context.gitea.repo;
let expected_tag = "v0.1.1";
let is_tag_created = || {
context.repo.git(&["fetch", "--tags"]).unwrap();
context.repo.tag_exists(expected_tag).unwrap()
};
assert!(!is_tag_created(), "Tag should not exist before release");
let outcome = context.run_release().success();
let expected_stdout = serde_json::json!({
"releases": [
{
"package_name": crate_name,
"prs": [],
"tag": expected_tag,
"version": "0.1.1",
}
]
})
.to_string();
outcome.stdout(format!("{expected_stdout}\n"));
assert!(is_tag_created(), "Tag should exist after release");
let dest_dir = Utf8TempDir::new().unwrap();
let packages = context.download_package(dest_dir.path());
assert!(packages.is_empty());
}