Skip to main content

gha_cache_proof/
model.rs

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}