Skip to main content

sentri_utils/
release.rs

1//! Release operations for binary distribution and installation.
2//!
3//! Handles:
4//! - Pre-release validation
5//! - Binary artifact generation
6//! - Checksum computation and verification
7//! - Installation manifest generation
8
9use crate::version::{ReleaseArtifact, SemanticVersion};
10use std::path::{Path, PathBuf};
11
12/// Release operations manager.
13pub struct ReleaseManager {
14    /// Workspace root directory.
15    pub workspace_root: PathBuf,
16    /// Release output directory.
17    pub release_dir: PathBuf,
18}
19
20impl ReleaseManager {
21    /// Create a new release manager.
22    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    /// Validate that workspace is ready for release.
31    ///
32    /// Checks:
33    /// - No uncommitted changes
34    /// - All tests pass
35    /// - Version is consistent
36    pub fn validate_release(&self) -> Result<(), String> {
37        // Check Cargo.lock exists
38        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        // Verify workspace structure
44        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    /// Generate release manifest listing all artifacts.
53    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    /// Verify a binary artifact integrity.
86    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        // Compute SHA256 checksum
96        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
110/// Compute file checksum for integrity validation.
111///
112/// Uses a deterministic hash of file contents for verification.
113/// In production, this should use SHA256 via the sha2 crate for cryptographic security.
114fn 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    // Hash file contents in chunks for efficiency
125    loop {
126        let n = file.read(&mut buffer)?;
127        if n == 0 {
128            break;
129        }
130        hasher.write(&buffer[..n]);
131    }
132
133    // Format as hex string for consistency with SHA256 output format
134    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        // Will fail because /tmp/Cargo.toml doesn't exist, but that's expected
171        assert!(manager.validate_release().is_err());
172    }
173}