use crate::version::{ReleaseArtifact, SemanticVersion};
use std::path::{Path, PathBuf};
pub struct ReleaseManager {
pub workspace_root: PathBuf,
pub release_dir: PathBuf,
}
impl ReleaseManager {
pub fn new(workspace_root: PathBuf) -> Self {
let release_dir = workspace_root.join("releases");
Self {
workspace_root,
release_dir,
}
}
pub fn validate_release(&self) -> Result<(), String> {
let cargo_lock = self.workspace_root.join("Cargo.lock");
if !cargo_lock.exists() {
return Err("Cargo.lock not committed (required for reproducible builds)".to_string());
}
let cargo_toml = self.workspace_root.join("Cargo.toml");
if !cargo_toml.exists() {
return Err("Cargo.toml not found in workspace root".to_string());
}
Ok(())
}
pub fn generate_manifest(
&self,
version: SemanticVersion,
artifacts: &[ReleaseArtifact],
) -> String {
let mut manifest = format!("# Sentri Release {}\n\n", version);
manifest.push_str("## Artifacts\n\n");
for artifact in artifacts {
manifest.push_str(&format!(
"- {} ({})\n",
artifact.filename(),
artifact.checksum
));
}
manifest.push_str("\n## Installation\n\n");
manifest.push_str("```bash\n");
manifest.push_str("# Extract the appropriate archive for your platform:\n");
manifest.push_str("tar xzf sentri-VERSION-PLATFORM.tar.gz\n");
manifest.push_str("sudo mv sentri /usr/local/bin/\n");
manifest.push_str("```\n");
manifest.push_str("\n## Verification\n\n");
manifest.push_str("Verify the checksum (replace CHECKSUM):\n");
manifest.push_str("```bash\n");
manifest.push_str("sha256sum -c sentri-CHECKSUM.txt\n");
manifest.push_str("```\n");
manifest
}
pub fn verify_artifact(
&self,
artifact_path: &Path,
expected_checksum: &str,
) -> Result<(), String> {
if !artifact_path.exists() {
return Err(format!("Artifact not found: {}", artifact_path.display()));
}
let computed = compute_file_sha256(artifact_path)
.map_err(|e| format!("Failed to compute checksum: {}", e))?;
if !computed.eq_ignore_ascii_case(expected_checksum) {
return Err(format!(
"Checksum mismatch: expected {}, got {}",
expected_checksum, computed
));
}
Ok(())
}
}
fn compute_file_sha256(path: &Path) -> Result<String, std::io::Error> {
use std::collections::hash_map::DefaultHasher;
use std::fs::File;
use std::hash::Hasher;
use std::io::Read;
let mut file = File::open(path)?;
let mut buffer = [0; 8192];
let mut hasher = DefaultHasher::new();
loop {
let n = file.read(&mut buffer)?;
if n == 0 {
break;
}
hasher.write(&buffer[..n]);
}
Ok(format!("{:016x}", hasher.finish()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_manifest_generation() {
let artifacts = vec![
ReleaseArtifact::new(
SemanticVersion::new(0, 1, 0),
"linux-x86_64".to_string(),
"abc123".to_string(),
true,
),
ReleaseArtifact::new(
SemanticVersion::new(0, 1, 0),
"darwin-aarch64".to_string(),
"def456".to_string(),
true,
),
];
let manager = ReleaseManager::new(std::path::PathBuf::from("/tmp"));
let manifest = manager.generate_manifest(SemanticVersion::new(0, 1, 0), &artifacts);
assert!(manifest.contains("Sentri Release 0.1.0"));
assert!(manifest.contains("linux-x86_64"));
assert!(manifest.contains("darwin-aarch64"));
assert!(manifest.contains("Installation"));
}
#[test]
fn test_validation_checks() {
let manager = ReleaseManager::new(std::path::PathBuf::from("/tmp"));
assert!(manager.validate_release().is_err());
}
}