Merge pull request #143 from jk-cluster/feat/renovate-config

renovate: chart-aware config with an automatic version bump
This commit is contained in:
jklein authored and GitHub committed 2026-08-24 09:47:54 +00:00
commit 35db2affea
6 files changed
+443 -3

No files matched your search

+1
View File
@@ -105,6 +105,7 @@ jobs:
matrix:
chart: ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }}
env:
# renovate: datasource=github-releases depName=aquasecurity/trivy
TRIVY_VERSION: v0.74.0
# "report" always exits 0. Switch to "gate" to make a finding that no
# documented exception covers fail the job - the intended step once the
+1
View File
@@ -40,6 +40,7 @@ Copy `template-chart/` and replace every `xxx` placeholder — in file names (`t
1. Add `.github/workflows/publish-<chart-name>.yml` (copy an existing one, e.g. `publish-outline.yml`) — it triggers on pushes to `main` touching `<chart-name>/**` and calls the reusable `publish-helm-chart.yml`.
2. Add the chart name to the `chart-name` choice list in `.github/workflows/change-app-version.yml`.
3. Add install-test fixtures under `ci/<chart-name>/` (`test-values.yaml`, `fixtures.yaml`) or an entry with justification in `ci/excluded-charts.txt` — the PR install-test fails if both are missing.
4. Add a `customManagers` block for the chart's main image in `renovate.json5` (one per chart, since the image repository lives in the template and the version in `Chart.yaml`) — without it Renovate never updates that chart's app version. See README section "Dependency updates (Renovate)".
## Versioning scheme
+43
View File
@@ -55,11 +55,54 @@ Fixtures live at the **repo root** under `ci/` (deliberately *not* inside the ch
Fixture images avoid Docker Hub where possible (`public.ecr.aws`/`ghcr.io`) to dodge runner rate limits. The job currently runs with `continue-on-error: true` (stabilization phase); it will be promoted to a required check once it has proven stable across several chart PRs. When adding a new chart, add its `ci/<chart>/` fixtures (or an entry in `ci/excluded-charts.txt`) — the install-test fails if both are missing. The script also runs locally against any existing cluster: `k3d cluster create t && ci/run-install-test.sh <chart> && k3d cluster delete t`.
## Dependency updates (Renovate)
Renovate runs as the self-hosted `jlab-renovate` GitHub App. Its config is **`renovate.json5`** in the repo root (JSON5 for the inline reasoning; requires Renovate >= 41 for `managerFilePatterns`/`customType`). There must be exactly one config file — do not add a second `renovate.json` or `.github/renovate.json`.
What Renovate keeps current:
| What | How |
| --- | --- |
| Chart app versions (`appVersion:` in `Chart.yaml`) | One `customManagers` entry per chart, hard-coding the image repository |
| Sidecar/init images in `values.yaml` (nginx, guacd, busybox, alpine) | Built-in `helm-values` manager |
| GitHub Actions | Built-in `github-actions` manager |
| Tool versions pinned as workflow env vars (`TRIVY_VERSION`) | `# renovate: datasource=… depName=…` comment above the line |
Each chart renders its main image as `<repo>:{{ .Chart.AppVersion }}` — repository in the template, version in `Chart.yaml`. Renovate cannot join two files, so the mapping lives in `renovate.json5`, one block per chart. It is deliberately *not* a `# renovate:` comment inside each `Chart.yaml`: that would touch all chart directories and force a version bump plus a publish per chart for a pure comment change. Charts whose tags carry a flavour or date suffix (`apache-tika`, `minecraft-server`, `rss-bridge`, `teamspeak-server`) override `versioning` in their block; the reasoning sits next to each one.
Not managed: `ci/**` (install-test fixtures pin throwaway infrastructure to major lines on purpose) and `template-chart/**` (placeholder image). A consequence worth knowing: `ci/matrix-synapse/fixtures.yaml` pins a Synapse image that Renovate will not follow — bump it by hand when the chart moves a major.
### The version-bump hook
Renovate only rewrites `appVersion:` or an image tag; it knows nothing about `version: <chart-semver>+up<app-version>`. Left alone, every chart PR would fail the `build-chart` version check. `ci/renovate-bump-chart.sh <chart-dir>` closes that gap and is wired up as a `postUpgradeTasks` command:
- **app major changed** → chart major (`6.0.0` → `7.0.0`), matching "runtime behavior changes" in the semver conventions
- **anything else** (plain app update, sidecar image) → chart patch
- the `+up` suffix is recomputed from the new `appVersion`, dashes normalized to dots
Everything is derived from the base commit, so the script is idempotent across the multiple updates a single Renovate branch can carry.
> **One-time setup outside this repo:** `postUpgradeTasks` only runs on self-hosted Renovate **and** only when the command is allow-listed in the *instance* config:
>
> ```yaml
> allowedCommands: ["^ci/renovate-bump-chart\\.sh"]
> ```
>
> Without that entry Renovate skips the task without an error and every chart PR stays red on the version check.
### PR policy
- **Automerge** for `patch` and `digest` updates, after a `minimumReleaseAge` of 3 days and only once the PR pipeline is green. Renovate merges itself rather than using GitHub auto-merge, which would merge immediately as long as `validate-charts` is not a required check. Everything else is merged by hand.
- **Majors** arrive one line at a time (`separateMultipleMajor`): a v1 → v3 jump produces a PR for the latest v2 first and then one for v3 — the stepwise procedure the versioning section requires.
- `prConcurrentLimit: 10` / `prHourlyLimit: 4` keep the k3d install-tests from all starting at once.
- The manually dispatched `change-app-version.yml` still exists for one-off bumps, but the normal path for app updates is now a Renovate PR.
## Creating a new chart
1. Copy `template-chart/` to `<chart-name>/` and replace every `xxx` placeholder: template file names (`xxx.deployment.yaml` → `<app>.deployment.yaml`), `Chart.yaml`, the `values.yaml` keys, the `.Values.xxx.*` references inside the templates, **and both the `xxx.` helper prefixes and the values paths in `values.schema.json`**. Then walk the five points in "Values that must not lie" below — the schema and the null-safe fallbacks are copied with the template, but their content is chart-specific and wrong until you adapt it.
2. Add `.github/workflows/publish-<chart-name>.yml` (copy an existing one, e.g. `publish-outline.yml`).
3. Add the chart name to the choice list in `.github/workflows/change-app-version.yml`.
4. Add a `customManagers` block for the chart's main image in `renovate.json5` (copy a neighbouring one, swap `managerFilePatterns` and `depNameTemplate`) — without it the chart's app version is never updated. Check the image's tag list first: if it carries a flavour, date or prerelease suffix, add a `versioningTemplate` with a comment saying why.
Validate locally with:
+111
View File
@@ -0,0 +1,111 @@
#!/bin/sh
# Recompute a chart's `version:` after Renovate changed something inside its
# directory.
#
# Renovate rewrites `appVersion:` in Chart.yaml (main image) or an image tag in
# values.yaml (sidecar/init image). It has no idea about this repo's scheme
#
# version: <chart-semver>+up<app-version-with-dashes-normalized-to-dots>
#
# and it never touches `version:` at all — which would leave every Renovate PR
# failing the `build-chart` version-bump check. This script closes that gap and
# is wired up as a `postUpgradeTasks` command in renovate.json5.
#
# Usage: ci/renovate-bump-chart.sh <chart-directory>
#
# Bump level (README "Versioning"):
# - app major changed -> chart major, minor/patch reset to 0
# - anything else -> chart patch (plain app updates, sidecar images)
#
# Everything is derived from the base commit (HEAD, which during
# postUpgradeTasks still points at the base branch), never from the working
# tree's `version:`. That makes repeated runs on the same branch idempotent —
# Renovate calls the script once per dependency update, and a branch may carry
# more than one update for the same chart.
#
# Runs inside the Renovate container, so: POSIX sh, no bashisms, no jq/yq.
set -eu
dir=${1:-}
[ -n "$dir" ] || exit 0
# Strip a trailing slash so "outline/" and "outline" behave the same.
dir=${dir%/}
# Renovate also calls this for .github/workflows and other non-chart updates.
case "$dir" in
"" | "." | .github* | ci*) exit 0 ;;
esac
chart="$dir/Chart.yaml"
[ -f "$chart" ] || exit 0
# The scaffold is never published and its version carries no meaning.
[ "$dir" != "template-chart" ] || exit 0
# Reads a top-level scalar field, unquoted and untrimmed of surrounding spaces.
field() {
sed -n "s/^$1:[[:space:]]*//p" | head -n 1 | tr -d "\"' "
}
base=$(git show "HEAD:$chart" 2>/dev/null) || {
echo "renovate-bump: $chart is new on this branch, leaving its version alone"
exit 0
}
base_version=$(printf '%s\n' "$base" | field version)
base_app=$(printf '%s\n' "$base" | field appVersion)
new_app=$(field appVersion <"$chart")
if [ -z "$base_version" ] || [ -z "$new_app" ]; then
echo "renovate-bump: $chart has no version/appVersion to work with" >&2
exit 1
fi
# <chart-semver> is everything before the +up build suffix.
semver=${base_version%%+*}
case "$semver" in
*.*.*) ;;
*)
echo "renovate-bump: cannot parse chart semver '$semver' in $chart" >&2
exit 1
;;
esac
major=${semver%%.*}
rest=${semver#*.}
minor=${rest%%.*}
patch=${rest#*.}
for part in "$major" "$minor" "$patch"; do
case "$part" in
'' | *[!0-9]*)
echo "renovate-bump: non-numeric component in chart semver '$semver'" >&2
exit 1
;;
esac
done
# Leading major of an app version: v1.159.0 -> 1, 3.3.1.0-full -> 3,
# 2025-06-03 -> 2025.
app_major() {
printf '%s' "$1" | sed -e 's/^[vV]//' -e 's/[^0-9].*$//'
}
if [ "$new_app" != "$base_app" ] && [ "$(app_major "$new_app")" != "$(app_major "$base_app")" ]; then
reason="app major $base_app -> $new_app"
new_semver="$((major + 1)).0.0"
else
reason="patch-level change"
new_semver="$major.$minor.$((patch + 1))"
fi
# Dashes in the app version become dots in the +up suffix.
suffix=$(printf '%s' "$new_app" | tr '-' '.')
new_version="$new_semver+up$suffix"
sed -i.bak "s|^version:.*|version: $new_version|" "$chart"
rm -f "$chart.bak"
echo "renovate-bump: $chart $base_version -> $new_version ($reason)"
-3
View File
@@ -1,3 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}
+287
View File
@@ -0,0 +1,287 @@
{
$schema: "https://docs.renovatebot.com/renovate-schema.json",
// JSON5 on purpose: the per-chart managers below only make sense with the
// reasoning next to them. Renovate reads renovate.json5 natively — there must
// be no second config file (renovate.json, .github/renovate.json, ...) or
// Renovate aborts with "found multiple config files".
//
// Requires Renovate >= 41 (`managerFilePatterns`, `customType`). See
// README.md → "Dependency updates (Renovate)" for the whole picture,
// including the one-time `allowedCommands` change the self-hosted instance
// needs for the version bump to work.
extends: [
"config:recommended",
// Commit subjects in this repo are plain sentences, not conventional
// commits. Renovate's own merged PRs already look like that.
":semanticCommitsDisabled",
],
timezone: "Europe/Berlin",
labels: ["dependencies"],
// 20 charts plus the workflows: without a cap the first run after a quiet
// week opens a wall of PRs, and every chart PR spins up a k3d install-test.
prConcurrentLimit: 10,
prHourlyLimit: 4,
// template-chart is the scaffold. Its image is the literal placeholder
// ghcr.io/ORG/IMAGE and its appVersion is "xxx" — nothing to update, and the
// chart is never published.
ignorePaths: ["template-chart/**"],
// CLAUDE.md/README require app major jumps to be taken stepwise: first the
// last release of the old major line, then the new major. With this flag a
// v1 -> v3 jump arrives as two PRs (latest v2, then v3), which is exactly
// that sequence — merge the first, and the second rebases onto it.
separateMultipleMajor: true,
// Any change inside a chart directory must bump `version:` in its
// Chart.yaml, otherwise the `build-chart` job fails the PR and the publish
// on main would collide with an already published version. Renovate only
// rewrites `appVersion:` (or an image tag in values.yaml), so this hook
// recomputes `version:` afterwards.
//
// IMPORTANT: postUpgradeTasks only runs on self-hosted Renovate and only if
// the command is allow-listed in the *instance* config (not in this repo):
//
// allowedCommands: ["^ci/renovate-bump-chart\\.sh"]
//
// Without that entry Renovate skips the task silently and every chart PR
// stays red on the version-bump check.
postUpgradeTasks: {
commands: ["ci/renovate-bump-chart.sh {{{packageFileDir}}}"],
fileFilters: ["**/Chart.yaml"],
// "update" runs the script once per dependency update with the package
// file's directory, which is what the script needs to find the chart. The
// script derives everything from the base commit, so repeated runs on the
// same branch are idempotent.
executionMode: "update",
},
packageRules: [
{
description: [
"Install-test fixtures deliberately pin throwaway infrastructure to major lines",
"(postgres:16-alpine, redis:7-alpine, busybox:1.36). They are test scaffolding, not",
"shipped artifacts, and churning them only risks breaking the install-test.",
],
matchFileNames: ["ci/**"],
enabled: false,
},
{
description: "Patch and digest updates merge themselves once the PR pipeline is green.",
matchUpdateTypes: ["patch", "digest"],
automerge: true,
automergeType: "pr",
// Renovate waits for the checks itself instead of handing the PR to
// GitHub's auto-merge. GitHub would merge immediately when the branch has
// no required status checks configured — and validate-charts is not (yet)
// a required check.
platformAutomerge: false,
// A release that is hours old should not walk into main unattended; a
// chart publish follows the merge straight away.
minimumReleaseAge: "3 days",
},
{
description: [
"Chart app versions. Kept out of the automerge grouping decisions above only by label,",
"so it is visible at a glance which PRs republish a chart.",
],
matchDepTypes: ["app-version"],
addLabels: ["app-version"],
},
],
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 20 chart directories and force 20
// version bumps plus 20 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".
{
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)$",
},
{
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",
},
{
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",
},
{
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})$",
},
{
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: "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",
},
{
customType: "regex",
managerFilePatterns: ["zipline/Chart.yaml"],
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
depNameTemplate: "ghcr.io/diced/zipline",
datasourceTemplate: "docker",
depTypeTemplate: "app-version",
},
],
}