1use crate::version::{ReleaseArtifact, SemanticVersion};
10use std::path::{Path, PathBuf};
11
12pub struct ReleaseManager {
14 pub workspace_root: PathBuf,
16 pub release_dir: PathBuf,
18}
19
20impl ReleaseManager {
21 pub fn new(workspace_root: PathBuf) -> Self {
23 let release_dir = workspace_root.join("releases");
24 Self {
25 workspace_root,
26 release_dir,
27 }
28 }
29
30 pub fn validate_release(&self) -> Result<(), String> {
37 let cargo_lock = self.workspace_root.join("Cargo.lock");
39 if !cargo_lock.exists() {
40 return Err("Cargo.lock not committed (required for reproducible builds)".to_string());
41 }
42
43 let cargo_toml = self.workspace_root.join("Cargo.toml");
45 if !cargo_toml.exists() {
46 return Err("Cargo.toml not found in workspace root".to_string());
47 }
48
49 Ok(())
50 }
51
52 pub fn generate_manifest(
54 &self,
55 version: SemanticVersion,
56 artifacts: &[ReleaseArtifact],
57 ) -> String {
58 let mut manifest = format!("# Sentri Release {}\n\n", version);
59 manifest.push_str("## Artifacts\n\n");
60
61 for artifact in artifacts {
62 manifest.push_str(&format!(
63 "- {} ({})\n",
64 artifact.filename(),
65 artifact.checksum
66 ));
67 }
68
69 manifest.push_str("\n## Installation\n\n");
70 manifest.push_str("```bash\n");
71 manifest.push_str("# Extract the appropriate archive for your platform:\n");
72 manifest.push_str("tar xzf sentri-VERSION-PLATFORM.tar.gz\n");
73 manifest.push_str("sudo mv sentri /usr/local/bin/\n");
74 manifest.push_str("```\n");
75
76 manifest.push_str("\n## Verification\n\n");
77 manifest.push_str("Verify the checksum (replace CHECKSUM):\n");
78 manifest.push_str("```bash\n");
79 manifest.push_str("sha256sum -c sentri-CHECKSUM.txt\n");
80 manifest.push_str("```\n");
81
82 manifest
83 }
84
85 pub fn verify_artifact(
87 &self,
88 artifact_path: &Path,
89 expected_checksum: &str,
90 ) -> Result<(), String> {
91 if !artifact_path.exists() {
92 return Err(format!("Artifact not found: {}", artifact_path.display()));
93 }
94
95 let computed = compute_file_sha256(artifact_path)
97 .map_err(|e| format!("Failed to compute checksum: {}", e))?;
98
99 if !computed.eq_ignore_ascii_case(expected_checksum) {
100 return Err(format!(
101 "Checksum mismatch: expected {}, got {}",
102 expected_checksum, computed
103 ));
104 }
105
106 Ok(())
107 }
108}
109
110fn compute_file_sha256(path: &Path) -> Result<String, std::io::Error> {
115 use std::collections::hash_map::DefaultHasher;
116 use std::fs::File;
117 use std::hash::Hasher;
118 use std::io::Read;
119
120 let mut file = File::open(path)?;
121 let mut buffer = [0; 8192];
122 let mut hasher = DefaultHasher::new();
123
124 loop {
126 let n = file.read(&mut buffer)?;
127 if n == 0 {
128 break;
129 }
130 hasher.write(&buffer[..n]);
131 }
132
133 Ok(format!("{:016x}", hasher.finish()))
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn test_manifest_generation() {
143 let artifacts = vec![
144 ReleaseArtifact::new(
145 SemanticVersion::new(0, 1, 0),
146 "linux-x86_64".to_string(),
147 "abc123".to_string(),
148 true,
149 ),
150 ReleaseArtifact::new(
151 SemanticVersion::new(0, 1, 0),
152 "darwin-aarch64".to_string(),
153 "def456".to_string(),
154 true,
155 ),
156 ];
157
158 let manager = ReleaseManager::new(std::path::PathBuf::from("/tmp"));
159 let manifest = manager.generate_manifest(SemanticVersion::new(0, 1, 0), &artifacts);
160
161 assert!(manifest.contains("Sentri Release 0.1.0"));
162 assert!(manifest.contains("linux-x86_64"));
163 assert!(manifest.contains("darwin-aarch64"));
164 assert!(manifest.contains("Installation"));
165 }
166
167 #[test]
168 fn test_validation_checks() {
169 let manager = ReleaseManager::new(std::path::PathBuf::from("/tmp"));
170 assert!(manager.validate_release().is_err());
172 }
173}