releasaurus-core 0.20.2

A comprehensive release automation tool that streamlines the software release process across multiple programming languages and forge platforms
Documentation
use regex::Regex;
use std::sync::LazyLock;

use crate::{
    forge::request::FileChange,
    result::Result,
    updater::{
        generic::updater::GenericUpdater, manager::UpdaterPackage,
        traits::PackageUpdater,
    },
};

/// Gradle properties-specific version regex that only matches a standalone
/// `version` property. Prevents false matches on properties like
/// `awsSoftwareVersion`, `kotlinVersion`, etc.
static GRADLE_PROPERTIES_VERSION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r#"(?mi)(?<start>^\s*version\s*=\s*['"]?)(?<version>\d+\.\d+\.\d+-?.*?)(?<end>['",].*)?$"#).unwrap()
});

/// Handles gradle.properties file parsing and version updates for Java packages.
pub struct GradleProperties {}

impl GradleProperties {
    /// Create GradleProperties handler for gradle.properties version updates.
    pub fn new() -> Self {
        Self {}
    }
}

impl Default for GradleProperties {
    fn default() -> Self {
        GradleProperties::new()
    }
}

impl PackageUpdater for GradleProperties {
    /// Update version fields in gradle.properties files for all Java packages.
    fn update(
        &self,
        package: &UpdaterPackage,
        _workspace_packages: &[UpdaterPackage],
    ) -> Result<Option<Vec<FileChange>>> {
        let mut file_changes: Vec<FileChange> = vec![];

        for manifest in package.manifest_files.iter() {
            if manifest.basename == "gradle.properties"
                && let Some(change) = GenericUpdater::update_manifest(
                    manifest,
                    &package.next_version.semver,
                    &GRADLE_PROPERTIES_VERSION_REGEX,
                )
            {
                file_changes.push(change);
            }
        }

        if file_changes.is_empty() {
            return Ok(None);
        }

        Ok(Some(file_changes))
    }
}

#[cfg(test)]
mod tests {
    use std::{path::Path, rc::Rc};

    use crate::{
        config::release_type::ReleaseType, forge::request::Tag,
        packages::manifests::ManifestFile, updater::dispatch::Updater,
    };

    use super::*;

    #[test]
    fn updates_version_property() {
        let properties = GradleProperties::new();
        let content = "version=1.0.0";
        let manifest = ManifestFile {
            path: Path::new("gradle.properties").to_path_buf(),
            basename: "gradle.properties".to_string(),
            content: content.to_string(),
        };
        let package = UpdaterPackage {
            package_name: "test".to_string(),
            manifest_files: vec![manifest.clone()],
            next_version: Tag {
                name: "v2.0.0".into(),
                semver: semver::Version::parse("2.0.0").unwrap(),
                sha: "abc".into(),
                ..Tag::default()
            },
            updater: Rc::new(Updater::new(ReleaseType::Java)),
        };

        let result = properties.update(&package, &[]).unwrap();

        let changes = result.unwrap();
        assert_eq!(changes.len(), 1);
        assert_eq!(changes[0].content, "version=2.0.0");
    }

    #[test]
    fn preserves_whitespace_around_equals() {
        let properties = GradleProperties::new();
        let content = "version  =  1.0.0";
        let manifest = ManifestFile {
            path: Path::new("gradle.properties").to_path_buf(),
            basename: "gradle.properties".to_string(),
            content: content.to_string(),
        };
        let package = UpdaterPackage {
            package_name: "test".to_string(),
            manifest_files: vec![manifest.clone()],
            next_version: Tag {
                name: "v3.0.0".into(),
                semver: semver::Version::parse("3.0.0").unwrap(),
                sha: "abc".into(),
                ..Tag::default()
            },
            updater: Rc::new(Updater::new(ReleaseType::Java)),
        };

        let result = properties.update(&package, &[]).unwrap();

        let changes = result.unwrap();
        assert_eq!(changes.len(), 1);
        assert_eq!(changes[0].content, "version  =  3.0.0");
    }

    #[test]
    fn preserves_leading_whitespace() {
        let properties = GradleProperties::new();
        let content = "  version=1.0.0";
        let manifest = ManifestFile {
            path: Path::new("gradle.properties").to_path_buf(),
            basename: "gradle.properties".to_string(),
            content: content.to_string(),
        };
        let package = UpdaterPackage {
            package_name: "test".to_string(),
            manifest_files: vec![manifest.clone()],
            next_version: Tag {
                name: "v2.5.0".into(),
                semver: semver::Version::parse("2.5.0").unwrap(),
                sha: "abc".into(),
                ..Tag::default()
            },
            updater: Rc::new(Updater::new(ReleaseType::Java)),
        };

        let result = properties.update(&package, &[]).unwrap();

        let changes = result.unwrap();
        assert_eq!(changes.len(), 1);
        assert_eq!(changes[0].content, "  version=2.5.0");
    }

    #[test]
    fn preserves_other_properties() {
        let properties = GradleProperties::new();
        let content =
            "org.gradle.jvmargs=-Xmx2048m\nversion=1.0.0\ngroup=com.example";
        let manifest = ManifestFile {
            path: Path::new("gradle.properties").to_path_buf(),
            basename: "gradle.properties".to_string(),
            content: content.to_string(),
        };
        let package = UpdaterPackage {
            package_name: "test".to_string(),
            manifest_files: vec![manifest.clone()],
            next_version: Tag {
                name: "v2.0.0".into(),
                semver: semver::Version::parse("2.0.0").unwrap(),
                sha: "abc".into(),
                ..Tag::default()
            },
            updater: Rc::new(Updater::new(ReleaseType::Java)),
        };

        let result = properties.update(&package, &[]).unwrap();

        let changes = result.unwrap();
        assert_eq!(changes.len(), 1);
        let updated = changes[0].content.clone();

        assert!(updated.contains("org.gradle.jvmargs=-Xmx2048m"));
        assert!(updated.contains("version=2.0.0"));
        assert!(updated.contains("group=com.example"));
    }

    #[test]
    fn returns_none_when_no_version_property() {
        let properties = GradleProperties::new();
        let content = "org.gradle.jvmargs=-Xmx2048m\ngroup=com.example";
        let manifest = ManifestFile {
            path: Path::new("gradle.properties").to_path_buf(),
            basename: "gradle.properties".to_string(),
            content: content.to_string(),
        };
        let package = UpdaterPackage {
            package_name: "test".to_string(),
            manifest_files: vec![manifest.clone()],
            next_version: Tag {
                name: "v2.0.0".into(),
                semver: semver::Version::parse("2.0.0").unwrap(),
                sha: "abc".into(),
                ..Tag::default()
            },
            updater: Rc::new(Updater::new(ReleaseType::Java)),
        };

        let result = properties.update(&package, &[]).unwrap();

        assert!(result.is_none());
    }

    #[test]
    fn process_package_handles_multiple_properties_files() {
        let properties = GradleProperties::new();
        let manifest1 = ManifestFile {
            path: Path::new("module1/gradle.properties").to_path_buf(),
            basename: "gradle.properties".to_string(),
            content: "version=1.0.0".to_string(),
        };
        let manifest2 = ManifestFile {
            path: Path::new("module2/gradle.properties").to_path_buf(),
            basename: "gradle.properties".to_string(),
            content: "version=1.0.0".to_string(),
        };
        let package = UpdaterPackage {
            package_name: "test".to_string(),
            manifest_files: vec![manifest1, manifest2],
            next_version: Tag {
                name: "v2.0.0".into(),
                semver: semver::Version::parse("2.0.0").unwrap(),
                sha: "abc".into(),
                ..Tag::default()
            },
            updater: Rc::new(Updater::new(ReleaseType::Java)),
        };

        let result = properties.update(&package, &[]).unwrap();

        let changes = result.unwrap();
        assert_eq!(changes.len(), 2);
        assert!(changes.iter().all(|c| c.content.contains("2.0.0")));
    }

    #[test]
    fn process_package_returns_none_when_no_gradle_properties() {
        let properties = GradleProperties::new();
        let manifest = ManifestFile {
            path: Path::new("build.gradle").to_path_buf(),
            basename: "build.gradle".to_string(),
            content: "version = \"1.0.0\"".to_string(),
        };
        let package = UpdaterPackage {
            package_name: "test".to_string(),
            manifest_files: vec![manifest],
            next_version: Tag {
                name: "v2.0.0".into(),
                semver: semver::Version::parse("2.0.0").unwrap(),
                sha: "abc".into(),
                ..Tag::default()
            },
            updater: Rc::new(Updater::new(ReleaseType::Java)),
        };

        let result = properties.update(&package, &[]).unwrap();

        assert!(result.is_none());
    }

    #[test]
    fn does_not_update_properties_containing_version_in_name() {
        let properties = GradleProperties::new();
        let content =
            "awsSoftwareVersion=1.0.0\nkotlinVersion=1.9.20\nversion=1.0.0";
        let manifest = ManifestFile {
            path: Path::new("gradle.properties").to_path_buf(),
            basename: "gradle.properties".to_string(),
            content: content.to_string(),
        };
        let package = UpdaterPackage {
            package_name: "test".to_string(),
            manifest_files: vec![manifest.clone()],
            next_version: Tag {
                name: "v2.0.0".into(),
                semver: semver::Version::parse("2.0.0").unwrap(),
                sha: "abc".into(),
                ..Tag::default()
            },
            updater: Rc::new(Updater::new(ReleaseType::Java)),
        };

        let result = properties.update(&package, &[]).unwrap();

        let changes = result.unwrap();
        assert_eq!(changes.len(), 1);
        let updated = &changes[0].content;
        assert!(
            updated.contains("awsSoftwareVersion=1.0.0"),
            "awsSoftwareVersion should not be updated"
        );
        assert!(
            updated.contains("kotlinVersion=1.9.20"),
            "kotlinVersion should not be updated"
        );
        assert!(
            updated.contains("version=2.0.0"),
            "version property should be updated"
        );
    }

    #[test]
    fn does_not_update_commented_version_lines() {
        let properties = GradleProperties::new();
        let content = "# version=0.0.1\nversion=1.0.0";
        let manifest = ManifestFile {
            path: Path::new("gradle.properties").to_path_buf(),
            basename: "gradle.properties".to_string(),
            content: content.to_string(),
        };
        let package = UpdaterPackage {
            package_name: "test".to_string(),
            manifest_files: vec![manifest.clone()],
            next_version: Tag {
                name: "v2.0.0".into(),
                semver: semver::Version::parse("2.0.0").unwrap(),
                sha: "abc".into(),
                ..Tag::default()
            },
            updater: Rc::new(Updater::new(ReleaseType::Java)),
        };

        let result = properties.update(&package, &[]).unwrap();

        let changes = result.unwrap();
        assert_eq!(changes.len(), 1);

        let updated = changes[0].content.clone();
        assert!(
            updated.contains("# version=0.0.1"),
            "commented version should not be updated"
        );
        assert!(updated.contains("version=2.0.0"));
    }
}