Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 9s
Validate Helm Charts / Build keycloak (pull_request) Successful in 4s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 11s
Install-test / Install-test keycloak (pull_request) Successful in 2m23s
Validate Helm Charts / Install-test (pull_request) Successful in 2m23s
Two replicas with cache=ispn, clustered through Keycloak's default jdbc-ping stack: discovery runs over the PostgreSQL that is there anyway, so no headless Service and no JGroups wiring. Deployment rather than StatefulSet - Keycloak 26 keeps everything in the database, user sessions included. The chart's defining property is that it has TWO INIT CONTAINERS and needs both. db, health-enabled and metrics-enabled are Keycloak BUILD options (verified in the 26.8.0 option table), the published image is augmented with their defaults, and supplying different ones at runtime makes Keycloak rewrite /opt/keycloak/lib/quarkus at start - which readOnlyRootFilesystem: true forbids and which upstream describes as a start that "creates an image that is mutable". Not setting db is no way out: the probes alone force it, because health-enabled is a build option too. An empty emptyDir over that path fails as well, because QuarkusEntryPoint loads quarkus/quarkus-application.dat before it dispatches any command, `build` included. So one init container seeds the directory into a volume, the next runs `kc.sh build` into it, and the serving container runs `start --optimized` - every container keeping the full hardening baseline, with no documented deviation to carry. Build options therefore exist in exactly one place, and the asymmetry is enforced rather than trusted: the serving container passes no build option at all, and a build option's variable name in keycloak.env fails the render with a message naming it. Hostname handling is the security-relevant part. KC_HOSTNAME is derived from ingress.domain as a full URL (which upstream requires alongside hostname-admin), hostname-strict stays at its default true, and proxy-headers is set rather than optional. The admin hostname is NOT presented as an access control: upstream states that hostname-admin "does not prevent accessing the Administration REST API endpoints via the frontend URL specified by the hostname option", so the chart renders no /admin deny rule and values.yaml, NOTES.txt and the wiki all say in the same words that restricting that API is reverse-proxy work. The management port 9000 is a containerPort and a ClusterIP Service for in-cluster scraping, and is in no Ingress: it serves /health/* and optionally /metrics without authentication. Liveness uses /health/live and deliberately not /health or /health/ready - those include the database, and a liveness probe on them would restart the whole cluster for as long as the database is away. Also in this change: - First PodDisruptionBudget in this repository, which makes it a convention (README section 11): minAvailable as a value, and rendered only while the computed replica count is >= 2, because minAvailable: 1 with one replica blocks every node drain forever with a message that names the PDB rather than the replica count. - First quay.io image, so quay.io joins the trusted registries in ci/trivy/data/ksv0125.yaml - configured rather than suppressed, so AVD-KSV-0125 keeps being able to say something. - Renovate block with NO version constraint, which is the counter-case to garage and stalwart and is measured rather than assumed: docker versioning already excludes the -N rebuild tags, the floating minors and latest/nightly. Three dry-run fixtures are recorded at the block, and against the committed config 26.7.4 offers exactly 26.7.5 (patch) and 26.8.0 (minor). - No automerge exception, by decision (Gitea #83): the global automerge applies, and the minor-upgrade procedure plus `kc.sh update-compatibility` is documented in NOTES.txt instead. - Two upstream defaults deviated from, each with its reason at the value: db-pool-max-size 20 instead of 100 (two replicas at 100 ask a default PostgreSQL for twice its max_connections), and uid 1000 / gid 0 instead of this repo's 20000/1000, because the image declares keycloak:x:1000:0 and chowns its install tree to that pair. Part of #83
719 lines
39 KiB
JSON
719 lines
39 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.
|
|
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: [
|
|
"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 patch",
|
|
"that publishes it (appVersion is unchanged, so it takes the patch branch). 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)$/",
|
|
},
|
|
],
|
|
|
|
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}}",
|
|
},
|
|
|
|
// --- 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",
|
|
},
|
|
{
|
|
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",
|
|
},
|
|
],
|
|
}
|