1use std::collections::HashSet;
11
12use serde::{Deserialize, Serialize};
13
14use crate::error::ReleaseError;
15use crate::release::VcsProvider;
16
17pub const MANIFEST_ASSET_NAME: &str = "sr-manifest.json";
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct Manifest {
23 pub sr_version: String,
25 pub tag: String,
27 pub commit_sha: String,
29 pub artifacts: Vec<String>,
33 pub completed_at: String,
35}
36
37#[derive(Debug, Clone)]
39pub enum ReleaseStatus {
40 Complete(Manifest),
42 Incomplete {
46 manifest: Manifest,
47 missing_artifacts: Vec<String>,
48 },
49 Unknown,
53}
54
55impl ReleaseStatus {
56 pub fn is_complete(&self) -> bool {
57 matches!(self, ReleaseStatus::Complete(_))
58 }
59}
60
61pub fn check_release_status<V: VcsProvider + ?Sized>(
67 vcs: &V,
68 tag: &str,
69) -> Result<ReleaseStatus, ReleaseError> {
70 let bytes = match vcs.fetch_asset(tag, MANIFEST_ASSET_NAME)? {
71 Some(b) => b,
72 None => return Ok(ReleaseStatus::Unknown),
73 };
74 let manifest: Manifest = serde_json::from_slice(&bytes).map_err(|e| {
75 ReleaseError::Vcs(format!(
76 "failed to parse {MANIFEST_ASSET_NAME} on release {tag}: {e}"
77 ))
78 })?;
79
80 let assets: HashSet<String> = vcs.list_assets(tag)?.into_iter().collect();
81 let missing: Vec<String> = manifest
82 .artifacts
83 .iter()
84 .filter(|a| !assets.contains(a.as_str()))
85 .cloned()
86 .collect();
87
88 if missing.is_empty() {
89 Ok(ReleaseStatus::Complete(manifest))
90 } else {
91 Ok(ReleaseStatus::Incomplete {
92 manifest,
93 missing_artifacts: missing,
94 })
95 }
96}
97
98pub(crate) fn utc_rfc3339_now() -> String {
100 let secs = std::time::SystemTime::now()
101 .duration_since(std::time::UNIX_EPOCH)
102 .unwrap_or_default()
103 .as_secs() as i64;
104
105 let z = secs / 86400 + 719468;
107 let era = (if z >= 0 { z } else { z - 146096 }) / 146097;
108 let doe = (z - era * 146097) as u32;
109 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
110 let y = yoe as i64 + era * 400;
111 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
112 let mp = (5 * doy + 2) / 153;
113 let d = doy - (153 * mp + 2) / 5 + 1;
114 let m = if mp < 10 { mp + 3 } else { mp - 9 };
115 let y = if m <= 2 { y + 1 } else { y };
116
117 let sod = secs.rem_euclid(86400);
118 let h = sod / 3600;
119 let min = (sod % 3600) / 60;
120 let s = sod % 60;
121
122 format!("{y:04}-{m:02}-{d:02}T{h:02}:{min:02}:{s:02}Z")
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn manifest_round_trip_json() {
131 let m = Manifest {
132 sr_version: "7.1.0".into(),
133 tag: "v1.2.3".into(),
134 commit_sha: "a".repeat(40),
135 artifacts: vec!["sr-linux.tar.gz".into(), "sr-macos.tar.gz".into()],
136 completed_at: "2026-04-18T12:34:56Z".into(),
137 };
138 let json = serde_json::to_string(&m).unwrap();
139 let back: Manifest = serde_json::from_str(&json).unwrap();
140 assert_eq!(back.tag, "v1.2.3");
141 assert_eq!(back.artifacts.len(), 2);
142 }
143
144 #[test]
145 fn release_status_complete_is_complete() {
146 let m = Manifest {
147 sr_version: "7.1.0".into(),
148 tag: "v1.0.0".into(),
149 commit_sha: "abc".into(),
150 artifacts: vec!["a".into()],
151 completed_at: "t".into(),
152 };
153 assert!(ReleaseStatus::Complete(m).is_complete());
154 assert!(!ReleaseStatus::Unknown.is_complete());
155 }
156
157 #[test]
158 fn utc_rfc3339_now_is_well_formed() {
159 let s = utc_rfc3339_now();
160 assert_eq!(s.len(), 20, "got {s}");
161 assert!(s.ends_with('Z'));
162 assert_eq!(&s[4..5], "-");
163 assert_eq!(&s[10..11], "T");
164 }
165}