release-tool 0.3.0

Configuration-driven release lifecycle for computed-parameter repositories
Documentation
use release_tool::config::{Config, PublisherConfig, TargetConfig};

#[test]
fn parses_target_and_publisher_adapters_without_duplicate_maven_coordinates() {
    let config = Config::parse(
        r#"
required_version = "0.1.0"

[repository]
github = "computed-parameter/sc-coordinator"
branch = "main"

[hooks]
preflight = ["just", "verify"]

[publishers.packages]
kind = "github_maven"
settings = ".mvn/settings.xml"
server_id = "github"

[[targets]]
name = "maven"
kind = "maven_reactor"
publisher = "packages"
default = true
wrapper = "./mvnw"
pom = "pom.xml"
projects = ["framework/java/coordinator-sdk", "jobs/cpe/cpe-job-model"]
also_make = true
remote_check = ["just", "_check-published-maven", "{version}"]
"#,
    )
    .unwrap();

    assert_eq!(config.hooks.preflight, vec!["just", "verify"]);
    assert!(matches!(
        config.publishers.get("packages"),
        Some(PublisherConfig::GithubMaven { server_id, .. }) if server_id == "github"
    ));
    assert!(matches!(
        &config.targets[0],
        TargetConfig::MavenReactor { projects, .. }
            if projects == &["framework/java/coordinator-sdk", "jobs/cpe/cpe-job-model"]
    ));
}

#[test]
fn command_hooks_must_be_argv_arrays() {
    let error = Config::parse(
        r#"
required_version = "0.1.0"

[repository]
github = "computed-parameter/example"
branch = "main"

[hooks]
preflight = "just verify && echo unsafe"
"#,
    )
    .unwrap_err();

    assert!(error.to_string().contains("config shape is invalid"));
}

#[test]
fn rejects_references_to_unknown_publishers_and_duplicate_target_names() {
    let error = Config::parse(
        r#"
required_version = "0.1.0"

[repository]
github = "computed-parameter/example"
branch = "main"

[publishers.release]
kind = "github_release"
title = "Example {version}"
prerelease = true

[[targets]]
name = "app"
kind = "docker_archive"
publisher = "missing"
default = true
platform = "linux/amd64"
image = "example:{version}"
asset = "example-{version}.tar.xz"
build = ["just", "build", "{image}"]
local_check = ["just", "check", "{image}"]

[[targets]]
name = "app"
kind = "docker_archive"
publisher = "release"
platform = "linux/amd64"
image = "example-other:{version}"
asset = "other-{version}.tar.xz"
build = ["just", "build", "{image}"]
local_check = ["just", "check", "{image}"]
"#,
    )
    .unwrap_err();

    assert!(error.to_string().contains("duplicate target name `app`"));
}

#[test]
fn rejects_incompatible_target_and_publisher_adapters() {
    let error = Config::parse(
        r#"required_version = "0.1.0"

[repository]
github = "computed-parameter/example"
branch = "main"

[publishers.packages]
kind = "github_maven"
settings = ".mvn/settings.xml"
server_id = "github"

[[targets]]
name = "app"
kind = "docker_archive"
publisher = "packages"
default = true
platform = "linux/amd64"
image = "example:{version}"
asset = "example-{version}.tar.xz"
build = ["just", "build", "{image}"]
local_check = ["just", "check", "{image}"]
"#,
    )
    .unwrap_err();

    assert!(
        error
            .to_string()
            .contains("target `app` is incompatible with publisher `packages`")
    );
}

#[test]
fn oci_targets_select_the_registry_publisher_implicitly() {
    let config = Config::parse(
        r#"required_version = "0.3.0"

[repository]
github = "computed-parameter/example"
branch = "main"

[[targets]]
name = "base"
kind = "oci_image"
image = "ghcr.io/computed-parameter/base:{version}"
platform = "linux/amd64"
reuse_check = ["just", "reuse", "base"]
build = ["just", "build", "base"]

[[targets]]
name = "app"
kind = "oci_image"
default = true
depends_on = ["base"]
image = "ghcr.io/computed-parameter/app:{version}"
platform = "linux/amd64"
reuse_check = ["just", "reuse", "app"]
build = ["just", "build", "app"]
"#,
    )
    .unwrap();

    assert!(config.publishers.is_empty());
    assert_eq!(config.targets[0].publisher(), "oci_registry");
    assert_eq!(config.targets[1].dependencies(), &["base"]);
}

#[test]
fn oci_targets_reject_explicit_registry_publisher_configuration() {
    let explicit_table = r#"required_version = "0.3.0"

[repository]
github = "computed-parameter/example"
branch = "main"

[publishers.registry]
kind = "oci_registry"
"#;
    assert!(Config::parse(explicit_table).is_err());

    let explicit_reference = r#"required_version = "0.3.0"

[repository]
github = "computed-parameter/example"
branch = "main"

[[targets]]
name = "base"
kind = "oci_image"
publisher = "registry"
image = "ghcr.io/computed-parameter/base:{version}"
platform = "linux/amd64"
reuse_check = ["just", "reuse", "base"]
build = ["just", "build", "base"]
"#;
    assert!(Config::parse(explicit_reference).is_err());
}

#[test]
fn rejects_unknown_duplicate_and_cyclic_target_dependencies() {
    let target = |name: &str, dependency: &str| {
        format!(
            r#"[[targets]]
name = "{name}"
kind = "oci_image"
depends_on = ["{dependency}"]
image = "ghcr.io/computed-parameter/{name}:{{version}}"
platform = "linux/amd64"
reuse_check = ["check", "{name}"]
build = ["build", "{name}"]
"#
        )
    };
    let prefix = r#"required_version = "0.3.0"

[repository]
github = "computed-parameter/example"
branch = "main"

"#;

    let error = Config::parse(&(prefix.to_owned() + &target("app", "missing"))).unwrap_err();
    assert!(error.to_string().contains("unknown dependency `missing`"));

    let error = Config::parse(&(prefix.to_owned() + &target("app", "app"))).unwrap_err();
    assert!(error.to_string().contains("cannot depend on itself"));

    let duplicate = format!(
        r#"{prefix}[[targets]]
name = "base"
kind = "oci_image"
image = "ghcr.io/computed-parameter/base:{{version}}"
platform = "linux/amd64"
reuse_check = ["check", "base"]
build = ["build", "base"]

[[targets]]
name = "app"
kind = "oci_image"
depends_on = ["base", "base"]
image = "ghcr.io/computed-parameter/app:{{version}}"
platform = "linux/amd64"
reuse_check = ["check", "app"]
build = ["build", "app"]
"#
    );
    let error = Config::parse(&duplicate).unwrap_err();
    assert!(error.to_string().contains("duplicate dependency `base`"));

    let cycle = prefix.to_owned() + &target("one", "two") + &target("two", "one");
    let error = Config::parse(&cycle).unwrap_err();
    assert!(error.to_string().contains("target dependency cycle"));

    let incompatible = format!(
        r#"{prefix}[publishers.release]
kind = "github_release"
title = "Example {{version}}"
prerelease = true

[[targets]]
name = "archive"
kind = "docker_archive"
publisher = "release"
platform = "linux/amd64"
image = "example:{{version}}"
asset = "example-{{version}}.tar.xz"
build = ["build"]
local_check = ["check"]

[[targets]]
name = "app"
kind = "oci_image"
depends_on = ["archive"]
image = "ghcr.io/computed-parameter/app:{{version}}"
platform = "linux/amd64"
reuse_check = ["check", "app"]
build = ["build", "app"]
"#
    );
    let error = Config::parse(&incompatible).unwrap_err();
    assert!(
        error
            .to_string()
            .contains("v1 only supports oci_image -> oci_image dependencies")
    );
}