text/x-rust
•
3.56 KB
•
127 lines
//! CI job persistence
//!
//! Handles saving, loading, and listing CI jobs from disk.
use std::path::Path;
use anyhow::{Context, Result};
use tokio::fs;
use crate::models::{
CiJob, CiJobResultResponse, CiRunStatusResponse, CiStepResultResponse,
};
/// Save job metadata to disk atomically (write to temp, then rename)
pub async fn save_job(job: &CiJob, ci_root: &Path) -> Result<()> {
let job_file = job.job_file(ci_root);
let tmp_file = job_file.with_extension("json.tmp");
let content = serde_json::to_string_pretty(job)?;
fs::write(&tmp_file, &content).await?;
fs::rename(&tmp_file, &job_file).await?;
Ok(())
}
/// Load a job from disk
pub async fn load_job(ci_root: &Path, owner: &str, project: &str, job_id: &str) -> Result<CiJob> {
let job_file = ci_root
.join(owner)
.join(project)
.join(job_id)
.join("job.json");
let content = fs::read_to_string(&job_file)
.await
.with_context(|| format!("Failed to read job file: {}", job_file.display()))?;
let job: CiJob = serde_json::from_str(&content)?;
Ok(job)
}
/// Load the log file for a specific job within a CI run
pub async fn load_job_log(
ci_root: &Path,
owner: &str,
project: &str,
run_id: &str,
job_name: &str,
) -> String {
let job_dir = ci_root
.join(owner)
.join(project)
.join(run_id)
.join("jobs")
.join(job_name);
fs::read_to_string(job_dir.join("output.log"))
.await
.unwrap_or_default()
}
/// Load a CI run with all job logs (for JSON API response)
pub async fn load_run_with_all_logs(
ci_root: &Path,
owner: &str,
project: &str,
run_id: &str,
) -> Result<CiRunStatusResponse> {
let run = load_job(ci_root, owner, project, run_id).await?;
let mut job_responses = Vec::new();
for job_result in &run.jobs {
let log = load_job_log(ci_root, owner, project, run_id, &job_result.name).await;
let step_responses: Vec<CiStepResultResponse> = job_result
.steps
.iter()
.map(|s| CiStepResultResponse {
name: s.name.clone(),
status: s.status.as_str().to_string(),
exit_code: s.exit_code,
})
.collect();
job_responses.push(CiJobResultResponse {
name: job_result.name.clone(),
status: job_result.status.as_str().to_string(),
exit_code: job_result.exit_code,
steps: step_responses,
log,
});
}
Ok(CiRunStatusResponse {
id: run.id.to_string(),
status: run.status.as_str().to_string(),
jobs: job_responses,
})
}
/// List all jobs for a project, sorted by creation time (newest first)
pub async fn list_jobs(ci_root: &Path, owner: &str, project: &str) -> Result<Vec<CiJob>> {
let project_dir = ci_root.join(owner).join(project);
if !project_dir.exists() {
return Ok(vec![]);
}
let mut jobs = Vec::new();
let mut entries = fs::read_dir(&project_dir).await?;
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
if path.is_dir() {
let job_file = path.join("job.json");
if job_file.exists()
&& let Ok(content) = fs::read_to_string(&job_file).await
&& let Ok(job) = serde_json::from_str::<CiJob>(&content)
{
jobs.push(job);
}
}
}
// Sort by created_at descending (newest first)
jobs.sort_by(|a, b| b.created_at.cmp(&a.created_at));
Ok(jobs)
}