Skip to main content

gha_cache_proof/
render.rs

1use anyhow::Result;
2
3use crate::model::{CacheProofReceipt, CheckStatus, OutputFormat};
4
5pub fn render_receipt(receipt: &CacheProofReceipt, format: OutputFormat) -> Result<String> {
6    match format {
7        OutputFormat::Text => Ok(render_text(receipt)),
8        OutputFormat::Json => Ok(format!("{}\n", serde_json::to_string_pretty(receipt)?)),
9        OutputFormat::Markdown => Ok(render_markdown(receipt)),
10    }
11}
12
13fn render_text(receipt: &CacheProofReceipt) -> String {
14    let mut out = String::new();
15    out.push_str(&format!(
16        "{} {} ({})\n",
17        receipt.tool.name, receipt.tool.version, receipt.mode
18    ));
19    out.push_str(&format!(
20        "summary: {} passed, {} warned, {} failed, {} skipped\n",
21        receipt.summary.passed,
22        receipt.summary.warnings,
23        receipt.summary.failed,
24        receipt.summary.skipped
25    ));
26
27    for op in &receipt.operations {
28        out.push_str(&format!(
29            "\n{:?}: key={} version={} scope={} cache-hit={:?}\n",
30            op.operation, op.key, op.version, op.scope, op.cache_hit
31        ));
32        if let Some(matched) = &op.matched {
33            out.push_str(&format!(
34                "  matched {} in {} via {:?}\n",
35                matched.key, matched.scope, matched.match_kind
36            ));
37        }
38        for check in &op.checks {
39            out.push_str(&format!(
40                "  {} {:<5} {}\n",
41                status_symbol(check.status),
42                status_word(check.status),
43                check.message
44            ));
45        }
46    }
47
48    for workflow in &receipt.workflows {
49        out.push_str(&format!(
50            "\nworkflow {}: {} cache steps\n",
51            workflow.workflow,
52            workflow.cache_steps.len()
53        ));
54        for step in &workflow.cache_steps {
55            out.push_str(&format!(
56                "  {}[{}] {} key={}\n",
57                step.job_id, step.step_index, step.uses, step.key
58            ));
59        }
60    }
61
62    out
63}
64
65fn render_markdown(receipt: &CacheProofReceipt) -> String {
66    let mut out = String::new();
67    out.push_str("# gha-cache-proof Receipt\n\n");
68    out.push_str(&format!(
69        "- Tool: `{}` `{}`\n",
70        receipt.tool.name, receipt.tool.version
71    ));
72    out.push_str(&format!("- Mode: `{}`\n", markdown_escape(&receipt.mode)));
73    out.push_str(&format!("- Checked at: `{}`\n", receipt.checked_at));
74    out.push_str(&format!("- Store: `{}`\n", receipt.store));
75    out.push_str(&format!(
76        "- Summary: **{} passed**, **{} warned**, **{} failed**, **{} skipped**\n\n",
77        receipt.summary.passed,
78        receipt.summary.warnings,
79        receipt.summary.failed,
80        receipt.summary.skipped
81    ));
82
83    out.push_str("## Operations\n\n");
84    out.push_str("| Operation | Key | Scope | Match | Cache hit | Checks |\n");
85    out.push_str("| --- | --- | --- | --- | --- | --- |\n");
86    for op in &receipt.operations {
87        let matched = op
88            .matched
89            .as_ref()
90            .map(|m| format!("{} via {:?}", m.key, m.match_kind))
91            .unwrap_or_else(|| "miss".to_owned());
92        let summary = op.summary();
93        out.push_str(&format!(
94            "| {:?} | `{}` | `{}` | {} | `{}` | {}/{}/{}/{} |\n",
95            op.operation,
96            markdown_escape(&op.key),
97            markdown_escape(&op.scope),
98            markdown_escape(&matched),
99            markdown_escape(&op.cache_hit),
100            summary.passed,
101            summary.warnings,
102            summary.failed,
103            summary.skipped
104        ));
105    }
106
107    if !receipt.workflows.is_empty() {
108        out.push_str("\n## Workflows\n\n");
109        for workflow in &receipt.workflows {
110            out.push_str(&format!(
111                "### `{}`\n\n{} cache steps.\n\n",
112                workflow.workflow,
113                workflow.cache_steps.len()
114            ));
115        }
116    }
117
118    out
119}
120
121fn status_symbol(status: CheckStatus) -> &'static str {
122    match status {
123        CheckStatus::Pass => "[PASS]",
124        CheckStatus::Warn => "[WARN]",
125        CheckStatus::Fail => "[FAIL]",
126        CheckStatus::Skip => "[SKIP]",
127    }
128}
129
130fn status_word(status: CheckStatus) -> &'static str {
131    match status {
132        CheckStatus::Pass => "pass",
133        CheckStatus::Warn => "warn",
134        CheckStatus::Fail => "fail",
135        CheckStatus::Skip => "skip",
136    }
137}
138
139fn markdown_escape(value: &str) -> String {
140    value.replace('|', "\\|").replace('\n', "<br>")
141}