Renovate von Poll auf Push: eine Fast-Lane für eigene Pakete, Images und CI-Komponenten
Renovate gehört zu den Tools, die still ihre Arbeit machen: Du zeigst es auf deine Repos, und es öffnet saubere kleine MRs, sobald sich upstream etwas bewegt. Eine strukturelle Eigenschaft hat mich aber gestört — Renovate ist poll-basiert. Wenn ich ein Release eines eigenen Bausteins schneide, warten alle Konsumenten bis zum nächsten Scan-Zyklus. Dieser Beitrag zeigt, wie aus einem Release ein Event wird, das in Minuten bei allen Konsumenten ankommt — ohne den CI-Runner zu fluten — und welche Stolperfallen auf dem Weg lagen.
TL;DR
Renovate pollt. Wenn du ein eigenes Paket, Image oder eine CI-Komponente releast, warten alle Konsumenten-Projekte bis zum nächsten Scan-Zyklus auf ihr Update-MR — bei mir bis zu einer Stunde. Die Lösung: Der Release-Job feuert einen Pipeline-Trigger auf den Renovate-Runner, der einen minimalen Lauf nur für First-Party-Dependencies startet. Ergebnis: Eigene Releases propagieren in Minuten statt in einer Stunde, Security-Updates laufen unverändert bei jedem Lauf, externe Updates gedrosselt im 4-Stunden-Fenster mit 24 Stunden Karenzzeit. Die CI-Last sinkt dabei, statt zu steigen. Aufwand: ein Shell-Step im Release-Job und eine zweite Renovate-Konfiguration.
Das Problem: Renovate tut nur etwas, wenn es läuft
Auf meinem selbst gehosteten GitLab deckt Renovate alles ab: Composer- und npm-Pakete, die Golden Images, die ich selbst baue, und die CI/CD-Komponenten, die meine Pipelines einbinden. Wenn ich ein Release eines eigenen Bausteins schneide — ein geteiltes Sitepackage, ein Base Image, eine CI-Komponente —, dann warten alle Projekte, die ihn konsumieren, einfach. Bis zu einem vollen Scan-Zyklus, bevor überhaupt ein Bump-MR entsteht. Für Third-Party-Dependencies ist das völlig in Ordnung. Für meine eigenen Artefakte, deren Änderung ich zwei Minuten vorher selbst gepusht habe und exakt kenne, fühlt sich eine Stunde Wartezeit falsch herum an.
Der naive Fix wäre: öfter pollen. Lass es. Ein selbst gehostetes Renovate, das alle 15 Minuten jedes Repo scannt, begräbt deinen CI-Runner unter Pipelines und Lookups, die in 99 % der Fälle nichts finden. Die Updates, auf die es wirklich ankommt, sind eine kleine, bekannte Teilmenge.
Also bin ich den umgekehrten Weg gegangen: Aus dem Release wird ein Event.
Erst die Kadenz sortieren
Bevor irgendein Trigger ins Spiel kommt, zahlt sich eine Beobachtung aus: Updates sind nicht gleich dringend. Es gibt drei Stufen.
Security: sofort
CVE-, OSV- und Advisory-Fixes fahren bei jedem Lauf mit, ohne Karenzzeit, mit Auto-Merge auf Patch-Level. Das steht nicht zur Debatte.
First-Party: so schnell wie möglich
Meine eigenen Pakete, Images und CI-Komponenten. Hier will ich Minuten, nicht Stunden — dafür ist die Fast-Lane da.
Extern: entspannt
Alles von anderen bekommt die Gegenbehandlung: ein Cron-Fenster etwa alle vier Stunden plus 24 Stunden Karenzzeit, bevor ein MR überhaupt aufgeht. Die Karenzzeit ist bewusste Verteidigung gegen kompromittierte Upstream-Tags. Das 4-Stunden-Fenster verhindert schlicht, dass externer Churn den CI-Runner mit Pipelines flutet, die sich niemand sofort ansehen muss.
In Renovate ist „externe Deps nur alle vier Stunden“ ein Cron-Schedule auf einer Package Rule. Wichtig dabei: Renovates Schedule-Cron verlangt * im Minutenfeld, Minuten-Granularität gibt es nicht.
Allein damit wurde das tägliche Update-Rauschen erträglich. Schnell macht das meine eigenen Sachen aber noch nicht — es macht nur die fremden leiser. Für den schnellen Teil brauchte ich das Event.
Der Kern: das Release meldet sich selbst
Jedes meiner First-Party-Artefakte wird auf demselben Weg released: Ein semantic-release-Job auf einem geschützten Branch macht aus Conventional Commits ein getaggtes Release. Dieser gemeinsame Release-Job ist der perfekte Ort für einen Seiteneffekt.
Ich habe ihm einen Opt-in-Step angehängt: Wenn ein Release tatsächlich veröffentlicht wurde, feuert er einen Pipeline-Trigger auf den Renovate-Runner.
# läuft nur, wenn semantic-release tatsächlich veröffentlicht hat (HEAD ist der chore(release)-Commit + Tag)
if [ "$TRIGGER_RENOVATE" = "true" ] && [ -n "$RENOVATE_TRIGGER_TOKEN" ]; then
curl -sf -X POST \
-F token="$RENOVATE_TRIGGER_TOKEN" -F ref=main \
-F "variables[RELEASED_PACKAGE]=$CI_PROJECT_PATH" \
"$CI_SERVER_URL/api/v4/projects/<renovate-runner>/trigger/pipeline" \
|| echo "trigger failed (non-fatal, ignored)"
fi
Drei Eigenschaften waren mir wichtig, und alle drei stecken sichtbar in diesen paar Zeilen. Erstens: nicht-fatal. Das Release ist längst live, wenn dieser Step läuft. Ein Schluckauf im Trigger darf ein grünes Release niemals rot machen, deshalb endet jeder Pfad mit Exit 0. Zweitens: geschaltet. Der Trigger feuert nur dort, wo ich das Repo explizit angemeldet habe und ein Token vorhanden ist — ein sauberer Kill-Switch pro Repo. Drittens: nur bei echten Releases. semantic-release läuft bei jedem Push auf main, die meisten Läufe veröffentlichen nichts. Der Step hängt am tatsächlichen chore(release)-Commit samt Tag. Kein Release, kein Trigger.
Damit sendet ein Release ein Event. Die andere Hälfte ist, dass die Renovate-Seite beim Empfang genau das Richtige tut — und zwar minimal.
Der getriggerte Lauf: die Datasource definiert, was „eigen“ ist
Der getriggerte Renovate-Lauf soll exakt eine Sache tun: First-Party-Dependencies in den Konsumenten bumpen. Sonst nichts. Keine externen Deps (die haben ihre eigene entspannte Spur), kein Security (das läuft ohnehin immer mit). Ein minimaler, fokussierter Durchlauf.
Mein erster Versuch war eine Allow-List aus Package-Name-Globs: moselwal/**, koh/** und so weiter. Für Composer-Pakete funktionierte das, dann fiel es sofort um. Meine Golden Images lösen auf depNames wie devops/images/php-runtime auf, meine CI-Komponenten auf devops/ci-cd-components/build-tools. Nichts davon matcht einen Package-Glob. Eine handgepflegte Liste aller Namensformen ist exakt die Sorte Konfiguration, die still verrottet.
Dann kam die schöne Erkenntnis: Jedes First-Party-Artefakt, das ich habe, wird gegen mein eigenes GitLab aufgelöst — und nichts Externes wird das. Pakete, Images, Komponenten laufen alle über gitlab-tags, gitlab-releases oder gitlab-packages. Externes läuft über packagist, npm, docker, github-tags und Verwandte. Ich muss also gar keine Namen aufzählen, ich invertiere über die Datasource:
Alles abschalten, dann nur die Self-hosted-GitLab-Datasources wieder anschalten. Diese eine Regel deckt Pakete, Images, Komponenten und sogar ein OCI-Sources-Artefakt ab, das ich sonst vergessen hätte. Sie ist strukturell korrekt statt eine Liste, an deren Pflege ich denken muss. Das ist die Sorte Definition, die richtig bleibt, nachdem man aufgehört hat hinzusehen.
Die Stolperfallen
Ein paar Dinge haben mich gebissen, und die sind der eigentlich interessante Teil.
Security-Fixes überstimmen enabled: false
Ich hatte externe Deps für die Fast-Lane deaktiviert, einen Testlauf gestartet, und Renovate öffnete fröhlich ein MR für einen phpunit-Security-Bump. Renovates Vulnerability-Pfad kann ein deaktiviertes Paket übersteuern, und das OSV-Flag war über ein CLI-Argument weiterhin aktiv — CLI schlägt Config-Datei. Das Flag wegzulassen reichte nicht, ich musste es explizit auf false setzen. Merke: Ein CLI-Flag gewinnt gegen deine Config, und „disabled“ heißt für einen Security-Fix nicht „disabled“.
Der Lauf wollte die Arbeit des geplanten Laufs löschen
Weil die Fast-Lane externe Deps deaktiviert, hielt Renovate deren bestehende Branches für verwaist und wollte sie aufräumen — sprich: die externen MRs des stündlichen Laufs schließen. Eine Zeile behebt das: pruneStaleBranches: false. Die Fast-Lane darf ausschließlich First-Party-Bumps hinzufügen. Aufräumen gehört dem langweiligen stündlichen Scan.
Eine Encoding-Falle bei Trigger-Variablen
variables[KEY]=value an die Pipeline-Trigger-API funktioniert mit curl -F, aber nicht mit jedem CLI-Wrapper. Einer davon verschluckte die Klammer-Syntax und schickte ein leeres Variables-Objekt — mein vermeintlicher Dry-Run lief still und leise scharf. Prüfe immer, ob die Variable wirklich angekommen ist, bevor du einem Dry-Run traust.
Bursts entprellen
Ein Release kann weitere Releases auslösen (eine Komponente bumpt die nächste). Eine dedizierte Resource Group plus interruptible kollabiert einen Schwall Trigger zu einem einzigen Lauf. Und weil Renovate idempotent ist, kostet ein verworfener Trigger nichts.
Das Ergebnis
Die Form ist jetzt:
First-Party-Release ──▶ release:semver veröffentlicht
│ (nur bei echtem Release)
▼
Pipeline-Trigger ──▶ Renovate-Fast-Lane-Lauf (nur eigene, Datasource-basiert)
▼
Bump-MRs in Konsumenten ──▶ Auto-Merge auf Patch/Minor ──▶ deployed
Ende zu Ende: Ein Release erreicht seine Konsumenten in Minuten statt in bis zu einer Stunde. Security behält seine Always-on-Spur ohne Karenzzeit. Externe Dependencies bleiben in ihrem entspannten 4-Stunden-Fenster, der CI-Runner ertrinkt nicht in Rauschen. Und die gesamte Fast-Lane ist additiv: Der schlichte stündliche Scan liegt weiter darunter als Sicherheitsnetz. Ein verpasster Trigger heilt sich beim nächsten Zyklus von selbst.
Häufige Fragen
Funktioniert das nur mit GitLab?+
Das Muster ist übertragbar. Auf GitHub wäre der Trigger ein repository_dispatch- oder workflow_dispatch-Aufruf aus dem Release-Workflow. Der Kern bleibt gleich: Release-Event statt kürzeres Poll-Intervall, plus ein fokussierter Renovate-Lauf, der nur die eigenen Datasources sieht.
Was passiert, wenn ein Trigger verloren geht?+
Nichts Dramatisches. Der stündliche Scan findet das Update spätestens beim nächsten Zyklus. Der Trigger beschleunigt den Normalfall, er trägt keine Verantwortung für die Korrektheit.
Warum nicht einfach alle 15 Minuten pollen?+
Weil du dafür in 99 % der Läufe nichts bekommst außer CI-Last. Jeder Scan kostet Pipelines und Registry-Lookups über alle Repos. Der Trigger kostet einen einzigen fokussierten Lauf, und zwar genau dann, wenn es etwas zu tun gibt.
Fazit
Wenn du dich dabei ertappst, ein Poll-Intervall verkürzen zu wollen, frag stattdessen, auf welches Event du hören könntest. Die Updates, die dich wirklich interessieren, sind fast immer eine kleine, bekannte Menge. Und lass die Datenlage deine Kategorien definieren: „First-Party“ war bei mir keine Namensliste, die ich pflegen muss, sondern eine Eigenschaft, die längst da war — die Datasource. Wer darüber invertiert, bleibt korrekt, ohne hinsehen zu müssen.
Der Rest ist Disziplin im Kleinen: Ein Seiteneffekt am Release darf das Release nie brechen, ein doppelter Trigger muss harmlos sein, und der langweilige stündliche Scan bleibt bestehen — er ist es, der die Fast-Lane gefahrlos scheitern lässt.
Poll-basierte Tools müssen sich nicht poll-basiert anfühlen. Du musst ihnen nur etwas geben, worauf sie hören können.
Bevor der nächste interne Release wieder eine Stunde liegen bleibt — sprechen wir über deine Pipeline-Architektur.
CI/CD-Pipelines, die sich selbst aktuell halten
Wenn deine Pipelines bei jedem internen Release auf den nächsten Scan warten — oder dein Renovate den CI-Runner flutet —, schaue ich mir mit dir an, welche Updates welche Kadenz verdienen. Ergebnis: Security sofort, eigene Artefakte in Minuten, externer Churn gedrosselt, und ein Release-Prozess, der sich selbst propagiert.
Programmiert seit 2002 – autodidaktisch gelernt, 2012 mit KO-Web selbständig gemacht. Über 100 Projekte, Fokus auf Security, Performance, Automatisierung und Qualität. Heute freiberuflich: DevSecOps-Beratung, Schulungen und Softwareentwicklung.