gha_command_proof/
render.rs1use anyhow::Result;
2use clap::ValueEnum;
3
4use crate::receipt::{CheckStatus, Receipt};
5
6#[derive(Clone, Copy, Debug, ValueEnum)]
7pub enum OutputFormat {
8 Text,
9 Json,
10 Markdown,
11}
12
13pub fn render_receipt(receipt: &Receipt, format: OutputFormat) -> Result<String> {
14 match format {
15 OutputFormat::Text => Ok(render_text(receipt)),
16 OutputFormat::Json => Ok(format!("{}\n", serde_json::to_string_pretty(receipt)?)),
17 OutputFormat::Markdown => Ok(render_markdown(receipt)),
18 }
19}
20
21fn render_text(receipt: &Receipt) -> String {
22 let mut out = String::new();
23 out.push_str(&format!(
24 "{} {} ({})\n",
25 receipt.tool.name, receipt.tool.version, receipt.mode
26 ));
27 out.push_str(&format!(
28 "summary: {} passed, {} warned, {} failed, {} skipped\n",
29 receipt.summary.passed,
30 receipt.summary.warned,
31 receipt.summary.failed,
32 receipt.summary.skipped
33 ));
34
35 for check in &receipt.checks {
36 out.push_str(&format!(
37 "{} {:<7} {}",
38 status_symbol(check.status),
39 status_word(check.status),
40 check.id
41 ));
42 if let Some(location) = &check.location {
43 if location.source.is_some() || location.line.is_some() {
44 out.push_str(" (");
45 if let Some(source) = &location.source {
46 out.push_str(source);
47 }
48 if let Some(line) = location.line {
49 out.push_str(&format!(":{line}"));
50 }
51 out.push(')');
52 }
53 }
54 out.push('\n');
55 out.push_str(&format!(" {}\n", check.message));
56 }
57
58 if !receipt.commands.is_empty() {
59 out.push_str("\ncommands:\n");
60 for command in &receipt.commands {
61 out.push_str(&format!(
62 " line {:<4} {:<11} {:<14} {}\n",
63 command.line,
64 syntax_word(command.syntax),
65 command.name,
66 command.outcome
67 ));
68 }
69 }
70
71 if !receipt.env_files.is_empty() {
72 out.push_str("\nenvironment files:\n");
73 for file in &receipt.env_files {
74 out.push_str(&format!(
75 " {} {} records, {} bytes",
76 file.kind.context_name(),
77 file.records.len(),
78 file.bytes
79 ));
80 if let Some(source) = &file.source {
81 out.push_str(&format!(" ({source})"));
82 }
83 out.push('\n');
84 }
85 }
86
87 out
88}
89
90fn render_markdown(receipt: &Receipt) -> String {
91 let mut out = String::new();
92 out.push_str("# GHA Command Proof\n\n");
93 out.push_str(&format!(
94 "- Tool: `{}` `{}`\n",
95 receipt.tool.name, receipt.tool.version
96 ));
97 out.push_str(&format!("- Mode: `{}`\n", markdown_escape(&receipt.mode)));
98 out.push_str(&format!("- Checked at: `{}`\n", receipt.checked_at));
99 out.push_str(&format!(
100 "- Summary: **{} passed**, **{} warned**, **{} failed**, **{} skipped**\n\n",
101 receipt.summary.passed,
102 receipt.summary.warned,
103 receipt.summary.failed,
104 receipt.summary.skipped
105 ));
106
107 out.push_str("| Status | Check | Location | Message |\n");
108 out.push_str("| --- | --- | --- | --- |\n");
109 for check in &receipt.checks {
110 let location = check
111 .location
112 .as_ref()
113 .map(|location| {
114 let mut rendered = String::new();
115 if let Some(source) = &location.source {
116 rendered.push_str(&markdown_escape(source));
117 }
118 if let Some(line) = location.line {
119 rendered.push_str(&format!(":{line}"));
120 }
121 rendered
122 })
123 .unwrap_or_default();
124 out.push_str(&format!(
125 "| {} {} | `{}` | {} | {} |\n",
126 status_symbol(check.status),
127 status_word(check.status),
128 markdown_escape(&check.id),
129 location,
130 markdown_escape(&check.message)
131 ));
132 }
133
134 out
135}
136
137fn status_symbol(status: CheckStatus) -> &'static str {
138 match status {
139 CheckStatus::Pass => "[PASS]",
140 CheckStatus::Warn => "[WARN]",
141 CheckStatus::Fail => "[FAIL]",
142 CheckStatus::Skip => "[SKIP]",
143 }
144}
145
146fn status_word(status: CheckStatus) -> &'static str {
147 match status {
148 CheckStatus::Pass => "pass",
149 CheckStatus::Warn => "warn",
150 CheckStatus::Fail => "fail",
151 CheckStatus::Skip => "skip",
152 }
153}
154
155fn syntax_word(syntax: crate::CommandSyntax) -> &'static str {
156 match syntax {
157 crate::CommandSyntax::Modern => "modern",
158 crate::CommandSyntax::Legacy => "legacy",
159 }
160}
161
162fn markdown_escape(value: &str) -> String {
163 value.replace('|', "\\|").replace('\n', "<br>")
164}