use crate::cli::args::ReleaseCommands;
use crate::cli::UI;
use crate::core::{Config, ScanEngine, Severity};
use crate::platform::{self, types::CreateRelease};
use anyhow::{bail, Result};
use std::path::PathBuf;
pub async fn execute(action: ReleaseCommands, ui: &UI) -> Result<()> {
match action {
ReleaseCommands::Create {
version,
name,
body,
draft,
prerelease,
no_scan,
fail_on,
attach,
} => {
create(
CreateReleaseParams {
version,
name,
body,
draft,
prerelease,
no_scan,
fail_on,
attach,
},
ui,
)
.await
}
ReleaseCommands::List { count } => list(count, ui).await,
}
}
struct CreateReleaseParams {
version: String,
name: Option<String>,
body: Option<String>,
draft: bool,
prerelease: bool,
no_scan: bool,
fail_on: String,
attach: Vec<PathBuf>,
}
async fn create(params: CreateReleaseParams, ui: &UI) -> Result<()> {
let path = PathBuf::from(".");
let remote = platform::detect_remote(&path)?;
let token = platform::resolve_token(&remote.host).ok_or_else(|| {
anyhow::anyhow!(
"Not authenticated. Run: securegit auth login --provider {}",
remote.host.to_string().to_lowercase()
)
})?;
let client = platform::create_client(&remote, token);
let release_name = params.name.unwrap_or_else(|| params.version.clone());
ui.header("SecureGit Release");
ui.blank();
ui.field("Version", ¶ms.version);
ui.field("Name", &release_name);
ui.field(
"Provider",
format!("{} ({}/{})", remote.host, remote.owner, remote.repo),
);
ui.blank();
let mut scan_report_json: Option<Vec<u8>> = None;
let mut security_summary = String::new();
if !params.no_scan {
let threshold = Severity::parse_str(¶ms.fail_on).unwrap_or(Severity::Critical);
let spinner = ui.spinner("Running security scan...");
let config = Config::default();
let engine = ScanEngine::new(config);
let report = engine.scan_directory(&path).await?;
ui.finish_progress(&spinner, "Scan complete");
let critical = report.count_by_severity(Severity::Critical);
let high = report.count_by_severity(Severity::High);
let medium = report.count_by_severity(Severity::Medium);
let low = report.count_by_severity(Severity::Low);
let info_count = report.count_by_severity(Severity::Info);
ui.blank();
ui.section("Scan Summary");
ui.severity_row(critical, high, medium, low, info_count);
if report.has_findings_at_or_above(threshold) {
let count = report
.findings
.iter()
.filter(|f| f.severity >= threshold)
.count();
ui.blank();
for finding in &report.findings {
if finding.severity >= threshold {
ui.finding(finding);
}
}
ui.blank();
bail!(
"Release blocked: {} finding(s) at or above {} severity. Use --no-scan to skip.",
count,
threshold
);
}
ui.blank();
ui.success(format!("Security gate passed (threshold: {})", threshold));
let commit_sha = crate::ops::open_repo(&path)
.ok()
.and_then(|r| {
let head = r.head().ok()?;
let commit = head.peel_to_commit().ok()?;
Some(commit.id().to_string())
})
.unwrap_or_default();
let attestation = serde_json::json!({
"securegit_version": env!("CARGO_PKG_VERSION"),
"scan_timestamp": chrono::Utc::now().to_rfc3339(),
"scanned_files": report.scanned_files,
"scanned_bytes": report.scanned_bytes,
"duration_ms": report.duration_ms,
"findings_summary": {
"critical": critical,
"high": high,
"medium": medium,
"low": low,
"info": info_count,
},
"findings": report.findings,
"commit_sha": commit_sha,
});
scan_report_json = Some(serde_json::to_vec_pretty(&attestation)?);
security_summary = format!(
"\n\n---\n**Security Scan** (by securegit {})\n- Files scanned: {}\n- Critical: {} | High: {} | Medium: {} | Low: {}\n- Scan report attached as release asset",
env!("CARGO_PKG_VERSION"),
report.scanned_files,
critical,
high,
medium,
low
);
}
let repo = crate::ops::open_repo(&path)?;
let tag_exists = repo
.revparse_single(&format!("refs/tags/{}", params.version))
.is_ok();
if !tag_exists {
let spinner = ui.spinner(&format!("Creating tag {}...", params.version));
let head = repo.head()?.peel_to_commit()?;
repo.tag_lightweight(¶ms.version, head.as_object(), false)?;
ui.finish_progress(&spinner, &format!("Tag {} created", params.version));
}
let spinner = ui.spinner("Pushing tag...");
let push_result = push_tag(&path, ¶ms.version);
if let Err(e) = push_result {
ui.finish_progress(&spinner, "");
ui.warning(format!("Could not auto-push tag: {}", e));
} else {
ui.finish_progress(&spinner, "Tag pushed");
}
let release_body = format!("{}{}", params.body.unwrap_or_default(), security_summary);
let spinner = ui.spinner("Creating release...");
let release = client
.create_release(&CreateRelease {
tag_name: params.version.clone(),
name: release_name,
body: release_body,
draft: params.draft,
prerelease: params.prerelease,
})
.await?;
ui.finish_progress(&spinner, "Release created");
if let Some(report_data) = scan_report_json {
let asset_name = format!("securegit-scan-report-{}.json", params.version);
let spinner = ui.spinner("Uploading scan report...");
client
.upload_release_asset(
&release.upload_url,
&asset_name,
"application/json",
report_data,
)
.await?;
ui.finish_progress(&spinner, "Scan report uploaded");
}
for asset_path in ¶ms.attach {
if !asset_path.exists() {
ui.warning(format!("Asset not found: {}", asset_path.display()));
continue;
}
let asset_name = asset_path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "unknown".to_string());
let data = std::fs::read(asset_path)?;
let content_type = "application/octet-stream";
let spinner = ui.spinner(&format!("Uploading {}...", asset_name));
client
.upload_release_asset(&release.upload_url, &asset_name, content_type, data)
.await?;
ui.finish_progress(&spinner, &format!("{} uploaded", asset_name));
}
ui.blank();
ui.success("Release created");
ui.blank();
ui.field("Tag", &release.tag_name);
ui.field("URL", &release.html_url);
if params.draft {
ui.field("Status", "Draft");
}
ui.blank();
if ui.json {
ui.json_out(&serde_json::to_value(&release).unwrap_or_default());
}
Ok(())
}
async fn list(count: usize, ui: &UI) -> Result<()> {
let path = PathBuf::from(".");
let remote = platform::detect_remote(&path)?;
let token = platform::resolve_token(&remote.host)
.ok_or_else(|| anyhow::anyhow!("Not authenticated. Run: securegit auth login"))?;
let client = platform::create_client(&remote, token);
ui.header("SecureGit Releases");
ui.blank();
ui.field("Repository", format!("{}/{}", remote.owner, remote.repo));
ui.blank();
let spinner = ui.spinner("Fetching releases...");
let releases = client.list_releases(count).await?;
ui.finish_progress(&spinner, "");
if releases.is_empty() {
ui.info("No releases found");
} else {
for release in &releases {
let pre = if release.prerelease {
" [pre-release]"
} else {
""
};
let d = if release.draft { " [draft]" } else { "" };
ui.list_item(format!(
"{:<12} {}{}{}",
release.tag_name, release.name, pre, d
));
}
}
if ui.json {
ui.json_out(&serde_json::to_value(&releases).unwrap_or_default());
}
ui.blank();
Ok(())
}
fn push_tag(path: &PathBuf, tag: &str) -> Result<()> {
let repo = crate::ops::open_repo(path)?;
let mut remote = repo.find_remote("origin").or_else(|_| {
let remotes = repo.remotes()?;
let name = remotes
.get(0)
.ok_or_else(|| git2::Error::from_str("no remotes"))?;
repo.find_remote(name)
})?;
let callbacks = crate::auth::build_git2_callbacks(None, None, None);
let mut push_opts = git2::PushOptions::new();
push_opts.remote_callbacks(callbacks);
let refspec = format!("refs/tags/{}:refs/tags/{}", tag, tag);
remote.push(&[&refspec], Some(&mut push_opts))?;
Ok(())
}