gha-expression-proof 1.0.0

GitHub Actions expression evaluator and receipt generator for offline CI compatibility testing
Documentation
use assert_cmd::Command;
use predicates::prelude::*;
use serde_json::{Value, json};
use tempfile::tempdir;

fn bin() -> Command {
    Command::cargo_bin("gha-expression-proof").unwrap()
}

#[test]
fn eval_reads_eventsmith_github_context() {
    let dir = tempdir().unwrap();
    let github_context = dir.path().join("github-context.json");
    std::fs::write(
        &github_context,
        serde_json::to_vec_pretty(&json!({
            "ref": "refs/heads/main",
            "ref_name": "main",
            "event_name": "issues",
            "event": {
                "issue": {
                    "labels": [
                        {"name": "bug"},
                        {"name": "help wanted"}
                    ]
                }
            }
        }))
        .unwrap(),
    )
    .unwrap();

    let assert = bin()
        .args([
            "eval",
            "--expr",
            "github.ref == 'refs/heads/main' && contains(github.event.issue.labels.*.name, 'BUG')",
            "--github-context",
            github_context.to_str().unwrap(),
            "--format",
            "json",
        ])
        .assert()
        .success();

    let receipt = serde_json::from_slice::<Value>(&assert.get_output().stdout).unwrap();
    assert_eq!(receipt["result"], true);
    assert_eq!(receipt["summary"]["failed"], 0);
    assert!(
        receipt["functions"]
            .as_array()
            .unwrap()
            .iter()
            .any(|v| v == "contains")
    );
}

#[test]
fn eval_supports_from_json_and_context_json() {
    let assert = bin()
        .args([
            "eval",
            "--expr",
            "fromJSON(env.time) > 2",
            "--context-json",
            r#"env={"time":"3"}"#,
            "--format",
            "json",
        ])
        .assert()
        .success();

    let receipt = serde_json::from_slice::<Value>(&assert.get_output().stdout).unwrap();
    assert_eq!(receipt["result"], true);
}

#[test]
fn template_interpolates_expressions() {
    let dir = tempdir().unwrap();
    let github_context = dir.path().join("github-context.json");
    std::fs::write(
        &github_context,
        serde_json::to_vec(&json!({
            "ref_name": "main",
            "sha": "2222222222222222222222222222222222222222"
        }))
        .unwrap(),
    )
    .unwrap();

    let assert = bin()
        .args([
            "template",
            "--template",
            "deploy-${{ github.ref_name }}-${{ github.sha }}",
            "--github-context",
            github_context.to_str().unwrap(),
            "--format",
            "json",
        ])
        .assert()
        .success();

    let receipt = serde_json::from_slice::<Value>(&assert.get_output().stdout).unwrap();
    assert_eq!(
        receipt["rendered"],
        "deploy-main-2222222222222222222222222222222222222222"
    );
}

#[test]
fn if_condition_applies_implicit_success() {
    let assert = bin()
        .args([
            "eval",
            "--expr",
            "github.ref == 'refs/heads/main'",
            "--context-json",
            r#"github={"ref":"refs/heads/main"}"#,
            "--if-condition",
            "--job-status",
            "failure",
            "--format",
            "json",
        ])
        .assert()
        .success();

    let receipt = serde_json::from_slice::<Value>(&assert.get_output().stdout).unwrap();
    assert_eq!(receipt["result"], false);
}

#[test]
fn hash_files_hashes_workspace_files() {
    let dir = tempdir().unwrap();
    std::fs::write(dir.path().join("package-lock.json"), "{}\n").unwrap();

    let assert = bin()
        .args([
            "eval",
            "--expr",
            "hashFiles('package-lock.json') != ''",
            "--workspace",
            dir.path().to_str().unwrap(),
            "--format",
            "json",
        ])
        .assert()
        .success();

    let receipt = serde_json::from_slice::<Value>(&assert.get_output().stdout).unwrap();
    assert_eq!(receipt["result"], true);
    assert_eq!(receipt["summary"]["failed"], 0);
}

#[test]
fn invalid_expression_fails_with_receipt() {
    bin()
        .args([
            "eval",
            "--expr",
            r#"github.ref == "main""#,
            "--format",
            "json",
        ])
        .assert()
        .failure()
        .stdout(predicate::str::contains("expression.syntax"));
}