Docs
Dependabot CI fix
Dependabot CI fix reacts when a dependency-update PR (Dependabot or Renovate)
goes red in CI. When a check suite completes with a failure, Last Light works
out why it broke, brings the branch up to date with its base, and
pushes the smallest fix that makes CI pass. It is fix-only:
it never classifies, labels, or merges the PR. Once the push turns the checks
green, the pr.checks_passed webhook hands off to
dependabot-pr-merge, the
single owner of the trivial-vs-functional classification and the auto-merge
decision — so the two workflows never duplicate each other's work.
The diagnosis is its own phase, and it runs first. Not every red dependency PR is broken in a way a bot can repair — the tests may need a database this sandbox doesn't have, or the base branch may already be red — and a blind repair attempt on one of those spends a full install and test cycle to learn nothing. So diagnose classifies the failure cheaply against the already-cloned workspace, and fix runs only when the verdict says a repair is worth attempting. It shares the same two phases and the same five failure classes as PR fix.
Diagnose is skipped when the PR isn't red at all. A
dependency PR can be unmergeable without a single failing check — it's
behind its base, it has a merge dirty conflict
(almost always the lockfile), or it's blocked on a required
gate. There is nothing to classify in that case, and the repair is the
mechanical base-merge the fix phase does first anyway, so the run goes
straight there. Only a genuinely red build pays for a diagnosis.
repo-write — it merges the
base branch into the PR branch and pushes commits, but always with a plain
(non-force) push so Dependabot / Renovate don't lose track of their own
branch. It never labels or merges — that's dependabot-pr-merge's
job once the checks go green.
Pipeline
What it does
- Clones the repo at the PR's head branch (the dependency bump is already committed there)
- Diagnoses the cause before touching anything — reads the CI failures and the repo's own
.github/workflows/*.yml, names what differs between CI and the sandbox, reproduces the exact failing command, and classifies the result: a stale lockfile, a breaking change in the new version, a call site or type that needs updating — or a failure nothing here can repair, in which case it stops and the fix phase never runs - Merges the base branch into the PR branch first (plain
git merge, no rebase or force-push) so the fix is built on current base and abehindPR is brought up to date — resolving any lockfile conflict by regenerating it - Makes the smallest fix that turns CI green, runs the test / lint / typecheck gate, and plain-pushes to the PR branch — only once that gate is green, never speculatively
- Stops there — it does not classify, comment, label (except the give-up case below), or merge. The push re-runs CI, and when it goes green the
pr.checks_passedwebhook fires dependabot-pr-merge to make the one classify / auto-merge decision
dependabot-pr-merge later sees is clean rather than
behind — so the merge workflow can act on it directly instead of
asking the bot to rebase.
Triggers
- GitHub webhook: a
check_suite.completedevent with afailure/timed_outconclusion (surfaced internally aspr.checks_failed). The connector already knows whether the PR is a dependency bump — it checks the head commit author and the branch prefix to decide whether to emit at all — so the router routes on that flag deterministically, with no classifier call: a bump comes here, any other red PR goes to PR fix. Requires the GitHub App to subscribe to Check suite events (Checks: read). - Cron backstop: a daily job (
fix-red-dependency-prs, 15:00 — one hour after the green sweep) whose runner finds, in code, the Dependabot / Renovate PRs across managed repos that can't merge on their own and that ci-fix can push toward: a settled-red check conclusion (failing/timed-out, so it never fires while checks are still running), or amergeable_stateofbehind/dirty/blocked. It fans out one bounded run per PR — the same single-PR shape as the webhook, carrying the PR head branch (pre-clone) and thereason(checks-failing|behind|dirty|blocked; failing CI wins). It runs alongside webhooks — a safety net for PRs that went red, or fell out of a mergeable state, before boot or during a missed delivery. (cleanis the green sweep's;unknownis left for a later tick.) Per-PR runs dedup by (repo, PR). - Maintainer mention: an
@last-lightcomment on a red Dependabot / Renovate PR (e.g. "can you look at this?"). The router hands the PR author + check state to the classifier, which routes a red dependency PR here — the on-demand version of the webhook. (A green one routes to dependabot-pr-merge instead; an explicit "review this" still gets a normal review.)
Whichever way a run is summoned, the same pre-flight decides whether it is worth spending: a fork PR, a base branch that is red on its own account, an exhausted attempt or cost budget for that PR, a head commit already assessed, or another run already working the PR all refuse it before a sandbox is provisioned. Only one run may be in flight for a PR at a time, across every PR-scoped workflow.
Two of those refusals are terminal — an exhausted attempt or cost
budget — and terminal refusals are never silent: the PR gets
requires-human and one comment saying which budget ran out and
what each attempt found, without a sandbox ever starting. The rest stay
quiet, because they are temporary or not this PR's fault.
Attempts are counted and remembered per pull request, not per
workflow — an @last-light comment routed to
PR fix and a cron dispatch of this
workflow share one counter, one budget and one attempt history, because
"how many times have we tried to fix this PR" is a fact about the PR
rather than about how the event arrived.
The PR state model covers the whole gate — every
refusal in order, what each one leaves on the PR, and how it is cleared;
PR fix covers what each attempt carries
forward and which verdicts cost one.
Labels
The dependency workflows share a small label state machine so status is
visible and the crons don't churn. ci-fix touches labels in only one case:
when it can't land the PR — it can't repair CI with a small,
safe change, or there's nothing to push and it can't unblock the PR (e.g. a
blocked PR awaiting a required human review) — it applies
requires-human and stops. A stopping diagnosis is the
opposite case: when the verdict is flaky,
infra-dependent or upstream-broken the fix phase
is skipped outright, so nothing is labelled and the run still finishes
successfully. A flaky test or a base branch that is red on its own account
shouldn't leave the PR permanently marked as needing a human. The
flaky skip is bounded, though: after
fix.maxFlakyDeferrals (2) in a row on the same problem, the
third verdict is taken at face value and the PR gets a real attempt —
three flaky reports running is an intermittent real failure, and without
the bound the nightly sweep would re-run for free forever. The verdict labels
(dependency-trivial / dependency-functional) — and
clearing a stale requires-human once a later fix lands green — are
owned entirely by dependabot-pr-merge,
which keeps the classification in one place. requires-human is a
notification, not a state, and nothing in the harness reads
it; the hold label (lastlight-ignore) is the one
that tells Last Light to stay off a PR entirely. Both, and the four ways to
re-arm an escalated PR, are covered on
the PR state model page — the rules are applied at
dispatch, so the crons and the webhooks agree, where previously the crons
treated requires-human as a one-way door while the webhooks
cleared it on success. Labels are created on demand
(github_ensure_labels).