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.

Permission profile: 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

Diagnose classify the failure Fix merge base + fix + push pr-merge once green: classify + merge

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 a behind PR 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_passed webhook fires dependabot-pr-merge to make the one classify / auto-merge decision
Because ci-fix brings the branch current with its base, the PR that 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.completed event with a failure / timed_out conclusion (surfaced internally as pr.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 a mergeable_state of behind / 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 the reason (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. (clean is the green sweep's; unknown is left for a later tick.) Per-PR runs dedup by (repo, PR).
  • Maintainer mention: an @last-light comment 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).