{ $schema: 'https://docs.renovatebot.com/renovate-schema.json', extends: [ 'config:recommended', ':dependencyDashboard', ':semanticCommits', 'helpers:pinGitHubActionDigests', ], timezone: 'America/New_York', schedule: [ 'after 3am and before 6am', ], prConcurrentLimit: 5, prHourlyLimit: 2, commitMessagePrefix: 'chore(deps):', labels: [ 'dependencies', 'automated-update', ], ignoreUnstable: true, respectLatest: true, enabledManagers: [ 'flux', 'kustomize', 'helmv3', 'kubernetes', 'custom.regex', 'github-actions', ], flux: { managerFilePatterns: [ '/(^|/)platform/.+\\.ya?ml$/', '/(^|/)apps/.+\\.ya?ml$/', ], }, kustomize: { managerFilePatterns: [ '/(^|/)kustomization\\.ya?ml$/', ], }, kubernetes: { managerFilePatterns: [ '/(^|/)platform/.+\\.ya?ml$/', '/(^|/)platform-config/.+\\.ya?ml$/', '/(^|/)apps/.+\\.ya?ml$/', '/(^|/)catalog/.+\\.ya?ml$/', '/(^|/)runbooks/serving/.+\\.ya?ml$/', '/(^|/)infra/agent/.+\\.ya?ml$/', ], }, customManagers: [ { customType: 'regex', description: 'Inline `# renovate: datasource=... depName=...` markers on pinned URLs / version literals (e.g. platform/kubevirt/kustomization.yaml release URLs, ci/versions.env, infra/devcontainer/versions.env).', managerFilePatterns: [ '/(^|/)kustomization\\.ya?ml$/', '/(^|/)versions\\.env$/', ], matchStrings: [ '#\\s*renovate:\\s*datasource=(?\\S+)\\s+depName=(?\\S+)(?:\\s+versioning=(?\\S+))?\\s*\\n.*?(?v?[0-9]+\\.[0-9]+\\.[0-9]+)', ], versioningTemplate: '{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}', }, { customType: 'regex', description: "This repo's own CI-published images, pinned by digest, following the tag named in a `# renovate-digest:` marker on the previous line.", managerFilePatterns: [ '/(^|/)versions\\.env$/', '/(^|/)platform/pm-agent/.+\\.ya?ml$/', ], matchStrings: [ '#\\s*renovate-digest:\\s*image=(?\\S+)\\s+tag=(?\\S+)\\s*\\n[^\\n]*?(?sha256:[a-f0-9]{64})', ], datasourceTemplate: 'docker', versioningTemplate: 'docker', }, { customType: 'regex', description: 'ServiceDefinition `source.image: :` (catalog/*.yaml) — the source of the rendered apps/wave-* image.', managerFilePatterns: [ '/(^|/)catalog/[^/]+\\.ya?ml$/', ], matchStrings: [ '\\n\\s+image:\\s*(?[^\\s:@#"\']+):(?[^\\s@#"\']+)', ], datasourceTemplate: 'docker', }, { customType: 'regex', description: 'catalog/render.py OAUTH2_PROXY_IMAGE — the per-app oauth2-proxy sidecar every forward-auth app is rendered with.', managerFilePatterns: [ '/(^|/)catalog/render\\.py$/', ], matchStrings: [ '\\nOAUTH2_PROXY_IMAGE\\s*=\\s*"(?[^":@]+):(?[^"@]+)"', ], datasourceTemplate: 'docker', }, { customType: 'regex', description: 'CloudNativePG Cluster `imageName: :`.', managerFilePatterns: [ '/(^|/)platform/.+\\.ya?ml$/', '/(^|/)apps/.+\\.ya?ml$/', ], matchStrings: [ '\\n\\s+imageName:\\s*(?[^\\s:@#"\']+):(?[^\\s@#"\']+)', ], datasourceTemplate: 'docker', }, ], packageRules: [ { matchCategories: [ 'docker', ], enabled: true, }, { description: 'Auto-merge the allowed low-risk classes on green CI — no human authoring (R-UPD-02). The Gitea merge gate (make -C ci gate) is the required check.', matchUpdateTypes: [ 'minor', 'patch', 'digest', ], automerge: true, automergeType: 'pr', platformAutomerge: true, }, { description: 'MAJOR / potentially-breaking bumps require human review (docs/11 safety rail). Never auto-merged.', matchUpdateTypes: [ 'major', ], automerge: false, addLabels: [ 'needs-review', 'breaking', ], }, { description: 'Keep the KubeVirt + CDI operator pair on a compatible track — grouped so they bump together (platform/kubevirt/kustomization.yaml).', matchDepNames: [ 'kubevirt/kubevirt', 'kubevirt/containerized-data-importer', ], groupName: 'kubevirt-cdi', }, { description: 'Pinned tool versions (ci/versions.env, infra/devcontainer/versions.env) are a deliberate change surface — label but do not auto-merge; a version bump there changes CI reproducibility.', matchFileNames: [ 'ci/versions.env', 'infra/devcontainer/versions.env', ], automerge: false, addLabels: [ 'ci-tooling', ], }, { description: 'ADDED 2026-10-17. NEVER AUTO-MERGE THE BLAST-RADIUS COMPONENTS, not even a patch. The default rule above auto-merges minor/patch/digest on a green gate, which is right for an app image: a bad Sonarr bump inconveniences one pod. It is wrong for these five, because the gate is a STATIC check — it renders manifests and lints them, it does not boot a node, drain a volume or form an etcd quorum (vars.CLUSTER_AVAILABLE is unset, so L2/L3 conformance does not run in CI at all). So for anything whose failure mode is the CLUSTER rather than a service, a green gate is not evidence and a human must read the changelog. kured is here because it reboots control-plane nodes; longhorn because a storage-engine bump migrates live volume data; cilium because it is the CNI and a bad bump is a total partition (and its version is DUAL-PINNED in infra/ansible/roles/cilium/defaults/main.yml, which Renovate does not see, so the two would silently desync); cloudnative-pg because it drives Postgres switchovers; kube-prometheus-stack because ci/versions.env asserts an exact promtool/Prometheus lockstep and a chart bump that moves Prometheus fails the gate until that file moves with it.', matchPackageNames: [ 'kured', 'longhorn', 'cilium', 'cloudnative-pg', 'kube-prometheus-stack', 'gitea/gitea', 'gitea/act_runner', 'ghcr.io/cloudnative-pg/postgresql', ], automerge: false, addLabels: [ 'needs-review', 'blast-radius', ], }, { description: 'Keycloak operator + CRDs + server (lockstep, one branch). IdP blast radius; passkey-reconciler.yaml encodes 26.0.6 Admin API shapes — re-check it against the new tag before merging. Never auto-merge.', matchDepNames: [ 'keycloak/keycloak-k8s-resources', ], groupName: 'keycloak-operator', automerge: false, addLabels: [ 'needs-review', 'blast-radius', ], }, { description: 'quay.io/keycloak/keycloak appears only in the documentation-only catalog/keycloak.yaml and its undeployed render; the operator (keycloak/keycloak-k8s-resources) chooses the real server image.', matchPackageNames: [ 'quay.io/keycloak/keycloak', ], enabled: false, }, { description: "ADDED 2026-10-29. This repo's own images are followed ONLY through the `# renovate-digest:` markers above. The `kubernetes` manager also sees platform/pm-agent/deployment.yaml and infra/agent/worker-job-template.yaml, where the image has a digest and no tag; left on, it would look those up as `latest` (a tag CI never pushes) and, for the template, re-open the self-referential loop the custom manager deliberately avoids.", matchManagers: [ 'kubernetes', ], matchPackageNames: [ 'gitea.home.keithjackson.dev/home-network/toolbox', 'gitea.home.keithjackson.dev/home-network/agent-runner', 'gitea.home.keithjackson.dev/home-network/conformance-runner', ], enabled: false, }, { description: "ADDED 2026-10-29. Digest bumps for this repo's own images: the image was built and verified by CI before it was published, so these auto-merge on a green gate like any digest (rule 1). Labelled so they are easy to find. NOTE: the conformance-runner bump edits conformance/versions.env, a sensitive path (infra/gitea/approval-policy.json), so it will wait for an administrator's /approve-override regardless of this rule.", matchManagers: [ 'custom.regex', ], matchPackageNames: [ 'gitea.home.keithjackson.dev/home-network/toolbox', 'gitea.home.keithjackson.dev/home-network/agent-runner', 'gitea.home.keithjackson.dev/home-network/conformance-runner', ], addLabels: [ 'own-image', ], }, { description: 'ADDED 2026-10-30. The `flux` manager also extracts `image:` lines inside HelmRelease values, which the `kubernetes` manager already owns. Measured: exactly two (alpine in platform/oauth2-proxy/helmrelease.yaml, velero-plugin-for-aws in platform/velero/helmrelease.yaml), each extracted twice; the second rewrite of the same line logged `Cannot find replaceString in current file content` in runs 353/354. Flux keeps its charts (datasource helm).', matchManagers: [ 'flux', ], matchDatasources: [ 'docker', ], enabled: false, }, { description: "ADDED 2026-10-30. MIRRORS catalog/.yaml `update.range` — the operator's declared per-service policy (schemas/servicedefinition.schema.json), which Renovate does not read. Without it the dry run proposed gitea 28.2.0, qbittorrent 20.04.1 (both stale/odd tags, not real successors), radarr 6.x and nextcloud 35 (four majors past 31; Nextcloud cannot skip majors). KEEP IN STEP with the catalog ranges.", matchPackageNames: [ 'gitea/gitea', ], allowedVersions: '<2.0.0', }, { matchPackageNames: [ 'nextcloud', ], allowedVersions: '<32.0.0', }, { matchPackageNames: [ 'lscr.io/linuxserver/qbittorrent', ], allowedVersions: '<6.0.0', }, { matchPackageNames: [ 'lscr.io/linuxserver/radarr', ], allowedVersions: '<6.0.0', }, { matchPackageNames: [ 'lscr.io/linuxserver/sonarr', ], allowedVersions: '<5.0.0', }, { matchPackageNames: [ 'semaphoreui/semaphore', ], allowedVersions: '<3.0.0', }, { description: "ADDED 2026-10-30. LinuxServer tags: `vX.Y.Z-lsN` is the IMMUTABLE build tag (bare `X.Y.Z` and `latest` are re-pushed for every ls rebuild). Docker versioning would treat `-ls7` as a compatibility suffix and only ever compare tags with the SAME suffix, so it would never move. This is Renovate's own regex-versioning example for linuxserver images: the ls build number becomes the `build` component, so an ls rebuild of the same Jackett is an update too. The `^v` and `$` anchors exclude `latest`, `development`, the bare mutable `X.Y.Z` tags and the arch-prefixed (`amd64-v...`) tags.", matchDepNames: [ 'lscr.io/linuxserver/jackett', ], versioning: 'regex:^v(?\\d+)\\.(?\\d+)\\.(?\\d+)-ls(?\\d+)$', }, { description: "ADDED 2026-10-30. LOOK JACKETT UP ON ghcr.io, NOT lscr.io. lscr.io is LinuxServer's redirector in front of ghcr.io/linuxserver (same manifests: v0.24.2865-ls7 is sha256:4fcf3cc2... on both). Renovate's docker datasource fetches up to 1000 pages of tags from ghcr.io but only `dockerMaxPages` (default 20, a GLOBAL-only option) from any other host (lib/modules/datasource/docker/index.js getTags). Jackett has 32,093 tags at ~1000 per page (measured 2026-10-30), oldest first; the running tag is #31,980, so a lookup via lscr.io never sees it or anything newer and silently proposes NOTHING (measured in a dry run before this rule). overridePackageName changes only the lookup; the file keeps `lscr.io/...`, so the deployed image string does not change. Matched by depName because packageName is what this rule rewrites.", matchDepNames: [ 'lscr.io/linuxserver/jackett', ], overridePackageName: 'ghcr.io/linuxserver/jackett', }, { description: "ADDED 2026-10-30. Jackett publishes ~daily (13 ls builds in 12 days, measured 2026-10-30) and every one is a `patch` under the regex above, i.e. auto-mergeable. Left on the global daily window that is a PR, a gate run and a jackett pod restart EVERY DAY for an indexer proxy whose individual builds nobody needs promptly. WEEKLY instead: Renovate proposes only the newest tag when the window opens, so nothing is skipped, it is just batched. The window is still inside the global 03:00-06:00 local one that .gitea/workflows/renovate.yaml's 08:15 UTC cron lands in. NOTE: catalog/jackett.yaml `update.cadence` still says daily (`0 4 * * *`); that field drove Flux image-automation, which is retired, and this rule is the effective cadence now.", matchDepNames: [ 'lscr.io/linuxserver/jackett', ], schedule: [ 'after 3am and before 6am on sunday', ], }, { description: 'ADDED 2026-10-30. Plex tags are `...-` (e.g. 1.43.4.10903-e5521bd8c). Docker versioning reads `-e5521bd8c` as a compatibility suffix that differs on EVERY release, so it would never find a successor. The trailing `$` excludes the per-arch variants and the floating `latest`/`public`/`beta`/`plexpass` tags. A version tag does NOT self-update inside the container: cont-init 50-plex-update exits early when /version.txt matches the installed package, so what is pinned is what runs.', matchPackageNames: [ 'plexinc/pms-docker', ], versioning: 'regex:^(?\\d+)\\.(?\\d+)\\.(?\\d+)\\.(?\\d+)-[0-9a-f]+$', }, { description: 'ADDED 2026-10-30. A CNPG Postgres MAJOR is a migration (new cluster + import), not an image bump; the dry run proposed 16.4 -> 18.6. Minors stay (and are blast-radius, above).', matchPackageNames: [ 'ghcr.io/cloudnative-pg/postgresql', ], allowedVersions: '<17.0.0', }, { description: "ADDED 2026-10-30. A HelmRelease semver RANGE is a policy ceiling, and Renovate does not respect it — it WIDENS it (dry run: external-dns `<1.22.0` -> `<1.24.0`, headlamp `<0.42.0` -> `<0.47.0`), and the default rule would then auto-merge that as a 'minor', silently letting Flux adopt every new minor. Raising a ceiling is deliberate per-component work: propose it, never auto-merge it.", matchManagers: [ 'flux', ], matchCurrentValue: '/^[<>=~^]|\\.x$/', automerge: false, addLabels: [ 'needs-review', 'range-widen', ], }, { description: 'ADDED 2026-10-30. velero-plugin-for-aws is pinned to a RELEASE CANDIDATE (v1.14.3-rc.2) and Renovate proposed NOTHING for it, although v1.14.3 and v1.14.4 are stable on Docker Hub. Docker versioning treats everything after the first `-` as a compatibility SUFFIX, so only other `*-rc.2` tags qualify and the RC is a fossil forever. Semver versioning reads it as a prerelease, and a prerelease upgrades to the stable release.', matchPackageNames: [ 'velero/velero-plugin-for-aws', ], versioning: 'semver', }, { description: 'ADDED 2026-10-30. A python MINOR is a language-runtime release (the dry run proposed 3.13 -> 3.15 for the Keycloak reconcilers), and the gate does not execute those scripts. Review it; patches still auto-merge.', matchPackageNames: [ 'python', ], matchUpdateTypes: [ 'minor', ], automerge: false, addLabels: [ 'needs-review', ], }, { description: 'ADDED 2026-10-30 (operator decision). The oauth2-proxy image, under the ONE depName every source of it emits: catalog/render.py OAUTH2_PROXY_IMAGE (custom regex manager), the rendered apps/wave-* sidecars and platform/crafty/deployment.yaml (kubernetes manager). WHY: since v7.13.0 oauth2-proxy validates REFRESHED OIDC sessions against the ACCESS token instead of the id_token (upstream CHANGELOG, v7.13.0), every sidecar sets --cookie-refresh, and that refresh path has never run against this Keycloak (the platform oauth2-proxy runs v7.15.5 WITHOUT --cookie-refresh). render-golden and render.py --selftest pass on the bump, but both check rendered text, not the binary, so a green gate is not evidence here.', matchPackageNames: [ 'quay.io/oauth2-proxy/oauth2-proxy', ], automerge: false, addLabels: [ 'needs-review', ], }, { description: "ADDED 2026-10-30 (operator decision). Renovate's OWN version: the job `container.image` pin in .gitea/workflows/renovate.yaml (ghcr.io/renovatebot/renovate), and renovatebot/github-action should it ever be reintroduced (ci/lint.sh rejects it today). WHY: nothing re-validates renovate.json5 against the NEW version before it takes effect. The gate does not run renovate-config-validator, and this exact gap has bitten before: the action's default 39.x rejected this file (managerFilePatterns needs >= 40.2.0) and every run stopped at CONFIG_VALIDATION. Run the validator from the new tag, then merge.", matchPackageNames: [ 'ghcr.io/renovatebot/renovate', 'renovatebot/github-action', ], automerge: false, addLabels: [ 'needs-review', ], }, { description: "ADDED 2026-10-30. MAJORS WAIT FOR A TICK ON THE DEPENDENCY DASHBOARD instead of opening a PR. They are never auto-merged anyway, and with prConcurrentLimit 5 the dry run's 10 majors would hold every PR slot open indefinitely, starving the patch/minor/digest PRs that can merge on green.", matchUpdateTypes: [ 'major', ], dependencyDashboardApproval: true, }, ], ignorePaths: [ '**/*.example', '**/*.sops.yaml', 'conformance/config/**', 'conformance/incluster/**', 'conformance/lib/**', 'conformance/requirements/**', 'conformance/runs/**', 'conformance/tests/**', 'platform/image-automation/per-service/**', 'platform/kyverno/tests/**', ], }