{ $schema: "https://docs.renovatebot.com/renovate-schema.json", // JSON5 on purpose: the per-chart managers below only make sense with the // reasoning next to them. Renovate reads renovate.json5 natively — there must // be no second config file (renovate.json, .github/renovate.json, ...) or // Renovate aborts with "found multiple config files". // // Requires Renovate >= 41 (`managerFilePatterns`, `customType`). See // README.md → "Dependency updates (Renovate)" for the whole picture, // including the one-time `allowedCommands` change the self-hosted instance // needs for the version bump to work. extends: [ "config:recommended", // Commit subjects in this repo are plain sentences, not conventional // commits. Renovate's own merged PRs already look like that. ":semanticCommitsDisabled", ], timezone: "Europe/Berlin", labels: ["dependencies"], // 21 charts plus the workflows: without a cap the first run after a quiet // week opens a wall of PRs, and every chart PR spins up a k3d install-test. prConcurrentLimit: 10, prHourlyLimit: 4, // template-chart is the scaffold. Its image is the literal placeholder // ghcr.io/ORG/IMAGE and its appVersion is "xxx" — nothing to update, and the // chart is never published. ignorePaths: ["template-chart/**"], // CLAUDE.md/README require app major jumps to be taken stepwise: first the // last release of the old major line, then the new major. With this flag a // v1 -> v3 jump arrives as two PRs (latest v2, then v3), which is exactly // that sequence — merge the first, and the second rebases onto it. separateMultipleMajor: true, // Any change inside a chart directory must bump `version:` in its // Chart.yaml, otherwise the `build-chart` job fails the PR and the publish // on main would collide with an already published version. Renovate only // rewrites `appVersion:` (or an image tag in values.yaml), so this hook // recomputes `version:` afterwards. // // IMPORTANT: postUpgradeTasks only runs on self-hosted Renovate and only if // the command is allow-listed in the *instance* config (not in this repo): // // allowedCommands: ["^ci/renovate-bump-chart\\.sh"] // // Without that entry Renovate skips the task silently and every chart PR // stays red on the version-bump check. postUpgradeTasks: { commands: ["ci/renovate-bump-chart.sh {{{packageFileDir}}}"], fileFilters: ["**/Chart.yaml"], // "update" runs the script once per dependency update with the package // file's directory, which is what the script needs to find the chart. The // script derives everything from the base commit, so repeated runs on the // same branch are idempotent. executionMode: "update", }, packageRules: [ { description: [ "Install-test fixtures deliberately pin throwaway infrastructure to major lines", "(postgres:16-alpine, redis:7-alpine, busybox:1.36). They are test scaffolding, not", "shipped artifacts, and churning them only risks breaking the install-test.", ], matchFileNames: ["ci/**"], enabled: false, }, { description: [ "Patch, minor and digest updates merge themselves once the PR pipeline is green.", "Minor is included because the sidecar images barely move in patch steps —", "alpine 3.22 -> 3.24 and nginx 1.30.4 -> 1.31.4 are both minor, so a patch-only", "rule would practically never fire. Majors stay manual.", ], matchUpdateTypes: ["patch", "minor", "digest"], automerge: true, automergeType: "pr", // Renovate waits for the checks itself instead of handing the PR to // GitHub's auto-merge. GitHub would merge immediately when the branch has // no required status checks configured — and validate-charts is not (yet) // a required check. platformAutomerge: false, // A release that is hours old should not walk into main unattended; a // chart publish follows the merge straight away. minimumReleaseAge: "3 days", }, { description: [ "Chart app versions. Kept out of the automerge grouping decisions above only by label,", "so it is visible at a glance which PRs republish a chart.", ], matchDepTypes: ["app-version"], addLabels: ["app-version"], }, { description: [ "nginx numbers its release branches by minor: even is the stable branch, odd is", "mainline. The Synapse delegation nginx belongs on stable, so only even minors are", "allowed — that is what PR #139 (1.30.4 -> 1.31.4) would otherwise have done,", "quietly moving the chart onto mainline. Patch updates inside the stable branch", "(1.30.4 -> 1.30.5) still come through and still automerge.", ], matchDatasources: ["docker"], matchPackageNames: ["nginx", "library/nginx", "docker.io/library/nginx"], // Even minor: any minor whose last digit is even, so 1.30.x passes and // 1.31.x does not. The trailing (-.+)? is load-bearing: #84 commissioned // a check on moving this sidecar to 1.30.4-alpine (outcome open, to be // done alongside the seccompProfile PR), and with a variant tag // like 1.30.4-alpine docker versioning only ever offers candidates // carrying the same suffix. Without the optional suffix, every one of // them would fail this filter and nginx would silently stop receiving // updates - no error, no warning, green dashboard. Verified: with the // anchored form and currentValue 1.28.0-alpine, the lookup returns // "updates": [] and "warnings": []. allowedVersions: "/^\\d+\\.\\d*[02468]\\.\\d+(-.+)?$/", }, { description: [ "linuxserver/qbittorrent carries two historic tags from earlier tagging schemes,", "14.3.9 and 20.04.1, which docker versioning sorts above every real release and", "offers as major updates — PRs #152 and #153 are exactly those. Both are excluded", "by name.", ], matchDatasources: ["docker"], matchPackageNames: [ "linuxserver/qbittorrent", "docker.io/linuxserver/qbittorrent", ], // Excluded by name rather than by a bound such as "<10": the strays are // finished artifacts that cannot grow, so naming them can never block a // release that does not exist yet, while any upper bound would sooner or // later reject a genuine major. Shape-based filters were measured and // rejected too: requiring a non-padded minor removes 20.04.1 and leaves // 14.3.9 (verified, the lookup then offers 14.3.9 instead). // // Verified with renovate 44.41.0 (dry-run=lookup, real registry), two // fixtures against the live tag list: // currentValue 5.2.1 -> updates [5.2.3 (patch)]; both strays gone. // currentValue 4.6.4 -> updates [4.6.7 (patch), 5.2.3 (major)]. // The second fixture is the guard the nginx rule taught us to run: it // proves a genuine major still comes through instead of the filter // silently emptying the update list. It also shows what the strays cost // today — without the rule that same lookup offers 20.04.1 as the major // candidate and never mentions the real 5.2.3, because Renovate fills // the major bucket once. allowedVersions: "!/^(14\\.3\\.9|20\\.04\\.1)$/", }, ], customManagers: [ { description: [ "Tool versions pinned as workflow env vars (TRIVY_VERSION). Annotate the line with", "`# renovate: datasource= depName=` and Renovate keeps it current.", ], customType: "regex", managerFilePatterns: [".github/workflows/*.yml"], matchStrings: [ "# renovate: datasource=(?\\S+) depName=(?\\S+)(?: versioning=(?\\S+))?\\s+\\w+: [\"']?(?[^\"'\\s]+)", ], versioningTemplate: "{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}", }, // --- Chart app versions ------------------------------------------------- // // Every chart renders its main image as `:{{ .Chart.AppVersion }}`, // so the repository lives in the template and the version in Chart.yaml. // Renovate cannot join the two across files, which is why each chart gets // its own manager that hard-codes the repository and reads `appVersion:`. // // Deliberately *not* solved with a `# renovate:` comment inside each // Chart.yaml: that would touch all 21 chart directories and force 21 // version bumps plus 21 publishes for a pure comment change. Keeping the // mapping here leaves the charts untouched. // // When adding a chart, add a block here — see README "Creating a new chart". // // Exception, deliberately no block: `freesailarr`. It bundles nine // independently versioned upstreams, so none of them is "the" app version // and no image tag comes from appVersion — all nine are pinned as // image.repository/image.tag in its values.yaml, where the built-in // helm-values manager already finds them and proposes one update per // image. Adding a block here would either duplicate those updates or // rewrite an appVersion that selects nothing. Its appVersion is a // hand-maintained marker; see freesailarr/Chart.yaml. { description: "apache-tika: four-part version plus flavour suffix (3.3.1.0-full); the compatibility group keeps the chart on -full.", customType: "regex", managerFilePatterns: ["apache-tika/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "apache/tika", datasourceTemplate: "docker", depTypeTemplate: "app-version", versioningTemplate: "regex:^(?\\d+)\\.(?\\d+)\\.(?\\d+)\\.(?\\d+)-(?full)$", }, { description: [ "bentopdf: the registry carries three tag families side by side - 2.8.7, v2.8.7 and", "v2.8.7-amd64/-arm64, plus latest/edge/sha-. The chart pins the suffix-free", "form, which is the one appVersion can carry unchanged.", ], customType: "regex", managerFilePatterns: ["bentopdf/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "ghcr.io/alam00000/bentopdf-simple", datasourceTemplate: "docker", depTypeTemplate: "app-version", }, { customType: "regex", managerFilePatterns: ["calibre-web-automated/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "ghcr.io/crocodilestick/calibre-web-automated", datasourceTemplate: "docker", depTypeTemplate: "app-version", }, { customType: "regex", managerFilePatterns: ["cyberchef/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "ghcr.io/gchq/cyberchef", datasourceTemplate: "docker", depTypeTemplate: "app-version", }, { customType: "regex", managerFilePatterns: ["gotenberg/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "gotenberg/gotenberg", datasourceTemplate: "docker", depTypeTemplate: "app-version", }, { description: "homeassistant: CalVer tags (2026.8.3) sort correctly under docker versioning; the floating stable/beta/dev tags are skipped because they parse as no version at all.", customType: "regex", managerFilePatterns: ["homeassistant/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "ghcr.io/home-assistant/home-assistant", datasourceTemplate: "docker", depTypeTemplate: "app-version", }, { customType: "regex", managerFilePatterns: ["matrix-synapse/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "ghcr.io/element-hq/synapse", datasourceTemplate: "docker", depTypeTemplate: "app-version", }, { customType: "regex", managerFilePatterns: ["minecraft-router/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "itzg/mc-router", datasourceTemplate: "docker", depTypeTemplate: "app-version", }, { description: "minecraft-server: tags encode the JRE as a suffix (2026.8.2-java21); the compatibility group prevents a silent runtime switch.", customType: "regex", managerFilePatterns: ["minecraft-server/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "itzg/minecraft-server", datasourceTemplate: "docker", depTypeTemplate: "app-version", versioningTemplate: "regex:^(?\\d+)\\.(?\\d+)\\.(?\\d+)-(?java\\d+)$", }, { customType: "regex", managerFilePatterns: ["outline/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "outlinewiki/outline", datasourceTemplate: "docker", depTypeTemplate: "app-version", }, { customType: "regex", managerFilePatterns: ["paperless-ngx/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "ghcr.io/paperless-ngx/paperless-ngx", datasourceTemplate: "docker", depTypeTemplate: "app-version", }, { customType: "regex", managerFilePatterns: ["patchmon/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "ghcr.io/patchmon/patchmon-server", datasourceTemplate: "docker", depTypeTemplate: "app-version", }, { customType: "regex", managerFilePatterns: ["rallly/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "lukevella/rallly", datasourceTemplate: "docker", depTypeTemplate: "app-version", }, { description: "rss-bridge: date-based tags (2025-06-03). The bump script normalizes the dashes to dots for the +up suffix, as the versioning scheme requires.", customType: "regex", managerFilePatterns: ["rss-bridge/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "docker.io/rssbridge/rss-bridge", datasourceTemplate: "docker", depTypeTemplate: "app-version", versioningTemplate: "regex:^(?\\d{4})-(?\\d{2})-(?\\d{2})$", }, { description: [ "searxng: every tag is - (2026.8.22-9fea41204) - no semver line at all,", "and 'latest' moves daily. The date carries the ordering; the commit hash is matched", "but deliberately NOT captured.", ], customType: "regex", managerFilePatterns: ["searxng/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "docker.io/searxng/searxng", datasourceTemplate: "docker", depTypeTemplate: "app-version", // The hash sits outside every capture group on purpose, and that is the // whole point of this line. Renovate's regex versioning reads a "build" // group NUMERICALLY - regex/index.js _parse() does // release.push(Number.parseInt(build)) - so a commit hash becomes // whatever digits happen to lead it: parseInt("9fea41204") is 9, // parseInt("777ba8fa4") is 777, and any hash starting with a letter // (6 of the 25 tags live today) is NaN. // // Verified against the real API on renovate 41.173.1 and 44.48.0, with // currentValue 2026.8.22-9fea41204: // with (?[0-9a-f]+): // isGreaterThan("2026.8.22-777ba8fa4", current) -> true // without it (this line): // isGreaterThan("2026.8.22-777ba8fa4", current) -> false, equals -> true // 777ba8fa4 was pushed at 03:06 and 9fea41204 at 05:38 the SAME day, so // the build group makes Renovate offer the older image as an update - // a downgrade, unattended, because date updates land in the patch/minor // automerge rule. With NaN the pair is neither equal nor ordered at all. // // Uncaptured, the hash still has to be THERE (the pattern is anchored), // it just takes no part in the comparison: two builds of one day are // equal, so no sideways update is proposed, and a newer date wins // whatever the hash looks like. // // "prerelease" instead of "build" was measured too and rejected: it // sorts correctly but marks every single tag isStable === false, which // hands the whole chart to ignoreUnstable. versioningTemplate: "regex:^(?\\d{4})\\.(?\\d{1,2})\\.(?\\d{1,2})-[0-9a-f]+$", }, { customType: "regex", managerFilePatterns: ["samba/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "ghcr.io/dockur/samba", datasourceTemplate: "docker", depTypeTemplate: "app-version", }, { customType: "regex", managerFilePatterns: ["satisfactory/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "ghcr.io/wolveix/satisfactory-server", datasourceTemplate: "docker", depTypeTemplate: "app-version", }, { description: "teamspeak-server: the 6.0.0 line only ships beta tags (6.0.0-beta12.1). Loose versioning compares the numeric parts left to right, so beta12.1 sorts above beta9; semver would rank every beta as an equal prerelease.", customType: "regex", managerFilePatterns: ["teamspeak-server/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "teamspeaksystems/teamspeak6-server", datasourceTemplate: "docker", depTypeTemplate: "app-version", versioningTemplate: "loose", }, { customType: "regex", managerFilePatterns: ["urlaubsverwaltung/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "urlaubsverwaltung/urlaubsverwaltung", datasourceTemplate: "docker", depTypeTemplate: "app-version", }, { customType: "regex", managerFilePatterns: ["voucher-vault/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "l4rm4nd/vouchervault", datasourceTemplate: "docker", depTypeTemplate: "app-version", }, { customType: "regex", managerFilePatterns: ["zipline/Chart.yaml"], matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], depNameTemplate: "ghcr.io/diced/zipline", datasourceTemplate: "docker", depTypeTemplate: "app-version", }, ], }