use serde_json::Value;
pub struct GateLine {
pub name: String,
pub status: String,
pub enforced: bool,
pub observed: Option<f64>,
pub threshold: Option<f64>,
pub threshold_label: Option<String>,
}
impl GateLine {
fn measured_clause(&self) -> String {
match (
self.observed,
self.threshold,
self.threshold_label.as_deref(),
) {
(Some(observed), Some(threshold), _) => {
format!(" ({} against {})", trim_num(observed), trim_num(threshold))
}
(Some(observed), None, Some(label)) => {
format!(" ({} at or above {label})", trim_num(observed))
}
(Some(observed), None, None) => format!(" ({})", trim_num(observed)),
_ => String::new(),
}
}
fn described(&self) -> String {
format!("{}{}", self.name, self.measured_clause())
}
}
fn trim_num(value: f64) -> String {
if value.fract() == 0.0 {
format!("{value:.0}")
} else {
format!("{value}")
}
}
pub fn read_gate_outcomes(envelope: &Value) -> Vec<GateLine> {
let Some(map) = envelope.get("gate_outcomes").and_then(Value::as_object) else {
return Vec::new();
};
map.iter()
.filter_map(|(name, entry)| {
Some(GateLine {
name: name.clone(),
status: entry.get("status")?.as_str()?.to_owned(),
enforced: entry.get("enforced").and_then(Value::as_bool)?,
observed: entry.get("observed").and_then(Value::as_f64),
threshold: entry.get("threshold").and_then(Value::as_f64),
threshold_label: entry
.get("threshold_label")
.and_then(Value::as_str)
.map(str::to_owned),
})
})
.collect()
}
struct Partitioned<'a> {
failed: Vec<&'a GateLine>,
warned: Vec<&'a GateLine>,
skipped: Vec<&'a GateLine>,
passed: Vec<&'a GateLine>,
}
fn partition(gates: &[GateLine]) -> Partitioned<'_> {
let mut out = Partitioned {
failed: Vec::new(),
warned: Vec::new(),
skipped: Vec::new(),
passed: Vec::new(),
};
for gate in gates {
match gate.status.as_str() {
"fail" => out.failed.push(gate),
"skipped" => out.skipped.push(gate),
"pass" => out.passed.push(gate),
_ => out.warned.push(gate),
}
}
out
}
fn join(gates: &[&GateLine]) -> String {
gates
.iter()
.map(|gate| gate.described())
.collect::<Vec<_>>()
.join(", ")
}
pub fn summary_line(envelope: &Value) -> Option<String> {
let gates = read_gate_outcomes(envelope);
if gates.is_empty() {
return None;
}
let parts = partition(&gates);
let mut clauses: Vec<String> = Vec::new();
if !parts.failed.is_empty() {
let enforced = parts.failed.iter().filter(|gate| gate.enforced).count();
let suffix = if enforced == 0 {
" (none of which fails this run)"
} else {
""
};
clauses.push(format!("failed {}{suffix}", join(&parts.failed)));
}
if !parts.warned.is_empty() {
clauses.push(format!("warned {}", join(&parts.warned)));
}
if !parts.skipped.is_empty() {
clauses.push(format!("stood down {}", join(&parts.skipped)));
}
if !parts.passed.is_empty() {
clauses.push(format!("passed {}", join(&parts.passed)));
}
Some(format!("Gate outcomes: {}.", clauses.join("; ")))
}
pub fn summary_line_for_gates(gates: Option<&fallow_output::GateOutcomes>) -> Option<String> {
let gates = gates?;
let envelope = serde_json::json!({ "gate_outcomes": gates });
summary_line(&envelope)
}
pub fn annotation_line(envelope: &Value) -> Option<String> {
let line = summary_line(envelope)?;
Some(format!("::notice::Fallow: {line}"))
}
#[cfg(test)]
mod tests {
use super::*;
fn envelope(gates: &Value) -> Value {
serde_json::json!({ "kind": "health", "gate_outcomes": gates })
}
#[test]
fn an_envelope_without_the_object_renders_nothing() {
let bare = serde_json::json!({ "kind": "dead-code" });
assert!(summary_line(&bare).is_none());
assert!(annotation_line(&bare).is_none());
}
#[test]
fn a_failing_gate_names_what_it_compared() {
let value = envelope(&serde_json::json!({
"health-min-score": {
"status": "fail", "enforced": true, "observed": 85.0, "threshold": 90.0
}
}));
assert_eq!(
summary_line(&value).expect("a gate ran"),
"Gate outcomes: failed health-min-score (85 against 90)."
);
}
#[test]
fn the_annotation_is_always_a_notice() {
for enforced in [true, false] {
let value = envelope(&serde_json::json!({
"audit-verdict": { "status": "fail", "enforced": enforced }
}));
let line = annotation_line(&value).expect("a gate ran");
assert!(
line.starts_with("::notice::"),
"the render states the fact and leaves the escalation to the consumer: {line}"
);
}
}
#[test]
fn an_unenforced_failure_says_it_does_not_fail_the_run() {
let value = envelope(&serde_json::json!({
"stale-baseline": { "status": "fail", "enforced": false }
}));
assert_eq!(
summary_line(&value).expect("a gate ran"),
"Gate outcomes: failed stale-baseline (none of which fails this run)."
);
}
#[test]
fn a_stood_down_gate_is_not_a_pass() {
let value = envelope(&serde_json::json!({
"stale-baseline": { "status": "skipped", "enforced": false }
}));
assert_eq!(
summary_line(&value).expect("a gate ran"),
"Gate outcomes: stood down stale-baseline."
);
}
#[test]
fn a_warn_tier_is_not_a_pass() {
let value = envelope(&serde_json::json!({
"audit-verdict": { "status": "warn", "enforced": true }
}));
assert_eq!(
summary_line(&value).expect("a gate ran"),
"Gate outcomes: warned audit-verdict."
);
}
#[test]
fn passing_gates_are_named_on_their_own() {
let value = envelope(&serde_json::json!({
"regression": { "status": "pass", "enforced": true }
}));
assert_eq!(
summary_line(&value).expect("a gate ran"),
"Gate outcomes: passed regression."
);
}
#[test]
fn all_four_outcomes_stay_apart_in_one_line() {
let value = envelope(&serde_json::json!({
"regression": { "status": "fail", "enforced": true },
"audit-verdict": { "status": "warn", "enforced": true },
"stale-baseline": { "status": "skipped", "enforced": false },
"duplication-threshold": { "status": "pass", "enforced": true }
}));
assert_eq!(
summary_line(&value).expect("gates ran"),
"Gate outcomes: failed regression; warned audit-verdict; \
stood down stale-baseline; passed duplication-threshold."
);
}
#[test]
fn a_named_floor_is_rendered_instead_of_a_number() {
let value = envelope(&serde_json::json!({
"health-min-severity": {
"status": "fail", "enforced": true, "observed": 3.0,
"threshold_label": "critical"
}
}));
assert_eq!(
summary_line(&value).expect("a gate ran"),
"Gate outcomes: failed health-min-severity (3 at or above critical)."
);
}
#[test]
fn an_unrecognised_gate_name_still_reports() {
let value = envelope(&serde_json::json!({
"some-future-gate": { "status": "fail", "enforced": true }
}));
assert_eq!(
annotation_line(&value).expect("a gate ran"),
"::notice::Fallow: Gate outcomes: failed some-future-gate."
);
}
#[test]
fn an_unrecognised_status_is_not_a_pass() {
let value = envelope(&serde_json::json!({
"some-future-gate": { "status": "deferred", "enforced": false }
}));
assert_eq!(
summary_line(&value).expect("a gate ran"),
"Gate outcomes: warned some-future-gate."
);
}
#[test]
fn a_mix_of_enforced_and_unenforced_failures_adds_no_note() {
let value = envelope(&serde_json::json!({
"regression": { "status": "fail", "enforced": true },
"stale-baseline": { "status": "fail", "enforced": false }
}));
assert_eq!(
summary_line(&value).expect("gates ran"),
"Gate outcomes: failed regression, stale-baseline."
);
}
}