release-tool 0.2.1

Configuration-driven release lifecycle for computed-parameter repositories
Documentation
use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use tempfile::TempDir;

#[test]
fn doctor_reports_the_active_actor_and_redacts_the_token() {
    let fixture = DoctorFixture::new("WRITE");

    Command::cargo_bin("release-tool")
        .unwrap()
        .current_dir(fixture.project.path())
        .env("PATH", fixture.path())
        .arg("doctor")
        .assert()
        .success()
        .stdout(predicate::str::contains("VERIFIED Git repository"))
        .stdout(predicate::str::contains(
            "VERIFIED GitHub account: release-bot",
        ))
        .stdout(predicate::str::contains(
            "VERIFIED repository permission: WRITE",
        ))
        .stdout(predicate::str::contains(
            "UNVERIFIABLE destination write permission",
        ))
        .stdout(predicate::str::contains("super-secret-token").not());
}

#[test]
fn doctor_rejects_an_account_without_repository_write_permission() {
    let fixture = DoctorFixture::new("READ");

    Command::cargo_bin("release-tool")
        .unwrap()
        .current_dir(fixture.project.path())
        .env("PATH", fixture.path())
        .arg("doctor")
        .assert()
        .failure()
        .stderr(predicate::str::contains(
            "GitHub account only has READ permission for computed-parameter/example",
        ));
}

#[test]
fn doctor_checks_target_commands_before_the_release_starts() {
    let fixture = DoctorFixture::new("WRITE");
    fs::write(
        fixture.project.path().join("release.toml"),
        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 = "release"
default = true
platform = "linux/amd64"
image = "example:{version}"
asset = "example-{version}.tar.xz"
build = ["definitely-missing-release-command", "{image}"]
local_check = ["gh", "--version"]
"#,
    )
    .unwrap();

    Command::cargo_bin("release-tool")
        .unwrap()
        .current_dir(fixture.project.path())
        .env("PATH", fixture.path())
        .arg("doctor")
        .assert()
        .failure()
        .stderr(predicate::str::contains(
            "required command `definitely-missing-release-command` was not found",
        ));
}

struct DoctorFixture {
    project: TempDir,
    bin: std::path::PathBuf,
}

impl DoctorFixture {
    fn new(permission: &str) -> Self {
        let project = tempfile::tempdir().unwrap();
        let bin = project.path().join("bin");
        fs::create_dir(&bin).unwrap();
        fs::write(
            project.path().join("release.toml"),
            r#"required_version = "0.1.0"

[repository]
github = "computed-parameter/example"
branch = "main"
"#,
        )
        .unwrap();
        executable(
            &bin.join("git"),
            r#"#!/usr/bin/env bash
set -eu
case "$*" in
  "status --porcelain --untracked-files=all") exit 0 ;;
  "branch --show-current") printf 'main\n' ;;
  "rev-parse HEAD") printf 'commit01\n' ;;
  "ls-remote origin refs/heads/main") printf 'commit01\trefs/heads/main\n' ;;
  "ls-remote --tags origin") exit 0 ;;
  "remote get-url origin") printf 'https://github.com/computed-parameter/example.git\n' ;;
  "--version") printf 'git version test\n' ;;
  *) printf 'unexpected fake git args: %s\n' "$*" >&2; exit 2 ;;
esac
"#,
        );
        executable(
            &bin.join("gh"),
            &format!(
                r#"#!/usr/bin/env bash
set -eu
case "$*" in
  "--version") printf 'gh version test\n' ;;
  "auth status --hostname github.com") exit 0 ;;
  "auth token --hostname github.com") printf 'super-secret-token\n' ;;
  "api --hostname github.com user --jq .login") printf 'release-bot\n' ;;
  "repo view computed-parameter/example --json nameWithOwner,viewerPermission")
    printf '{{"nameWithOwner":"computed-parameter/example","viewerPermission":"{permission}"}}\n' ;;
  *) printf 'unexpected fake gh args: %s\n' "$*" >&2; exit 2 ;;
esac
"#
            ),
        );
        Self { project, bin }
    }

    fn path(&self) -> String {
        let inherited = std::env::var("PATH").unwrap();
        format!("{}:{inherited}", self.bin.display())
    }
}

fn executable(path: &std::path::Path, source: &str) {
    fs::write(path, source).unwrap();
    let mut permissions = fs::metadata(path).unwrap().permissions();
    permissions.set_mode(0o755);
    fs::set_permissions(path, permissions).unwrap();
}