The chart bump follows the app bump (Gitea #128)

The decision from issue #128: app major -> chart major, app minor ->
chart MINOR, patch/digest -> chart patch, as a maximum against the
chart's own classification rather than a replacement. The level comes
from Renovate's update type, since an appVersion comparison is blind to
the update that prompted it - freesailarr's appVersion never moves.

Versioning carries the rule, Renovate the hook and its measurements,
Workflow the wiki exception where "plain" is now mechanically defined,
CI-CD the new bump-level job, and Lessons-Learned the decision together
with its limit: the reading duty was rejected, so the incident that
lesson is about stays repeatable and the honest number is the whole of
the remedy.

Chart-Style-Guide, ytdlp-web-player, bulwark and stalwart each carried
a sentence calling the hook's result a chart patch, which it no longer
always is.
jklein committed 2026-10-13 18:48:54 +00:00
1 parent 7159e15564
commit 6c1c01453e
9 files changed
+62 -23

No files matched your search

+15 -4
@@ -74,7 +74,7 @@ Three things were listed here as only that run could prove, and they were the wh
## The PR pipeline — `validate-charts.yml` ## The PR pipeline — `validate-charts.yml`
Six jobs. Everything except the three render tests follows the **changed** chart directories. Seven jobs. Everything except the three render tests and the bump-level test follows the **changed** chart directories.
### 1. `detect-changed-charts` ### 1. `detect-changed-charts`
@@ -180,7 +180,18 @@ And it only counts because the overlay was extended to enter the new shape (`ci/
Note which half that leaves uncovered: an overlay switches a chart into Gateway mode, so **the Ingress half of a both-modes feature is never rendered here.** For `stalwart`'s MTA-STS objects that half sits in `ci/stalwart/test-values.yaml` instead, where the install-test has a real API server accept them. Note which half that leaves uncovered: an overlay switches a chart into Gateway mode, so **the Ingress half of a both-modes feature is never rendered here.** For `stalwart`'s MTA-STS objects that half sits in `ci/stalwart/test-values.yaml` instead, where the install-test has a real API server accept them.
### 6. `trivy-config` (reporting only) ### 6. `bump-level` (the version-bump hook, every PR)
The fourth job that ignores the changed-chart matrix, and the only one that renders no chart at all: `ci/run-bump-level-test.sh` tests **`ci/renovate-bump-chart.sh`**, the `postUpgradeTasks` hook that computes a chart's `version:` after Renovate moved an app version. Added with Gitea #128, which is what made it necessary — until then the hook was a calculator with one branch (app major → chart major, everything else → patch); with the bump level taken from Renovate's update type it became the thing that states **how big a change a cluster is being offered**, and the chart version is the only signal anything downstream sees.
Its failure mode is the silent kind: **a wrong level is a valid version string.** The PR goes green, the chart publishes, and the only symptom is a number that understates what it ships. Nothing else in the pipeline reads that number, so nothing but a test can.
- **A case is a repository, not a function call.** The script reads the **base commit** (`git show HEAD:<chart>/Chart.yaml`) *and* the working tree, so each of the 49 cases gets a throwaway `git init` under `mktemp -d`, one commit with the base `Chart.yaml`, and the new one written over it uncommitted — the state the hook really runs in. Signing is switched off per command, because this workspace signs through an SSH signer and a throwaway repo has no business invoking it.
- **No Helm, no cluster, no Python, no Docker.** The fixtures are six-line `Chart.yaml` files written with `printf` and the assertion is one `version:` line read back with `sed`, so the job carries no Helm step and takes about a second. Same runner constraints as the render tests (no pyyaml, no `yq`) — simply not needed here.
- **What is covered:** each update type; the `freesailarr` shape (an app minor while `appVersion` stands still); all seven non-semver app versions plus the majors that must *not* fall back; the maximum rule in both directions, including a stale branch version that must **not** be kept; a missing, an empty and an unknown second argument; and the inputs the script must leave alone (a chart new on the branch, `template-chart`, a `.github/` or `ci/` argument, no argument at all). The full `version:` line is asserted every time, so the `+up` suffix and its dash-to-dot normalization are covered throughout.
- **What it cannot assert, named so the gap is not mistaken for coverage:** that Renovate passes the update type at all. The hook runs only where the command is allow-listed, which is on the shared instance and outside this repository — which is exactly why a *missing* second argument is one of the cases. See [Renovate → the version-bump hook](Renovate#the-version-bump-hook).
### 7. `trivy-config` (reporting only)
Renders each chart in the scan matrix and lists what `trivy config` finds. **It never fails the job** (`TRIVY_CONFIG_MODE: report`). Deliberately not a gate yet: the repo has several documented deviations, and a check that goes red on those every time is one people learn to scroll past. Switching the mode to `gate` is the intended step once the seccomp gap is closed. Renders each chart in the scan matrix and lists what `trivy config` finds. **It never fails the job** (`TRIVY_CONFIG_MODE: report`). Deliberately not a gate yet: the repo has several documented deviations, and a check that goes red on those every time is one people learn to scroll past. Switching the mode to `gate` is the intended step once the seccomp gap is closed.
@@ -188,7 +199,7 @@ Trivy is installed from its release tarball, not via `aquasecurity/setup-trivy`
Exceptions live in `ci/trivy/trivyignore.yaml`, each with a mandatory `statement` — the script refuses to run without one and prints every suppressed finding together with its reason. **The trusted-registry list for AVD-KSV-0125 is a separate file on purpose**, `ci/trivy/data/ksv0125.yaml`: it is *configured* rather than suppressed, so the check keeps being able to say something about an image from a genuinely foreign source. A new registry therefore means an entry there, not an ignore — `quay.io` joined it with [keycloak](keycloak), the first chart in the repo using it. Note what the check does *not* forgive: an unqualified Docker Hub image puts the organisation in the registry position and is still reported, which is why every chart writes its image fully qualified. Exceptions live in `ci/trivy/trivyignore.yaml`, each with a mandatory `statement` — the script refuses to run without one and prints every suppressed finding together with its reason. **The trusted-registry list for AVD-KSV-0125 is a separate file on purpose**, `ci/trivy/data/ksv0125.yaml`: it is *configured* rather than suppressed, so the check keeps being able to say something about an image from a genuinely foreign source. A new registry therefore means an entry there, not an ignore — `quay.io` joined it with [keycloak](keycloak), the first chart in the repo using it. Note what the check does *not* forgive: an unqualified Docker Hub image puts the organisation in the registry position and is still reported, which is why every chart writes its image fully qualified.
### 7. `install-test` (per changed, non-excluded chart) ### 8. `install-test` (per changed, non-excluded chart)
`ci/run-install-test.sh <chart>` against a throwaway [k3d](https://k3d.io) cluster: apply the stub `OnePasswordItem` CRD plus the chart's fixtures, `helm install` with the CI values, and wait for every Deployment and StatefulSet to roll out. Since probes and hardening are active, "Ready" means the app actually answers. On failure the job dumps pod descriptions, events and container logs. `ci/run-install-test.sh <chart>` against a throwaway [k3d](https://k3d.io) cluster: apply the stub `OnePasswordItem` CRD plus the chart's fixtures, `helm install` with the CI values, and wait for every Deployment and StatefulSet to roll out. Since probes and hardening are active, "Ready" means the app actually answers. On failure the job dumps pod descriptions, events and container logs.
@@ -384,7 +395,7 @@ It runs `helm dependency update`, not `build`: `Chart.lock` is not committed, be
## A PR that touches only `ci/` ## A PR that touches only `ci/`
Both matrices follow the changed **chart directories**, so a PR that only changes the harness exercises nothing by itself. The Trivy scan has a fixed-sample fallback for that case; the install-test has none — bump the affected chart's patch version to pull it into the matrix, or run the [all-charts install-test](#the-all-charts-install-test-on-demand) once the change is on `main`. The quantity render test, the selector-match test and the HTTPRoute render test are the three exceptions to the rule: all of them always run over everything. Both matrices follow the changed **chart directories**, so a PR that only changes the harness exercises nothing by itself. The Trivy scan has a fixed-sample fallback for that case; the install-test has none — bump the affected chart's patch version to pull it into the matrix, or run the [all-charts install-test](#the-all-charts-install-test-on-demand) once the change is on `main`. The quantity render test, the selector-match test and the HTTPRoute render test are the three exceptions to the rule: all of them always run over everything. The [`bump-level` job](#6-bump-level-the-version-bump-hook-every-pr) is a fourth exception of a different kind — a PR that changes only `ci/renovate-bump-chart.sh` *is* exercised by it, since that job tests the harness rather than the charts.
## What a Gitea runner does differently ## What a Gitea runner does differently
+1 -1
@@ -303,7 +303,7 @@ The former "no dependencies" rule is gone, replaced by a bar rather than a ban.
**What it costs, all of it visible before the next one is added:** **What it costs, all of it visible before the next one is added:**
- **The subchart is invisible to everything downstream.** Its version is inside our packaged `.tgz`, so anything watching Helm release versions — Fleet's `fleet.yaml` included — sees only *our* chart version. A new upstream release, security fix included, reaches a cluster **only** when we publish a new version of the outer chart, and nothing reports the gap. Renovate's built-in `helmv3` manager opens the PR and `ci/renovate-bump-chart.sh` bumps the patch that ships it; that chain is the entire safety net. State this in a comment at the pinned version, not only in a review. - **The subchart is invisible to everything downstream.** Its version is inside our packaged `.tgz`, so anything watching Helm release versions — Fleet's `fleet.yaml` included — sees only *our* chart version. A new upstream release, security fix included, reaches a cluster **only** when we publish a new version of the outer chart, and nothing reports the gap. Renovate's built-in `helmv3` manager opens the PR and `ci/renovate-bump-chart.sh` bumps the chart version that ships it — since Gitea #128 at the **subchart's own level**, so an upstream minor makes a chart minor even though our `appVersion` did not move; that chain is the entire safety net, and that number is the only place the jump becomes visible. State this in a comment at the pinned version, not only in a review.
- **Every pipeline that touches the chart needs a network fetch first.** `helm lint`, `package`, `template` and `install` all exit 1 while a declared dependency is missing, so `ci/helm-dependencies.sh` runs before each. One call site is the render test that runs over **every** chart on **every** PR — a dependency-carrying chart can turn a third-party Helm repository's bad day into a red pipeline on an unrelated PR. - **Every pipeline that touches the chart needs a network fetch first.** `helm lint`, `package`, `template` and `install` all exit 1 while a declared dependency is missing, so `ci/helm-dependencies.sh` runs before each. One call site is the render test that runs over **every** chart on **every** PR — a dependency-carrying chart can turn a third-party Helm repository's bad day into a red pipeline on an unrelated PR.
- **`charts/*.tgz` and `Chart.lock` are not committed.** The archive is a third-party binary nobody reviews in a diff, and a committed lock would arrive stale because Renovate rewrites `Chart.yaml` and cannot regenerate it — which is also why the pipelines run `helm dependency update`, not `build`. - **`charts/*.tgz` and `Chart.lock` are not committed.** The archive is a third-party binary nobody reviews in a diff, and a committed lock would arrive stale because Renovate rewrites `Chart.yaml` and cannot regenerate it — which is also why the pipelines run `helm dependency update`, not `build`.
- **The repo's own mechanisms stop at the subchart boundary.** `checksum/config`, the 1Password auto-restart annotation and `scaleDown` reach our templates only. A chart rendering a `OnePasswordItem` for its subchart documents the manual `kubectl rollout restart` that replaces the missing restart-on-change. - **The repo's own mechanisms stop at the subchart boundary.** `checksum/config`, the 1Password auto-restart annotation and `scaleDown` reach our templates only. A chart rendering a `OnePasswordItem` for its subchart documents the manual `kubectl rollout restart` that replaces the missing restart-on-change.
+9 -1
@@ -670,7 +670,15 @@ prowlarr 2.6.3 and radarr 6.4.3 removed all five and replaced them with a `Trust
**What the chart does about it, and what it deliberately does not.** A `<app>.trustedNetworks` value was added for all three apps — sonarr included, because repairing two and knowingly leaving the third is the same silent pattern — rendered as `<APP>__SERVER__TRUSTEDNETWORKS` and **only when non-empty**, since an empty variable is indistinguishable from an unset one to the app but looks handled to a reader of the manifest. Its default is empty, so **the chart does not restore the old behaviour**: it follows upstream's intent and makes the state visible, with a `NOTES.txt` warning that fires in exactly the combination that costs something — the app published through an Ingress *or* an HTTPRoute, and the value empty. `AllowedHosts`, added by the same upstream commits, is deliberately not exposed: its default is `string.Empty` too and the app then accepts any host, so there is nothing to repair. If anyone ever sets it, the trap to know is that the app appends only `localhost`, `127.0.0.1`, `[::1]` and its own hostname, so neither the pod IP nor the Service DNS name is covered — and the kubelet probes the pod *by IP*, which an incomplete list answers with HTTP 400. **What the chart does about it, and what it deliberately does not.** A `<app>.trustedNetworks` value was added for all three apps — sonarr included, because repairing two and knowingly leaving the third is the same silent pattern — rendered as `<APP>__SERVER__TRUSTEDNETWORKS` and **only when non-empty**, since an empty variable is indistinguishable from an unset one to the app but looks handled to a reader of the manifest. Its default is empty, so **the chart does not restore the old behaviour**: it follows upstream's intent and makes the state visible, with a `NOTES.txt` warning that fires in exactly the combination that costs something — the app published through an Ingress *or* an HTTPRoute, and the value empty. `AllowedHosts`, added by the same upstream commits, is deliberately not exposed: its default is `string.Empty` too and the app then accepts any host, so there is nothing to repair. If anyone ever sets it, the trap to know is that the app appends only `localhost`, `127.0.0.1`, `[::1]` and its own hostname, so neither the pod IP nor the Service DNS name is covered — and the kubelet probes the pod *by IP*, which an incomplete list answers with HTTP 400.
**Left open on purpose:** whether the convention that exempts a plain app-version bump from the wiki obligation should change. It is the mechanism that let this through, and it is a decision of its own rather than an appendix to a version PR. **Decided since, in Gitea #128 — and the decision does less than this incident asks for, on purpose.** The convention now ties the chart jump to the app jump: app major → chart major, app **minor → chart minor**, patch and digest → chart patch, as a **maximum** against the chart's own classification rather than a replacement. The level comes from Renovate's own update type, passed into `ci/renovate-bump-chart.sh`, because an `appVersion` comparison would have been blind to the very update that started this — prowlarr's `2.5.2 → 2.6.5` was an image tag in `freesailarr/values.yaml`, where `appVersion` never moves. Non-semver app versions (CalVer, date tags, four-part and suffixed tags — seven charts) stay at a patch for a minor, since a month rolling over says nothing about risk.
**What was explicitly rejected, and therefore what stays broken.** The variant that would have caught *this* incident at its origin — a chart minor or major obliges reading the upstream release notes, with the result stated in the PR even when it is "nothing found" — was turned down. So:
> **This lesson's own mechanism survives the fix.** The classification stops understating the jump; it creates no duty to look behind it. A patch-sized app update that inverts a default can still reach `main` with nobody reading a release note, exactly as it did here. The repair is that the number is now honest, not that somebody is now watching.
What the honest number buys is not cosmetic, and it is worth knowing precisely because it is indirect: anything that watches Helm release versions — Fleet's `fleet.yaml` in `cluster-config` included — sees only *our* chart version and nothing inside the package, so a minor is a value other gates can act on where a patch is invisible to them. **Whether `cluster-config` actually distinguishes update types is not verifiable from this repository**, and was explicitly left out of scope: it is the mechanism that would make the number act, not a claim that it does.
And the smaller, transferable piece of that decision: **a classification that is not defined is decided by whoever is in a hurry.** "Plain" sat in two convention texts for two years without a definition, and its effect was to tell the reviewer there was nothing to check. Replacing it with something mechanical — here, a field Renovate had already computed — costs one argument and removes the judgement entirely.
## The claim four pull requests refused to make, and the measurement that finally reached it — one layer further out than wanted ## The claim four pull requests refused to make, and the measurement that finally reached it — one layer further out than wanted
+21 -7
@@ -119,21 +119,35 @@ The pattern is anchored on the three keys in their `values.yaml` order (`reposit
## The version-bump hook ## 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 version check. **`ci/renovate-bump-chart.sh <chart-dir>`** closes that gap as a `postUpgradeTasks` command: 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 version check. **`ci/renovate-bump-chart.sh <chart-dir> [update-type]`** closes that gap as a `postUpgradeTasks` command, and **since the #128 decision the chart jump follows the app jump**:
| Update type | Chart jump |
|---|---|
| `major` | chart major (`6.0.0` → `7.0.0`) |
| `minor` | chart **minor** (`6.0.0` → `6.1.0`) — the level that used to be missing |
| `patch`, `digest`, `pin`, `pinDigest` | chart patch |
| none, or one this script does not know | the pre-#128 fallback: app major changed → major, otherwise patch |
- **app major changed** → chart major (`6.0.0` → `7.0.0`)
- **anything else** (plain app update, sidecar image) → chart patch
- the `+up` suffix is recomputed from the new `appVersion`, dashes normalized to dots - the `+up` suffix is recomputed from the new `appVersion`, dashes normalized to dots
- the level is **never lowered** below what the branch already carries (the maximum rule, below)
Everything is derived from the base commit, so it is idempotent across the multiple updates a single Renovate branch can carry. **The level comes from Renovate, not from comparing `appVersion`**, and that is the decision's load-bearing half: `freesailarr` keeps a hand-maintained `appVersion` for ten upstreams, so prowlarr's `2.5.2 → 2.6.5` — the update that prompted all of this — moved an image tag in `values.yaml` while `appVersion` stood still. A comparison is blind to exactly the case the rule exists for.
**Verified rather than assumed** (Renovate 44.155.7, `keycloak` pinned one minor back). Two runs, because the obvious one cannot answer the question: `--platform=local --dry-run=full` classifies the update (`"updateType": "minor"`) but never reaches the branch worker — not one `Processing branch` line — so the hook never runs under the local platform. Driving Renovate's own post-upgrade executor against a throwaway checkout does, and logs `cmd: "ci/renovate-bump-chart.sh keycloak minor"` with the script answering `1.3.0+up26.7.0 -> 1.4.0+up26.8.1 (update type minor)`.
**Non-semver app versions stay at a patch for a minor.** Seven charts: `rss-bridge` (`2026-09-30`), `homeassistant` (`2026.8.3`), `searxng` (`2026.9.13-3b8009065`), `minecraft-server` (`2026.8.2-java25`), `apache-tika` (`3.3.1.0-full`), `teamspeak-server` (`6.0.0-beta13.1`), `valkey` (`9.1.2-alpine`). In CalVer and date tags the second position is a **month**, so following it would mint a chart minor every month that says nothing about risk; in a four-part or suffixed version the positions are not semver positions at all. An app **major** is not subject to the fallback, so a year rolling over is a chart major as before. The honest edge: for `6.0.0-beta13.1` and `9.1.2-alpine` the numeric part *is* a semver line, so the fallback understates a real minor there — the accepted trade, with a documented per-chart exception as the way out rather than a widened pattern.
**Maximum, not replacement.** The chart jump is the *greater* of the chart's own classification and the app jump. Mechanically that is also what makes a grouped branch deterministic: the hook runs once per update and recomputes from the same base commit every time, so without it the last call would win and `freesailarr`'s minor-among-patches group would take whatever level it happened to end on.
`ci/run-bump-level-test.sh` holds all of this — 49 cases, each a throwaway `git init` with a committed base `Chart.yaml` and a dirty one over it, because a wrong level is a *valid* version string and nothing else in the pipeline reads that number. See [CI/CD](CI-CD).
> **`postUpgradeTasks` only runs when the command is allow-listed in the *instance* config** (`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. > **`postUpgradeTasks` only runs when the command is allow-listed in the *instance* config** (`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.
>
> **The second argument depends on that pattern's shape.** Renovate matches the allow-list against the **compiled** command, so the front-anchored form above still matches with an argument appended (measured). A pattern anchored at the end (`…\.sh$`) would stop matching and produce the same silent skip a missing entry does — and the allow-list is not readable from this repository, so only a look at the instance settles it. That is why a missing update type is a **fallback and not an error**: the worst case is the old classification, not red chart PRs.
On the Gitea instance that entry **exists and was verified rather than hoped for**: the 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` change. Nothing but this hook writes `version:`, so that line can only come from it. The allow-list is not visible from this repo, though — if it is ever dropped, the symptom here is red chart PRs with no explanation. On the Gitea instance that entry **exists and was verified rather than hoped for**: the 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` change. Nothing but this hook writes `version:`, so that line can only come from it. The allow-list is not visible from this repo, though — if it is ever dropped, the symptom here is red chart PRs with no explanation.
A consequence of `freesailarr` pinning every tag in `values.yaml`: the script sees an unchanged `appVersion` and bumps the chart **patch**. That holds for all ten images alike, the digest-pinned one included, and it is the intended outcome — `appVersion` there is the chart's own marker rather than a tracked upstream version. **The open point recorded here is now decided (#128), and the decision is deliberately smaller than the problem.** The gap was real: the automatic patch was also a *classification*, and the convention exempts a plain app-version bump from the wiki obligation — so an image update that quietly changed a **default** was the one case nothing reviewed and nothing documented. It had already happened: sonarr's 4.0.19 bump carried upstream's removal of the hard-coded trusted proxy networks and reached `main` unremarked — see [The security default that a patch bump switched off](Lessons-Learned#the-security-default-that-a-patch-bump-switched-off). What was decided is the **classification, not a reading duty**. The variant that would have caught that incident at its origin — *a chart minor or major obliges reading the upstream release notes, with the result in the PR even when it is "nothing found"* — was **rejected**, and its price stays on the table: nobody is obliged to look behind the number, so an app minor that changes a default can still arrive unremarked. What the bigger number buys is visibility where a patch has none, since anything watching Helm release versions (Fleet's `fleet.yaml` in `cluster-config` included) sees only our chart version. **Whether `cluster-config` actually distinguishes update types is not visible from here** — that is the mechanism which would make the number act, not a claim that it does.
**An open point this leaves, recorded rather than solved.** That automatic patch is also a *classification*, and the convention exempts a plain app-version bump from the wiki obligation — so an image update that quietly changes a **default** is the one case nothing reviews and nothing documents. It has already happened: sonarr's 4.0.19 bump carried upstream's removal of the hard-coded trusted proxy networks and reached `main` unremarked — see [The security default that a patch bump switched off](Lessons-Learned#the-security-default-that-a-patch-bump-switched-off). Whether the convention should change is a decision of its own and is deliberately not proposed here.
## Constrained dependencies ## Constrained dependencies
+10 -6
@@ -20,14 +20,18 @@ appVersion: 1.9.2
## Bump levels for the chart part ## Bump levels for the chart part
| Level | When | **The chart jump is the greater of the chart's own classification and the jump of what it ships** — a maximum, never a replacement. So a value that becomes required is a chart major even when the app moved by a patch, and the chart version never stops reporting the chart's own breaks.
|---|---|
| **Patch** | bug fixes without a behaviour or values-schema change; plain app-version updates | | Level | When — chart's own change | When — app/dependency jump |
| **Minor** | additive, backward-compatible changes — a new optional value whose default leaves the render unchanged; a new or reworked `NOTES.txt` (it renders no Kubernetes object) | |---|---|---|
| **Major** | runtime behaviour changes or breaking values changes: probes, security hardening, computed limits, resource renames, a value that becomes required, adding `checksum/config` to an existing chart | | **Patch** | bug fixes without a behaviour or values-schema change | app **patch**, digest or pin |
| **Minor** | additive, backward-compatible changes — a new optional value whose default leaves the render unchanged; a new or reworked `NOTES.txt` (it renders no Kubernetes object) | app **minor** |
| **Major** | runtime behaviour changes or breaking values changes: probes, security hardening, computed limits, resource renames, a value that becomes required, adding `checksum/config` to an existing chart | app **major** |
The major cases share one property: after the upgrade the cluster does something it did not do before. Adding a `checksum/config` annotation is the least obvious of them — from then on a config change costs a restart, and some workloads take minutes to come back. Name that cost in the PR. The major cases share one property: after the upgrade the cluster does something it did not do before. Adding a `checksum/config` annotation is the least obvious of them — from then on a config change costs a restart, and some workloads take minutes to come back. Name that cost in the PR.
The right-hand column is not a reviewer's judgement: `ci/renovate-bump-chart.sh` writes it from **Renovate's update type**, which is why the two columns can be compared at all. Details, including the seven charts whose non-semver app versions stay at a patch and what the rule deliberately does **not** oblige anyone to read: [Renovate → the version-bump hook](Renovate#the-version-bump-hook).
## Every chart change must bump the version ## Every chart change must bump the version
A push to `main` touching a chart directory **publishes** that chart. The [`build-chart` job](CI-CD#2-build-chart-per-changed-chart) compares `version:` against the base branch and fails the PR if it is unchanged. `template-chart` is exempt — it is never published. A push to `main` touching a chart directory **publishes** that chart. The [`build-chart` job](CI-CD#2-build-chart-per-changed-chart) compares `version:` against the base branch and fails the PR if it is unchanged. `template-chart` is exempt — it is never published.
@@ -49,7 +53,7 @@ The reason is migrations: most apps support an upgrade from the previous major,
## Who writes the version ## Who writes the version
- **A human**, in any PR that changes the chart. - **A human**, in any PR that changes the chart.
- **`ci/renovate-bump-chart.sh`**, wired up as a Renovate `postUpgradeTasks` command: app major changed → chart major; anything else (plain app update, sidecar image) → chart patch; the `+up` suffix recomputed from the new `appVersion`. Everything is derived from the base commit, so it is idempotent across the several updates one Renovate branch can carry. - **`ci/renovate-bump-chart.sh`**, wired up as a Renovate `postUpgradeTasks` command. It receives the chart directory **and the update type**, and the chart jump follows it: `major` → chart major, `minor` → chart **minor**, `patch`/`digest`/`pin` → chart patch. No or an unknown update type falls back to the pre-decision behaviour (app major changed → major, otherwise patch), so a hook that loses its second argument still bumps the version instead of leaving every chart PR red. The `+up` suffix is recomputed from the new `appVersion`. Everything is derived from the base commit, so it is idempotent across the several updates one Renovate branch can carry — and it never lowers a higher version already on the branch, which is what makes a grouped PR's level independent of the order the updates are processed in.
Nothing else writes `version:`. That is also how one can tell whether the hook ran at all — see [Renovate](Renovate#the-version-bump-hook). Nothing else writes `version:`. That is also how one can tell whether the hook ran at all — see [Renovate](Renovate#the-version-bump-hook).
+3 -1
@@ -36,7 +36,9 @@ This wiki is the knowledge base. A change that lands in `main` while the wiki st
| a Renovate rule added or changed | [Renovate](Renovate) | | a Renovate rule added or changed | [Renovate](Renovate) |
| something broke in a way that a rule now prevents | [Lessons learned](Lessons-Learned) | | something broke in a way that a rule now prevents | [Lessons learned](Lessons-Learned) |
**What does not oblige an edit:** a plain app-version bump. Renovate automerges those continuously and does not touch the wiki; chart pages therefore carry no version numbers at all and point at `Chart.yaml` instead. If an app-version bump *does* change behaviour or values, it is no longer a plain bump and the table above applies. **What does not oblige an edit:** an app-version bump, at any level. Renovate automerges those continuously and does not touch the wiki; chart pages therefore carry no version numbers at all and point at `Chart.yaml` instead. If an app-version bump *does* change behaviour or values, it is no longer a plain bump and the table above applies.
Since Gitea #128, **"plain" is mechanically defined rather than left to judgement**: the chart version now states the *size* of the jump, because `ci/renovate-bump-chart.sh` takes it from Renovate's update type — an app minor is a chart minor (see [Versioning](Versioning#bump-levels-for-the-chart-part)). What that decision deliberately did **not** add is a duty to look: **nobody is obliged to read the release notes of a bump**, whatever level it carries. The limit is named here rather than hidden, because it is the one that let [the security default a patch bump switched off](Lessons-Learned#the-security-default-that-a-patch-bump-switched-off) through — the number stopped understating, and that is the whole of the remedy.
**Where the duty sits in the workflow:** the development agent updates the wiki as part of its issue, before asking for the merge, so that the reviewer sees code and documentation together. The manager checks it at step 8, along with the issue closure. **Where the duty sits in the workflow:** the development agent updates the wiki as part of its issue, before asking for the merge, so that the reviewer sees code and documentation together. The manager checks it at step 8, along with the issue closure.
+1 -1
@@ -458,7 +458,7 @@ And the sharper caveat specific to this application: an environment variable is
## Upgrades and backup ## Upgrades and backup
- **A plain app-version bump** is a chart patch and needs nothing special: no database, no migration, no schema. Renovate opens it and `ci/renovate-bump-chart.sh` bumps the chart version. - **A plain app-version bump** needs nothing special: no database, no migration, no schema. Renovate opens it and `ci/renovate-bump-chart.sh` bumps the chart version — since Gitea #128 at the app's own level, so an app patch is a chart patch and an app minor a chart minor (see [Versioning](Versioning#bump-levels-for-the-chart-part)).
- **Back up the PVC and the `appSecretRef` vault item together.** Neither is sufficient alone — see [Backups](#backups-the-volume-alone-is-not-one-and-it-may-need-protecting-itself). - **Back up the PVC and the `appSecretRef` vault item together.** Neither is sufficient alone — see [Backups](#backups-the-volume-alone-is-not-one-and-it-may-need-protecting-itself).
- **Before any upgrade worth the caution**, read `config.json` off the volume. It tells you which of the chart's values are actually in force. - **Before any upgrade worth the caution**, read `config.json` off the volume. It tells you which of the chart's values are actually in force.
- `scaleDown: true` stops the release without uninstalling it. - `scaleDown: true` stops the release without uninstalling it.
+1 -1
@@ -704,7 +704,7 @@ Entries on any **other** key (`topology.kubernetes.io/zone`, for instance) are a
## Upgrades and backup ## Upgrades and backup
- **A plain app-version bump** is a chart patch and needs no action here; Renovate opens it and `ci/renovate-bump-chart.sh` bumps the version. Chart pages carry no version numbers — `Chart.yaml` is the source. - **A plain app-version bump** needs no action here; Renovate opens it and `ci/renovate-bump-chart.sh` bumps the version — since Gitea #128 at the app's own level, so an app patch is a chart patch and an app minor a chart minor (see [Versioning](Versioning#bump-levels-for-the-chart-part)). Chart pages carry no version numbers — `Chart.yaml` is the source.
- **Read `UPGRADING/` for an app major.** The v0.15 → v0.16 migration was offline, multi-step and irreversible without a backup; nothing suggests the next major will be gentler. For app major jumps the repo rule applies: first to the last release of the old line, then to the newest major, each in its own PR. - **Read `UPGRADING/` for an app major.** The v0.15 → v0.16 migration was offline, multi-step and irreversible without a backup; nothing suggests the next major will be gentler. For app major jumps the repo rule applies: first to the last release of the old line, then to the newest major, each in its own PR.
- **The backup is Postgres plus the S3 bucket.** No PVC is involved — there is nothing in the pods worth keeping. The configuration lives in Postgres too, which means a Postgres restore also rolls back every listener, domain and role. - **The backup is Postgres plus the S3 bucket.** No PVC is involved — there is nothing in the pods worth keeping. The configuration lives in Postgres too, which means a Postgres restore also rolls back every listener, domain and role.
- **`stalwart-cli snapshot` is the configuration backup**, and the only one that is readable and diffable. It emits an `apply` plan; keep one in version control after any WebUI change, or the eight steps above are the recovery procedure. - **`stalwart-cli snapshot` is the configuration backup**, and the only one that is readable and diffable. It emits an `apply` plan; keep one in version control after any WebUI change, or the eight steps above are the recovery procedure.
+1 -1
@@ -216,7 +216,7 @@ Checked 2026-10-12: the release runs in namespace `jlab-player` with both Deploy
A dependency here is admissible, not free ([style guide §14](Chart-Style-Guide#14-chart-dependencies)). The bar it clears: the proxy would otherwise be a second Helm release that only exists to serve this one, upstream is the right owner of it, and it is optional and off by default. The price, all of it: A dependency here is admissible, not free ([style guide §14](Chart-Style-Guide#14-chart-dependencies)). The bar it clears: the proxy would otherwise be a second Helm release that only exists to serve this one, upstream is the right owner of it, and it is optional and off by default. The price, all of it:
- **The subchart is invisible to everything downstream.** Its version is inside our packaged `.tgz`, so anything watching Helm release versions — Fleet's `fleet.yaml` included — sees only *our* chart version. A new oauth2-proxy release, a security fix included, reaches a cluster **only** when a new `ytdlp-web-player` version is published. Nothing reports the gap. Renovate's built-in `helmv3` manager opens the PR (no `customManagers` entry needed) and `ci/renovate-bump-chart.sh` bumps the chart patch that ships it; **that chain is the entire safety net**. The subchart version is independent of `appVersion` — raising it is a patch or minor bump of this chart. - **The subchart is invisible to everything downstream.** Its version is inside our packaged `.tgz`, so anything watching Helm release versions — Fleet's `fleet.yaml` included — sees only *our* chart version. A new oauth2-proxy release, a security fix included, reaches a cluster **only** when a new `ytdlp-web-player` version is published. Nothing reports the gap. Renovate's built-in `helmv3` manager opens the PR (no `customManagers` entry needed) and `ci/renovate-bump-chart.sh` bumps the chart version that ships it — since Gitea #128 at the **subchart's own level**, so an oauth2-proxy minor makes a chart minor here; **that chain is the entire safety net**. The subchart version is independent of `appVersion`, which is exactly why the level is taken from Renovate's update type rather than from comparing `appVersion`.
- **Upstream's chart is consumed as it comes.** We do not patch its templates, do not re-harden its workload and do not re-implement its values. Its resource limits, probes and security context follow **its** defaults, not this repo's; set them under `oauth2-proxy.resources` / `.securityContext` / `.livenessProbe` if you want this repo's shape. - **Upstream's chart is consumed as it comes.** We do not patch its templates, do not re-harden its workload and do not re-implement its values. Its resource limits, probes and security context follow **its** defaults, not this repo's; set them under `oauth2-proxy.resources` / `.securityContext` / `.livenessProbe` if you want this repo's shape.
- **This repo's mechanisms stop at the subchart boundary.** `checksum/config`, the 1Password auto-restart annotation and `scaleDown` reach our templates only. - **This repo's mechanisms stop at the subchart boundary.** `checksum/config`, the 1Password auto-restart annotation and `scaleDown` reach our templates only.
- **Every pipeline needs a network fetch first.** `helm lint`, `package`, `template` and `install` all exit 1 while `charts/` is unfilled, so `ci/helm-dependencies.sh` runs before each — including the resource-quantity render test that covers every chart on every PR. A third-party Helm repository's bad day can therefore redden a PR that touched nothing related. `charts/*.tgz` and `Chart.lock` are not committed, which is why the pipelines run `helm dependency update` rather than `build`. - **Every pipeline needs a network fetch first.** `helm lint`, `package`, `template` and `install` all exit 1 while `charts/` is unfilled, so `ci/helm-dependencies.sh` runs before each — including the resource-quantity render test that covers every chart on every PR. A third-party Helm repository's bad day can therefore redden a PR that touched nothing related. `charts/*.tgz` and `Chart.lock` are not committed, which is why the pipelines run `helm dependency update` rather than `build`.