From 6b25d082a7c21f7b0da085c72cbc5bfb6fb286e0 Mon Sep 17 00:00:00 2001 From: jklein Date: Mon, 24 Aug 2026 11:42:43 +0200 Subject: [PATCH] renovate: chart-aware config with an automatic version bump Renovate only ever rewrites appVersion or an image tag, so every PR it opened against a chart directory failed the build-chart version check (#138, #139). ci/renovate-bump-chart.sh recomputes version: +up as a postUpgradeTasks hook: chart major when the app major moves, chart patch otherwise. Each chart renders its main image as :{{ .Chart.AppVersion }}, so repository and version live in different files and Renovate cannot join them. The mapping is one customManager per chart in renovate.json5, deliberately not a comment inside each Chart.yaml - that would touch all 20 chart directories and force 20 publishes for a comment change. Patch and digest updates automerge after a 3-day minimumReleaseAge once the pipeline is green; majors arrive one line at a time to match the stepwise app-major procedure. ci/** stays excluded: the install-test fixtures pin throwaway infrastructure to major lines on purpose. --- .github/workflows/validate-charts.yml | 1 + CLAUDE.md | 1 + README.md | 43 ++++ ci/renovate-bump-chart.sh | 111 ++++++++++ renovate.json | 3 - renovate.json5 | 287 ++++++++++++++++++++++++++ 6 files changed, 443 insertions(+), 3 deletions(-) create mode 100755 ci/renovate-bump-chart.sh delete mode 100644 renovate.json create mode 100644 renovate.json5 diff --git a/.github/workflows/validate-charts.yml b/.github/workflows/validate-charts.yml index 9c6e5cf..5a56f3b 100644 --- a/.github/workflows/validate-charts.yml +++ b/.github/workflows/validate-charts.yml @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 3e971f5..bcb5458 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,6 +40,7 @@ Copy `template-chart/` and replace every `xxx` placeholder — in file names (`t 1. Add `.github/workflows/publish-.yml` (copy an existing one, e.g. `publish-outline.yml`) — it triggers on pushes to `main` touching `/**` 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//` (`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 diff --git a/README.md b/README.md index 556c733..3d511c6 100644 --- a/README.md +++ b/README.md @@ -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//` 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 && 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 `:{{ .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: +up`. Left alone, every chart PR would fail the `build-chart` version check. `ci/renovate-bump-chart.sh ` 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 `/` and replace every `xxx` placeholder: template file names (`xxx.deployment.yaml` → `.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-.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: diff --git a/ci/renovate-bump-chart.sh b/ci/renovate-bump-chart.sh new file mode 100755 index 0000000..d29e81d --- /dev/null +++ b/ci/renovate-bump-chart.sh @@ -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: +up +# +# 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 +# +# 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 + +# 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)" diff --git a/renovate.json b/renovate.json deleted file mode 100644 index 7190a60..0000000 --- a/renovate.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json" -} diff --git a/renovate.json5 b/renovate.json5 new file mode 100644 index 0000000..ef29f8a --- /dev/null +++ b/renovate.json5 @@ -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= depName=` and Renovate keeps it current.", + ], + customType: "regex", + managerFilePatterns: [".github/workflows/*.yml"], + matchStrings: [ + "# renovate: datasource=(?\\S+) depName=(?\\S+)(?: versioning=(?\\S+))?\\s+\\w+: [\"']?(?[^\"'\\s]+)", + ], + versioningTemplate: "{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}", + }, + + // --- Chart app versions ------------------------------------------------- + // + // Every chart renders its main image as `:{{ .Chart.AppVersion }}`, + // so the repository lives in the template and the version in Chart.yaml. + // Renovate cannot join the two across files, which is why each chart gets + // its own manager that hard-codes the repository and reads `appVersion:`. + // + // Deliberately *not* solved with a `# renovate:` comment inside each + // Chart.yaml: that would touch all 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*\"?(?[^\"\\s]+)\"?"], + depNameTemplate: "apache/tika", + datasourceTemplate: "docker", + depTypeTemplate: "app-version", + versioningTemplate: "regex:^(?\\d+)\\.(?\\d+)\\.(?\\d+)\\.(?\\d+)-(?full)$", + }, + { + customType: "regex", + managerFilePatterns: ["calibre-web-automated/Chart.yaml"], + matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], + depNameTemplate: "ghcr.io/crocodilestick/calibre-web-automated", + datasourceTemplate: "docker", + depTypeTemplate: "app-version", + }, + { + customType: "regex", + managerFilePatterns: ["cyberchef/Chart.yaml"], + matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], + depNameTemplate: "ghcr.io/gchq/cyberchef", + datasourceTemplate: "docker", + depTypeTemplate: "app-version", + }, + { + customType: "regex", + managerFilePatterns: ["gotenberg/Chart.yaml"], + matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], + depNameTemplate: "gotenberg/gotenberg", + datasourceTemplate: "docker", + depTypeTemplate: "app-version", + }, + { + description: "homeassistant: CalVer tags (2026.8.3) sort correctly under docker versioning; the floating stable/beta/dev tags are skipped because they parse as no version at all.", + customType: "regex", + managerFilePatterns: ["homeassistant/Chart.yaml"], + matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], + depNameTemplate: "ghcr.io/home-assistant/home-assistant", + datasourceTemplate: "docker", + depTypeTemplate: "app-version", + }, + { + customType: "regex", + managerFilePatterns: ["matrix-synapse/Chart.yaml"], + matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], + depNameTemplate: "ghcr.io/element-hq/synapse", + datasourceTemplate: "docker", + depTypeTemplate: "app-version", + }, + { + customType: "regex", + managerFilePatterns: ["minecraft-router/Chart.yaml"], + matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], + depNameTemplate: "itzg/mc-router", + datasourceTemplate: "docker", + depTypeTemplate: "app-version", + }, + { + description: "minecraft-server: tags encode the JRE as a suffix (2026.8.2-java21); the compatibility group prevents a silent runtime switch.", + customType: "regex", + managerFilePatterns: ["minecraft-server/Chart.yaml"], + matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], + depNameTemplate: "itzg/minecraft-server", + datasourceTemplate: "docker", + depTypeTemplate: "app-version", + versioningTemplate: "regex:^(?\\d+)\\.(?\\d+)\\.(?\\d+)-(?java\\d+)$", + }, + { + customType: "regex", + managerFilePatterns: ["outline/Chart.yaml"], + matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], + depNameTemplate: "outlinewiki/outline", + datasourceTemplate: "docker", + depTypeTemplate: "app-version", + }, + { + customType: "regex", + managerFilePatterns: ["paperless-ngx/Chart.yaml"], + matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], + depNameTemplate: "ghcr.io/paperless-ngx/paperless-ngx", + datasourceTemplate: "docker", + depTypeTemplate: "app-version", + }, + { + customType: "regex", + managerFilePatterns: ["patchmon/Chart.yaml"], + matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], + depNameTemplate: "ghcr.io/patchmon/patchmon-server", + datasourceTemplate: "docker", + depTypeTemplate: "app-version", + }, + { + customType: "regex", + managerFilePatterns: ["rallly/Chart.yaml"], + matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], + depNameTemplate: "lukevella/rallly", + datasourceTemplate: "docker", + depTypeTemplate: "app-version", + }, + { + description: "rss-bridge: date-based tags (2025-06-03). The bump script normalizes the dashes to dots for the +up suffix, as the versioning scheme requires.", + customType: "regex", + managerFilePatterns: ["rss-bridge/Chart.yaml"], + matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], + depNameTemplate: "docker.io/rssbridge/rss-bridge", + datasourceTemplate: "docker", + depTypeTemplate: "app-version", + versioningTemplate: "regex:^(?\\d{4})-(?\\d{2})-(?\\d{2})$", + }, + { + customType: "regex", + managerFilePatterns: ["samba/Chart.yaml"], + matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], + depNameTemplate: "ghcr.io/dockur/samba", + datasourceTemplate: "docker", + depTypeTemplate: "app-version", + }, + { + customType: "regex", + managerFilePatterns: ["satisfactory/Chart.yaml"], + matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], + depNameTemplate: "ghcr.io/wolveix/satisfactory-server", + datasourceTemplate: "docker", + depTypeTemplate: "app-version", + }, + { + description: "teamspeak-server: the 6.0.0 line only ships beta tags (6.0.0-beta12.1). Loose versioning compares the numeric parts left to right, so beta12.1 sorts above beta9; semver would rank every beta as an equal prerelease.", + customType: "regex", + managerFilePatterns: ["teamspeak-server/Chart.yaml"], + matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], + depNameTemplate: "teamspeaksystems/teamspeak6-server", + datasourceTemplate: "docker", + depTypeTemplate: "app-version", + versioningTemplate: "loose", + }, + { + customType: "regex", + managerFilePatterns: ["urlaubsverwaltung/Chart.yaml"], + matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], + depNameTemplate: "urlaubsverwaltung/urlaubsverwaltung", + datasourceTemplate: "docker", + depTypeTemplate: "app-version", + }, + { + customType: "regex", + managerFilePatterns: ["voucher-vault/Chart.yaml"], + matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], + depNameTemplate: "l4rm4nd/vouchervault", + datasourceTemplate: "docker", + depTypeTemplate: "app-version", + }, + { + customType: "regex", + managerFilePatterns: ["zipline/Chart.yaml"], + matchStrings: ["(?m)^appVersion:\\s*\"?(?[^\"\\s]+)\"?"], + depNameTemplate: "ghcr.io/diced/zipline", + datasourceTemplate: "docker", + depTypeTemplate: "app-version", + }, + ], +}