Files
helm-charts/renovate.json5
T
jklein 59d2614384
Validate Helm Charts / Detect changed charts (pull_request) Successful in 5s
Validate Helm Charts / Build ${{ matrix.chart }} (pull_request) Skipped
Validate Helm Charts / Install-test (pull_request) Skipped
Validate Helm Charts / Resource quantities (pull_request) Successful in 18s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 14s
Validate Helm Charts / Bump level (pull_request) Successful in 7s
Validate Helm Charts / Trivy config cyberchef (pull_request) Successful in 20s
Validate Helm Charts / Trivy config matrix-synapse (pull_request) Successful in 22s
Validate Helm Charts / Trivy config samba (pull_request) Successful in 21s
SonarQube Analysis / SonarQube Analysis (push) Successful in 48s
feat(ci): the chart bump follows the app bump
The version-bump hook classified every app update that was not a major
as a chart patch, so an app minor arrived as a patch - the mechanism
behind the sonarr incident, where a security default was switched off
inside a bump the conventions told the reviewer not to read (Gitea #128).

ci/renovate-bump-chart.sh now takes the level from Renovate's update
type, passed in as a second argument: major -> major, minor -> MINOR,
patch/digest/pin -> patch. The level comes from Renovate rather than
from comparing appVersion because an appVersion comparison is blind to
the case that prompted the rule - freesailarr keeps a hand-maintained
appVersion for ten upstreams, and prowlarr's 2.5.2 -> 2.6.5 moved an
image tag in values.yaml while appVersion stood still.

Three properties come with it:

- Maximum, not replacement. The chart jump is the greater of the
  chart's own classification and the app jump, so a value that becomes
  required stays a major under an app patch. Mechanically it is also
  what makes a grouped branch deterministic: the hook runs once per
  update from the same base commit, so without it the last call would
  win.
- Non-semver app versions stay at patch for a minor (seven charts). In
  CalVer and date tags the second position is a month; in a four-part
  or suffixed version the positions are not semver positions. An app
  major is not subject to it.
- A missing or unknown update type falls back to the pre-#128
  behaviour. postUpgradeTasks only runs where the command is
  allow-listed on the shared instance, which is not visible from here
  and fails silently, so the worst case is the old classification
  rather than every chart PR red on the version check.

ci/run-bump-level-test.sh holds 49 cases against the version string the
script writes, wired up as the bump-level job: a wrong level is a valid
version string, so the PR would go green and the only symptom would be
a number that understates what it publishes.

The convention texts follow in CLAUDE.md and README, including the
limit the decision deliberately keeps: the reading duty was rejected,
so the incident stays repeatable and the honest number is the whole of
the remedy.

Fixes #128
2026-10-13 18:47:44 +00:00

979 lines
57 KiB
JSON

{
$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",
// Same preset as cluster-config, so both repos behave identically on the
// shared jlab-renovate-bot instance. Two of the things it pulls in are
// load-bearing here: `helpers:pinGitHubActionDigests` (wanted — the
// workflow files move from `actions/checkout@v7` to SHA pins) and
// `docker:pinDigests`, which is turned off again for the two managers that
// cannot carry a digest — see the pinDigests rule below. The
// npm-flavoured members (`:pinDevDependencies`,
// `security:minimumReleaseAgeNpm`, `:maintainLockFilesWeekly`) are inert
// in a repo without a package manager.
"config:best-practices",
],
timezone: "Europe/Berlin",
labels: ["renovate"],
dependencyDashboard: true,
// No caps, as in cluster-config. The first run after a quiet week can open
// many PRs at once and every chart PR starts a k3d install-test; on Gitea a
// whole matrix runs inside one job on one runner, so those queue instead of
// fanning out. That queue is the accepted price for not having updates
// trickle in four per hour — nothing merges without a review anyway.
prConcurrentLimit: 100,
prHourlyLimit: 0,
// Minor and patch arrive as separate PRs rather than one combined branch.
// With 24 charts a shared branch means one red install-test blocks unrelated
// updates, and the separation is what lets a patch be reviewed in seconds.
separateMinorPatch: true,
// Every update automerges once the PR is mergeable, majors included, and
// `platformAutomerge` hands that decision to the forge instead of letting
// Renovate poll: Gitea arms the PR and merges it when the required check
// (`validate-charts`) passes AND the required review exists. Both are active
// on `main`, which is why this is not unattended merging — the review is the
// gate, and without it the armed PR simply sits there. Same reasoning as
// cluster-config, where the dashboard approval was retired for it.
automerge: true,
automergeType: "pr",
platformAutomerge: true,
// 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.
//
// On the Gitea side (#225) that entry EXISTS and was verified rather than
// hoped for: the shared instance's first run on the migrated repo produced
// a searxng PR whose diff carries `version: 1.0.4+up... -> 1.0.5+up...`
// next to the appVersion. No built-in manager touches `version:`, so that
// line can only come from this hook. Nothing has to be configured for the
// move - but the allow-list lives on that instance, not in this repo, so it
// is also not visible from here if it is ever removed.
//
// THE SECOND ARGUMENT IS THE UPDATE TYPE (Gitea #128), and it is what lets
// the chart version follow the app version: major -> major, minor -> MINOR,
// patch/digest -> patch. It is passed rather than derived because Renovate
// has already classified the jump and the script cannot: `freesailarr` keeps
// a hand-maintained `appVersion: "1.0.0"` for ten upstreams, so prowlarr's
// 2.5.2 -> 2.6.5 moved an image tag in values.yaml while appVersion stood
// still - an appVersion comparison is blind to exactly the case the rule was
// written for (README, "The version-bump hook").
//
// VERIFIED, NOT ASSUMED, and it took two runs because one of them cannot
// answer the question. Renovate 44.155.7, keycloak pinned one minor back:
//
// 1. --platform=local --dry-run=full reads THIS file and classifies the
// update ("updateType": "minor", newValue 26.8.1, alongside a separate
// "patch" bucket for 26.7.5). What it does NOT do is reach the branch
// worker at all - there is not one "Processing branch" line in the log -
// so the hook never runs under the local platform and that run can
// prove the lookup and the config, never the command.
// 2. Renovate's own post-upgrade executor
// (dist/workers/repository/update/branch/execute-post-upgrade-commands)
// driven against a throwaway checkout with allowedCommands set to the
// pattern above:
// DEBUG: Executed post-upgrade task
// "cmd": "ci/renovate-bump-chart.sh keycloak minor"
// "stdout": "renovate-bump: keycloak/Chart.yaml 1.3.0+up26.7.0 ->
// 1.4.0+up26.8.1 (update type minor)"
// So the field arrives populated, the front-anchored allow-list accepts
// the command WITH the argument, and the result is a chart minor.
//
// `updateType` is in Renovate's template allowedFields (util/template:
// "One of digest, pin, rollback, patch, minor, major, replacement,
// pinDigest") and the command is compiled per UPDATE here, which is what
// executionMode: "update" below means.
//
// THE RISK THIS ADDS, and it cannot be settled from inside this repo: the
// allow-list is matched against the COMPILED command
// (`allowedCommands.some((p) => regEx(p).test(compiledCmd))`), so the
// front-anchored pattern above still matches with an argument appended. A
// pattern anchored at the END on the instance (`...\.sh$`) would stop
// matching and the task would be skipped with every chart PR red on the
// version check - the same silent failure a missing entry produces. Which
// form the instance uses is not visible from here. The script therefore
// treats a MISSING second argument as the pre-#128 behaviour instead of an
// error, so the worst case is the old classification rather than an outage.
postUpgradeTasks: {
commands: [
"ci/renovate-bump-chart.sh {{{packageFileDir}}} {{{updateType}}}",
],
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: [
"Routine updates wait three days before Renovate even opens the PR. Automerge is",
"global now (top of this file), so this rule no longer decides WHAT merges by itself —",
"it decides how fresh a release may be when it does. A merge here is never just a",
"merge: every chart change publishes a chart straight away, and a release that is",
"yanked hours after it appeared should not reach the registry at all.",
"cluster-config spends the same three days on the dependencies where a merge is",
"expensive (authentik, rancher); here that is every one of them.",
"",
"Majors deliberately carry no age gate: they are not the updates that slip through",
"unnoticed, they are the ones held at the review, and separateMultipleMajor makes",
"them arrive one line at a time anyway.",
],
matchUpdateTypes: ["patch", "minor", "digest"],
minimumReleaseAge: "3 days",
},
{
description: [
"`config:best-practices` brings `docker:pinDigests`, and two of this repo's managers",
"have nowhere to put a digest. custom.regex reads `appVersion:` out of a Chart.yaml,",
"and that value is a TAG by construction: the templates render",
"`<repo>:{{ .Chart.AppVersion }}`, and the chart version's `+up<appVersion>` suffix is",
"built from the same string — an `@sha256:...` fits into neither. cluster-config makes",
"the identical opt-out for its fleet manager, for the identical reason.",
"",
"helm-values is opted out too, but as a decision rather than a necessity: sidecar",
"images there COULD be pinned by digest, and every digest change would then be a chart",
"patch plus a publish. Whether that is worth it is its own question, not part of this",
"alignment.",
"",
"Unaffected: the freesailarr/mediathekarr block below, which extracts a digest that is",
"written in values.yaml on purpose (pinDigests only ADDS digests where none exists),",
"and the GitHub Actions pinning from `helpers:pinGitHubActionDigests` — a different",
"datasource, and wanted.",
],
matchManagers: ["custom.regex", "helm-values"],
pinDigests: false,
},
{
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: [
"Helm subchart dependencies (the `dependencies:` block of a Chart.yaml). Handled by",
"the BUILT-IN helmv3 manager - no customManager needed, and none would help: the",
"repository URL and the version sit next to each other in one file, which is the",
"case the built-in manager exists for. It is enabled by config:recommended and this",
"config sets no enabledManagers, so nothing has to switch it on. Verified by dry-run",
"for #214: with ytdlp-web-player pinned one release back, the lookup returns",
"oauth2-proxy 10.6.2 -> 10.7.0 as a manager=helmv3 minor update.",
"",
"This rule adds only a label. It exists because the update matters more here than",
"its size suggests: a subchart is invisible to everything downstream that watches",
"Helm release versions, so this PR is the ONLY way a new proxy version - a security",
"fix included - ever reaches a cluster. Automerge carries that PR to main once it is",
"reviewed and the check is green, and ci/renovate-bump-chart.sh bumps the chart version",
"that publishes it - since #128 at the SUBCHART'S level, so an oauth2-proxy minor is a",
"chart minor even though our appVersion did not move. That is the point of taking the",
"level from Renovate rather than from appVersion: the one number anything downstream",
"sees now reports the size of what is inside the package. A major goes the same way -",
"it is the review, not a separate policy, that gives it a look.",
],
matchManagers: ["helmv3"],
addLabels: ["subchart"],
},
{
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)$/",
},
{
description: [
"The freesailarr stack arrives as ONE grouped PR per cycle instead of one PR per image.",
"This is the only groupName in this file and a deliberate reversal of the stance written",
"above the app-version managers, which says the built-in helm-values manager finds the ten",
"images in that chart's values.yaml and 'proposes one update per image'. That sentence",
"described what Renovate does; it was never an argument that one PR per image is what this",
"chart wants. For every other chart in this repo it still is — one chart, one image, one",
"PR is already the grouping — and that is why this rule is scoped to one file rather than",
"turning into a repo-wide policy.",
"",
"WHAT CHANGED, measured on 2026-10-12 against the Gitea API: eight of this chart's ten",
"images had an open Renovate PR at the same moment — #102 bazarr, #103 qbittorrent, #104",
"sonarr, #105 flaresolverr, #106 recyclarr, #107 prowlarr, #108 radarr, #111 seerr. Each",
"one edits freesailarr/values.yaml and, through the postUpgradeTasks hook above, also",
"freesailarr/Chart.yaml. The API reported `mergeable: false` for all eight, because the",
"`version:` line they rewrite is the same line in all eight. One of them can merge; the",
"other seven then need a rebase round each, and every rebase restarts the most expensive",
"install-test in this repo — a real Wireguard tunnel and ten containers in one pod.",
"",
"The second symptom is the sharper one. All eight carried",
"`version: 3.1.1+up1.0.0 -> 3.1.2+up1.0.0` while main already stood at 3.3.0+up1.0.0:",
"the hook derives everything from the BASE COMMIT (see its comment above), and these",
"branches were cut before the Gateway-API rounds landed. Merging one unrebased would have",
"moved the chart version backwards by two minors and republished the chart under a version",
"the registry already has. A rebase repairs it — the point is that nothing FAILS when the",
"rebase does not happen first, so the repair depends on the bot reaching the branch before",
"a human reaches the merge button.",
"",
"WHY GROUPING FIXES BOTH: one branch, one `version:` bump computed once from one base, one",
"publish, one install-test run for the whole stack. Nothing is left to serialize, so",
"nothing is left to rebase.",
"",
"WHAT LEVEL A MIXED GROUP GETS, since #128 made that a question: the hook still runs once",
"per update and still recomputes from the same base, so the group takes the HIGHEST level",
"in it - three minors among five patches is a chart minor, whatever order the updates are",
"processed in. That is the maximum rule in ci/renovate-bump-chart.sh, and this branch is",
"the reason it is not optional: without it the last call would win.",
"",
"MATCHED BY FILE, NOT BY A LIST OF IMAGE NAMES. What creates the collision is 'touches",
"freesailarr/Chart.yaml', and that is true of every dependency Renovate finds in that",
"chart's values.yaml, whatever its manager, datasource or role in the pod. A",
"matchPackageNames list would be a second place to keep the chart's image set current, and",
"its failure mode is silence: an image added later simply never joins the group, with no",
"error and no warning (#158 — a valid rule is not a matching rule). matchFileNames needs",
"no maintenance to stay complete.",
"",
"Consequences of that choice, each one intended and not a side effect:",
" - mediathekarr IS in the group, even though it comes from its own customManagers block",
" further down rather than from helm-values. Its digest PR edits the same two files and",
" blocks the other seven exactly as they block each other, so leaving it beside the",
" group would keep a smaller copy of the problem alive. Its own block is untouched; a",
" group decides which BRANCH an update lands on, not how it is found. `digest` is in",
" matchUpdateTypes precisely so this one is covered. Verified the way that block's own",
" comment demands, because while the pin is current a working rule and a broken one look",
" identical: with the real digest the run produces no mediathekarr update at all, so it",
" proves nothing; with the digest set to sha256:0000... the digest update appears ON THE",
" GROUP BRANCH next to the other eight, not on renovate/docker.io-pcjones-mediathekarr-",
" beta as it would without this rule.",
" - gluetun and the busybox init image are in the group too, against the instinct to hold",
" them apart because gluetun is the tunnel every other container's network namespace",
" depends on and busybox is 'only' an init container. Neither property has anything to",
" do with the blocking, and a grouped PR hides nothing: the review still sees every",
" changed line, it just no longer sees them eight times. (The busybox pinned in",
" ci/<chart>/ is a different thing and stays unmanaged — see the ci/** rule at the top.)",
"",
"MAJORS STAY OUT, which is what matchUpdateTypes is doing here. CLAUDE.md and README",
"require an app major to be taken stepwise, in its own PR, and separateMultipleMajor above",
"splits a multi-major jump into one PR per major line to produce exactly that sequence. A",
"group that threw a major in with seven patches would undo both, and a major is the update",
"where the review most needs one image at a time. Majors therefore keep arriving",
"individually and keep their deliberately missing age gate.",
"",
"A groupName ALONE DOES NOT DO THE JOB, which is why the rule directly below exists and",
"is not a stylistic split. The global `separateMinorPatch: true` at the top of this file",
"keeps splitting minor from patch INSIDE the group. Measured (renovate 44.x,",
"--platform=local --dry-run=full, real registries, 2026-10-12, against a checkout at the",
"eight pending updates): with the group rule and nothing else, the eight land on TWO",
"branches —",
" renovate/freesailarr-stack prowlarr 2.6.5, radarr 6.4.4, seerr v3.5.0 (minor)",
" renovate/patch-freesailarr-stack bazarr, qbittorrent, sonarr, flaresolverr, recyclarr",
"— and note the shape: separateMinorPatch prefixes the branch, it does not suffix it, so",
"the second name is easy to miss when scanning a branch list. Two PRs editing the same",
"`version:` line is the original problem at a quarter of the size, so the override is",
"required for the result this rule is for. With it, the same run puts all eight on one",
"branch.",
"",
"And the override CANNOT live in this rule: renovate rejects the combination outright with",
" packageRules[n]: packageRules cannot combine both matchUpdateTypes and",
" separateMinorPatch",
"which is a config-validation error, not a warning — found by running the dry-run instead",
"of trusting the draft. It is consistent, too: separateMinorPatch is what DECIDES the",
"updateType split, so a rule may not both perform the split and select on its outcome.",
"Hence two rules over the same matchFileNames, one selecting updateTypes to group and one",
"setting the split. Keep them together and keep them in this order.",
"",
"WHAT THIS RULE CANNOT DO: it takes effect on the next run of the shared instance, and",
"that instance is outside this repository. What is verifiable from here is that the config",
"validates and that the lookup groups as intended; that the bot then behaves that way is",
"not provable from this side (see README, 'Renovate on Gitea').",
],
matchFileNames: ["freesailarr/values.yaml"],
matchUpdateTypes: ["patch", "minor", "digest"],
groupName: "freesailarr stack",
groupSlug: "freesailarr-stack",
},
{
description: [
"The other half of the freesailarr grouping directly above — read that block first, the",
"whole argument is there. Separate only because Renovate refuses matchUpdateTypes and",
"separateMinorPatch in one rule ('packageRules cannot combine both matchUpdateTypes and",
"separateMinorPatch', a validation error). Without this, the group still arrives as two",
"PRs, one of them on the easily missed renovate/patch-freesailarr-stack.",
"",
"It deliberately carries NO matchUpdateTypes, so it also applies to a major — which costs",
"nothing: separateMinorPatch only ever decides whether minor and patch share a branch, and",
"a major is in neither bucket. Majors stay single PRs because the rule above does not",
"group them, not because of anything here.",
],
matchFileNames: ["freesailarr/values.yaml"],
separateMinorPatch: false,
},
],
customManagers: [
{
description: [
"Tool versions pinned as workflow env vars (TRIVY_VERSION). Annotate the line with",
"`# renovate: datasource=<ds> depName=<dep>` and Renovate keeps it current.",
],
customType: "regex",
managerFilePatterns: [".github/workflows/*.yml"],
matchStrings: [
"# renovate: datasource=(?<datasource>\\S+) depName=(?<depName>\\S+)(?: versioning=(?<versioning>\\S+))?\\s+\\w+: [\"']?(?<currentValue>[^\"'\\s]+)",
],
versioningTemplate: "{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}",
},
{
description: [
"The same `# renovate:` comment convention for tool versions pinned in a ci/ SHELL",
"script, where the assignment is `VAR=value` instead of the workflow's `VAR: value`.",
"Today that is GATEWAY_API_VERSION in ci/gateway-api-crds.sh — the Gateway-API CRDs the",
"install-test applies for a chart rendering an HTTPRoute (Gitea #114). They are fetched",
"at a pinned version rather than vendored, exactly as Trivy's tarball and kubectl are,",
"so without this manager the pin would simply age in place.",
"",
"A SEPARATE block and not a second matchString on the one above, deliberately: that",
"rule was verified by dry run and covers every workflow in the repo, and widening its",
"managerFilePatterns would put that verification back in question for a second file",
"shape. The cost of the split is one more block; the cost of being wrong is an update",
"path that silently stops matching (#158: a valid rule is not a matching rule).",
"",
"Prereleases are excluded repo-wide, which matters here: upstream publishes -rc.N tags",
"as GitHub releases, and the standard channel of a release candidate is not what a",
"pipeline should validate against.",
],
customType: "regex",
managerFilePatterns: ["ci/*.sh"],
matchStrings: [
"# renovate: datasource=(?<datasource>\\S+) depName=(?<depName>\\S+)(?: versioning=(?<versioning>\\S+))?\\s+\\w+=[\"']?(?<currentValue>[^\"'\\s]+)",
],
versioningTemplate: "{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}",
},
// --- Chart app versions -------------------------------------------------
//
// Every chart renders its main image as `<repo>:{{ .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 APP-VERSION block: `freesailarr`. It bundles
// ten independently versioned upstreams, so none of them is "the" app
// version and no image tag comes from appVersion — they 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 an appVersion 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. The one image the
// built-in manager cannot follow — mediathekarr, pinned by digest — has its
// own block directly below.
{
description: [
"freesailarr / mediathekarr: the only image in this repo pinned by DIGEST, because its",
"tags carry no version at all — 'latest' is a 2024 build (v0.2.5) and the upstream README",
"points at the moving 'beta'. The built-in helm-values manager reads repository/tag and",
"would have nothing to compare, so this manager captures all three keys and lets Renovate",
"propose digest updates for the beta tag (currentDigest + currentValue = a digest-only",
"lookup; it arrives as a 'digest' update, which the three-day age gate above covers and",
"automerge then carries once the PR is reviewed and green).",
"",
"The pattern is anchored on the three keys in their values.yaml order: repository, tag,",
"digest. Reordering or renaming them does not fail anything — it silently stops matching,",
"and the image then never gets an update again (#158: a valid rule is not a matching rule).",
"",
"Verified by dry-run (renovate 44.69.11, --platform=local --dry-run=lookup, real registry),",
"two fixtures, because 'no update' is what BOTH a working and a broken rule look like while",
"the pin is current:",
" currentDigest = the pinned one -> extracted with currentValue 'beta', updates [] (the",
" beta tag really does still point at it).",
" currentDigest = sha256:0000... -> updates [{updateType: digest, newValue: beta,",
" newDigest: sha256:3ed7e14...}] on branch renovate/docker.io-pcjones-mediathekarr-beta.",
"The same run also shows the built-in helm-values manager finding this image and skipping it",
"(skipReason invalid-value, 'no currentDigest or pinDigests' — it does not read the digest",
"key), so the two managers cannot produce competing PRs.",
],
customType: "regex",
managerFilePatterns: ["freesailarr/values.yaml"],
matchStrings: [
"repository: (?<depName>docker\\.io/pcjones/mediathekarr)\\s+tag: (?<currentValue>[^\\s]+)\\s+digest: (?<currentDigest>sha256:[0-9a-f]{64})",
],
datasourceTemplate: "docker",
},
{
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*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "apache/tika",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
versioningTemplate: "regex:^(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)\\.(?<build>\\d+)-(?<compatibility>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-<short>. 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*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "ghcr.io/alam00000/bentopdf-simple",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
},
{
description: [
"bulwark: the busiest tag landscape of any chart in this file, and the reason this block",
"was measured instead of copied. Counted on 2026-10-05 against the ghcr.io tags API,",
"315 tags in EIGHT shapes side by side:",
"",
" 1. release tags, suffix-free 1.12.0, 1.11.2, 1.11.1, ... <- what we track",
" 2. the SAME releases with a 'v' v1.12.0, v1.11.2, ...",
" 3. a '-always' build flavour 1.12.0-always, v1.12.0-always",
" 4. floating minors and majors 1.12, 1.11, 1, v1.12, v1 - they move",
" 5. floating names latest, main, dev, latest-always - they move",
" 6. commit-SHA tags 0080d27, main-<sha>, dev-<sha> (the bulk of the",
" 315, pushed per commit) plus one typo'd tag, mainfc38427",
" 7. PRERELEASES 1.11.0-beta.1, -beta.2, -beta.3 and their",
" -always twins",
" 8. attestation tags sha256-<64 hex>",
"",
"Shapes 4-7 are each a way to pin something nobody chose into appVersion and republish",
"the chart for it - and shape 2 is the subtler one, because it is the same image under a",
"second name, so selecting it would rewrite appVersion and the +up suffix for no change",
"at all.",
"",
"LIKE keycloak AND UNLIKE garage, THE ANSWER IS AN ABSENCE: no versioningTemplate. Plain",
"`docker` versioning already excludes every unwanted shape, so a constraint here would be",
"a line that does nothing and one more thing to keep correct. It splits a tag at the first",
"dash and only ever offers candidates carrying the SAME suffix as the current value -",
"ours has none, so shapes 3 and 7 are invisible to it; the SHA and floating-name tags",
"parse as no version at all; the floating minors have fewer parts than our value and",
"cannot be more precise; and a 'v'-prefixed tag normalizes to the same version as ours,",
"so it is never GREATER and never an update. `ignoreUnstable` (a Renovate default, also",
"pulled in by config:recommended above) is a second, independent guard on shape 7.",
"",
"MEASURED, NOT ASSUMED, and written down because a later round will otherwise add a",
"constraint 'to be safe'. Renovate 44.138.0, --platform=local --dry-run=lookup against",
"the real registry, reading THIS file (so separateMinorPatch and the rest of the config",
"above are in effect), 2026-10-05:",
" appVersion 1.12.0 -> updates [], warnings []",
" appVersion 1.11.0 -> exactly two: 1.11.2 (patch), 1.12.0 (minor)",
" appVersion 1.10.0 -> exactly one: 1.12.0 (minor)",
"Every offered newValue was the suffix-free form. No run offered a beta, an -always tag, a",
"'v' form, a floating tag or a SHA tag. The 1.11.0 and 1.10.0 fixtures are the guard the",
"nginx and qbittorrent rules taught this repo to run: they prove real updates still come",
"through rather than the filter quietly emptying the list.",
"",
"ONE CAVEAT ABOUT THAT RUN, stated rather than hidden because it was visible in the log:",
"ghcr.io served NO releaseTimestamp anonymously, so Renovate marked both updates",
"`pendingChecks: true` under the minimumReleaseAge rule below",
"(minimumReleaseAgeBehaviour=timestamp-required). Whether the shared instance sees the",
"timestamps is NOT verified here - it authenticates to github.com with GITHUB_COM_TOKEN",
"and this run did not. If it does not, the consequence is a held PR rather than a wrong",
"one, and it would apply to every ghcr.io-based chart in this file alike (bentopdf,",
"calibre-web-automated, cyberchef, matrix-synapse, ytdlp-web-player), so it is not a",
"decision about this chart.",
],
customType: "regex",
managerFilePatterns: ["bulwark/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "ghcr.io/bulwarkmail/webmail",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
},
{
customType: "regex",
managerFilePatterns: ["calibre-web-automated/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "ghcr.io/crocodilestick/calibre-web-automated",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
},
{
customType: "regex",
managerFilePatterns: ["cyberchef/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "ghcr.io/gchq/cyberchef",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
},
{
description: [
"garage: the registry carries three kinds of tag, and the first one is the reason",
"this block cannot be the plain four-liner most charts get. Measured on 2026-10-03",
"against docker.io/dxflrs/garage (340 tags):",
"",
" 1. COMMIT-SHA TAGS, 40 hex characters, pushed almost daily - the newest 22 tags",
" in the registry are all of that kind (99923a87... on 2026-10-03), while the",
" current release v2.4.1 dates from 2026-09-08. Without a version constraint",
" Renovate offers those as 'updates', and an appVersion of 40 hex characters",
" pins a commit nobody chose and republishes the chart for it.",
" 2. release tags v<major>.<minor>.<patch>, which is what we track.",
" 3. prereleases and internal builds: v2.0.0-rc1, v2.0.0-beta1 and the four",
" v1.99.x-internal tags. Note that those sort ABOVE v1.3.1 numerically, so they",
" are not kept out by ordering - they have to be excluded by shape.",
"",
"The regex versioning below does both jobs at once, as in the stalwart block: a",
"candidate that does not match is not a version at all, so the SHA tags (no 'v', no",
"dots) and every suffixed tag (the terminating anchor) are skipped. The three",
"numeric parts additionally reject a two-part line tag, should upstream ever add one.",
"",
"The 'v' stays outside the capture groups: it is part of the tag and carries no",
"ordering, and a group is a claim about how the value is compared. Being part of the",
"tag, it is part of appVersion (+upv2.4.1) - precedent: stalwart, matrix-synapse,",
"calibre-web-automated, ytdlp-web-player.",
],
customType: "regex",
managerFilePatterns: ["garage/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "docker.io/dxflrs/garage",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
versioningTemplate: "regex:^v(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)$",
},
{
customType: "regex",
managerFilePatterns: ["gotenberg/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\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*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "ghcr.io/home-assistant/home-assistant",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
},
{
description: [
"keycloak: the counter-case to garage and stalwart, and the one block in this file whose",
"DESIGN DECISION IS AN ABSENCE. quay.io/keycloak/keycloak carries four tag shapes side by",
"side (measured 2026-10-04 against the tag API): release tags 26.8.0; REBUILD tags with a",
"numeric suffix, 26.8.0-0 / 26.7.3-1 / 26.7.2-2, where the same app version is rebuilt for",
"a base-image or CVE fix (26.8.0-0 resolves to the SAME manifest digest as 26.8.0);",
"floating minor tags 26.8 / 26.7; and the floating names latest and nightly, the latter",
"rebuilt daily.",
"",
"That is the same genus of trap as garage's commit-SHA tags - and here plain `docker`",
"versioning already handles all of it, so this block carries NO versioningTemplate.",
"Docker versioning splits a tag at the first dash and only ever offers candidates with the",
"SAME suffix as the current value; ours has none, so the -N rebuilds are invisible to it.",
"The floating minors fail on the missing third part, and latest/nightly parse as no",
"version at all.",
"",
"MEASURED, NOT ASSUMED, and written down because a later round will otherwise add a",
"constraint 'to be safe' - which would be a line that does nothing, and one more thing to",
"keep correct. Renovate 44.132.5, --platform=local --dry-run=lookup, real registry:",
" appVersion 26.8.0 -> updates [], warnings []",
" appVersion 26.7.0 -> exactly one: 26.8.0 (updateType minor)",
" appVersion 26.7.4 -> exactly two: 26.7.5 (patch), 26.8.0 (minor)",
"No run offered a rebuild tag, a floating minor, latest or nightly. The 26.7.4 fixture is",
"the guard the nginx and qbittorrent rules taught us to run: it proves real updates still",
"come through rather than the filter quietly emptying the list.",
"",
"AND WHAT THIS BLOCK DELIBERATELY DOES NOT CARRY: an automerge exception. Keycloak",
"supports a rolling update only WITHIN a minor line - a minor jump needs every node shut",
"down first - so a packageRules entry switching automerge off for minor and major was",
"proposed and DECIDED AGAINST (Gitea #83): the global automerge at the top of this file",
"applies here too. That is an accepted risk rather than an oversight, and the consequence",
"is documented instead of prevented: keycloak/templates/NOTES.txt and the wiki page carry",
"the minor-upgrade procedure (scaleDown, back up, bump, scale up) together with the",
"statement that the automerge does not know about it, plus the `kc.sh",
"update-compatibility metadata|check` calls whose exit code is meant to drive exactly this",
"decision. If a minor PR has merged itself, that procedure is the repair.",
],
customType: "regex",
managerFilePatterns: ["keycloak/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "quay.io/keycloak/keycloak",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
},
{
customType: "regex",
managerFilePatterns: ["matrix-synapse/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "ghcr.io/element-hq/synapse",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
},
{
customType: "regex",
managerFilePatterns: ["minecraft-router/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\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*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "itzg/minecraft-server",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
versioningTemplate: "regex:^(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)-(?<compatibility>java\\d+)$",
},
{
customType: "regex",
managerFilePatterns: ["outline/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "outlinewiki/outline",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
},
{
customType: "regex",
managerFilePatterns: ["paperless-ngx/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "ghcr.io/paperless-ngx/paperless-ngx",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
},
{
customType: "regex",
managerFilePatterns: ["patchmon/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "ghcr.io/patchmon/patchmon-server",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
},
{
customType: "regex",
managerFilePatterns: ["rallly/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\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*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "docker.io/rssbridge/rss-bridge",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
versioningTemplate: "regex:^(?<major>\\d{4})-(?<minor>\\d{2})-(?<patch>\\d{2})$",
},
{
description: [
"searxng: every tag is <date>-<commit> (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*\"?(?<currentValue>[^\"\\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 (?<build>[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, and one that automerge would carry to main as soon as the
// PR is waved through as the routine date bump it looks like. 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:^(?<major>\\d{4})\\.(?<minor>\\d{1,2})\\.(?<patch>\\d{1,2})-[0-9a-f]+$",
},
{
customType: "regex",
managerFilePatterns: ["samba/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "ghcr.io/dockur/samba",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
},
{
customType: "regex",
managerFilePatterns: ["satisfactory/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "ghcr.io/wolveix/satisfactory-server",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
},
{
description: [
"stalwart: the registry carries three kinds of tag - versioned ones (v0.16.24), the",
"same versions with an -alpine suffix (v0.16.24-alpine), and three MOVING names:",
"'latest', 'latest-alpine' and the truncated line tags 'v0.16' / 'v0.16-alpine'.",
"Measured on 2026-10-02: 'latest', 'v0.16' and 'v0.16.24' all resolve to the same",
"digest (sha256:ec011be2285...), pushed within seconds of each other - so 'latest'",
"and 'v0.16' pin nothing and would move the running image with no commit here.",
"",
"The anchored regex below is what keeps all five out, and it does so for two",
"independent reasons rather than one: the v prefix plus THREE numeric parts rejects",
"'latest' and 'latest-alpine' (no numbers) and 'v0.16' / 'v0.16-alpine' (two parts),",
"and the terminating anchor rejects the -alpine suffix. Choosing the glibc image over",
"the alpine one is a deployment decision and belongs in the chart, not in a",
"compatibility suffix Renovate would then have to maintain - contrast the valkey",
"block below, where '-alpine' IS the chosen flavour and docker versioning carries it.",
"",
"The 'v' stays outside every capture group, as in the ytdlp-web-player block: it is",
"part of the tag and carries no ordering, and a group is a claim about how the value",
"is compared. It is part of the tag, so it is part of appVersion (+upv0.16.24) -",
"precedent: calibre-web-automated, matrix-synapse, ytdlp-web-player.",
],
customType: "regex",
managerFilePatterns: ["stalwart/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "docker.io/stalwartlabs/stalwart",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
versioningTemplate: "regex:^v(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)$",
},
{
description: [
"sure: the registry carries four kinds of tag side by side - releases (0.7.4), the",
"prereleases -alpha.N / -rc.N / -hotfix.N, the moving names stable/latest/nightly,",
"and one sha-<commit> per build. semver versioning is the whole filter: it parses",
"the release line, marks every prerelease isStable === false so `ignoreUnstable`",
"never offers one, and rejects the moving names and the sha- tags as unparseable",
"rather than ever selecting one. That matters more here than the tag list suggests,",
"because automerge is global in this config (top of the file) and majors carry no",
"age gate: an update this manager offers is one that merges as soon as the required",
"check and the required review are there, so the filtering has to happen at the",
"lookup, not at the merge. It is also the reason NOT to write a capture-group regex",
"here - the moving tags have to stay outside whatever pattern is used, and 'outside'",
"is free with semver.",
"",
"Upstream calls 'latest' the alpha channel and 'stable' the latest release; at the",
"time of writing 'stable' resolves to the same digest as 0.7.4, so pinning the",
"version tag loses nothing and keeps the chart's appVersion honest.",
],
customType: "regex",
managerFilePatterns: ["sure/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "ghcr.io/we-promise/sure",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
versioningTemplate: "semver",
},
{
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*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "teamspeaksystems/teamspeak6-server",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
versioningTemplate: "loose",
},
{
customType: "regex",
managerFilePatterns: ["urlaubsverwaltung/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "urlaubsverwaltung/urlaubsverwaltung",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
},
{
customType: "regex",
managerFilePatterns: ["voucher-vault/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "l4rm4nd/vouchervault",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
},
{
description: [
"valkey: the simplest block in this file, and deliberately so. docker versioning",
"splits the tag at the first dash, reads 9.1.2 as the version and 'alpine' as the",
"compatibility suffix, and only ever offers candidates carrying that same suffix -",
"which is exactly what is wanted here, because upstream keeps building an -alpine",
"tag for every release. No versioningTemplate, no capture groups, nothing to prove.",
"",
"That is the SECOND design for this block. The first pinned the fully numbered",
"9.1.2-alpine3.24 and needed a regex versioning to see a move to alpine3.25, since",
"upstream builds exactly one Alpine base per release and the next base arrives as",
"9.1.3-alpine3.25 rather than 9.1.2-alpine3.25. It was dropped because the premise",
"was wrong: that tag is not immutable either - upstream rebuilds the whole matrix",
"periodically (9.1-alpine and 9.0.6-alpine3.24 were pushed minutes apart on",
"2026-09-21, two different release lines). Only a digest is immutable. Given two",
"equally moving tags, the one whose suffix never changes is the safer pin, and its",
"rule needs no proof - which mattered, because a Renovate dry run is impossible in",
"the work environment (no node, see #34).",
"",
"The price, stated rather than discovered later: a rebuild of 9.1.2-alpine changes",
"the running image with no commit here and nothing reports it. The remedy exists if",
"it ever matters - pinDigests for this one image. It is off globally for",
"custom.regex since #34 because an appVersion is a tag by construction, but a",
"per-package exception is possible, and it would then be a chart patch plus a",
"publish per digest change.",
],
customType: "regex",
managerFilePatterns: ["valkey/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "docker.io/valkey/valkey",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
},
{
description: [
"ytdlp-web-player: the registry carries exactly two kinds of tag - four versioned",
"ones (v1.0.0, v1.0.1, v1.1.0, v1.2.0) and the two floating ones, 'latest' and",
"'stable'. Both floating tags MOVE: 'stable' was republished at the same minute as",
"v1.2.0 and 'latest' days later, so either of them as an appVersion would pin",
"nothing and change the running image without a single commit.",
"The anchored regex versioning below is what keeps them out - it requires the v",
"prefix and three numeric parts, which neither 'latest' nor 'stable' can satisfy.",
"docker versioning alone would probably have dropped them too, but 'probably' is",
"the wrong basis for the value that selects the image.",
],
customType: "regex",
managerFilePatterns: ["ytdlp-web-player/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "docker.io/matszwe02/ytdlp_web_player",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
// The "v" stays outside every capture group: it is part of the tag and
// carries no ordering, and the same rule the searxng block documents
// applies - a group is a claim about how the value is compared. The
// anchors are what exclude latest/stable. Precedent for keeping the v in
// appVersion at all: calibre-web-automated (+upv4.0.6) and matrix-synapse
// (+upv1.159.0) - it is part of the tag, so it is part of appVersion.
versioningTemplate: "regex:^v(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)$",
},
{
customType: "regex",
managerFilePatterns: ["zipline/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "ghcr.io/diced/zipline",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
},
],
}