@@ -153,9 +153,28 @@ jobs:
153153 }),
154154 ]);
155155
156+ // listForRef returns every check run ever recorded on the ref, including
157+ // stale superseded ones (e.g. a failed run later re-run green). Branch
158+ // protection and the PR UI only consider the latest run per check name, so
159+ // reduce to that before evaluating — otherwise a single stale failure makes
160+ // ciFailed true forever and state labels never come back. See issue #884.
161+ //
162+ // Unlike listReviews (which documents oldest-first order), listForRef's
163+ // ordering is unspecified, so we pick the latest by run.id — GitHub assigns
164+ // monotonically increasing IDs, and id is never null (a freshly re-queued
165+ // run can have started_at: null, which would lose a string comparison
166+ // against an older completed run's timestamp).
167+ const latestByName = new Map();
168+ for (const run of checkRuns) {
169+ const prev = latestByName.get(run.name);
170+ if (!prev || run.id > prev.id) {
171+ latestByName.set(run.name, run);
172+ }
173+ }
174+
156175 // Filter to required checks only (or all checks if rules unavailable).
157176 // Always exclude this workflow's own run to avoid self-referential loops.
158- const relevantRuns = checkRuns .filter(run => {
177+ const relevantRuns = [...latestByName.values()] .filter(run => {
159178 if (run.name === 'Reconcile PR review state labels') return false;
160179 return requiredCheckNames ? requiredCheckNames.has(run.name) : true;
161180 });
0 commit comments