1use async_trait::async_trait;
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use sha2::Sha256;
15
16use crate::error::StorageError;
17
18pub type StorageResult<T> = std::result::Result<T, StorageError>;
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
30pub struct ContentDigest(String);
31
32impl ContentDigest {
33 pub fn from_bytes(data: &[u8]) -> Self {
35 use sha2::Digest;
36 let mut hasher = Sha256::new();
37 hasher.update(data);
38 ContentDigest(hex::encode(hasher.finalize()))
39 }
40
41 pub fn as_str(&self) -> &str {
43 &self.0
44 }
45
46 pub fn short(&self) -> String {
48 self.0.chars().take(12).collect()
49 }
50}
51
52impl TryFrom<String> for ContentDigest {
53 type Error = StorageError;
54
55 fn try_from(s: String) -> std::result::Result<Self, Self::Error> {
56 if s.len() != 64 || !s.chars().all(|c| c.is_ascii_hexdigit()) {
57 return Err(StorageError::InvalidDigest { digest: s });
58 }
59 Ok(ContentDigest(s.to_ascii_lowercase()))
60 }
61}
62
63impl std::fmt::Display for ContentDigest {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 write!(f, "{}", self.0)
66 }
67}
68
69#[async_trait]
76pub trait CasStore: Send + Sync {
77 async fn put(&self, data: &[u8]) -> StorageResult<ContentDigest>;
79
80 async fn get(&self, digest: &ContentDigest) -> StorageResult<Vec<u8>>;
82
83 async fn contains(&self, digest: &ContentDigest) -> StorageResult<bool>;
85
86 async fn delete(&self, digest: &ContentDigest) -> StorageResult<()>;
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
96pub struct RunId(pub String);
97
98impl RunId {
99 pub fn new() -> Self {
101 RunId(uuid::Uuid::new_v4().to_string())
102 }
103}
104
105impl Default for RunId {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111impl std::fmt::Display for RunId {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 write!(f, "{}", self.0)
114 }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct RunMetadata {
120 pub git_sha: Option<String>,
122 pub agent_name: String,
124 pub tags: serde_json::Value,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct RunEvent {
131 pub seq: u64,
133 pub kind: String,
135 pub payload: serde_json::Value,
137 pub timestamp: DateTime<Utc>,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct RunSummary {
144 pub total_events: u64,
146 pub final_state_digest: Option<ContentDigest>,
148 pub duration_ms: u64,
150 pub success: bool,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156#[serde(rename_all = "UPPERCASE")]
157pub enum RunStatus {
158 Running,
159 Completed,
160 Failed,
161 Cancelled,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct RunRecord {
167 pub run_id: RunId,
168 pub spec_digest: ContentDigest,
169 pub metadata: RunMetadata,
170 pub status: RunStatus,
171 pub summary: Option<RunSummary>,
172 pub created_at: DateTime<Utc>,
173 pub completed_at: Option<DateTime<Utc>>,
174}
175
176#[async_trait]
183pub trait RunLedger: Send + Sync {
184 async fn create_run(
186 &self,
187 spec_digest: &ContentDigest,
188 metadata: RunMetadata,
189 ) -> StorageResult<RunId>;
190
191 async fn append_event(&self, run_id: &RunId, event: RunEvent) -> StorageResult<()>;
193
194 async fn complete_run(&self, run_id: &RunId, summary: RunSummary) -> StorageResult<()>;
196
197 async fn fail_run(&self, run_id: &RunId, summary: RunSummary) -> StorageResult<()>;
199
200 async fn cancel_run(&self, run_id: &RunId, summary: RunSummary) -> StorageResult<()>;
202
203 async fn get_run(&self, run_id: &RunId) -> StorageResult<RunRecord>;
205
206 async fn get_events(&self, run_id: &RunId) -> StorageResult<Vec<RunEvent>>;
208
209 async fn list_runs(&self, spec_digest: Option<&ContentDigest>)
211 -> StorageResult<Vec<RunRecord>>;
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct ReleaseMetadata {
221 pub version_label: Option<String>,
223 pub promoted_by: String,
225 pub notes: Option<String>,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct ReleaseRecord {
232 pub name: String,
234 pub spec_digest: ContentDigest,
236 pub metadata: ReleaseMetadata,
238 pub created_at: DateTime<Utc>,
240}
241
242#[async_trait]
251pub trait ReleaseRegistry: Send + Sync {
252 async fn promote(
254 &self,
255 name: &str,
256 spec_digest: &ContentDigest,
257 metadata: ReleaseMetadata,
258 ) -> StorageResult<ReleaseRecord>;
259
260 async fn rollback(&self, name: &str) -> StorageResult<ReleaseRecord>;
262
263 async fn current(&self, name: &str) -> StorageResult<Option<ReleaseRecord>>;
265
266 async fn history(&self, name: &str) -> StorageResult<Vec<ReleaseRecord>>;
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn test_run_status_serialization() {
276 let status = RunStatus::Running;
277 let json = serde_json::to_string(&status).unwrap();
278 assert_eq!(json, "\"RUNNING\"");
279
280 let status = RunStatus::Completed;
281 let json = serde_json::to_string(&status).unwrap();
282 assert_eq!(json, "\"COMPLETED\"");
283 }
284}