feat(renovate): alert when an update is parked in the dashboard indefinitely #437
Inga granskare
Etiketter
Inga etiketter
area/backups
area/ci
area/control-panel
area/identity
area/infra
area/observability
area/payments
area/security
area/storage
area/web
blocked
needs-info
needs-triage
ready-for-implementation
type
bug
type
chore
type
docs
type
epic
type
feature
type
task
wontfix
Ingen milstolpe
Inget projekt
Inga tilldelade
1 deltagare
Notiser
Förfallodatum
Inget förfallodatum satt.
Beroenden
Inga beroenden satta
Referens
bitborg/bitborg-infra!437
Läser in…
Hänvisa till i nytt ärende
Ingen beskrivning angiven.
Ta bort grenen "feat/renovate-dashboard-ageing"
Borttagning av en gren är permanent. Även om den borttagna grenen kan fortsätta existera en kort tid innan den faktiskt tas bort, kan det INTE ångras i de flesta fall. Vill du fortsätta?
Closes the last open acceptance criterion on #430: "If the cause is systemic, a check catches it."
What it catches
Renovate can park an update for ever with no error anywhere.
minimumReleaseAgeBehaviourdefaultsto
timestamp-required, the docker datasource often returns noreleaseTimestamp, and a release-agecheck that needs a timestamp can never pass without one. Seven image updates — the git server and the
database among them — sat in the Dependency Dashboard's "Pending Status Checks" section for weeks,
each showing an
approvePr-branchaction as though a branch existed. None was created. Nothing loggedan error at
info. The only symptom was an entry that never moved.scripts/check-renovate-annotations.pycannot see it, and says so in its own docstring's terms: theannotation is present and correct, so the dependency IS watched. It is watched and then silently
parked. This measures the one signal that separates the two — how long an entry has sat still.
Shape
An exporter runs at the tail of each nightly Renovate run (no new timer, no new role), reads every
dashboard the bot's token can see, and publishes a first-seen timestamp per held update. Three alerts
consume it:
RenovateUpdateHeldTooLongRenovateDashboardGuardDegradedRenovateDashboardScanStaleThe second and third exist because a guard whose failure is invisible is not a guard, and the bug this
replaces was itself invisible.
Decisions worth reviewing
Identity is the Renovate branch, not the entry title.
renovate/…-vmagent-1.xstayed stable whileits target moved v1.149.0 → v1.150.0 mid-stall. Keying on the title would have reset the clock on
every upstream release and hidden the stall for ever.
The clock spans all holding sections. Shuffling from "Pending Status Checks" to "Awaiting Schedule"
is not progress, so the age carries over. It resets only when the update leaves every holding section,
i.e. a PR finally exists.
State is pruned only for repos that scanned cleanly. A transient API error must not be read as "the
update progressed" and quietly restart a weeks-old clock.
A first-seen timestamp, not a long
for:. vmalert's pending timer resets when vmalert restarts, sofor: 10dmight never fire — a guard that cannot fire is worse than none. Same shape asForgejoTokenRotationDue.pending_approvalis excluded in the alert, not the exporter.dependencyDashboardApprovalgatesmajors of the git server and database on purpose (ADR 0023), so one legitimately sits there for ever.
The exporter stays a measurement and the policy lives with the policy.
Unrecognised section headings are counted and alerted on. If upstream renames one, the parser goes
blind and reports zero held updates — which reads as "all healthy". That vacuous pass is the exact
failure mode this whole change exists to prevent, so it is made loud.
Deployed as a plain
.py, not a.j2. ruff lintsansible/**/*.pyand never a template, and thelogic is unit-testable. Environment-specific values arrive as CLI arguments. Same pattern as
runner-controller/files/controller.py.Threshold 10 days, tunable via
alert_renovate_update_held_days.minimumReleaseAgeis 3 days andthe shared preset's window is weekly, so a legitimately waiting update can be ~10 days old. 10 is the
smallest value that cannot false-fire.
Known, accepted weakness: losing the state file resets the clocks. It is derived data, regenerable by
waiting, and
RenovateDashboardScanStalecovers the case that matters more (the exporter not running).Verified — every check shown able to fail
and a clock surviving an unscanned repo. Mutation-tested: treating "Open" as a holding section
fails 2 checks; dropping the unscanned-repo guard fails 1.
alert-rules.yml.j2passespromtool check rules(49 rules). An unbalanced paren in thenew expression makes it fail with
unclosed left parenthesis.promtool check metrics(rc 0); a malformed sample exits 1.ruff(pinned 0.16.1) clean,ansible-lintclean on the production profile,--syntax-checkclean,check-metric-names.pyconsistent across 209 files,check-renovate-annotations.pyclean.The fixture in the test file is a trimmed copy of the real dashboard as it stood on 2026-08-18,
including the seven stalled entries. A synthetic fixture would have been weaker: the bug was invisible
precisely because the dashboard looked healthy, so the test asserts against the shape that fooled us.
Two things for reviewers
Nothing in this repo validated alert rules before.
promtool check ruleswas run by hand here.Wiring it into CI needs Jinja rendering with resolved role vars, so it is worth its own change rather
than being smuggled into this one.
This needs one apply to take effect (
--tags renovate,monitoring). Predicted changes: install theexporter, create its state dir, re-render the wrapper script, re-render the alert rules and reload
vmalert. No service interruption — the exporter first runs on the next nightly Renovate run, and
publishes nothing until then.
Refs #430