use crate::types::{GhaWorkflowConfig, ImportConfig};
pub fn content_hash(bytes: &[u8]) -> String {
blake3::hash(bytes).to_hex().to_string()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImportFreshness {
Unpinned,
Fresh,
Stale { pinned: String, actual: String },
}
impl ImportFreshness {
pub fn is_current(&self) -> bool {
!matches!(self, ImportFreshness::Stale { .. })
}
}
impl ImportConfig {
pub fn freshness(&self, actual: &str) -> ImportFreshness {
match self.hash.as_deref() {
None => ImportFreshness::Unpinned,
Some(pinned) if pinned == actual => ImportFreshness::Fresh,
Some(pinned) => ImportFreshness::Stale {
pinned: pinned.to_string(),
actual: actual.to_string(),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImportExpansion {
Delegated(GhaWorkflowConfig),
}
pub fn expand_import(cfg: &ImportConfig) -> ImportExpansion {
ImportExpansion::Delegated(GhaWorkflowConfig {
path: cfg.source.clone(),
event: cfg.event.clone(),
inputs: cfg.inputs.clone(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn cfg(hash: Option<&str>) -> ImportConfig {
ImportConfig {
source: PathBuf::from(".github/workflows/release.yml"),
hash: hash.map(str::to_string),
materialize: false,
event: None,
inputs: Default::default(),
}
}
#[test]
fn content_hash_is_stable_and_byte_sensitive() {
let a = content_hash(b"name: release\n");
let b = content_hash(b"name: release\n");
let c = content_hash(b"name: release \n"); assert_eq!(a, b, "same bytes hash identically");
assert_ne!(a, c, "a one-byte edit changes the pin");
assert_eq!(a.len(), 64);
}
#[test]
fn freshness_unpinned_when_no_hash() {
assert_eq!(cfg(None).freshness("deadbeef"), ImportFreshness::Unpinned);
}
#[test]
fn freshness_fresh_on_match() {
let h = content_hash(b"on: push\n");
assert_eq!(cfg(Some(&h)).freshness(&h), ImportFreshness::Fresh);
}
#[test]
fn freshness_stale_on_drift_carries_both_hashes() {
let pinned = content_hash(b"on: push\n");
let actual = content_hash(b"on: workflow_dispatch\n");
let f = cfg(Some(&pinned)).freshness(&actual);
assert_eq!(
f,
ImportFreshness::Stale {
pinned: pinned.clone(),
actual: actual.clone(),
}
);
assert!(!f.is_current(), "stale is not current");
assert!(ImportFreshness::Fresh.is_current());
assert!(ImportFreshness::Unpinned.is_current());
}
#[test]
fn expand_forwards_source_event_and_inputs() {
let mut c = cfg(None);
c.event = Some("workflow_dispatch".into());
c.inputs.insert("tag".into(), "v1.2.3".into());
let ImportExpansion::Delegated(gha) = expand_import(&c);
assert_eq!(gha.path, c.source);
assert_eq!(gha.event.as_deref(), Some("workflow_dispatch"));
assert_eq!(gha.inputs.get("tag").map(String::as_str), Some("v1.2.3"));
}
}