Menu
Public repository / a0c9d4ff8c1ee8397ccd5287465783b1f0f23d04
AkurAI-Build / src/statuses.rs
Latest change e023668b5652d0489e44a3b96fb4b0bd5c1f13ea - Commit statuses API: per-SHA job statuses and rollup from runs, branch badges (OLI-269) by Ólafur Búi Ólafsson
//! Commit statuses: the per-SHA view of run and job results. Statuses are
//! derived from the runs/jobs tables, so they cannot drift from the pipeline.
use std::collections::BTreeMap;
use anyhow::Result;
use serde::Serialize;
use crate::db::Database;
/// Most SHAs a single batch lookup accepts.
pub const MAX_BATCH_SHAS: usize = 100;
const RUN_CONTEXT: &str = "run";
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum State {
Pending,
Success,
Failure,
Error,
Skipped,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Rollup {
None,
Pending,
Success,
Failure,
}
#[derive(Clone, Debug, Serialize)]
pub struct CommitStatus {
pub context: String,
pub state: State,
pub run_id: i64,
pub git_ref: String,
pub created_at: i64,
pub finished_at: Option<i64>,
}
#[derive(Clone, Debug, Serialize)]
pub struct CombinedStatus {
pub sha: String,
pub state: Rollup,
pub statuses: Vec<CommitStatus>,
}
pub fn state_of(pipeline_status: &str) -> State {
match pipeline_status {
"succeeded" => State::Success,
"failed" => State::Failure,
"skipped" => State::Skipped,
"canceled" | "interrupted" => State::Error,
_ => State::Pending,
}
}
pub fn rollup(statuses: &[CommitStatus]) -> Rollup {
let mut result = Rollup::None;
for status in statuses {
match status.state {
State::Failure | State::Error => return Rollup::Failure,
State::Pending => result = Rollup::Pending,
State::Success if result == Rollup::None => result = Rollup::Success,
State::Success | State::Skipped => {}
}
}
result
}
/// One status per job name, taken from the latest run of the commit; a run
/// that has no jobs yet reports as a single `run` status.
pub fn combined(database: &Database, repository: &str, sha: &str) -> Result<CombinedStatus> {
let mut latest: BTreeMap<String, CommitStatus> = BTreeMap::new();
for run in database.runs_for_commit(repository, sha)? {
let jobs = database.jobs(run.id)?;
let entries = if jobs.is_empty() {
vec![(RUN_CONTEXT.to_owned(), run.status.clone(), run.finished_at)]
} else {
jobs.into_iter()
.map(|job| (job.name, job.status, job.finished_at))
.collect()
};
for (context, status, finished_at) in entries {
latest.insert(
context.clone(),
CommitStatus {
context,
state: state_of(&status),
run_id: run.id,
git_ref: run.git_ref.clone(),
created_at: run.created_at,
finished_at,
},
);
}
}
let statuses: Vec<CommitStatus> = latest.into_values().collect();
Ok(CombinedStatus {
sha: sha.to_owned(),
state: rollup(&statuses),
statuses,
})
}
#[cfg(test)]
mod tests {
use super::*;
const KEY: &str = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
const SHA: &str = "1111111111111111111111111111111111111111";
fn status(state: State) -> CommitStatus {
CommitStatus {
context: "j".into(),
state,
run_id: 1,
git_ref: "main".into(),
created_at: 0,
finished_at: None,
}
}
#[test]
fn pipeline_states_map_to_commit_states() {
for (raw, expected) in [
("queued", State::Pending),
("running", State::Pending),
("waiting", State::Pending),
("succeeded", State::Success),
("failed", State::Failure),
("canceled", State::Error),
("interrupted", State::Error),
("skipped", State::Skipped),
] {
assert_eq!(state_of(raw), expected, "{raw}");
}
}
#[test]
fn rollup_prefers_failure_then_pending_then_success() {
assert_eq!(rollup(&[]), Rollup::None);
assert_eq!(rollup(&[status(State::Skipped)]), Rollup::None);
assert_eq!(
rollup(&[status(State::Success), status(State::Skipped)]),
Rollup::Success
);
assert_eq!(
rollup(&[status(State::Success), status(State::Pending)]),
Rollup::Pending
);
assert_eq!(
rollup(&[
status(State::Pending),
status(State::Error),
status(State::Success)
]),
Rollup::Failure
);
}
#[test]
fn combined_uses_latest_run_per_job_and_ignores_other_shas() -> Result<()> {
let database = Database::memory(KEY)?;
let repo = database.add_repository("demo", "https://example.com/demo.git", "main")?;
let first = database.create_run(repo.id, "main", Some(SHA), "manual")?;
let retry = database.create_run(repo.id, "main", Some(SHA), "retry")?;
let other = database.create_run(repo.id, "main", Some(&"2".repeat(40)), "manual")?;
for run in [first, retry, other] {
database.insert_jobs(run, &[crate::db::tests::dummy_job_spec("verify", "verify")])?;
}
let job_of = |run| database.jobs(run).map(|jobs| jobs[0].id);
database.set_job_status(job_of(first)?, "failed", None, Some(1))?;
database.set_job_status(job_of(retry)?, "succeeded", None, Some(0))?;
database.set_job_status(job_of(other)?, "failed", None, Some(1))?;
let result = combined(&database, "demo", SHA)?;
assert_eq!(result.state, Rollup::Success);
assert_eq!(result.statuses.len(), 1);
assert_eq!(result.statuses[0].run_id, retry);
assert_eq!(
combined(&database, "demo", &"3".repeat(40))?.state,
Rollup::None
);
Ok(())
}
}