1mod command;
9mod envfile;
10mod receipt;
11mod render;
12
13use std::collections::BTreeMap;
14
15use chrono::Utc;
16
17pub use command::{
18 CommandRecord, CommandSyntax, LogAnalysis, ParsedCommand, analyze_command_stream, escape_data,
19 escape_legacy, escape_property, parse_command_line, unescape_data, unescape_legacy,
20 unescape_property,
21};
22pub use envfile::{EnvFileAnalysis, EnvFileKind, EnvFileRecord, analyze_env_file};
23pub use receipt::{Check, CheckStatus, Location, Receipt, Summary, ToolReceipt};
24pub use render::{OutputFormat, render_receipt};
25
26#[derive(Clone, Debug, Default)]
28pub struct ProofOptions {
29 pub strict: bool,
31}
32
33#[derive(Clone, Debug, Default)]
35pub struct StepInput {
36 pub log: Option<NamedText>,
38 pub github_env: Option<NamedText>,
40 pub github_output: Option<NamedText>,
42 pub github_state: Option<NamedText>,
44 pub github_path: Option<NamedText>,
46 pub github_step_summary: Option<NamedText>,
48}
49
50#[derive(Clone, Debug)]
52pub struct NamedText {
53 pub source: String,
54 pub text: String,
55}
56
57impl NamedText {
58 pub fn new(source: impl Into<String>, text: impl Into<String>) -> Self {
59 Self {
60 source: source.into(),
61 text: text.into(),
62 }
63 }
64}
65
66pub fn prove_log(input: NamedText, options: &ProofOptions) -> Receipt {
68 let analysis = analyze_command_stream(&input.text, Some(input.source));
69 receipt_from_parts(
70 "log",
71 options,
72 analysis.checks,
73 analysis.commands,
74 Vec::new(),
75 )
76}
77
78pub fn prove_env_file(kind: EnvFileKind, input: NamedText, options: &ProofOptions) -> Receipt {
80 let analysis = analyze_env_file(kind, &input.text, Some(input.source), &[]);
81 receipt_from_parts(
82 "env-file",
83 options,
84 analysis.checks.clone(),
85 Vec::new(),
86 vec![analysis],
87 )
88}
89
90pub fn prove_step(input: StepInput, options: &ProofOptions) -> Receipt {
92 let mut checks = Vec::new();
93 let mut commands = Vec::new();
94 let mut files = Vec::new();
95 let mut masks = Vec::new();
96
97 if let Some(log) = input.log {
98 let analysis = analyze_command_stream(&log.text, Some(log.source));
99 masks = analysis.mask_values;
100 checks.extend(analysis.checks);
101 commands.extend(analysis.commands);
102 } else {
103 checks.push(Check::skip(
104 "step.log",
105 "no command stream supplied for this step",
106 None,
107 ));
108 }
109
110 for (kind, text) in [
111 (EnvFileKind::Env, input.github_env),
112 (EnvFileKind::Output, input.github_output),
113 (EnvFileKind::State, input.github_state),
114 (EnvFileKind::Path, input.github_path),
115 (EnvFileKind::StepSummary, input.github_step_summary),
116 ] {
117 if let Some(text) = text {
118 let analysis = analyze_env_file(kind, &text.text, Some(text.source), &masks);
119 checks.extend(analysis.checks.clone());
120 files.push(analysis);
121 }
122 }
123
124 if files.is_empty() {
125 checks.push(Check::skip(
126 "step.env_files",
127 "no environment files supplied for this step",
128 None,
129 ));
130 }
131
132 receipt_from_parts("step", options, checks, commands, files)
133}
134
135fn receipt_from_parts(
136 mode: impl Into<String>,
137 options: &ProofOptions,
138 checks: Vec<Check>,
139 commands: Vec<CommandRecord>,
140 env_files: Vec<EnvFileAnalysis>,
141) -> Receipt {
142 let mut summary = Summary::from_checks(&checks);
143 if options.strict && summary.warned > 0 {
144 summary.failed += summary.warned;
145 summary.warned = 0;
146 }
147
148 Receipt {
149 schema_version: 1,
150 tool: ToolReceipt {
151 name: "gha-command-proof",
152 version: env!("CARGO_PKG_VERSION"),
153 },
154 checked_at: Utc::now(),
155 mode: mode.into(),
156 summary,
157 checks,
158 commands,
159 env_files,
160 metadata: BTreeMap::new(),
161 }
162}