From 3579a5075ce13fd1d8e139ffbc5d7065cd00fedf Mon Sep 17 00:00:00 2001 From: Alan Fitzgerald Date: Wed, 30 Sep 2026 10:59:28 -0400 Subject: [PATCH] fix(prstatus): ignore CI runs superseded by a newer run of the same job A run cancelled by a newer one (concurrency group, manual re-run) stays in statusCheckRollup next to its replacement, and aggregateCI counted its CANCELLED conclusion as a failure, so a green PR showed as failing. Keep only the latest run per workflow+job before aggregating. --- internal/prstatus/prstatus.go | 40 +++++++++++++++++++++++++++++- internal/prstatus/prstatus_test.go | 24 ++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/internal/prstatus/prstatus.go b/internal/prstatus/prstatus.go index d34fc03..45f04b5 100644 --- a/internal/prstatus/prstatus.go +++ b/internal/prstatus/prstatus.go @@ -88,6 +88,12 @@ type rawCheck struct { Status string `json:"status"` Conclusion string `json:"conclusion"` State string `json:"state"` + // Name, WorkflowName and StartedAt identify re-runs of the same job: + // a run cancelled by a newer one (a concurrency group, a manual + // re-run) stays in the rollup alongside its replacement. + Name string `json:"name"` + WorkflowName string `json:"workflowName"` + StartedAt string `json:"startedAt"` } // Fetch reports on a pull request via the gh CLI. With prURL set it looks up @@ -201,7 +207,7 @@ func aggregateCI(checks []rawCheck) string { return "NONE" } pending := false - for _, c := range checks { + for _, c := range latestRuns(checks) { if c.Typename == "StatusContext" { switch c.State { case "FAILURE", "ERROR": @@ -227,3 +233,35 @@ func aggregateCI(checks []rawCheck) string { } return "PASSING" } + +// latestRuns drops CheckRuns superseded by a newer run of the same job, so a +// run cancelled in favour of its replacement doesn't mark the PR failing. +// Unnamed checks and StatusContexts (already latest-per-context) pass through. +func latestRuns(checks []rawCheck) []rawCheck { + started := func(c rawCheck) string { + // A queued run has no start time yet (or Go's zero time) but is + // the newest of its job. + if c.StartedAt == "" || strings.HasPrefix(c.StartedAt, "0001-") { + return "9999" + } + return c.StartedAt // RFC 3339 UTC, so it sorts as a string + } + latest := map[string]int{} + var out []rawCheck + for _, c := range checks { + if c.Typename == "StatusContext" || c.Name == "" { + out = append(out, c) + continue + } + key := c.WorkflowName + "\x00" + c.Name + if i, ok := latest[key]; ok { + if started(c) > started(out[i]) { + out[i] = c + } + continue + } + latest[key] = len(out) + out = append(out, c) + } + return out +} diff --git a/internal/prstatus/prstatus_test.go b/internal/prstatus/prstatus_test.go index 5620552..f2614f8 100644 --- a/internal/prstatus/prstatus_test.go +++ b/internal/prstatus/prstatus_test.go @@ -140,6 +140,30 @@ func TestAggregateCI(t *testing.T) { []rawCheck{{Status: "IN_PROGRESS"}, {Status: "COMPLETED", Conclusion: "FAILURE"}}, "FAILING", }, + { + "run cancelled by a newer successful run of the same job", + []rawCheck{ + {Name: "build", WorkflowName: "CI", StartedAt: "2026-09-30T12:55:43Z", Status: "COMPLETED", Conclusion: "CANCELLED"}, + {Name: "build", WorkflowName: "CI", StartedAt: "2026-09-30T12:55:49Z", Status: "COMPLETED", Conclusion: "SUCCESS"}, + }, + "PASSING", + }, + { + "newer run of a job is queued, older one failed", + []rawCheck{ + {Name: "build", WorkflowName: "CI", StartedAt: "2026-09-30T12:55:43Z", Status: "COMPLETED", Conclusion: "FAILURE"}, + {Name: "build", WorkflowName: "CI", StartedAt: "0001-01-01T00:00:00Z", Status: "QUEUED"}, + }, + "PENDING", + }, + { + "latest run of a job cancelled still fails", + []rawCheck{ + {Name: "build", WorkflowName: "CI", StartedAt: "2026-09-30T12:55:49Z", Status: "COMPLETED", Conclusion: "CANCELLED"}, + {Name: "build", WorkflowName: "CI", StartedAt: "2026-09-30T12:55:43Z", Status: "COMPLETED", Conclusion: "SUCCESS"}, + }, + "FAILING", + }, { "legacy StatusContext failure", []rawCheck{{Typename: "StatusContext", State: "FAILURE"}},