Mein Lab ist ein einziges Repository: ein GitOps-Monorepo, aus dem
Flux
mehrere Talos-Cluster reconciled. Alles, was eine Version trägt, steht darin — Helm-Charts, Container-Images, die Talos- und Kubernetes-Version, dazu CLI-Pins in einem Dockerfile und in shell.nix. Manuell halte ich das nicht aktuell. Das macht
Renovate
, und zwar so, dass es mir die langweiligen Bumps abnimmt und mich genau bei den drei, vier Dingen stoppt, bei denen ein blinder Merge teuer wäre.
Der Trick mit der lokalen Config
Ich fahre Renovate selbst-gehostet mit --platform=local — kein Hosted-App, das auf einen Forge zugreift, sondern ein Lauf gegen den Checkout. Genau dabei stolpert man über eine Eigenheit: lokale Presets in extends werden nicht unterstützt. Ich kann also nicht einfach "extends": [".renovate/groups.json"] schreiben und die Config in Fragmente zerlegen.
Die Lösung ist ein kleiner JS-Entrypoint, der die Fragmente zur Laufzeit zusammenführt:
// renovate.config.mjs — merged renovate.json mit .renovate/*.json
const base = readJson("renovate.json");
const { customManagers } = readJson(".renovate/customManagers.json");
const { packageRules: groupRules } = readJson(".renovate/groups.json");
export default {
...base,
extends: (base.extends ?? []).filter((p) => !p.startsWith(".renovate/")),
customManagers,
packageRules: [...(base.packageRules ?? []), ...groupRules],
};
So bleibt die Basis in renovate.json, die Custom-Manager und die Gruppen liegen getrennt in .renovate/ — und die .renovate/-Einträge fliegen aus extends raus, bevor Renovate sie als (nicht existente) Presets misszuversteht.
Die Grundhaltung: automerge by default, Handbremse für das Fundament
Die Basis ist bewusst aggressiv. platformAutomerge, prConcurrentLimit: 0 (kein Limit), und die SOPS-Dateien sind komplett ausgeklammert:
{
"extends": ["config:recommended", ":dependencyDashboard"],
"platformAutomerge": true,
"prConcurrentLimit": 0,
"ignorePaths": ["**/*.sops*"]
}
Die drei Kubernetes-Manager (flux, helm-values, kubernetes) sind auf k8s/clusters/.+\.ya?ml$ eingegrenzt. Was darunter liegt und nicht zum Fundament gehört, wird automatisch gemerged:
{
"matchManagers": ["flux", "helm-values", "kubernetes", "kustomize", "regex"],
"matchFileNames": ["k8s/clusters/**"],
"automerge": true
}
Und dann die Handbremse. Für alles, was im Fehlerfall den ganzen Cluster mitnimmt, ist automerge explizit aus:
{
"matchFileNames": [
"k8s/clusters/**/applications/kube-system/cilium/**",
"k8s/clusters/**/applications/flux-system/flux/**",
"k8s/clusters/**/applications/rook-ceph/rook-ceph/**",
"k8s/clusters/**/applications/cert-manager/cert-manager/**",
"k8s/clusters/**/applications/vault/vault/**",
"k8s/clusters/**/secrets/**"
],
"automerge": false
}
Das ist die ganze Philosophie in zwei Regeln: Die Blätter pflegen sich selbst, die Wurzel fasse ich von Hand an.
Reihenfolge erzwingen: Rook vor Ceph
Bei Storage reicht „nicht automergen" nicht — hier ist die Reihenfolge kritisch. Der Rook-Operator muss vor den Ceph-Daemon-Images hoch. Renovate kann das nicht erzwingen, aber es kann es mir unübersehbar machen — über Labels und einen PR-Body, der zur Checkliste wird:
{
"matchPackageNames": ["rook-ceph"],
"groupName": "rook-ceph-operator",
"labels": ["CRITICAL-STORAGE", "MERGE-1st-BEFORE-CEPH"],
"prBodyNotes": ["## :warning: CRITICAL: STORAGE INFRASTRUCTURE", "..."]
},
{
"matchPackageNames": ["quay.io/ceph/ceph"],
"groupName": "ceph-images",
"labels": ["CRITICAL-STORAGE", "MERGE-2nd-AFTER-ROOK"]
}
Die Labels MERGE-1st / MERGE-2nd stehen direkt im PR-Titel-Kontext. Selbst wenn ich um drei Uhr morgens einen Storage-PR sehe, sagt mir das Label die Reihenfolge, bevor ich den Body lese. Ceph-Images sind zusätzlich auf /^v\d+\.\d+\.\d+$/ gepinnt — keine RCs, keine latest-Überraschungen.
Custom-Manager: Versionen, die in Kommentaren leben
Nicht jede Version steckt in einem Helm-values.yaml. Talos- und Kubernetes-Versionen für tuppr, CLI-Pins im ZeroClaw-Dockerfile, Tool-Versionen in shell.nix — die annotiere ich mit einem Kommentar, und ein Regex-Manager liest ihn:
# renovate: datasource=docker depName=ghcr.io/siderolabs/installer
talosVersion: v1.11.2
Der zugehörige Manager greift dieses Muster für alle YAML-Dateien:
{
"customType": "regex",
"managerFilePatterns": ["/\\.ya?ml$/"],
"matchStrings": [
"#\\s*renovate:\\s*datasource=(?<datasource>\\S+)\\s+depName=(?<depName>\\S+)\\n\\s*(?:talos|kubernetes)?[Vv]?ersion:\\s*(?<currentValue>\\S+)"
]
}
So bleiben selbst Versionen, die in einer Kustomize-URL oder einem ARG im Dockerfile stecken, im Renovate-Blick. Ein eigener Manager pinnt sogar die Gateway-API-CRDs über die Release-URL und die shell.nix-Tool-Pins.
Gruppieren, damit ein PR eine Entscheidung ist
groups.json bündelt zusammengehörige Bumps zu einem PR — weil ich nicht über zehn einzelne Sidecar-Bumps entscheiden will, sondern über ein Upgrade:
| Gruppe | Was zusammenfällt |
|---|---|
talos | siderolabs/installer — die Talos-Version für tuppr |
kubernetes | siderolabs/kubelet — die K8s-Version für tuppr |
flux | alle ghcr.io/fluxcd/*-Controller + fluxcd/flux2 |
envoy-gateway | Controller-Chart und vendored CRD-Chart (selber Release-Train) |
zeroclaw-cli-tools | alle CLI-Pins im ZeroClaw-Image auf einmal |
Was bleibt
Das Dependency-Dashboard ist mein einziger Cockpit-Blick: ein Issue, das alles Offene listet. Der Rest läuft im Hintergrund — und passt nahtlos zu zwei Dingen, über die ich schon geschrieben habe: zur Supply-Chain mit cosign und Kyverno (signierte Images, die Renovate bumpt, werden beim Admission trotzdem verifiziert) und zu meinem Hang, wiederkehrende Operationen in Tasks zu gießen statt sie zu merken.
Die Lektion nach ein paar Monaten ist banal und trägt trotzdem: Automerge ist eine Vertrauensgrenze. Was darunter liegt, will ich nie wieder von Hand bumpen. Was darüber liegt — Cilium, Flux, Rook, Cert-Manager, Vault —, will ich nie wieder nicht von Hand bumpen.
