gg: warning jobs read as an amber warning, and the run counts them

52481e7 · Gabriel Morell · 2026-10-27 18:57

7 files +53 -4
Message
{commit_body(@commit)}

Files changed

modified cli/src/commands/ci.rs
+33 −0
@@ -116,6 +116,11 @@ fn print_run(ctx: &Ctx, repo: &RepoRef, res: &Res<RunDetailResponse>, steps: boo
116 116 if let Some(took) = duration(run.started_at.as_deref(), run.completed_at.as_deref()) {
117 117 println!("took {took}");
118 118 }
119 + // A run its warning jobs passed is `success`; the count says there's
120 + // still something to look at, as the run page's header does.
121 + if let Some(line) = warnings_line(&res.jobs) {
122 + println!("{}", paint(&line, Tone::Warn));
123 + }
119 124 if run.awaiting_approval {
120 125 println!("\n{}", paint(&approval_notice(run), Tone::Busy));
121 126 }
@@ -358,6 +363,19 @@ fn pick_job<'a>(jobs: &'a [Job], wanted: Option<&str>) -> Result<&'a Job> {
358 363 }
359 364 }
360 365
366 +/// "⚠ 2 warnings" for a run with warning jobs, nothing for one without.
367 +fn warnings_line(jobs: &[Job]) -> Option<String> {
368 + match jobs
369 + .iter()
370 + .filter(|j| j.display_status() == "warning")
371 + .count()
372 + {
373 + 0 => None,
374 + 1 => Some("⚠ 1 warning".into()),
375 + n => Some(format!("⚠ {n} warnings")),
376 + }
377 +}
378 +
361 379 fn job_names(jobs: &[Job]) -> String {
362 380 jobs.iter()
363 381 .map(|j| j.job_id.clone().unwrap_or_else(|| j.label()))
@@ -660,6 +678,21 @@ mod tests {
660 678 assert!(pick_job(&jobs, None).is_err());
661 679 }
662 680
681 + #[test]
682 + fn warning_jobs_are_counted_and_not_failures() {
683 + let jobs = vec![
684 + job(1, "build", "success", None),
685 + job(2, "audit", "warning", Some("warning")),
686 + job(3, "image", "warning", Some("warning")),
687 + ];
688 + assert_eq!(warnings_line(&jobs).as_deref(), Some("⚠ 2 warnings"));
689 + assert_eq!(warnings_line(&jobs[..2]).as_deref(), Some("⚠ 1 warning"));
690 + assert_eq!(warnings_line(&jobs[..1]), None);
691 + assert_eq!(status_badge(jobs[1].display_status()), "⚠ warning");
692 + // No failed job to pick: a warning isn't one.
693 + assert!(pick_job(&jobs, None).is_err());
694 + }
695 +
663 696 #[test]
664 697 fn superseded_jobs_read_as_superseded_not_cancelled() {
665 698 let mut j = job(1, "build", "cancelled", None);
modified cli/src/output.rs
+19 −3
@@ -17,6 +17,7 @@ pub enum Tone {
17 17 Good,
18 18 Bad,
19 19 Busy,
20 + Warn,
20 21 Muted,
21 22 Accent,
22 23 Plain,
@@ -28,6 +29,7 @@ impl Tone {
28 29 Tone::Good => "\x1b[32m",
29 30 Tone::Bad => "\x1b[31m",
30 31 Tone::Busy => "\x1b[33m",
32 + Tone::Warn => "\x1b[1;33m",
31 33 Tone::Muted => "\x1b[90m",
32 34 Tone::Accent => "\x1b[36m",
33 35 Tone::Plain => "",
@@ -76,10 +78,13 @@ pub fn effective_status<'a>(status: &'a str, conclusion: Option<&'a str>) -> &'a
76 78 /// ([`Run::display_status`] — the server says `queued`) and `superseded`,
77 79 /// a job cancelled because a newer one in its concurrency group replaced
78 80 /// it ([`Job::display_status`]). Superseded is dim, not red: the newer
79 −/// run decides, as its commit status (`cancelled`) says.
81 +/// run decides, as its commit status (`cancelled`) says. A `warning` job
82 +/// passed but wants a look — it failed under `continue-on-error`, or
83 +/// reported `gitgud-conclusion: warning` — so it's amber, not red.
80 84 pub fn status_tone(status: &str) -> Tone {
81 85 match status {
82 86 "success" => Tone::Good,
87 + "warning" => Tone::Warn,
83 88 "failure" | "cancelled" | "timed_out" => Tone::Bad,
84 89 "queued" | "running" | "pending" | "blocked" | "waiting" | "awaiting_approval" => {
85 90 Tone::Busy
@@ -93,6 +98,7 @@ pub fn status_tone(status: &str) -> Tone {
93 98 pub fn status_mark(status: &str) -> &'static str {
94 99 match status {
95 100 "success" => "✔",
101 + "warning" => "⚠",
96 102 "failure" | "timed_out" => "✘",
97 103 "cancelled" | "superseded" => "⊘",
98 104 "running" => "▶",
@@ -119,7 +125,7 @@ pub fn status_label(status: &str) -> String {
119 125 pub fn finished(status: &str) -> bool {
120 126 matches!(
121 127 status,
122 − "success" | "failure" | "cancelled" | "skipped" | "timed_out" | "superseded"
128 + "success" | "failure" | "cancelled" | "skipped" | "timed_out" | "superseded" | "warning"
123 129 )
124 130 }
125 131
@@ -396,6 +402,9 @@ mod tests {
396 402 assert_eq!(status_tone("awaiting_approval"), Tone::Busy);
397 403 assert_eq!(status_tone("superseded"), Tone::Muted);
398 404 assert_eq!(status_tone("skipped"), Tone::Muted);
405 + assert_eq!(status_tone("warning"), Tone::Warn);
406 + assert_eq!(status_mark("warning"), "⚠");
407 + assert_eq!(status_label("warning"), "warning");
399 408 assert_ne!(status_mark("awaiting_approval"), status_mark("queued"));
400 409 assert_eq!(status_label("awaiting_approval"), "needs approval");
401 410 assert_eq!(status_label("timed_out"), "timed out");
@@ -404,7 +413,14 @@ mod tests {
404 413
405 414 #[test]
406 415 fn only_known_endings_are_finished() {
407 − for done in ["success", "failure", "cancelled", "skipped", "superseded"] {
416 + for done in [
417 + "success",
418 + "failure",
419 + "cancelled",
420 + "skipped",
421 + "superseded",
422 + "warning",
423 + ] {
408 424 assert!(finished(done), "{done}");
409 425 }
410 426 for busy in [
modified cli/src/tui/mod.rs
+1 −1
@@ -547,7 +547,7 @@ fn tone_color(status: &str) -> Color {
547 547 match status_tone(status) {
548 548 Tone::Good => Color::Green,
549 549 Tone::Bad => Color::Red,
550 − Tone::Busy => Color::Yellow,
550 + Tone::Busy | Tone::Warn => Color::Yellow,
551 551 Tone::Accent => Color::Cyan,
552 552 Tone::Muted | Tone::Plain => Color::DarkGray,
553 553 }

Parents: d8a7c7f