Login
4 branches 0 tags
Ben (Desktop/Arch) Code cleanup ada8ea6 11 days ago 251 Commits
rubhub / crates / ci / src / storage.rs
//! 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)
}