Two defects with the same root cause: the provider contract could read one CI log, but could not see a branch's run history.
failsOnBase never fired
attemptFix has always known how to decline a pre-existing failure. The guard, its skip message and its tests all date from the original implementation — but nothing ever set the flag. No production caller computed it, so undefined was falsy on every run.
A pull request opened against a broken base branch had that breakage diagnosed, and sometimes repaired, as its own — attributing someone else's regression to the change, and hiding it where nobody was looking.
The flake branch retried nothing
It returned action: 'retry' and a report saying "re-running the failed job is likely to clear it", then re-ran nothing, because no provider could be asked to.
The capability
ciRuns, gating listWorkflowRuns and rerunWorkflowRun.
| GitHub | GitLab | Bitbucket | |
|---|---|---|---|
ciRuns | ✅ | ✅ (jobs) | ❌ |
Separate from ciLogs for GitLab's sake — reading one run's log and reasoning about a branch's runs are the same API on GitHub and two different ones there. GitLab's listWorkflowRuns returns jobs, deliberately: a job is what getWorkflowRunLogs reads there, so returning pipeline ids would hand callers an id that looks interchangeable and is not.
Whose failure is it
Not "is the base red" — that would down tools on exactly the repositories that need the help most — but is it red in the same way. The newest run of each workflow on the base branch is classified from its own log and compared by kind.
- Per workflow, so a workflow that has since gone green stops counting.
- Capped at three log reads, because a base red in more than three distinct ways isn't worth optimising for.
- An unreadable history reads as no evidence, not as breakage — a 403 on the list call cannot silently switch repairs off.
Comparing kinds is an approximation and errs deliberately. Two unrelated type errors both classify as type-error, so a real regression on a base that already fails to type-check reads as inherited. Buddy declines and explains, rather than committing a guess about code it has already been told is broken.
Retrying a flake
A transient failure needs no repair, only another go at the same commit. actions: write is already granted at the workflow level, so generated setups need no change.
Reported as fixed: false — the jobs were asked to run again and may still fail, and calling that fixed would be a guess dressed up as a result. The attempt counter from #1407 is what bounds the loop this opens.
What this deliberately does not claim
Re-running after a mechanical lock-file repair. A re-run executes against the commit it originally ran on, which is the broken one. The docs section explaining why those checks stay red stands — now with the second half of the reason written down, since the first half alone reads like an oversight rather than a constraint.
Tests
1929 → 1966. New: test/ci-base.test.ts (11), test/ci-runs.test.ts (17, real transport mapping against stubbed fetch), plus 9 in test/ci-fix.test.ts. ciRuns added to both conformance arrays.
Lint ✓ · typecheck ✓ · check:docs ✓ · 1966 pass / 0 fail
🤖 Generated with Claude Code