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
268 lines
9.9 KiB
Bash
Executable File
268 lines
9.9 KiB
Bash
Executable File
#!/bin/sh
|
|
# Recompute a chart's `version:` after Renovate changed something inside its
|
|
# directory.
|
|
#
|
|
# Renovate rewrites `appVersion:` in Chart.yaml (main image) or an image tag in
|
|
# values.yaml (sidecar/init image). It has no idea about this repo's scheme
|
|
#
|
|
# version: <chart-semver>+up<app-version-with-dashes-normalized-to-dots>
|
|
#
|
|
# and it never touches `version:` at all — which would leave every Renovate PR
|
|
# failing the `build-chart` version-bump check. This script closes that gap and
|
|
# is wired up as a `postUpgradeTasks` command in renovate.json5.
|
|
#
|
|
# Usage: ci/renovate-bump-chart.sh <chart-directory> [update-type]
|
|
#
|
|
# Bump level (README "Versioning", decided in Gitea #128):
|
|
# - update type major -> chart major, minor/patch reset to 0
|
|
# - 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
|
|
# postUpgradeTasks still points at the base branch), never from the working
|
|
# tree's `version:`. That makes repeated runs on the same branch idempotent —
|
|
# Renovate calls the script once per dependency update, and a branch may carry
|
|
# more than one update for the same chart. 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.
|
|
#
|
|
# Tested by ci/run-bump-level-test.sh, which is where the cases live.
|
|
|
|
set -eu
|
|
|
|
dir=${1:-}
|
|
update_type=${2:-}
|
|
[ -n "$dir" ] || exit 0
|
|
|
|
# Strip a trailing slash so "outline/" and "outline" behave the same.
|
|
dir=${dir%/}
|
|
|
|
# Renovate also calls this for .github/workflows and other non-chart updates.
|
|
case "$dir" in
|
|
"" | "." | .github* | ci*) exit 0 ;;
|
|
esac
|
|
|
|
chart="$dir/Chart.yaml"
|
|
[ -f "$chart" ] || exit 0
|
|
|
|
# The scaffold is never published and its version carries no meaning.
|
|
[ "$dir" != "template-chart" ] || exit 0
|
|
|
|
# Reads a top-level scalar field, unquoted and untrimmed of surrounding spaces.
|
|
field() {
|
|
sed -n "s/^$1:[[:space:]]*//p" | head -n 1 | tr -d "\"' "
|
|
}
|
|
|
|
# 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) || {
|
|
echo "renovate-bump: $chart is new on this branch, leaving its version alone"
|
|
exit 0
|
|
}
|
|
|
|
base_version=$(printf '%s\n' "$base" | field version)
|
|
base_app=$(printf '%s\n' "$base" | field appVersion)
|
|
new_app=$(field appVersion <"$chart")
|
|
tree_version=$(field version <"$chart")
|
|
|
|
if [ -z "$base_version" ] || [ -z "$new_app" ]; then
|
|
echo "renovate-bump: $chart has no version/appVersion to work with" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# <chart-semver> is everything before the +up build suffix.
|
|
semver=${base_version%%+*}
|
|
parse_semver "$semver" || {
|
|
echo "renovate-bump: cannot parse chart semver '$semver' in $chart" >&2
|
|
exit 1
|
|
}
|
|
major=$p_major
|
|
minor=$p_minor
|
|
patch=$p_patch
|
|
|
|
# Leading major of an app version: v1.159.0 -> 1, 3.3.1.0-full -> 3,
|
|
# 2025-06-03 -> 2025.
|
|
app_major() {
|
|
printf '%s' "$1" | sed -e 's/^[vV]//' -e 's/[^0-9].*$//'
|
|
}
|
|
|
|
# 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
|
|
echo major
|
|
else
|
|
echo patch
|
|
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
|
|
|
|
# Dashes in the app version become dots in the +up suffix.
|
|
suffix=$(printf '%s' "$new_app" | tr '-' '.')
|
|
new_version="$new_major.$new_minor.$new_patch+up$suffix"
|
|
|
|
sed -i.bak "s|^version:.*|version: $new_version|" "$chart"
|
|
rm -f "$chart.bak"
|
|
|
|
echo "renovate-bump: $chart $base_version -> $new_version ($reason)"
|