feat(ci): the chart bump follows the app bump
Validate Helm Charts / Detect changed charts (pull_request) Successful in 5s
Validate Helm Charts / Build ${{ matrix.chart }} (pull_request) Skipped
Validate Helm Charts / Install-test (pull_request) Skipped
Validate Helm Charts / Resource quantities (pull_request) Successful in 18s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 14s
Validate Helm Charts / Bump level (pull_request) Successful in 7s
Validate Helm Charts / Trivy config cyberchef (pull_request) Successful in 20s
Validate Helm Charts / Trivy config matrix-synapse (pull_request) Successful in 22s
Validate Helm Charts / Trivy config samba (pull_request) Successful in 21s
SonarQube Analysis / SonarQube Analysis (push) Successful in 48s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 5s
Validate Helm Charts / Build ${{ matrix.chart }} (pull_request) Skipped
Validate Helm Charts / Install-test (pull_request) Skipped
Validate Helm Charts / Resource quantities (pull_request) Successful in 18s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 14s
Validate Helm Charts / Bump level (pull_request) Successful in 7s
Validate Helm Charts / Trivy config cyberchef (pull_request) Successful in 20s
Validate Helm Charts / Trivy config matrix-synapse (pull_request) Successful in 22s
Validate Helm Charts / Trivy config samba (pull_request) Successful in 21s
SonarQube Analysis / SonarQube Analysis (push) Successful in 48s
The version-bump hook classified every app update that was not a major as a chart patch, so an app minor arrived as a patch - the mechanism behind the sonarr incident, where a security default was switched off inside a bump the conventions told the reviewer not to read (Gitea #128). ci/renovate-bump-chart.sh now takes the level from Renovate's update type, passed in as a second argument: major -> major, minor -> MINOR, patch/digest/pin -> patch. The level comes from Renovate rather than from comparing appVersion because an appVersion comparison is blind to the case that prompted the rule - freesailarr keeps a hand-maintained appVersion for ten upstreams, and prowlarr's 2.5.2 -> 2.6.5 moved an image tag in values.yaml while appVersion stood still. Three properties come with it: - Maximum, not replacement. The chart jump is the greater of the chart's own classification and the app jump, so a value that becomes required stays a major under an app patch. Mechanically it is also what makes a grouped branch deterministic: the hook runs once per update from the same base commit, so without it the last call would win. - Non-semver app versions stay at patch for a minor (seven charts). In CalVer and date tags the second position is a month; in a four-part or suffixed version the positions are not semver positions. An app major is not subject to it. - A missing or unknown update type falls back to the pre-#128 behaviour. postUpgradeTasks only runs where the command is allow-listed on the shared instance, which is not visible from here and fails silently, so the worst case is the old classification rather than every chart PR red on the version check. ci/run-bump-level-test.sh holds 49 cases against the version string the script writes, wired up as the bump-level job: a wrong level is a valid version string, so the PR would go green and the only symptom would be a number that understates what it publishes. The convention texts follow in CLAUDE.md and README, including the limit the decision deliberately keeps: the reading duty was rejected, so the incident stays repeatable and the honest number is the whole of the remedy. Fixes #128
This commit was merged in pull request #130.
This commit is contained in:
6 files changed
+544
-51
No files matched your search
@@ -237,6 +237,32 @@ jobs:
|
|||||||
- name: "Check every rendered HTTPRoute against the rendered Services"
|
- name: "Check every rendered HTTPRoute against the rendered Services"
|
||||||
run: ci/run-httproute-match-test.sh
|
run: ci/run-httproute-match-test.sh
|
||||||
|
|
||||||
|
bump-level:
|
||||||
|
name: "Bump level"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# The fourth job that ignores the changed-chart matrix, and the only one
|
||||||
|
# that touches no chart at all: it tests ci/renovate-bump-chart.sh, the
|
||||||
|
# postUpgradeTasks hook that computes a chart's `version:` after Renovate
|
||||||
|
# moved an app version (Gitea #128).
|
||||||
|
#
|
||||||
|
# Why it is a job rather than a review: since #128 the hook takes the bump
|
||||||
|
# level from Renovate's update type, so an app minor becomes a chart minor.
|
||||||
|
# That makes it deciding logic, and its failure mode is silence - a wrong
|
||||||
|
# level is a VALID version string, so the PR goes green, the chart
|
||||||
|
# publishes, and the only symptom is a number that understates what it
|
||||||
|
# ships. Nothing else in this pipeline reads that number.
|
||||||
|
#
|
||||||
|
# It needs no Helm, no cluster and no Python: each case is a throwaway
|
||||||
|
# `git init` under mktemp with one commit plus a dirty Chart.yaml - the
|
||||||
|
# state the hook really runs in, since it reads the base commit - and the
|
||||||
|
# assertion is one `version:` line read back with sed. About a second for
|
||||||
|
# all 49 cases, and it depends on no other job.
|
||||||
|
steps:
|
||||||
|
- name: "Checkout Repository"
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||||
|
- name: "Check every bump level of the version-bump hook"
|
||||||
|
run: ci/run-bump-level-test.sh
|
||||||
|
|
||||||
trivy-config:
|
trivy-config:
|
||||||
name: "Trivy config ${{ matrix.chart }}"
|
name: "Trivy config ${{ matrix.chart }}"
|
||||||
needs: detect-changed-charts
|
needs: detect-changed-charts
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ Flat layout, one Markdown file per page, page name = file name, links are relati
|
|||||||
| a Renovate rule added or changed | `Renovate` |
|
| a Renovate rule added or changed | `Renovate` |
|
||||||
| something broke in a way a rule now prevents | `Lessons-Learned` |
|
| something broke in a way a rule now prevents | `Lessons-Learned` |
|
||||||
|
|
||||||
**What does not oblige an edit:** a plain app-version bump. Renovate automerges those continuously and never touches the wiki, which is why chart pages carry no version numbers at all and name `Chart.yaml` as the source of truth. An app update that *does* change values or behaviour is not a plain bump, and the table applies.
|
**What does not oblige an edit:** an app-version bump, at any level. Renovate automerges those continuously and never touches the wiki, which is why chart pages carry no version numbers at all and name `Chart.yaml` as the source of truth. Since Gitea #128 **"plain" is mechanically defined instead of being a judgement**: the chart version now states the size of the jump, because `ci/renovate-bump-chart.sh` takes that size from Renovate's update type (app minor → chart minor). An app update that *does* change values or behaviour is not a plain bump and the table applies — but **nothing obliges anyone to go and find out**. That is the deliberate limit of #128, not an oversight: the reading duty was rejected, so the `sonarr` incident in `Lessons-Learned` stays repeatable and a bigger number is the only warning that is given.
|
||||||
|
|
||||||
Wiki commits follow the same rules as repo commits: no `Co-Authored-By`, no "Generated with Claude Code" footer.
|
Wiki commits follow the same rules as repo commits: no `Co-Authored-By`, no "Generated with Claude Code" footer.
|
||||||
|
|
||||||
@@ -94,11 +94,13 @@ Copy `template-chart/` and replace every `xxx` placeholder — in file names (`t
|
|||||||
|
|
||||||
`Chart.yaml` versions follow `version: <chart-semver>+up<appVersion>` (e.g. `1.4.0+up1.9.2`), with `appVersion` matching the suffix (dashes in the app version are normalized to dots in the suffix). Any push to `main` touching a chart directory publishes that chart, so the version must be bumped in the same change.
|
`Chart.yaml` versions follow `version: <chart-semver>+up<appVersion>` (e.g. `1.4.0+up1.9.2`), with `appVersion` matching the suffix (dashes in the app version are normalized to dots in the suffix). Any push to `main` touching a chart directory publishes that chart, so the version must be bumped in the same change.
|
||||||
|
|
||||||
Semver conventions for the chart part:
|
Semver conventions for the chart part. **The chart jump is the greater of the chart's own classification and the jump of what it ships** (Gitea #128) — a maximum, never a replacement, so a value that becomes required stays a major even under an app patch:
|
||||||
|
|
||||||
- **Patch**: bug fixes without behavior or values-schema change; plain app-version updates.
|
- **Patch**: bug fixes without behavior or values-schema change; an app **patch** or digest update.
|
||||||
- **Minor**: additive, backward-compatible changes (new optional values with unchanged defaults).
|
- **Minor**: additive, backward-compatible changes (new optional values with unchanged defaults); an app **minor** update.
|
||||||
- **Major**: runtime behavior changes or breaking values changes (e.g. probes, security hardening, computed limits, values that become required).
|
- **Major**: runtime behavior changes or breaking values changes (e.g. probes, security hardening, computed limits, values that become required); an app **major** update.
|
||||||
|
|
||||||
|
The app half of that is mechanical: `ci/renovate-bump-chart.sh` takes the level from **Renovate's update type**, passed in as `{{{updateType}}}`, and not from comparing `appVersion` — `freesailarr` pins every tag in `values.yaml` and its `appVersion` never moves, so a comparison is blind to exactly the case the rule exists for. Non-semver app versions (CalVer, date tags, four-part and suffixed tags — seven charts) stay at **patch** for a minor, because their second position is a month or a flavour and says nothing about the jump; an app **major** still produces a chart major. **It obliges nobody to read the release notes** — that variant was rejected in #128 — so an app minor that changes a default can still reach `main` unremarked; the number stops understating, that is all it does. See the wiki, `Lessons-Learned`.
|
||||||
|
|
||||||
App-version updates go through PRs (so the validate and install-test pipelines run), not through the `change-app-version.yml` dispatch workflow (which commits directly to main and bypasses both). For app **major** jumps, update stepwise: first to the last release of the old major line (own PR, merge, publish), then to the newest major.
|
App-version updates go through PRs (so the validate and install-test pipelines run), not through the `change-app-version.yml` dispatch workflow (which commits directly to main and bypasses both). For app **major** jumps, update stepwise: first to the last release of the old major line (own PR, merge, publish), then to the newest major.
|
||||||
|
|
||||||
@@ -117,4 +119,4 @@ The binding style guide for all current and future charts lives in **README.md (
|
|||||||
- Resources: requests as values defaults; limits auto-computed by the `_helpers.tpl` helper (`limitFactor` default 3; no CPU limits unless set explicitly).
|
- Resources: requests as values defaults; limits auto-computed by the `_helpers.tpl` helper (`limitFactor` default 3; no CPU limits unless set explicitly).
|
||||||
- Replicas per workload (explicit `0` allowed) plus a chart-global `scaleDown` flag that overrides them.
|
- Replicas per workload (explicit `0` allowed) plus a chart-global `scaleDown` flag that overrides them.
|
||||||
- `icon:` mandatory in every `Chart.yaml`; `helm lint --strict` without findings; CI fixtures under `ci/<chart>/` or a justified exclusion.
|
- `icon:` mandatory in every `Chart.yaml`; `helm lint --strict` without findings; CI fixtures under `ci/<chart>/` or a justified exclusion.
|
||||||
- **Chart dependencies are admissible but expensive** (the former "no dependencies" rule fell with #214; `ytdlp-web-player` was the first to use one, `stalwart` the second, `paperless-ngx` the third). A dependency has to be optional and off by default via `condition:`, and upstream's chart is consumed as it comes — where its defaults fall short of these conventions, that is **documented in our `values.yaml`, never patched**. The price to know before adding the next one: a subchart is invisible to everything downstream that watches Helm release versions, so a new upstream version — a security fix included — reaches a cluster only when the outer chart is republished (Renovate's built-in `helmv3` manager plus `ci/renovate-bump-chart.sh` is the whole safety net, and it belongs in a comment at the pinned version); and every helm command exits 1 while `charts/` is unfilled, which is why `ci/helm-dependencies.sh` runs before lint, package, render and install — including the render test that covers **all** charts on **every** PR. Full rules and the rest of the cost: README, "Chart dependencies".
|
- **Chart dependencies are admissible but expensive** (the former "no dependencies" rule fell with #214; `ytdlp-web-player` was the first to use one, `stalwart` the second, `paperless-ngx` the third). A dependency has to be optional and off by default via `condition:`, and upstream's chart is consumed as it comes — where its defaults fall short of these conventions, that is **documented in our `values.yaml`, never patched**. The price to know before adding the next one: a subchart is invisible to everything downstream that watches Helm release versions, so a new upstream version — a security fix included — reaches a cluster only when the outer chart is republished (Renovate's built-in `helmv3` manager plus `ci/renovate-bump-chart.sh` is the whole safety net, and since #128 that republish carries the **subchart's own level** — an upstream minor is a chart minor, which is the only place that jump becomes visible at all; it belongs in a comment at the pinned version); and every helm command exits 1 while `charts/` is unfilled, which is why `ci/helm-dependencies.sh` runs before lint, package, render and install — including the render test that covers **all** charts on **every** PR. Full rules and the rest of the cost: README, "Chart dependencies".
|
||||||
@@ -69,7 +69,7 @@ Every chart has a wiki page of its own — `https://git.jk-dev.de/jlab-cluster/h
|
|||||||
Note: the registry **accepts a re-upload of an already published version** (HTTP 201) and replaces the stored artifact, so a republish of an existing version is still silent — see #160. That is also what makes a manual `workflow_dispatch` of any `publish-<chart>.yml` a harmless way to prove the shared block works.
|
Note: the registry **accepts a re-upload of an already published version** (HTTP 201) and replaces the stored artifact, so a republish of an existing version is still silent — see #160. That is also what makes a manual `workflow_dispatch` of any `publish-<chart>.yml` a harmless way to prove the shared block works.
|
||||||
- `change-app-version.yml` — manually dispatched app-version bump; the chart must be listed in its `chart-name` choice list.
|
- `change-app-version.yml` — manually dispatched app-version bump; the chart must be listed in its `chart-name` choice list.
|
||||||
- `ci/helm-dependencies.sh <chart-dir>` — not a workflow but the step they all share: fetches a chart's declared dependencies into `charts/`, and returns silently for the charts that declare none (all but three: `ytdlp-web-player`, `stalwart`, `paperless-ngx`). It runs in **seven** places, because every helm command this repo builds on exits 1 while a declared dependency is missing: before lint and package in `validate-charts.yml`, before package in the shared publish block (which is why that block names this script as a prerequisite of its callers — see above), and inside `ci/run-install-test.sh`, `ci/run-quantity-render-test.sh`, `ci/run-selector-match-test.sh`, `ci/run-httproute-match-test.sh` and `ci/run-trivy-config.sh`. See "Chart dependencies" in the style guidelines for what that costs.
|
- `ci/helm-dependencies.sh <chart-dir>` — not a workflow but the step they all share: fetches a chart's declared dependencies into `charts/`, and returns silently for the charts that declare none (all but three: `ytdlp-web-player`, `stalwart`, `paperless-ngx`). It runs in **seven** places, because every helm command this repo builds on exits 1 while a declared dependency is missing: before lint and package in `validate-charts.yml`, before package in the shared publish block (which is why that block names this script as a prerequisite of its callers — see above), and inside `ci/run-install-test.sh`, `ci/run-quantity-render-test.sh`, `ci/run-selector-match-test.sh`, `ci/run-httproute-match-test.sh` and `ci/run-trivy-config.sh`. See "Chart dependencies" in the style guidelines for what that costs.
|
||||||
- `validate-charts.yml` — PR pipeline: detects which charts a PR touches and, for exactly those, runs `helm lint --strict`, `helm package`, and a chart-version bump check (`template-chart` is exempt from the version check). PRs without chart changes skip the build matrix. After the build stage, an **install-test** job (the same changed-charts matrix, minus the charts listed in `ci/excluded-charts.txt`) installs each changed chart into an ephemeral [k3d](https://k3d.io) cluster on the runner and requires every workload to become Ready — see below. Both matrices follow the changed chart directories, so a PR that only changes the harness under `ci/` 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 after the merge. The **resource-quantity render test**, the **selector-match render test** and the **HTTPRoute render test** are the three exceptions to that rule: all of them run over every chart on every PR — see below.
|
- `validate-charts.yml` — PR pipeline: detects which charts a PR touches and, for exactly those, runs `helm lint --strict`, `helm package`, and a chart-version bump check (`template-chart` is exempt from the version check). PRs without chart changes skip the build matrix. After the build stage, an **install-test** job (the same changed-charts matrix, minus the charts listed in `ci/excluded-charts.txt`) installs each changed chart into an ephemeral [k3d](https://k3d.io) cluster on the runner and requires every workload to become Ready — see below. Both matrices follow the changed chart directories, so a PR that only changes the harness under `ci/` 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 after the merge. The **resource-quantity render test**, the **selector-match render test** and the **HTTPRoute render test** are the three exceptions to that rule: all of them run over every chart on every PR — see below. A fourth job, the **bump-level test**, ignores the matrix for a different reason: it tests no chart at all, but the `postUpgradeTasks` hook that computes their versions.
|
||||||
- `install-test.yml` — reusable workflow (`workflow_call`) holding the install-test **job** itself: the runner-label expression, the pinned `kubectl` with its skew check, `envsubst`, the containerised-job detection, the per-leg k3d cluster and the `freesailarr` VPN credentials. Its one input is a JSON array of chart names; its callers decide *which* charts are tested and whether the job runs at all, never *how*. Two callers: `validate-charts.yml` and `install-test-all-charts.yml`. **It is not copied for the second caller** — a second copy is a second chance to test differently, unnoticed, the same argument that gives `ci/excluded-charts.txt` exactly one parser.
|
- `install-test.yml` — reusable workflow (`workflow_call`) holding the install-test **job** itself: the runner-label expression, the pinned `kubectl` with its skew check, `envsubst`, the containerised-job detection, the per-leg k3d cluster and the `freesailarr` VPN credentials. Its one input is a JSON array of chart names; its callers decide *which* charts are tested and whether the job runs at all, never *how*. Two callers: `validate-charts.yml` and `install-test-all-charts.yml`. **It is not copied for the second caller** — a second copy is a second chance to test differently, unnoticed, the same argument that gives `ci/excluded-charts.txt` exactly one parser.
|
||||||
|
|
||||||
**It stays in this repository, and that is a decision, not an oversight** (Gitea #66, decision 4). It is reusable in form but not in substance: it is welded to the `ci/` fixtures, to the `ci/excluded-charts.txt` parser and to this repository's chart matrix. A building block that only one repository can use is not shared, it is merely moved somewhere else — and moving it would buy nothing while putting a tag between us and a file we change often. `publish-helm-chart.yml` was the opposite case: it takes a chart name and a registry owner and knows nothing about this repository, which is exactly why it could go. This note exists so the question is not asked again in half a year.
|
**It stays in this repository, and that is a decision, not an oversight** (Gitea #66, decision 4). It is reusable in form but not in substance: it is welded to the `ci/` fixtures, to the `ci/excluded-charts.txt` parser and to this repository's chart matrix. A building block that only one repository can use is not shared, it is merely moved somewhere else — and moving it would buy nothing while putting a tag between us and a file we change often. `publish-helm-chart.yml` was the opposite case: it takes a chart name and a registry owner and knows nothing about this repository, which is exactly why it could go. This note exists so the question is not asked again in half a year.
|
||||||
@@ -187,6 +187,18 @@ Measured in CI (run 719, the pilot): **30 charts, 4 assertions held**, whole job
|
|||||||
|
|
||||||
The fail path was exercised rather than assumed: a Service renamed in the route, a port the Service does not expose, a port left out entirely, an emptied `parentRefs` with the chart's own guard disabled, an overlay that renders no route, a run with zero assertions, and — for the pairing — both the redirect rule deleted outright and the redirect pointed at a path other than the one stripped; each produced a `FAIL` naming both sides and a non-zero exit.
|
The fail path was exercised rather than assumed: a Service renamed in the route, a port the Service does not expose, a port left out entirely, an emptied `parentRefs` with the chart's own guard disabled, an overlay that renders no route, a run with zero assertions, and — for the pairing — both the redirect rule deleted outright and the redirect pointed at a path other than the one stripped; each produced a `FAIL` naming both sides and a non-zero exit.
|
||||||
|
|
||||||
|
### Bump-level test (the version-bump hook, every PR)
|
||||||
|
|
||||||
|
`ci/run-bump-level-test.sh` (no arguments) holds every case of `ci/renovate-bump-chart.sh` against the version string it writes. Added with Gitea #128, which is what made it necessary: until then the hook was a calculator with one branch, and with the update type coming from Renovate it became the thing that *says how big a change a cluster is being offered* — while the chart version is the only signal anything downstream sees (see "Chart dependencies": Fleet reads our chart version and nothing inside the package).
|
||||||
|
|
||||||
|
Its failure mode is the silent kind this section's two rules are about: **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 in the pipeline reads that number, so nothing but a test can.
|
||||||
|
|
||||||
|
A case is not a function call, because the script reads the **base commit** (`git show HEAD:<chart>/Chart.yaml`) *and* the working tree: each of the 49 cases gets a throwaway `git init` under `mktemp -d`, one commit carrying the base `Chart.yaml`, and the new one written over it uncommitted — the state Renovate's `postUpgradeTasks` hook actually runs in. Signing is switched off per command, since this workspace signs through an SSH signer and a throwaway repo has no business invoking it. No Helm, no cluster, no `pyyaml`, no `yq`, no Docker: the fixtures are six-line `Chart.yaml` files written with `printf`, and the assertion is one `version:` line read back with `sed`. The job therefore carries no Helm step and takes about a second.
|
||||||
|
|
||||||
|
What the cases cover: 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 and 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 in every case, so the `+up` suffix and its dash-to-dot normalization are covered throughout.
|
||||||
|
|
||||||
|
**What it cannot assert, and does not pretend to:** that Renovate passes the update type at all. The hook runs only when the command is allow-listed on the shared instance, which is outside this repository — which is why a *missing* second argument is itself a case here, and why the change that introduced it carries a dry run instead of a claim (see "The version-bump hook").
|
||||||
|
|
||||||
### Install-test (PR pipeline stage 2)
|
### Install-test (PR pipeline stage 2)
|
||||||
|
|
||||||
For every chart a PR changes, `ci/run-install-test.sh <chart>` runs against a throwaway k3d cluster: it applies the stub `OnePasswordItem` CRD plus the chart's fixtures, `helm install`s the chart with its CI values, and waits for all Deployments/StatefulSets to roll out (per-chart timeout). Since probes and security hardening are active on all workloads, "Ready" means the app actually answers. On failure the job dumps pod descriptions, events and container logs.
|
For every chart a PR changes, `ci/run-install-test.sh <chart>` runs against a throwaway k3d cluster: it applies the stub `OnePasswordItem` CRD plus the chart's fixtures, `helm install`s the chart with its CI values, and waits for all Deployments/StatefulSets to roll out (per-chart timeout). Since probes and security hardening are active on all workloads, "Ready" means the app actually answers. On failure the job dumps pod descriptions, events and container logs.
|
||||||
@@ -267,9 +279,9 @@ Each chart renders its main image as `<repo>:{{ .Chart.AppVersion }}` — reposi
|
|||||||
|
|
||||||
One rule those overrides share, learned from `searxng` and worth knowing before writing the next one: **a capture group is a claim about how the value is compared.** Renovate's regex versioning parses `major`, `minor`, `patch`, `build` and `revision` with `Number.parseInt` (`modules/versioning/regex/index.js`, `_parse`), so a non-numeric commit hash captured as `build` becomes whatever digits lead it — `9fea41204` is `9`, `777ba8fa4` is `777`, and a hash starting with a letter is `NaN`. Measured on renovate 41.173.1 and 44.48.0, that makes Renovate offer an image pushed *earlier the same day* as an update to a newer one — a downgrade wearing the clothes of a routine date bump, which is exactly what a review waves through and automerge then carries to `main`. Parts of a tag that carry no ordering belong **outside** every group: the anchored pattern still requires them to be there, they simply take no part in the comparison. `prerelease` is not the way out either — it sorts correctly but marks every tag `isStable === false`, which hands the whole chart to `ignoreUnstable`. `freesailarr` has no **app-version** block on purpose — see the images exception above; a block would either duplicate the `helm-values` updates or rewrite an `appVersion` that selects no image. It does carry one `customManagers` block, for `mediathekarr`: that image is pinned by digest because its tags carry no version (`latest` is a 2024 build, `beta` moves), and the built-in `helm-values` manager reads only `repository`/`tag` and skips it (`skipReason: invalid-value`), so the two cannot collide. Digest rules need the same guard as versioning rules and for a sharper reason: while the pin is current, a matching rule and a broken one produce the identical empty result. Verified with two dry-run fixtures — the real digest yields no update, a deliberately stale one yields exactly one `digest` update back to the pinned digest.
|
One rule those overrides share, learned from `searxng` and worth knowing before writing the next one: **a capture group is a claim about how the value is compared.** Renovate's regex versioning parses `major`, `minor`, `patch`, `build` and `revision` with `Number.parseInt` (`modules/versioning/regex/index.js`, `_parse`), so a non-numeric commit hash captured as `build` becomes whatever digits lead it — `9fea41204` is `9`, `777ba8fa4` is `777`, and a hash starting with a letter is `NaN`. Measured on renovate 41.173.1 and 44.48.0, that makes Renovate offer an image pushed *earlier the same day* as an update to a newer one — a downgrade wearing the clothes of a routine date bump, which is exactly what a review waves through and automerge then carries to `main`. Parts of a tag that carry no ordering belong **outside** every group: the anchored pattern still requires them to be there, they simply take no part in the comparison. `prerelease` is not the way out either — it sorts correctly but marks every tag `isStable === false`, which hands the whole chart to `ignoreUnstable`. `freesailarr` has no **app-version** block on purpose — see the images exception above; a block would either duplicate the `helm-values` updates or rewrite an `appVersion` that selects no image. It does carry one `customManagers` block, for `mediathekarr`: that image is pinned by digest because its tags carry no version (`latest` is a 2024 build, `beta` moves), and the built-in `helm-values` manager reads only `repository`/`tag` and skips it (`skipReason: invalid-value`), so the two cannot collide. Digest rules need the same guard as versioning rules and for a sharper reason: while the pin is current, a matching rule and a broken one produce the identical empty result. Verified with two dry-run fixtures — the real digest yields no update, a deliberately stale one yields exactly one `digest` update back to the pinned digest.
|
||||||
|
|
||||||
A consequence of pinning every tag in `values.yaml`: `ci/renovate-bump-chart.sh` sees an unchanged `appVersion` and bumps the chart **patch**. That holds for all ten images alike, the digest-pinned one included — none of them is special — and it is the intended outcome, since `appVersion` here is the chart's own marker rather than a tracked upstream version.
|
A consequence of pinning every tag in `values.yaml`, and the measurement that forced the change in Gitea #128: `ci/renovate-bump-chart.sh` used to see an unchanged `appVersion` and bump the chart **patch** — for all ten images alike, the digest-pinned one included. `appVersion` here is the chart's own marker rather than a tracked upstream version, so a comparison had nothing to read: prowlarr's `2.5.2 → 2.6.5` was an upstream **minor** that arrived as a chart patch. Since #128 the level comes from Renovate's classification instead (`{{{updateType}}}`, the hook's second argument), so a minor among the ten makes a chart minor and a mixed group takes the highest level in it. The `+up` suffix still follows `appVersion` and therefore still stands still.
|
||||||
|
|
||||||
**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 where nothing reviews it and nothing documents it. It has already happened once: the `sonarr` 4.0.19 bump carried upstream's removal of the hard-coded trusted proxy networks, and it reached `main` unremarked (see [Lessons learned](https://git.jk-dev.de/jlab-cluster/helm-charts/wiki/Lessons-Learned)). Whether the convention should change is a separate decision and deliberately not proposed here.
|
**That open point is now decided (Gitea #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 where nothing reviewed it and nothing documented it. It had already happened: the `sonarr` 4.0.19 bump carried upstream's removal of the hard-coded trusted proxy networks and reached `main` unremarked (see [Lessons learned](https://git.jk-dev.de/jlab-cluster/helm-charts/wiki/Lessons-Learned)). What was decided is **the classification and not a reading duty**: the chart jump follows the app jump, so the number stops understating what it ships. 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 stated in the PR even when it is "nothing found"* — was **rejected**, and its price is the part that stays: nobody is obliged to look behind the number, so an app minor that changes a default can still reach `main` unremarked. What the bigger number does buy is visibility where a patch has none — Fleet's `fleet.yaml` in `cluster-config` sees only our chart version (see "Chart dependencies"), so a minor is a number other gates can act on. Whether `cluster-config` actually distinguishes update types is **not** visible from this repository; that is the mechanism which makes the number effective, not a claim that it is in use.
|
||||||
|
|
||||||
Two dependencies are constrained beyond their versioning. **nginx** (the Synapse delegation sidecar) is limited to **even minors** via `allowedVersions`. nginx numbers its release branches by minor — even is stable, odd is mainline — and the chart belongs on stable. Patch updates inside the stable branch still flow through normally. The constraint tolerates a variant suffix (`1.30.4-alpine`) on purpose: docker versioning only ever offers candidates carrying the current tag's suffix, so a suffix-blind constraint would silently freeze nginx the day the sidecar moves to a slimmer base image — a switch to `1.30.4-alpine` that #84 has commissioned a check on — with no error and no warning.
|
Two dependencies are constrained beyond their versioning. **nginx** (the Synapse delegation sidecar) is limited to **even minors** via `allowedVersions`. nginx numbers its release branches by minor — even is stable, odd is mainline — and the chart belongs on stable. Patch updates inside the stable branch still flow through normally. The constraint tolerates a variant suffix (`1.30.4-alpine`) on purpose: docker versioning only ever offers candidates carrying the current tag's suffix, so a suffix-blind constraint would silently freeze nginx the day the sidecar moves to a slimmer base image — a switch to `1.30.4-alpine` that #84 has commissioned a check on — with no error and no warning.
|
||||||
|
|
||||||
@@ -296,13 +308,22 @@ Not managed: `ci/**` (install-test fixtures pin throwaway infrastructure to majo
|
|||||||
|
|
||||||
### 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 `build-chart` version check. `ci/renovate-bump-chart.sh <chart-dir>` closes that gap and is wired up 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 `build-chart` version check. `ci/renovate-bump-chart.sh <chart-dir> [update-type]` closes that gap and is wired up as a `postUpgradeTasks` command. **Since Gitea #128 the chart jump follows the app jump**, which turned the script from a calculator into deciding logic:
|
||||||
|
|
||||||
- **app major changed** → chart major (`6.0.0` → `7.0.0`), matching "runtime behavior changes" in the semver conventions
|
- **update type `major`** → 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
|
- **update type `minor`** → chart **minor** (`6.0.0` → `6.1.0`) — the part that was missing, and where the incident sat
|
||||||
|
- **`patch`, `digest`, `pin`, `pinDigest`** → chart patch
|
||||||
|
- **no or unknown update type** → the pre-#128 behaviour: app major changed → chart major, anything else → chart patch
|
||||||
|
- **never below what the branch already carries** — the maximum rule below
|
||||||
- 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
|
||||||
|
|
||||||
Everything is derived from the base commit, so the script is idempotent across the multiple updates a single Renovate branch can carry.
|
**The level comes from Renovate, not from `appVersion`,** and that is the whole point: `freesailarr` keeps a hand-maintained `appVersion` for ten upstreams, so the update that prompted the rule moved an image tag in `values.yaml` while `appVersion` stood still (see "The one grouped chart" above). Verified rather than assumed — Renovate 44.155.7, keycloak pinned one minor back: `--platform=local --dry-run=full` classifies the update (`"updateType": "minor"`) but **never reaches the branch worker**, so it cannot show the hook at all; 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 patch for a minor**, with the reason in the convention rather than in a footnote: 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 the risk, and in a four-part or suffixed version the positions are not semver positions at all. Seven charts on `main` are in that class — `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`) — and an app **major** is not subject to it, so a year rolling over in a date tag is a chart major exactly as it was 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. That is the accepted trade — conservative by default; a chart that wants the minor earns a documented exception rather than a widened pattern.
|
||||||
|
|
||||||
|
**Maximum, not replacement.** The chart jump is the *greater* of the chart's own classification and the app jump, so a value that becomes required stays a chart major under an app patch. Mechanically it is also what makes a grouped branch deterministic: the hook runs once per update and every call recomputes from the same base commit, so without the maximum the last call would win and `freesailarr`'s minor-among-patches group would take whatever level it happened to end on. The script only ever keeps a *higher* version than it computes, which is why repeated runs stay idempotent, and everything else is derived from the base commit for the same reason.
|
||||||
|
|
||||||
|
`ci/run-bump-level-test.sh` holds every one of those cases (49 of them) against the version string the script writes — a wrong level is a valid version string, so the PR would go green and the only symptom would be a number that understates what it publishes.
|
||||||
|
|
||||||
> **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:
|
> **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:
|
||||||
>
|
>
|
||||||
@@ -311,6 +332,8 @@ Everything is derived from the base commit, so the script is idempotent across t
|
|||||||
> ```
|
> ```
|
||||||
>
|
>
|
||||||
> Without that entry Renovate skips the task without an error and every chart PR stays red on the version check.
|
> 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, and the shape is not visible from here.** Renovate matches the allow-list against the **compiled** command (`allowedCommands.some((p) => regEx(p).test(compiledCmd))`), so the front-anchored form above still matches with an argument appended — measured, see the dry run noted in `renovate.json5`. A pattern anchored at the end on the instance (`…\.sh$`) would stop matching and produce the same silent skip a missing entry does. Only the maintainer can check that at the instance. It is why the script treats a **missing** update type as the pre-#128 behaviour instead of an error: the worst case of a narrower allow-list, an older Renovate or an unpopulated template field is the old classification, not red chart PRs.
|
||||||
|
|
||||||
If the dependency dashboard reports `Failed to look up github-releases package …: no-result` for third-party repos, that is the instance's github.com credentials, not this config. The instance needs a read-only PAT as `GITHUB_COM_TOKEN` for github.com datasource lookups; its own platform token is a Gitea token and is worth nothing there.
|
If the dependency dashboard reports `Failed to look up github-releases package …: no-result` for third-party repos, that is the instance's github.com credentials, not this config. The instance needs a read-only PAT as `GITHUB_COM_TOKEN` for github.com datasource lookups; its own platform token is a Gitea token and is worth nothing there.
|
||||||
|
|
||||||
@@ -371,10 +394,11 @@ These guidelines are **binding for every current and future chart** in this repo
|
|||||||
### 2. Versioning
|
### 2. Versioning
|
||||||
|
|
||||||
- `Chart.yaml` uses `version: <chart-semver>+up<appVersion>`; the `+up` suffix matches `appVersion`, with dashes normalized to dots (e.g. `0.0.36-beta` → `+up0.0.36.beta`). `appVersion` is the **exact image tag** of the main app image.
|
- `Chart.yaml` uses `version: <chart-semver>+up<appVersion>`; the `+up` suffix matches `appVersion`, with dashes normalized to dots (e.g. `0.0.36-beta` → `+up0.0.36.beta`). `appVersion` is the **exact image tag** of the main app image.
|
||||||
- Semver rules for the chart part:
|
- Semver rules for the chart part. **The chart jump is the greater of the chart's own classification and the jump of what it ships** (Gitea #128) — a maximum, never a replacement, so a value that becomes required is a chart major even under an app patch:
|
||||||
- **Patch**: bug fixes without behavior or values-schema change; plain app-version updates.
|
- **Patch**: bug fixes without behavior or values-schema change; an app **patch** or digest update.
|
||||||
- **Minor**: additive, backward-compatible changes (new optional values with unchanged defaults).
|
- **Minor**: additive, backward-compatible changes (new optional values with unchanged defaults); an app **minor** update.
|
||||||
- **Major**: runtime behavior changes or breaking values changes (e.g. resource renames, probes, security hardening, values that become required).
|
- **Major**: runtime behavior changes or breaking values changes (e.g. resource renames, probes, security hardening, values that become required); an app **major** update.
|
||||||
|
- The app half of that is mechanical and belongs to the hook rather than to a reviewer's judgement: `ci/renovate-bump-chart.sh` takes the level from Renovate's update type, with non-semver app versions staying at patch for a minor and a missing update type falling back to the old behaviour. The full rule, the seven affected charts and what it deliberately does **not** oblige anybody to read are in "The version-bump hook" above.
|
||||||
- Any push to `main` touching a chart directory publishes that chart, so the version must be bumped in the same change.
|
- Any push to `main` touching a chart directory publishes that chart, so the version must be bumped in the same change.
|
||||||
|
|
||||||
### 3. Images
|
### 3. Images
|
||||||
@@ -584,7 +608,7 @@ One more trap, and it is the reason `stalwart` gates its subchart on `valkeySubc
|
|||||||
|
|
||||||
**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 that watches Helm release versions — Fleet's `fleet.yaml` in `cluster-config` 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. Nothing reports the gap. Renovate's built-in `helmv3` manager opens the PR (no `customManagers` entry needed — the repository URL and version sit together in one file) and `ci/renovate-bump-chart.sh` bumps the chart 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 that watches Helm release versions — Fleet's `fleet.yaml` in `cluster-config` 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. Nothing reports the gap. Renovate's built-in `helmv3` manager opens the PR (no `customManagers` entry needed — the repository URL and version sit together in one file) 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 is a chart minor even though our `appVersion` did not move. That chain is the entire safety net, and the number it writes is the only place the subchart's jump is visible at all. 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`, `helm package`, `helm template` and `helm install` all exit 1 while a declared dependency is missing from `charts/`, so `ci/helm-dependencies.sh` runs before each of them (see CI/CD above). One of those call sites is the resource-quantity render test, which runs over **every** chart on **every** PR — a dependency-carrying chart can therefore turn a third-party Helm repository's bad day into a red pipeline on a PR that touched nothing related.
|
- **Every pipeline that touches the chart needs a network fetch first.** `helm lint`, `helm package`, `helm template` and `helm install` all exit 1 while a declared dependency is missing from `charts/`, so `ci/helm-dependencies.sh` runs before each of them (see CI/CD above). One of those call sites is the resource-quantity render test, which runs over **every** chart on **every** PR — a dependency-carrying chart can therefore turn a third-party Helm repository's bad day into a red pipeline on a PR that touched nothing related.
|
||||||
- **`charts/*.tgz` and `Chart.lock` are not committed** (`.gitignore` says why): 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` and not `helm dependency build`.
|
- **`charts/*.tgz` and `Chart.lock` are not committed** (`.gitignore` says why): 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` and not `helm dependency build`.
|
||||||
- **The repo's own mechanisms stop at the subchart boundary.** `checksum/config`, the 1Password auto-restart annotation and the `scaleDown` flag reach our templates only. A chart that renders 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 the `scaleDown` flag reach our templates only. A chart that renders a `OnePasswordItem` for its subchart documents the manual `kubectl rollout restart` that replaces the missing restart-on-change.
|
||||||
|
|||||||
+185
-29
@@ -11,23 +11,56 @@
|
|||||||
# failing the `build-chart` version-bump check. This script closes that gap and
|
# failing the `build-chart` version-bump check. This script closes that gap and
|
||||||
# is wired up as a `postUpgradeTasks` command in renovate.json5.
|
# is wired up as a `postUpgradeTasks` command in renovate.json5.
|
||||||
#
|
#
|
||||||
# Usage: ci/renovate-bump-chart.sh <chart-directory>
|
# Usage: ci/renovate-bump-chart.sh <chart-directory> [update-type]
|
||||||
#
|
#
|
||||||
# Bump level (README "Versioning"):
|
# Bump level (README "Versioning", decided in Gitea #128):
|
||||||
# - app major changed -> chart major, minor/patch reset to 0
|
# - update type major -> chart major, minor/patch reset to 0
|
||||||
# - anything else -> chart patch (plain app updates, sidecar images)
|
# - update type minor -> chart MINOR, patch reset to 0
|
||||||
|
# - patch / digest / pin -> chart patch
|
||||||
|
# - no or unknown update type -> the appVersion comparison below, i.e. the
|
||||||
|
# behaviour this script had before #128
|
||||||
|
# - and never BELOW what the branch already carries (see the maximum rule
|
||||||
|
# further down)
|
||||||
|
#
|
||||||
|
# THE UPDATE TYPE COMES FROM RENOVATE, AND THAT IS THE POINT (Gitea #128).
|
||||||
|
# Renovate has already classified the jump; passing `{{{updateType}}}` in is
|
||||||
|
# what makes a chart minor possible at all. Deriving it here from appVersion
|
||||||
|
# would be blind to exactly the case that prompted the rule: `freesailarr`
|
||||||
|
# carries a hand-maintained `appVersion: "1.0.0"` for ten upstreams and it never
|
||||||
|
# moves, so prowlarr's 2.5.2 -> 2.6.5 — an image tag in values.yaml — is a
|
||||||
|
# minor with an unchanged appVersion. README, "Dependency updates (Renovate)".
|
||||||
|
#
|
||||||
|
# WHAT A CHART MINOR BUYS, AND WHAT IT DOES NOT. It makes the number honest: an
|
||||||
|
# app minor stops arriving as a chart patch. It does NOT oblige anyone to read
|
||||||
|
# the release notes — that variant was decided against — so an app minor that
|
||||||
|
# changes a default can still reach `main` unremarked. What the number does is
|
||||||
|
# make the jump VISIBLE to everything downstream that sees only our chart
|
||||||
|
# version (Fleet's `fleet.yaml` in `cluster-config` among them), where a patch
|
||||||
|
# is invisible. See the wiki, Lessons-Learned.
|
||||||
|
#
|
||||||
|
# THE SECOND ARGUMENT IS OPTIONAL ON PURPOSE. `postUpgradeTasks` only runs when
|
||||||
|
# the command is allow-listed in the *instance* config, which is not visible
|
||||||
|
# from this repository, and a command that does not match the allow-list is
|
||||||
|
# skipped SILENTLY. So a missing update type — a narrower allow-list pattern, an
|
||||||
|
# older Renovate, a template field that is not populated — must not be a
|
||||||
|
# failure: without it the script falls back to what it did before #128 and every
|
||||||
|
# chart PR still gets its version bump.
|
||||||
#
|
#
|
||||||
# Everything is derived from the base commit (HEAD, which during
|
# Everything is derived from the base commit (HEAD, which during
|
||||||
# postUpgradeTasks still points at the base branch), never from the working
|
# postUpgradeTasks still points at the base branch), never from the working
|
||||||
# tree's `version:`. That makes repeated runs on the same branch idempotent —
|
# 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
|
# Renovate calls the script once per dependency update, and a branch may carry
|
||||||
# more than one update for the same chart.
|
# more than one update for the same chart. The one place the working tree IS
|
||||||
|
# read is the maximum rule below, and only to keep a higher version.
|
||||||
#
|
#
|
||||||
# Runs inside the Renovate container, so: POSIX sh, no bashisms, no jq/yq.
|
# Runs inside the Renovate container, so: POSIX sh, no bashisms, no jq/yq.
|
||||||
|
#
|
||||||
|
# Tested by ci/run-bump-level-test.sh, which is where the cases live.
|
||||||
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
dir=${1:-}
|
dir=${1:-}
|
||||||
|
update_type=${2:-}
|
||||||
[ -n "$dir" ] || exit 0
|
[ -n "$dir" ] || exit 0
|
||||||
|
|
||||||
# Strip a trailing slash so "outline/" and "outline" behave the same.
|
# Strip a trailing slash so "outline/" and "outline" behave the same.
|
||||||
@@ -49,6 +82,72 @@ field() {
|
|||||||
sed -n "s/^$1:[[:space:]]*//p" | head -n 1 | tr -d "\"' "
|
sed -n "s/^$1:[[:space:]]*//p" | head -n 1 | tr -d "\"' "
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Splits "<major>.<minor>.<patch>" into p_major/p_minor/p_patch. Returns 1 if
|
||||||
|
# the string is not exactly three numeric parts, which is also how a four-part
|
||||||
|
# version (3.3.1.0) and a suffixed one (9.1.2-alpine) are rejected: the last
|
||||||
|
# part then is not numeric.
|
||||||
|
parse_semver() {
|
||||||
|
_sv=$1
|
||||||
|
case "$_sv" in
|
||||||
|
*.*.*) ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
p_major=${_sv%%.*}
|
||||||
|
_rest=${_sv#*.}
|
||||||
|
p_minor=${_rest%%.*}
|
||||||
|
p_patch=${_rest#*.}
|
||||||
|
for _part in "$p_major" "$p_minor" "$p_patch"; do
|
||||||
|
case "$_part" in
|
||||||
|
'' | *[!0-9]*) return 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Is <a.b.c> strictly greater than <d.e.f>? Six arguments, componentwise.
|
||||||
|
semver_gt() {
|
||||||
|
if [ "$1" -ne "$4" ]; then
|
||||||
|
if [ "$1" -gt "$4" ]; then return 0; else return 1; fi
|
||||||
|
fi
|
||||||
|
if [ "$2" -ne "$5" ]; then
|
||||||
|
if [ "$2" -gt "$5" ]; then return 0; else return 1; fi
|
||||||
|
fi
|
||||||
|
if [ "$3" -gt "$6" ]; then return 0; else return 1; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Does this app version's MINOR POSITION carry a statement about jump width?
|
||||||
|
# Only a plain [v]X.Y.Z does. Everything else keeps the pre-#128 behaviour, i.e.
|
||||||
|
# a patch, and the reason is in the convention rather than in a footnote: in
|
||||||
|
# CalVer and in date tags the second position is a MONTH, so following it would
|
||||||
|
# produce a chart minor every month that says nothing about the risk, and in a
|
||||||
|
# four-part or suffixed version the positions are not semver positions at all.
|
||||||
|
#
|
||||||
|
# Measured on origin/main, this covers seven charts and only those seven:
|
||||||
|
# rss-bridge 2026-09-30 (no dots at all)
|
||||||
|
# homeassistant 2026.8.3 (CalVer — caught by the year guard)
|
||||||
|
# searxng 2026.9.13-3b8009065 (CalVer plus a commit hash)
|
||||||
|
# minecraft-server 2026.8.2-java25 (CalVer plus a JRE suffix)
|
||||||
|
# apache-tika 3.3.1.0-full (four parts plus a flavour)
|
||||||
|
# teamspeak-server 6.0.0-beta13.1 (prerelease line)
|
||||||
|
# valkey 9.1.2-alpine (flavour suffix)
|
||||||
|
#
|
||||||
|
# The last two are the honest edge of this rule: their numeric part IS a semver
|
||||||
|
# line, so the fallback understates a real minor there. That is the decided
|
||||||
|
# trade — conservative by default, and a chart that wants the minor gets a
|
||||||
|
# documented exception rather than a widened pattern.
|
||||||
|
#
|
||||||
|
# A `major` update type is NOT subject to this: a year rolling over in a date
|
||||||
|
# tag arrives as a major, and that was a chart major before #128 too.
|
||||||
|
app_minor_is_meaningful() {
|
||||||
|
_av=${1#v}
|
||||||
|
_av=${_av#V}
|
||||||
|
parse_semver "$_av" || return 1
|
||||||
|
# A four-digit leading component from 2000 on is a calendar year, not a
|
||||||
|
# semver major.
|
||||||
|
if [ "$p_major" -ge 2000 ]; then return 1; fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
base=$(git show "HEAD:$chart" 2>/dev/null) || {
|
base=$(git show "HEAD:$chart" 2>/dev/null) || {
|
||||||
echo "renovate-bump: $chart is new on this branch, leaving its version alone"
|
echo "renovate-bump: $chart is new on this branch, leaving its version alone"
|
||||||
exit 0
|
exit 0
|
||||||
@@ -57,6 +156,7 @@ base=$(git show "HEAD:$chart" 2>/dev/null) || {
|
|||||||
base_version=$(printf '%s\n' "$base" | field version)
|
base_version=$(printf '%s\n' "$base" | field version)
|
||||||
base_app=$(printf '%s\n' "$base" | field appVersion)
|
base_app=$(printf '%s\n' "$base" | field appVersion)
|
||||||
new_app=$(field appVersion <"$chart")
|
new_app=$(field appVersion <"$chart")
|
||||||
|
tree_version=$(field version <"$chart")
|
||||||
|
|
||||||
if [ -z "$base_version" ] || [ -z "$new_app" ]; then
|
if [ -z "$base_version" ] || [ -z "$new_app" ]; then
|
||||||
echo "renovate-bump: $chart has no version/appVersion to work with" >&2
|
echo "renovate-bump: $chart has no version/appVersion to work with" >&2
|
||||||
@@ -65,27 +165,13 @@ fi
|
|||||||
|
|
||||||
# <chart-semver> is everything before the +up build suffix.
|
# <chart-semver> is everything before the +up build suffix.
|
||||||
semver=${base_version%%+*}
|
semver=${base_version%%+*}
|
||||||
case "$semver" in
|
parse_semver "$semver" || {
|
||||||
*.*.*) ;;
|
|
||||||
*)
|
|
||||||
echo "renovate-bump: cannot parse chart semver '$semver' in $chart" >&2
|
echo "renovate-bump: cannot parse chart semver '$semver' in $chart" >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
}
|
||||||
esac
|
major=$p_major
|
||||||
|
minor=$p_minor
|
||||||
major=${semver%%.*}
|
patch=$p_patch
|
||||||
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,
|
# Leading major of an app version: v1.159.0 -> 1, 3.3.1.0-full -> 3,
|
||||||
# 2025-06-03 -> 2025.
|
# 2025-06-03 -> 2025.
|
||||||
@@ -93,17 +179,87 @@ app_major() {
|
|||||||
printf '%s' "$1" | sed -e 's/^[vV]//' -e 's/[^0-9].*$//'
|
printf '%s' "$1" | sed -e 's/^[vV]//' -e 's/[^0-9].*$//'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The pre-#128 level: app major changed -> major, anything else -> patch. Also
|
||||||
|
# the fallback whenever no usable update type arrives.
|
||||||
|
app_version_level() {
|
||||||
if [ "$new_app" != "$base_app" ] && [ "$(app_major "$new_app")" != "$(app_major "$base_app")" ]; then
|
if [ "$new_app" != "$base_app" ] && [ "$(app_major "$new_app")" != "$(app_major "$base_app")" ]; then
|
||||||
reason="app major $base_app -> $new_app"
|
echo major
|
||||||
new_semver="$((major + 1)).0.0"
|
|
||||||
else
|
else
|
||||||
reason="patch-level change"
|
echo patch
|
||||||
new_semver="$major.$minor.$((patch + 1))"
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$update_type" in
|
||||||
|
major)
|
||||||
|
level=major
|
||||||
|
reason="update type major"
|
||||||
|
;;
|
||||||
|
minor)
|
||||||
|
if app_minor_is_meaningful "$new_app"; then
|
||||||
|
level=minor
|
||||||
|
reason="update type minor"
|
||||||
|
else
|
||||||
|
level=$(app_version_level)
|
||||||
|
reason="update type minor, but appVersion '$new_app' is not a plain semver line -> $level"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
patch | digest | pin | pinDigest)
|
||||||
|
level=patch
|
||||||
|
reason="update type $update_type"
|
||||||
|
;;
|
||||||
|
'')
|
||||||
|
level=$(app_version_level)
|
||||||
|
reason="no update type given, derived from appVersion -> $level"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
level=$(app_version_level)
|
||||||
|
reason="unknown update type '$update_type', derived from appVersion -> $level"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$level" in
|
||||||
|
major)
|
||||||
|
new_major=$((major + 1))
|
||||||
|
new_minor=0
|
||||||
|
new_patch=0
|
||||||
|
;;
|
||||||
|
minor)
|
||||||
|
new_major=$major
|
||||||
|
new_minor=$((minor + 1))
|
||||||
|
new_patch=0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
new_major=$major
|
||||||
|
new_minor=$minor
|
||||||
|
new_patch=$((patch + 1))
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# THE MAXIMUM RULE (Gitea #128): the chart level is the GREATER of the chart's
|
||||||
|
# own classification and the app jump — never a replacement. Two cases, and the
|
||||||
|
# second is the mechanical one:
|
||||||
|
#
|
||||||
|
# - a chart-own bump already in the working tree stands. A value that becomes
|
||||||
|
# required is a chart major even when the app jump is a patch, and the chart
|
||||||
|
# version must not stop reporting the chart's own breaks.
|
||||||
|
# - a branch carrying SEVERAL updates. Renovate calls this script once per
|
||||||
|
# update and every call recomputes from the same base commit, so without
|
||||||
|
# this the last call would win: `freesailarr`'s grouped PR has arrived with
|
||||||
|
# three minors among five patches, and the chart level would then depend on
|
||||||
|
# the order the hook happened to run in.
|
||||||
|
#
|
||||||
|
# It only ever keeps a HIGHER version, so repeated runs stay idempotent.
|
||||||
|
if parse_semver "${tree_version%%+*}" &&
|
||||||
|
semver_gt "$p_major" "$p_minor" "$p_patch" "$new_major" "$new_minor" "$new_patch"; then
|
||||||
|
reason="$reason, kept the higher $p_major.$p_minor.$p_patch already on the branch"
|
||||||
|
new_major=$p_major
|
||||||
|
new_minor=$p_minor
|
||||||
|
new_patch=$p_patch
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Dashes in the app version become dots in the +up suffix.
|
# Dashes in the app version become dots in the +up suffix.
|
||||||
suffix=$(printf '%s' "$new_app" | tr '-' '.')
|
suffix=$(printf '%s' "$new_app" | tr '-' '.')
|
||||||
new_version="$new_semver+up$suffix"
|
new_version="$new_major.$new_minor.$new_patch+up$suffix"
|
||||||
|
|
||||||
sed -i.bak "s|^version:.*|version: $new_version|" "$chart"
|
sed -i.bak "s|^version:.*|version: $new_version|" "$chart"
|
||||||
rm -f "$chart.bak"
|
rm -f "$chart.bak"
|
||||||
|
|||||||
Executable
+229
@@ -0,0 +1,229 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Bump-level test: every case of ci/renovate-bump-chart.sh, held against the
|
||||||
|
# version string it writes into a throwaway chart.
|
||||||
|
#
|
||||||
|
# Usage: ci/run-bump-level-test.sh
|
||||||
|
#
|
||||||
|
# WHY THIS TEST EXISTS (Gitea #128)
|
||||||
|
#
|
||||||
|
# Until #128 the hook was a calculator with one branch: app major -> chart
|
||||||
|
# major, everything else -> chart patch. With the update type coming from
|
||||||
|
# Renovate it became DECIDING LOGIC — it is now the thing that says how big a
|
||||||
|
# change the cluster is being offered, and the chart version is the only signal
|
||||||
|
# anything downstream sees (README, "Chart dependencies": Fleet's `fleet.yaml`
|
||||||
|
# in `cluster-config` sees our chart version and nothing inside the package).
|
||||||
|
#
|
||||||
|
# And its failure mode is silence. 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 in the pipeline reads it, so nothing but a
|
||||||
|
# test can.
|
||||||
|
#
|
||||||
|
# HOW A CASE IS BUILT, AND WHY A REAL GIT REPO
|
||||||
|
#
|
||||||
|
# The script reads the BASE commit (`git show HEAD:<chart>/Chart.yaml`) and the
|
||||||
|
# working tree, so a case is not a function call — it is a repository with a
|
||||||
|
# commit and a dirty file. Each case therefore gets a throwaway `git init` in a
|
||||||
|
# unique temp directory, one commit carrying the base Chart.yaml, and the new
|
||||||
|
# Chart.yaml written over it uncommitted. That is exactly the state Renovate's
|
||||||
|
# `postUpgradeTasks` hook runs in.
|
||||||
|
#
|
||||||
|
# Signing is switched off per command (`-c commit.gpgsign=false`): this
|
||||||
|
# workspace signs every commit through an SSH signer, and a throwaway repo has
|
||||||
|
# no business invoking it. The temp root comes from `mktemp -d`, because /tmp is
|
||||||
|
# shared with whatever else runs on this machine.
|
||||||
|
#
|
||||||
|
# The runners have no pyyaml, no yq and no Docker — the same constraint
|
||||||
|
# ci/run-selector-match-test.sh and ci/run-quantity-render-test.sh work under.
|
||||||
|
# None is needed: the fixtures are six-line Chart.yaml files written by printf,
|
||||||
|
# and the assertion is one `version:` line read back with sed. Helm is not
|
||||||
|
# needed either, which is why this job carries no Helm step.
|
||||||
|
#
|
||||||
|
# WHAT IS ASSERTED, AND WHAT IS NOT
|
||||||
|
#
|
||||||
|
# Asserted: the full `version:` line (so the `+up` suffix and its dash-to-dot
|
||||||
|
# normalization are covered by every case), the exit code, and for the cases
|
||||||
|
# that must leave the chart alone, that the line is untouched.
|
||||||
|
#
|
||||||
|
# NOT asserted, and not assertable from here: that Renovate passes the update
|
||||||
|
# type at all. The hook only runs when the command is allow-listed in the
|
||||||
|
# *instance* config, which lives outside this repository — and a pattern that
|
||||||
|
# does not match is skipped silently. That is why a missing second argument is
|
||||||
|
# itself a case here (it must fall back, not fail), and why the PR that
|
||||||
|
# introduced this carries a dry-run instead of a claim.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
SCRIPT="$ROOT/ci/renovate-bump-chart.sh"
|
||||||
|
|
||||||
|
[ -x "$SCRIPT" ] || {
|
||||||
|
echo "FAIL: $SCRIPT is not executable" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
TMP="$(mktemp -d "${TMPDIR:-/tmp}/bump-level-test.XXXXXX")"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
GIT_OPTS=(-c user.name=ci -c user.email=ci@example.invalid -c commit.gpgsign=false -c tag.gpgsign=false)
|
||||||
|
|
||||||
|
passed=0
|
||||||
|
failed=0
|
||||||
|
|
||||||
|
chart_yaml() { # <name> <version> <appVersion>
|
||||||
|
printf 'apiVersion: v2\nname: %s\ndescription: test fixture\ntype: application\nversion: %s\nappVersion: "%s"\nicon: https://example.com/icon.png\n' \
|
||||||
|
"$1" "$2" "$3"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Builds a repository whose HEAD carries <base-content> for <dir>/Chart.yaml
|
||||||
|
# (skipped when empty, which is the "new chart on this branch" case) and whose
|
||||||
|
# working tree carries <tree-content>. Prints the repository path.
|
||||||
|
scenario() { # <dir> <base-content> <tree-content>
|
||||||
|
local dir=$1 base=$2 tree=$3 repo
|
||||||
|
repo="$(mktemp -d "$TMP/case.XXXXXX")"
|
||||||
|
git -C "$repo" init -q
|
||||||
|
printf 'root commit so HEAD exists\n' >"$repo/README.md"
|
||||||
|
git -C "$repo" add README.md
|
||||||
|
git -C "$repo" "${GIT_OPTS[@]}" commit -qm "root"
|
||||||
|
mkdir -p "$repo/$dir"
|
||||||
|
if [ -n "$base" ]; then
|
||||||
|
printf '%s' "$base" >"$repo/$dir/Chart.yaml"
|
||||||
|
git -C "$repo" add "$dir/Chart.yaml"
|
||||||
|
git -C "$repo" "${GIT_OPTS[@]}" commit -qm "base chart"
|
||||||
|
fi
|
||||||
|
[ -z "$tree" ] || printf '%s' "$tree" >"$repo/$dir/Chart.yaml"
|
||||||
|
printf '%s' "$repo"
|
||||||
|
}
|
||||||
|
|
||||||
|
version_of() { sed -n 's/^version:[[:space:]]*//p' "$1" | head -n 1; }
|
||||||
|
|
||||||
|
report() { # <name> <expected> <got> <expected-rc> <got-rc> <output>
|
||||||
|
if [ "$2" = "$3" ] && [ "$4" = "$5" ]; then
|
||||||
|
passed=$((passed + 1))
|
||||||
|
printf ' ok %s\n' "$1"
|
||||||
|
else
|
||||||
|
failed=$((failed + 1))
|
||||||
|
printf ' FAIL %s\n expected version %-24s rc %s\n got version %-24s rc %s\n output: %s\n' \
|
||||||
|
"$1" "$2" "$4" "$3" "$5" "$6"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# The ordinary case: one chart, a base version, an app version that may or may
|
||||||
|
# not have moved, and an update type. UPDATE_TYPE of "-" calls the script with
|
||||||
|
# ONE argument (the allow-list / older-Renovate case), "" passes an empty
|
||||||
|
# second argument (a template field that resolved to nothing).
|
||||||
|
bump() { # <name> <base-version> <base-app> <tree-version> <tree-app> <update-type> <expected-version> [expected-rc]
|
||||||
|
local name=$1 base_version=$2 base_app=$3 tree_version=$4 tree_app=$5 utype=$6 expected=$7 rc_expected=${8:-0}
|
||||||
|
local repo out rc got
|
||||||
|
repo="$(scenario demo "$(chart_yaml demo "$base_version" "$base_app")" "$(chart_yaml demo "$tree_version" "$tree_app")")"
|
||||||
|
rc=0
|
||||||
|
if [ "$utype" = "-" ]; then
|
||||||
|
out="$(cd "$repo" && "$SCRIPT" demo 2>&1)" || rc=$?
|
||||||
|
else
|
||||||
|
out="$(cd "$repo" && "$SCRIPT" demo "$utype" 2>&1)" || rc=$?
|
||||||
|
fi
|
||||||
|
got="$(version_of "$repo/demo/Chart.yaml")"
|
||||||
|
report "$name" "$expected" "$got" "$rc_expected" "$rc" "$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "== update type decides the level (plain semver appVersion) =="
|
||||||
|
bump "major -> chart major" 1.2.3+up1.4.0 1.4.0 1.2.3+up1.4.0 2.0.0 major 2.0.0+up2.0.0
|
||||||
|
bump "minor -> chart minor" 1.2.3+up1.4.0 1.4.0 1.2.3+up1.4.0 1.5.0 minor 1.3.0+up1.5.0
|
||||||
|
bump "patch -> chart patch" 1.2.3+up1.4.0 1.4.0 1.2.3+up1.4.0 1.4.1 patch 1.2.4+up1.4.1
|
||||||
|
bump "digest -> chart patch" 1.2.3+up1.4.0 1.4.0 1.2.3+up1.4.0 1.4.0 digest 1.2.4+up1.4.0
|
||||||
|
bump "pin -> chart patch" 1.2.3+up1.4.0 1.4.0 1.2.3+up1.4.0 1.4.0 pin 1.2.4+up1.4.0
|
||||||
|
bump "pinDigest -> chart patch" 1.2.3+up1.4.0 1.4.0 1.2.3+up1.4.0 1.4.0 pinDigest 1.2.4+up1.4.0
|
||||||
|
bump "minor resets the patch" 1.2.3+up1.4.0 1.4.0 1.2.3+up1.4.0 1.5.0 minor 1.3.0+up1.5.0
|
||||||
|
bump "major resets minor and patch" 4.5.6+up1.4.0 1.4.0 4.5.6+up1.4.0 2.0.0 major 5.0.0+up2.0.0
|
||||||
|
|
||||||
|
echo "== the case that prompted the rule: appVersion never moves (freesailarr) =="
|
||||||
|
# prowlarr 2.5.2 -> 2.6.5 is an image tag in values.yaml; freesailarr's
|
||||||
|
# appVersion is a hand-maintained marker and stays at 1.0.0. An appVersion
|
||||||
|
# comparison sees nothing and would bump the patch.
|
||||||
|
bump "sidecar/bundle minor, appVersion unchanged" 3.1.1+up1.0.0 1.0.0 3.1.1+up1.0.0 1.0.0 minor 3.2.0+up1.0.0
|
||||||
|
bump "sidecar/bundle patch, appVersion unchanged" 3.1.1+up1.0.0 1.0.0 3.1.1+up1.0.0 1.0.0 patch 3.1.2+up1.0.0
|
||||||
|
|
||||||
|
echo "== non-semver app versions stay at patch (all seven on origin/main) =="
|
||||||
|
bump "rss-bridge date tag, month roll" 2.1.1+up2026.09.30 2026-09-30 2.1.1+up2026.09.30 2026-10-05 minor 2.1.2+up2026.10.05
|
||||||
|
bump "homeassistant CalVer" 6.2.0+up2026.8.3 2026.8.3 6.2.0+up2026.8.3 2026.9.0 minor 6.2.1+up2026.9.0
|
||||||
|
bump "searxng CalVer plus hash" 1.1.0+up2026.9.13.3b8009065 2026.9.13-3b8009065 1.1.0+up2026.9.13.3b8009065 2026.10.1-aa12bb34c minor 1.1.1+up2026.10.1.aa12bb34c
|
||||||
|
bump "minecraft-server CalVer plus JRE" 6.0.1+up2026.8.2.java25 2026.8.2-java25 6.0.1+up2026.8.2.java25 2026.9.1-java25 minor 6.0.2+up2026.9.1.java25
|
||||||
|
bump "apache-tika four parts" 10.0.2+up3.3.1.0.full 3.3.1.0-full 10.0.2+up3.3.1.0.full 3.4.0.0-full minor 10.0.3+up3.4.0.0.full
|
||||||
|
bump "teamspeak prerelease line" 5.1.3+up6.0.0.beta13.1 6.0.0-beta13.1 5.1.3+up6.0.0.beta13.1 6.0.0-beta14.1 minor 5.1.4+up6.0.0.beta14.1
|
||||||
|
bump "valkey flavour suffix" 1.0.0+up9.1.2.alpine 9.1.2-alpine 1.0.0+up9.1.2.alpine 9.2.0-alpine minor 1.0.1+up9.2.0.alpine
|
||||||
|
# A major is NOT subject to the fallback - a year rolling over was a chart major
|
||||||
|
# before #128 as well.
|
||||||
|
bump "date tag, year roll, major" 2.1.1+up2026.09.30 2026-09-30 2.1.1+up2026.09.30 2027-01-05 major 3.0.0+up2027.01.05
|
||||||
|
bump "four parts, app major, major" 10.0.2+up3.3.1.0.full 3.3.1.0-full 10.0.2+up3.3.1.0.full 4.0.0.0-full major 11.0.0+up4.0.0.0.full
|
||||||
|
# The year guard is a guard, not a blanket: a v-prefixed plain line still counts.
|
||||||
|
bump "v-prefixed plain semver, minor" 7.2.0+upv1.159.0 v1.159.0 7.2.0+upv1.159.0 v1.160.0 minor 7.3.0+upv1.160.0
|
||||||
|
|
||||||
|
echo "== maximum, not replacement =="
|
||||||
|
# A chart-own bump already in the working tree stands, whatever the app jump is.
|
||||||
|
bump "chart major already on branch, app patch" 1.2.3+up1.4.0 1.4.0 2.0.0+up1.4.0 1.4.1 patch 2.0.0+up1.4.1
|
||||||
|
bump "chart minor already on branch, app patch" 1.2.3+up1.4.0 1.4.0 1.3.0+up1.4.0 1.4.1 patch 1.3.0+up1.4.1
|
||||||
|
# A grouped branch: the hook runs once per update from the same base, so the
|
||||||
|
# minor must survive a patch call that follows it.
|
||||||
|
bump "minor on branch, later patch call" 3.1.1+up1.0.0 1.0.0 3.2.0+up1.0.0 1.0.0 patch 3.2.0+up1.0.0
|
||||||
|
bump "patch on branch, later minor call" 3.1.1+up1.0.0 1.0.0 3.1.2+up1.0.0 1.0.0 minor 3.2.0+up1.0.0
|
||||||
|
bump "repeated minor call is idempotent" 3.1.1+up1.0.0 1.0.0 3.2.0+up1.0.0 1.0.0 minor 3.2.0+up1.0.0
|
||||||
|
bump "repeated major call is idempotent" 1.2.3+up1.4.0 1.4.0 2.0.0+up2.0.0 2.0.0 major 2.0.0+up2.0.0
|
||||||
|
bump "major on branch wins over a later minor" 1.2.3+up1.4.0 1.4.0 2.0.0+up1.4.0 1.5.0 minor 2.0.0+up1.5.0
|
||||||
|
# Stale branch after a rebase: the base moved ahead of what the branch carries,
|
||||||
|
# so the recomputed version wins and nothing is kept.
|
||||||
|
bump "stale branch version is not kept" 3.3.0+up1.0.0 1.0.0 3.1.2+up1.0.0 1.0.0 patch 3.3.1+up1.0.0
|
||||||
|
|
||||||
|
echo "== missing and unknown update type fall back to the pre-#128 behaviour =="
|
||||||
|
bump "no second argument, app major" 1.2.3+up1.4.0 1.4.0 1.2.3+up1.4.0 2.0.0 - 2.0.0+up2.0.0
|
||||||
|
bump "no second argument, app minor" 1.2.3+up1.4.0 1.4.0 1.2.3+up1.4.0 1.5.0 - 1.2.4+up1.5.0
|
||||||
|
bump "no second argument, app patch" 1.2.3+up1.4.0 1.4.0 1.2.3+up1.4.0 1.4.1 - 1.2.4+up1.4.1
|
||||||
|
bump "no second argument, unchanged" 3.1.1+up1.0.0 1.0.0 3.1.1+up1.0.0 1.0.0 - 3.1.2+up1.0.0
|
||||||
|
bump "empty second argument, app major" 1.2.3+up1.4.0 1.4.0 1.2.3+up1.4.0 2.0.0 "" 2.0.0+up2.0.0
|
||||||
|
bump "empty second argument, app minor" 1.2.3+up1.4.0 1.4.0 1.2.3+up1.4.0 1.5.0 "" 1.2.4+up1.5.0
|
||||||
|
bump "unknown type, app minor" 1.2.3+up1.4.0 1.4.0 1.2.3+up1.4.0 1.5.0 replacement 1.2.4+up1.5.0
|
||||||
|
bump "unknown type, app major" 1.2.3+up1.4.0 1.4.0 1.2.3+up1.4.0 2.0.0 lockFileMaintenance 2.0.0+up2.0.0
|
||||||
|
bump "unknown type, maximum still holds" 1.2.3+up1.4.0 1.4.0 2.0.0+up1.4.0 1.4.1 nonsense 2.0.0+up1.4.1
|
||||||
|
|
||||||
|
echo "== charts the script must leave alone, and the hard failures =="
|
||||||
|
bump "unparsable chart semver fails loudly" 1.2+up1.4.0 1.4.0 1.2+up1.4.0 1.5.0 minor 1.2+up1.4.0 1
|
||||||
|
bump "missing appVersion fails loudly" 1.2.3+up1.4.0 1.4.0 1.2.3+up1.4.0 "" minor 1.2.3+up1.4.0 1
|
||||||
|
|
||||||
|
new_chart_repo="$(scenario demo "" "$(chart_yaml demo 0.1.0+up1.0.0 1.0.0)")"
|
||||||
|
rc=0
|
||||||
|
out="$(cd "$new_chart_repo" && "$SCRIPT" demo minor 2>&1)" || rc=$?
|
||||||
|
report "a chart new on the branch keeps its version" \
|
||||||
|
"0.1.0+up1.0.0" "$(version_of "$new_chart_repo/demo/Chart.yaml")" 0 "$rc" "$out"
|
||||||
|
case "$out" in
|
||||||
|
*"new on this branch"*) passed=$((passed + 1)); printf ' ok and says so\n' ;;
|
||||||
|
*) failed=$((failed + 1)); printf ' FAIL new chart: no explanation in the output: %s\n' "$out" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
tpl_repo="$(scenario template-chart "$(chart_yaml template-chart 4.2.0+upxxx xxx)" "$(chart_yaml template-chart 4.2.0+upxxx xxx)")"
|
||||||
|
rc=0
|
||||||
|
out="$(cd "$tpl_repo" && "$SCRIPT" template-chart minor 2>&1)" || rc=$?
|
||||||
|
report "template-chart is never touched" \
|
||||||
|
"4.2.0+upxxx" "$(version_of "$tpl_repo/template-chart/Chart.yaml")" 0 "$rc" "$out"
|
||||||
|
|
||||||
|
# Renovate calls the hook for every update it makes, chart or not.
|
||||||
|
other_repo="$(scenario demo "$(chart_yaml demo 1.2.3+up1.4.0 1.4.0)" "$(chart_yaml demo 1.2.3+up1.4.0 1.4.0)")"
|
||||||
|
for arg in ".github/workflows" "ci/gateway-api-crds.sh" "." "" "not-a-chart"; do
|
||||||
|
rc=0
|
||||||
|
out="$(cd "$other_repo" && "$SCRIPT" "$arg" major 2>&1)" || rc=$?
|
||||||
|
report "non-chart argument '$arg' is a no-op" \
|
||||||
|
"1.2.3+up1.4.0" "$(version_of "$other_repo/demo/Chart.yaml")" 0 "$rc" "$out"
|
||||||
|
done
|
||||||
|
|
||||||
|
rc=0
|
||||||
|
out="$(cd "$other_repo" && "$SCRIPT" 2>&1)" || rc=$?
|
||||||
|
report "no arguments at all is a no-op" \
|
||||||
|
"1.2.3+up1.4.0" "$(version_of "$other_repo/demo/Chart.yaml")" 0 "$rc" "$out"
|
||||||
|
|
||||||
|
rc=0
|
||||||
|
out="$(cd "$other_repo" && "$SCRIPT" demo/ patch 2>&1)" || rc=$?
|
||||||
|
report "a trailing slash is accepted" \
|
||||||
|
"1.2.4+up1.4.0" "$(version_of "$other_repo/demo/Chart.yaml")" 0 "$rc" "$out"
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ "$failed" -gt 0 ]; then
|
||||||
|
printf 'bump-level test: %d passed, %d FAILED\n' "$passed" "$failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf 'bump-level test: %d cases passed\n' "$passed"
|
||||||
+60
-4
@@ -88,8 +88,55 @@
|
|||||||
// line can only come from this hook. Nothing has to be configured for the
|
// 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
|
// 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.
|
// is also not visible from here if it is ever removed.
|
||||||
|
//
|
||||||
|
// THE SECOND ARGUMENT IS THE UPDATE TYPE (Gitea #128), and it is what lets
|
||||||
|
// the chart version follow the app version: major -> major, minor -> MINOR,
|
||||||
|
// patch/digest -> patch. It is passed rather than derived because Renovate
|
||||||
|
// has already classified the jump and the script cannot: `freesailarr` keeps
|
||||||
|
// a hand-maintained `appVersion: "1.0.0"` for ten upstreams, so prowlarr's
|
||||||
|
// 2.5.2 -> 2.6.5 moved an image tag in values.yaml while appVersion stood
|
||||||
|
// still - an appVersion comparison is blind to exactly the case the rule was
|
||||||
|
// written for (README, "The version-bump hook").
|
||||||
|
//
|
||||||
|
// VERIFIED, NOT ASSUMED, and it took two runs because one of them cannot
|
||||||
|
// answer the question. Renovate 44.155.7, keycloak pinned one minor back:
|
||||||
|
//
|
||||||
|
// 1. --platform=local --dry-run=full reads THIS file and classifies the
|
||||||
|
// update ("updateType": "minor", newValue 26.8.1, alongside a separate
|
||||||
|
// "patch" bucket for 26.7.5). What it does NOT do is reach the branch
|
||||||
|
// worker at all - there is not one "Processing branch" line in the log -
|
||||||
|
// so the hook never runs under the local platform and that run can
|
||||||
|
// prove the lookup and the config, never the command.
|
||||||
|
// 2. Renovate's own post-upgrade executor
|
||||||
|
// (dist/workers/repository/update/branch/execute-post-upgrade-commands)
|
||||||
|
// driven against a throwaway checkout with allowedCommands set to the
|
||||||
|
// pattern above:
|
||||||
|
// DEBUG: Executed post-upgrade task
|
||||||
|
// "cmd": "ci/renovate-bump-chart.sh keycloak minor"
|
||||||
|
// "stdout": "renovate-bump: keycloak/Chart.yaml 1.3.0+up26.7.0 ->
|
||||||
|
// 1.4.0+up26.8.1 (update type minor)"
|
||||||
|
// So the field arrives populated, the front-anchored allow-list accepts
|
||||||
|
// the command WITH the argument, and the result is a chart minor.
|
||||||
|
//
|
||||||
|
// `updateType` is in Renovate's template allowedFields (util/template:
|
||||||
|
// "One of digest, pin, rollback, patch, minor, major, replacement,
|
||||||
|
// pinDigest") and the command is compiled per UPDATE here, which is what
|
||||||
|
// executionMode: "update" below means.
|
||||||
|
//
|
||||||
|
// THE RISK THIS ADDS, and it cannot be settled from inside this repo: the
|
||||||
|
// allow-list is matched against the COMPILED command
|
||||||
|
// (`allowedCommands.some((p) => regEx(p).test(compiledCmd))`), so the
|
||||||
|
// front-anchored pattern above still matches with an argument appended. A
|
||||||
|
// pattern anchored at the END on the instance (`...\.sh$`) would stop
|
||||||
|
// matching and the task would be skipped with every chart PR red on the
|
||||||
|
// version check - the same silent failure a missing entry produces. Which
|
||||||
|
// form the instance uses is not visible from here. The script therefore
|
||||||
|
// treats a MISSING second argument as the pre-#128 behaviour instead of an
|
||||||
|
// error, so the worst case is the old classification rather than an outage.
|
||||||
postUpgradeTasks: {
|
postUpgradeTasks: {
|
||||||
commands: ["ci/renovate-bump-chart.sh {{{packageFileDir}}}"],
|
commands: [
|
||||||
|
"ci/renovate-bump-chart.sh {{{packageFileDir}}} {{{updateType}}}",
|
||||||
|
],
|
||||||
fileFilters: ["**/Chart.yaml"],
|
fileFilters: ["**/Chart.yaml"],
|
||||||
// "update" runs the script once per dependency update with the package
|
// "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
|
// file's directory, which is what the script needs to find the chart. The
|
||||||
@@ -169,9 +216,12 @@
|
|||||||
"its size suggests: a subchart is invisible to everything downstream that watches",
|
"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",
|
"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",
|
"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",
|
"reviewed and the check is green, and ci/renovate-bump-chart.sh bumps the chart version",
|
||||||
"that publishes it (appVersion is unchanged, so it takes the patch branch). A major",
|
"that publishes it - since #128 at the SUBCHART'S level, so an oauth2-proxy minor is a",
|
||||||
"goes the same way - it is the review, not a separate policy, that gives it a look.",
|
"chart minor even though our appVersion did not move. That is the point of taking the",
|
||||||
|
"level from Renovate rather than from appVersion: the one number anything downstream",
|
||||||
|
"sees now reports the size of what is inside the package. A major goes the same way -",
|
||||||
|
"it is the review, not a separate policy, that gives it a look.",
|
||||||
],
|
],
|
||||||
matchManagers: ["helmv3"],
|
matchManagers: ["helmv3"],
|
||||||
addLabels: ["subchart"],
|
addLabels: ["subchart"],
|
||||||
@@ -262,6 +312,12 @@
|
|||||||
"publish, one install-test run for the whole stack. Nothing is left to serialize, so",
|
"publish, one install-test run for the whole stack. Nothing is left to serialize, so",
|
||||||
"nothing is left to rebase.",
|
"nothing is left to rebase.",
|
||||||
"",
|
"",
|
||||||
|
"WHAT LEVEL A MIXED GROUP GETS, since #128 made that a question: the hook still runs once",
|
||||||
|
"per update and still recomputes from the same base, so the group takes the HIGHEST level",
|
||||||
|
"in it - three minors among five patches is a chart minor, whatever order the updates are",
|
||||||
|
"processed in. That is the maximum rule in ci/renovate-bump-chart.sh, and this branch is",
|
||||||
|
"the reason it is not optional: without it the last call would win.",
|
||||||
|
"",
|
||||||
"MATCHED BY FILE, NOT BY A LIST OF IMAGE NAMES. What creates the collision is 'touches",
|
"MATCHED BY FILE, NOT BY A LIST OF IMAGE NAMES. What creates the collision is 'touches",
|
||||||
"freesailarr/Chart.yaml', and that is true of every dependency Renovate finds in that",
|
"freesailarr/Chart.yaml', and that is true of every dependency Renovate finds in that",
|
||||||
"chart's values.yaml, whatever its manager, datasource or role in the pod. A",
|
"chart's values.yaml, whatever its manager, datasource or role in the pod. A",
|
||||||
|
|||||||
Reference in new issue
Block a user