1use camino::Utf8PathBuf;
2use chrono::{DateTime, Utc};
3use clap::ValueEnum;
4use gha_expression_proof::EvaluationReceipt;
5use serde::{Deserialize, Serialize};
6
7pub type SchemaVersion = u32;
8pub const SCHEMA_VERSION: SchemaVersion = 1;
9
10#[derive(Clone, Copy, Debug, ValueEnum, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum OutputFormat {
13 Text,
14 Json,
15 Markdown,
16}
17
18#[derive(Clone, Copy, Debug, ValueEnum, Serialize, Deserialize, PartialEq, Eq)]
19#[serde(rename_all = "lowercase")]
20pub enum RunnerOs {
21 Linux,
22 Windows,
23 Macos,
24}
25
26impl RunnerOs {
27 pub fn gha_name(self) -> &'static str {
28 match self {
29 Self::Linux => "Linux",
30 Self::Windows => "Windows",
31 Self::Macos => "macOS",
32 }
33 }
34}
35
36#[derive(Clone, Copy, Debug, ValueEnum, Serialize, Deserialize, PartialEq, Eq)]
37#[serde(rename_all = "lowercase")]
38pub enum Compression {
39 Gzip,
40 Zstd,
41}
42
43impl Compression {
44 pub fn default_for(runner_os: RunnerOs, cross_os: bool) -> Self {
45 if cross_os {
46 return Self::Zstd;
47 }
48
49 match runner_os {
50 RunnerOs::Windows => Self::Gzip,
51 RunnerOs::Linux | RunnerOs::Macos => Self::Zstd,
52 }
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ToolInfo {
58 pub name: String,
59 pub version: String,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "lowercase")]
64pub enum CheckStatus {
65 Pass,
66 Warn,
67 Fail,
68 Skip,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct Check {
73 pub id: String,
74 pub status: CheckStatus,
75 pub message: String,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub location: Option<String>,
78}
79
80impl Check {
81 pub fn pass(id: impl Into<String>, message: impl Into<String>) -> Self {
82 Self {
83 id: id.into(),
84 status: CheckStatus::Pass,
85 message: message.into(),
86 location: None,
87 }
88 }
89
90 pub fn warn(id: impl Into<String>, message: impl Into<String>) -> Self {
91 Self {
92 id: id.into(),
93 status: CheckStatus::Warn,
94 message: message.into(),
95 location: None,
96 }
97 }
98
99 pub fn fail(id: impl Into<String>, message: impl Into<String>) -> Self {
100 Self {
101 id: id.into(),
102 status: CheckStatus::Fail,
103 message: message.into(),
104 location: None,
105 }
106 }
107
108 pub fn skip(id: impl Into<String>, message: impl Into<String>) -> Self {
109 Self {
110 id: id.into(),
111 status: CheckStatus::Skip,
112 message: message.into(),
113 location: None,
114 }
115 }
116
117 pub fn at(mut self, location: impl Into<String>) -> Self {
118 self.location = Some(location.into());
119 self
120 }
121}
122
123#[derive(Debug, Clone, Default, Serialize, Deserialize)]
124pub struct ReceiptSummary {
125 pub passed: usize,
126 pub warnings: usize,
127 pub failed: usize,
128 pub skipped: usize,
129}
130
131impl ReceiptSummary {
132 pub fn from_checks(checks: &[Check]) -> Self {
133 let mut summary = Self::default();
134 for check in checks {
135 match check.status {
136 CheckStatus::Pass => summary.passed += 1,
137 CheckStatus::Warn => summary.warnings += 1,
138 CheckStatus::Fail => summary.failed += 1,
139 CheckStatus::Skip => summary.skipped += 1,
140 }
141 }
142 summary
143 }
144
145 pub fn add(&mut self, other: &Self) {
146 self.passed += other.passed;
147 self.warnings += other.warnings;
148 self.failed += other.failed;
149 self.skipped += other.skipped;
150 }
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct CacheProofReceipt {
155 pub schema_version: SchemaVersion,
156 pub tool: ToolInfo,
157 pub checked_at: DateTime<Utc>,
158 pub mode: String,
159 pub store: Utf8PathBuf,
160 pub workspace: Utf8PathBuf,
161 pub summary: ReceiptSummary,
162 pub operations: Vec<CacheOperationReceipt>,
163 #[serde(default, skip_serializing_if = "Vec::is_empty")]
164 pub workflows: Vec<WorkflowCacheReport>,
165 pub checks: Vec<Check>,
166}
167
168#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
169#[serde(rename_all = "kebab-case")]
170pub enum CacheOperationKind {
171 Cache,
172 Restore,
173 Save,
174}
175
176#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
177#[serde(rename_all = "kebab-case")]
178pub enum CacheMatchKind {
179 ExactKey,
180 PrefixKey,
181 RestoreExact,
182 RestorePrefix,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct CacheMatch {
187 pub entry_id: String,
188 pub key: String,
189 pub version: String,
190 pub scope: String,
191 pub match_kind: CacheMatchKind,
192 pub created_at: DateTime<Utc>,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct CachePathRecord {
197 pub input: String,
198 pub resolved: Utf8PathBuf,
199 pub files: usize,
200 pub bytes: u64,
201 pub exists: bool,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct CacheOperationReceipt {
206 pub operation: CacheOperationKind,
207 pub key: String,
208 pub restore_keys: Vec<String>,
209 pub paths: Vec<String>,
210 pub version: String,
211 pub scope: String,
212 pub accessible_scopes: Vec<String>,
213 pub runner_os: RunnerOs,
214 pub compression: Compression,
215 pub enable_cross_os_archive: bool,
216 pub lookup_only: bool,
217 pub fail_on_cache_miss: bool,
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub matched: Option<CacheMatch>,
220 pub cache_hit: String,
221 pub restored_files: usize,
222 pub restored_bytes: u64,
223 pub saved_files: usize,
224 pub saved_bytes: u64,
225 pub path_records: Vec<CachePathRecord>,
226 pub checks: Vec<Check>,
227}
228
229impl CacheOperationReceipt {
230 pub fn summary(&self) -> ReceiptSummary {
231 ReceiptSummary::from_checks(&self.checks)
232 }
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct WorkflowCacheReport {
237 pub workflow: Utf8PathBuf,
238 pub cache_steps: Vec<WorkflowCacheStep>,
239 pub summary: ReceiptSummary,
240 pub checks: Vec<Check>,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct WorkflowCacheStep {
245 pub job_id: String,
246 pub step_index: usize,
247 #[serde(skip_serializing_if = "Option::is_none")]
248 pub name: Option<String>,
249 pub uses: String,
250 pub operation: CacheOperationKind,
251 pub key_template: String,
252 pub key: String,
253 pub restore_key_templates: Vec<String>,
254 pub restore_keys: Vec<String>,
255 pub path_templates: Vec<String>,
256 pub paths: Vec<String>,
257 pub expression_receipts: Vec<EvaluationReceipt>,
258 pub operation_receipt: CacheOperationReceipt,
259 pub checks: Vec<Check>,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct CacheIndex {
264 pub schema_version: SchemaVersion,
265 pub entries: Vec<CacheEntry>,
266}
267
268impl Default for CacheIndex {
269 fn default() -> Self {
270 Self {
271 schema_version: SCHEMA_VERSION,
272 entries: Vec::new(),
273 }
274 }
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct CacheEntry {
279 pub id: String,
280 pub key: String,
281 pub version: String,
282 pub scope: String,
283 pub paths: Vec<String>,
284 pub runner_os: RunnerOs,
285 pub compression: Compression,
286 pub enable_cross_os_archive: bool,
287 pub created_at: DateTime<Utc>,
288 pub last_accessed_at: DateTime<Utc>,
289 pub files: usize,
290 pub bytes: u64,
291}