gha-cache-proof 1.0.1

GitHub Actions cache compatibility checker and local cache-store receipt tool for offline CI
Documentation
use anyhow::Result;

use crate::model::{CacheProofReceipt, CheckStatus, OutputFormat};

pub fn render_receipt(receipt: &CacheProofReceipt, format: OutputFormat) -> Result<String> {
    match format {
        OutputFormat::Text => Ok(render_text(receipt)),
        OutputFormat::Json => Ok(format!("{}\n", serde_json::to_string_pretty(receipt)?)),
        OutputFormat::Markdown => Ok(render_markdown(receipt)),
    }
}

fn render_text(receipt: &CacheProofReceipt) -> String {
    let mut out = String::new();
    out.push_str(&format!(
        "{} {} ({})\n",
        receipt.tool.name, receipt.tool.version, receipt.mode
    ));
    out.push_str(&format!(
        "summary: {} passed, {} warned, {} failed, {} skipped\n",
        receipt.summary.passed,
        receipt.summary.warnings,
        receipt.summary.failed,
        receipt.summary.skipped
    ));

    for op in &receipt.operations {
        out.push_str(&format!(
            "\n{:?}: key={} version={} scope={} cache-hit={:?}\n",
            op.operation, op.key, op.version, op.scope, op.cache_hit
        ));
        if let Some(matched) = &op.matched {
            out.push_str(&format!(
                "  matched {} in {} via {:?}\n",
                matched.key, matched.scope, matched.match_kind
            ));
        }
        for check in &op.checks {
            out.push_str(&format!(
                "  {} {:<5} {}\n",
                status_symbol(check.status),
                status_word(check.status),
                check.message
            ));
        }
    }

    for workflow in &receipt.workflows {
        out.push_str(&format!(
            "\nworkflow {}: {} cache steps\n",
            workflow.workflow,
            workflow.cache_steps.len()
        ));
        for step in &workflow.cache_steps {
            out.push_str(&format!(
                "  {}[{}] {} key={}\n",
                step.job_id, step.step_index, step.uses, step.key
            ));
        }
    }

    out
}

fn render_markdown(receipt: &CacheProofReceipt) -> String {
    let mut out = String::new();
    out.push_str("# gha-cache-proof Receipt\n\n");
    out.push_str(&format!(
        "- Tool: `{}` `{}`\n",
        receipt.tool.name, receipt.tool.version
    ));
    out.push_str(&format!("- Mode: `{}`\n", markdown_escape(&receipt.mode)));
    out.push_str(&format!("- Checked at: `{}`\n", receipt.checked_at));
    out.push_str(&format!("- Store: `{}`\n", receipt.store));
    out.push_str(&format!(
        "- Summary: **{} passed**, **{} warned**, **{} failed**, **{} skipped**\n\n",
        receipt.summary.passed,
        receipt.summary.warnings,
        receipt.summary.failed,
        receipt.summary.skipped
    ));

    out.push_str("## Operations\n\n");
    out.push_str("| Operation | Key | Scope | Match | Cache hit | Checks |\n");
    out.push_str("| --- | --- | --- | --- | --- | --- |\n");
    for op in &receipt.operations {
        let matched = op
            .matched
            .as_ref()
            .map(|m| format!("{} via {:?}", m.key, m.match_kind))
            .unwrap_or_else(|| "miss".to_owned());
        let summary = op.summary();
        out.push_str(&format!(
            "| {:?} | `{}` | `{}` | {} | `{}` | {}/{}/{}/{} |\n",
            op.operation,
            markdown_escape(&op.key),
            markdown_escape(&op.scope),
            markdown_escape(&matched),
            markdown_escape(&op.cache_hit),
            summary.passed,
            summary.warnings,
            summary.failed,
            summary.skipped
        ));
    }

    if !receipt.workflows.is_empty() {
        out.push_str("\n## Workflows\n\n");
        for workflow in &receipt.workflows {
            out.push_str(&format!(
                "### `{}`\n\n{} cache steps.\n\n",
                workflow.workflow,
                workflow.cache_steps.len()
            ));
        }
    }

    out
}

fn status_symbol(status: CheckStatus) -> &'static str {
    match status {
        CheckStatus::Pass => "[PASS]",
        CheckStatus::Warn => "[WARN]",
        CheckStatus::Fail => "[FAIL]",
        CheckStatus::Skip => "[SKIP]",
    }
}

fn status_word(status: CheckStatus) -> &'static str {
    match status {
        CheckStatus::Pass => "pass",
        CheckStatus::Warn => "warn",
        CheckStatus::Fail => "fail",
        CheckStatus::Skip => "skip",
    }
}

fn markdown_escape(value: &str) -> String {
    value.replace('|', "\\|").replace('\n', "<br>")
}