use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use argonauts_core::events::RunEvent;
use argonauts_core::events::workflow::{StepEnd, WorkflowEvent};
use argonauts_core::names::{ApprovalId, WorkflowId};
use argonauts_core::workflow::{
Phase, StandardPhase, Step, StepKind, StepRef, Workflow, WorkflowRef,
};
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Workflows {
pub by_id: BTreeMap<WorkflowId, WorkflowRecord>,
pub root: Option<WorkflowId>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WorkflowRecord {
pub id: WorkflowId,
pub from: Option<WorkflowRef>,
pub version: String,
pub parent: Option<StepRef>,
pub workflow: Workflow,
pub steps: Vec<StepProgress>,
pub finished: bool,
#[serde(default)]
pub changed_from_phase: Option<u32>,
#[serde(default)]
pub since: u64,
#[serde(default)]
pub changes: Vec<ApprovalId>,
#[serde(default)]
pub declared: Option<String>,
#[serde(default)]
pub phases_started: BTreeSet<u32>,
#[serde(default)]
pub phases_ended: BTreeSet<u32>,
#[serde(default)]
pub phases_skipped: BTreeSet<u32>,
#[serde(default)]
pub left: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StepProgress {
pub step: StepRef,
pub kind: StepKind,
pub standard: StandardPhase,
pub round: u32,
pub since: u64,
pub end: Option<StepEnd>,
pub summary: Option<String>,
#[serde(default)]
pub ended_at: Option<u64>,
}
impl WorkflowRecord {
pub fn phase_definition(&self, phase: u32) -> Option<&Phase> {
if phase < self.changed_from_phase.unwrap_or(0) {
return None;
}
self.workflow.phases.get(phase as usize)
}
pub fn step_definition(&self, step: &StepRef) -> Option<&Step> {
let (first, inner) = step.path.split_first()?;
let phase = self.phase_definition(step.phase)?;
let mut at = phase.steps.get(*first as usize)?;
for index in inner {
at = at.steps.get(*index as usize)?;
}
Some(at)
}
pub fn progress(&self, step: &StepRef) -> Option<&StepProgress> {
self.steps
.iter()
.rev()
.find(|progress| progress.step == *step)
}
pub fn phase_finished(&self, phase: u32) -> bool {
if phase < self.changed_from_phase.unwrap_or(0) {
return !self.phases_started.contains(&phase)
|| self.phases_ended.contains(&phase)
|| self.phases_skipped.contains(&phase);
}
let Some(definition) = self.workflow.phases.get(phase as usize) else {
return false;
};
(0..definition.steps.len() as u32).all(|index| {
let step = StepRef {
workflow: self.id.clone(),
phase,
path: vec![index],
};
self.progress(&step)
.is_some_and(|progress| progress.end.is_some())
})
}
pub fn phase_started(&self, phase: u32) -> bool {
self.steps
.iter()
.any(|progress| progress.step.phase == phase)
}
pub fn round(&self, step: &StepRef, round: u32) -> Option<&StepProgress> {
self.steps
.iter()
.rev()
.find(|progress| progress.step == *step && progress.round == round)
}
pub fn next_phase(&self) -> u32 {
let phases = self.workflow.phases.len() as u32;
(0..phases)
.find(|phase| !self.phase_finished(*phase))
.unwrap_or(phases)
}
pub fn change_from(&self) -> u32 {
let next = self.next_phase();
if self.phase_started(next) {
next + 1
} else {
next
}
}
}
impl Workflows {
pub fn apply(&mut self, event: &WorkflowEvent, envelope: &RunEvent) {
match event {
WorkflowEvent::Started(started) => {
if started.parent.is_none() {
self.root = Some(started.id.clone());
}
self.by_id.insert(
started.id.clone(),
WorkflowRecord {
id: started.id.clone(),
from: started.from.clone(),
version: started.version.clone(),
parent: started.parent.clone(),
workflow: started.workflow.clone(),
steps: Vec::new(),
finished: false,
changed_from_phase: None,
since: envelope.seq,
changes: Vec::new(),
declared: None,
phases_started: BTreeSet::new(),
phases_ended: BTreeSet::new(),
phases_skipped: BTreeSet::new(),
left: Vec::new(),
},
);
}
WorkflowEvent::Declared(declared) => {
if let Some(record) = self.by_id.get_mut(&declared.id) {
record.declared = Some(record.version.clone());
}
}
WorkflowEvent::PhaseStarted(mark) => {
if let Some(record) = self.by_id.get_mut(&mark.id) {
record.phases_started.insert(mark.index);
}
}
WorkflowEvent::PhaseEnded { phase, .. } => {
if let Some(record) = self.by_id.get_mut(&phase.id) {
record.phases_ended.insert(phase.index);
}
}
WorkflowEvent::PhaseSkipped { phase, .. } => {
if let Some(record) = self.by_id.get_mut(&phase.id) {
record.phases_skipped.insert(phase.index);
}
}
WorkflowEvent::Note(note) if note.left => {
let id = note.workflow.as_ref().or(self.root.as_ref());
if let Some(record) = id.and_then(|id| self.by_id.get_mut(id)) {
record.left.push(note.text.clone());
}
}
WorkflowEvent::PhaseAdded { .. }
| WorkflowEvent::Note(_)
| WorkflowEvent::LeftOut(_)
| WorkflowEvent::Reported(_) => {}
WorkflowEvent::StepStarted(started) => {
if let Some(record) = self.by_id.get_mut(&started.step.workflow) {
record.steps.push(StepProgress {
step: started.step.clone(),
kind: started.kind,
standard: started.standard,
round: started.round,
since: envelope.seq,
end: None,
summary: None,
ended_at: None,
});
}
}
WorkflowEvent::StepFinished(finished) => {
let Some(record) = self.by_id.get_mut(&finished.step.workflow) else {
return;
};
match record.steps.iter_mut().rev().find(|progress| {
progress.step == finished.step
&& progress.round == finished.round
&& progress.end.is_none()
}) {
Some(progress) => {
progress.end = Some(finished.end);
progress.summary = finished.summary.clone();
progress.ended_at = Some(envelope.seq);
}
None => record.steps.push(StepProgress {
step: finished.step.clone(),
kind: finished.kind,
standard: finished.standard,
round: finished.round,
since: envelope.seq,
end: Some(finished.end),
summary: finished.summary.clone(),
ended_at: Some(envelope.seq),
}),
}
let top_level = finished.step.path.len() == 1;
let last_phase = record.workflow.phases.len().saturating_sub(1) as u32;
if top_level
&& finished.step.phase == last_phase
&& record.phase_finished(last_phase)
{
record.finished = true;
}
if matches!(finished.end, StepEnd::Stopped) && top_level {
record.finished = true;
}
}
WorkflowEvent::Changed(changed) => {
if let Some(record) = self.by_id.get_mut(&changed.id) {
record.changed_from_phase = Some(record.change_from());
record.version = changed.version.clone();
record.workflow = changed.workflow.clone();
record.finished = false;
record.changes.extend(changed.approval.clone());
}
}
}
}
pub fn progress(&self, step: &StepRef) -> Option<&StepProgress> {
self.by_id.get(&step.workflow)?.progress(step)
}
pub fn depth(&self, id: &WorkflowId) -> u32 {
let mut depth = 0;
let mut at = self.by_id.get(id);
while let Some(parent) = at.and_then(|record| record.parent.as_ref()) {
depth += 1;
at = self.by_id.get(&parent.workflow);
}
depth
}
pub fn current_standard(&self) -> Option<StandardPhase> {
self.running().last().map(|(_, progress)| progress.standard)
}
pub fn running(&self) -> Vec<(&WorkflowRecord, &StepProgress)> {
let mut levels = Vec::new();
let mut at = self.root.as_ref().and_then(|id| self.by_id.get(id));
while let Some(record) = at {
let Some(running) = record
.steps
.iter()
.rev()
.find(|progress| progress.end.is_none())
else {
break;
};
levels.push((record, running));
at = self
.by_id
.values()
.find(|child| child.parent.as_ref() == Some(&running.step) && !child.finished);
}
levels
}
pub fn step_at(&self, seq: u64) -> Option<&StepProgress> {
self.by_id
.values()
.flat_map(|record| &record.steps)
.filter(|progress| {
progress.since <= seq && progress.ended_at.is_none_or(|ended| seq <= ended)
})
.max_by_key(|progress| progress.since)
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use time::OffsetDateTime;
use argonauts_core::RunId;
use argonauts_core::events::RunEventKind;
use argonauts_core::events::workflow::{
PhaseMark, StepFinished, StepStarted, WorkflowChanged, WorkflowStarted,
};
use argonauts_core::names::ApprovalId;
use super::*;
fn envelope(seq: u64, event: &WorkflowEvent) -> RunEvent {
RunEvent {
seq,
ts: OffsetDateTime::UNIX_EPOCH,
run_id: RunId::new("run-01").unwrap(),
task_id: None,
session: None,
source: "argonauts-code".to_owned(),
kind: RunEventKind::Workflow(event.clone()),
}
}
fn step(id: &WorkflowId, phase: u32, path: &[u32]) -> StepRef {
StepRef {
workflow: id.clone(),
phase,
path: path.to_vec(),
}
}
fn started(step: StepRef, kind: StepKind) -> WorkflowEvent {
WorkflowEvent::StepStarted(StepStarted {
step,
kind,
standard: StandardPhase::Do,
round: 1,
})
}
fn finished(step: StepRef, kind: StepKind) -> WorkflowEvent {
WorkflowEvent::StepFinished(StepFinished {
step,
kind,
standard: StandardPhase::Do,
round: 1,
end: StepEnd::Done,
summary: None,
})
}
fn workflow(first_phase: &str) -> Workflow {
serde_json::from_value(json!({
"name": "loop",
"phases": [
{"name": first_phase, "standard": "do", "steps": [
{"kind": "loop_until", "until": "checks_pass", "max": 3, "steps": [
{"kind": "build", "name": "Fix"},
{"kind": "checks"},
]},
]},
{"name": "Check", "standard": "check", "steps": [{"kind": "review"}]},
],
}))
.unwrap()
}
#[test]
fn steps_found_by_seq_and_by_path() {
let id = WorkflowId::numbered(1);
let events = [
WorkflowEvent::Started(Box::new(WorkflowStarted {
id: id.clone(),
from: None,
version: "v1".to_owned(),
parent: None,
workflow: workflow("Build"),
})),
started(step(&id, 0, &[0]), StepKind::LoopUntil),
started(step(&id, 0, &[0, 0]), StepKind::Build),
finished(step(&id, 0, &[0, 0]), StepKind::Build),
started(step(&id, 0, &[0, 1]), StepKind::Checks),
];
let mut workflows = Workflows::default();
for (seq, event) in (1..).zip(&events) {
workflows.apply(event, &envelope(seq, event));
}
let kind_at = |seq| workflows.step_at(seq).map(|progress| progress.kind);
assert_eq!(kind_at(1), None);
assert_eq!(kind_at(2), Some(StepKind::LoopUntil));
assert_eq!(kind_at(3), Some(StepKind::Build));
assert_eq!(kind_at(4), Some(StepKind::Build));
assert_eq!(kind_at(9), Some(StepKind::Checks));
let levels = workflows.running();
assert_eq!(levels.len(), 1);
assert_eq!(levels[0].1.step.path, [0, 1]);
let record = &workflows.by_id[&id];
let fix = record.step_definition(&step(&id, 0, &[0, 0])).unwrap();
assert_eq!(fix.name.as_deref(), Some("Fix"));
assert!(record.step_definition(&step(&id, 0, &[0, 2])).is_none());
let change = WorkflowEvent::Changed(Box::new(WorkflowChanged {
id: id.clone(),
version: "v2".to_owned(),
workflow: workflow("Rebuild"),
approval: Some(ApprovalId::numbered(1)),
reason: "one more check".to_owned(),
}));
workflows.apply(&change, &envelope(6, &change));
let record = &workflows.by_id[&id];
assert!(record.phase_definition(0).is_none());
assert!(record.step_definition(&step(&id, 0, &[0, 0])).is_none());
assert_eq!(record.phase_definition(1).unwrap().name, "Check");
}
#[test]
fn the_phase_running_when_a_change_came_is_finished_only_when_it_ends() {
let id = WorkflowId::numbered(1);
let mark = PhaseMark {
id: id.clone(),
index: 0,
name: "Build".to_owned(),
};
let events = [
WorkflowEvent::Started(Box::new(WorkflowStarted {
id: id.clone(),
from: None,
version: "v1".to_owned(),
parent: None,
workflow: workflow("Build"),
})),
WorkflowEvent::PhaseStarted(mark.clone()),
started(step(&id, 0, &[0]), StepKind::LoopUntil),
WorkflowEvent::Changed(Box::new(WorkflowChanged {
id: id.clone(),
version: "v2".to_owned(),
workflow: workflow("Build"),
approval: Some(ApprovalId::numbered(1)),
reason: "one more check".to_owned(),
})),
];
let mut workflows = Workflows::default();
for (seq, event) in (1..).zip(&events) {
workflows.apply(event, &envelope(seq, event));
}
let record = &workflows.by_id[&id];
assert_eq!(record.changed_from_phase, Some(1));
assert!(!record.phase_finished(0));
assert_eq!(record.next_phase(), 0);
assert_eq!(record.change_from(), 1, "a second change also waits for it");
let ended = WorkflowEvent::PhaseEnded {
phase: mark,
end: StepEnd::Done,
};
workflows.apply(&ended, &envelope(5, &ended));
let record = &workflows.by_id[&id];
assert!(record.phase_finished(0));
assert_eq!(record.next_phase(), 1);
}
}