use camino::Utf8PathBuf;
use chrono::{DateTime, Utc};
use clap::ValueEnum;
use gha_expression_proof::EvaluationReceipt;
use serde::{Deserialize, Serialize};
pub type SchemaVersion = u32;
pub const SCHEMA_VERSION: SchemaVersion = 1;
#[derive(Clone, Copy, Debug, ValueEnum, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OutputFormat {
Text,
Json,
Markdown,
}
#[derive(Clone, Copy, Debug, ValueEnum, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum RunnerOs {
Linux,
Windows,
Macos,
}
impl RunnerOs {
pub fn gha_name(self) -> &'static str {
match self {
Self::Linux => "Linux",
Self::Windows => "Windows",
Self::Macos => "macOS",
}
}
}
#[derive(Clone, Copy, Debug, ValueEnum, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Compression {
Gzip,
Zstd,
}
impl Compression {
pub fn default_for(runner_os: RunnerOs, cross_os: bool) -> Self {
if cross_os {
return Self::Zstd;
}
match runner_os {
RunnerOs::Windows => Self::Gzip,
RunnerOs::Linux | RunnerOs::Macos => Self::Zstd,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CheckStatus {
Pass,
Warn,
Fail,
Skip,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Check {
pub id: String,
pub status: CheckStatus,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>,
}
impl Check {
pub fn pass(id: impl Into<String>, message: impl Into<String>) -> Self {
Self {
id: id.into(),
status: CheckStatus::Pass,
message: message.into(),
location: None,
}
}
pub fn warn(id: impl Into<String>, message: impl Into<String>) -> Self {
Self {
id: id.into(),
status: CheckStatus::Warn,
message: message.into(),
location: None,
}
}
pub fn fail(id: impl Into<String>, message: impl Into<String>) -> Self {
Self {
id: id.into(),
status: CheckStatus::Fail,
message: message.into(),
location: None,
}
}
pub fn skip(id: impl Into<String>, message: impl Into<String>) -> Self {
Self {
id: id.into(),
status: CheckStatus::Skip,
message: message.into(),
location: None,
}
}
pub fn at(mut self, location: impl Into<String>) -> Self {
self.location = Some(location.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ReceiptSummary {
pub passed: usize,
pub warnings: usize,
pub failed: usize,
pub skipped: usize,
}
impl ReceiptSummary {
pub fn from_checks(checks: &[Check]) -> Self {
let mut summary = Self::default();
for check in checks {
match check.status {
CheckStatus::Pass => summary.passed += 1,
CheckStatus::Warn => summary.warnings += 1,
CheckStatus::Fail => summary.failed += 1,
CheckStatus::Skip => summary.skipped += 1,
}
}
summary
}
pub fn add(&mut self, other: &Self) {
self.passed += other.passed;
self.warnings += other.warnings;
self.failed += other.failed;
self.skipped += other.skipped;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheProofReceipt {
pub schema_version: SchemaVersion,
pub tool: ToolInfo,
pub checked_at: DateTime<Utc>,
pub mode: String,
pub store: Utf8PathBuf,
pub workspace: Utf8PathBuf,
pub summary: ReceiptSummary,
pub operations: Vec<CacheOperationReceipt>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub workflows: Vec<WorkflowCacheReport>,
pub checks: Vec<Check>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum CacheOperationKind {
Cache,
Restore,
Save,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum CacheMatchKind {
ExactKey,
PrefixKey,
RestoreExact,
RestorePrefix,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheMatch {
pub entry_id: String,
pub key: String,
pub version: String,
pub scope: String,
pub match_kind: CacheMatchKind,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CachePathRecord {
pub input: String,
pub resolved: Utf8PathBuf,
pub files: usize,
pub bytes: u64,
pub exists: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheOperationReceipt {
pub operation: CacheOperationKind,
pub key: String,
pub restore_keys: Vec<String>,
pub paths: Vec<String>,
pub version: String,
pub scope: String,
pub accessible_scopes: Vec<String>,
pub runner_os: RunnerOs,
pub compression: Compression,
pub enable_cross_os_archive: bool,
pub lookup_only: bool,
pub fail_on_cache_miss: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub matched: Option<CacheMatch>,
pub cache_hit: String,
pub restored_files: usize,
pub restored_bytes: u64,
#[serde(default, skip_serializing_if = "is_zero_usize")]
pub skipped_absolute_files: usize,
pub saved_files: usize,
pub saved_bytes: u64,
pub path_records: Vec<CachePathRecord>,
pub checks: Vec<Check>,
}
impl CacheOperationReceipt {
pub fn summary(&self) -> ReceiptSummary {
ReceiptSummary::from_checks(&self.checks)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowCacheReport {
pub workflow: Utf8PathBuf,
pub cache_steps: Vec<WorkflowCacheStep>,
pub summary: ReceiptSummary,
pub checks: Vec<Check>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowCacheStep {
pub job_id: String,
pub step_index: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub uses: String,
pub operation: CacheOperationKind,
pub key_template: String,
pub key: String,
pub restore_key_templates: Vec<String>,
pub restore_keys: Vec<String>,
pub path_templates: Vec<String>,
pub paths: Vec<String>,
pub expression_receipts: Vec<EvaluationReceipt>,
pub operation_receipt: CacheOperationReceipt,
pub checks: Vec<Check>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheIndex {
pub schema_version: SchemaVersion,
pub entries: Vec<CacheEntry>,
}
impl Default for CacheIndex {
fn default() -> Self {
Self {
schema_version: SCHEMA_VERSION,
entries: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheEntry {
pub id: String,
pub key: String,
pub version: String,
pub scope: String,
pub paths: Vec<String>,
pub runner_os: RunnerOs,
pub compression: Compression,
pub enable_cross_os_archive: bool,
pub created_at: DateTime<Utc>,
pub last_accessed_at: DateTime<Utc>,
pub files: usize,
pub bytes: u64,
}
fn is_zero_usize(value: &usize) -> bool {
*value == 0
}