Skip to content

Commit 752d40b

Browse files
authored
ci: enforce version sync invariant (#34)
* ci: enforce version sync invariant across dependency groups Add hack/check-versions.sh and wire it into the Lint job. The script fails the build if any of three tracked dependency groups drifts across its authoritative files: - cozy-installer chart version: galaxy.yml, defaults/main.yml, and the three examples/*/requirements.yml (compared without the leading "v") - k3s binary version: ci-inventory.yml and all examples/*/inventory.yml - k3s.orchestration collection version: tests/requirements.yml and all examples/*/requirements.yml Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> * ci: address review feedback on version-sync invariant - Fix stale cozystack_chart_version default in README (1.2.2 -> 1.3.0) - Move CHANGELOG Unreleased section to the top (reverse-chronological) - check-versions.sh: guard missing yq, normalise strip_v across all cozy-installer values, return status from report() instead of mutating a global so calls from subshells stay correct - Add hack/test-check-versions.sh self-test covering the positive path and one negative per invariant; wire into the Lint job Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> * ci: tighten version-invariant coverage after second review - Add a middle-file perturbation (examples/suse/inventory.yml) to test-check-versions.sh so the self-test does not implicitly rely on report() always comparing every value against pairs[1] - Document why the k3s group does not use strip_v (every inventory already uses the v-prefixed form and adding one without it should fail on purpose) Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> * ci(versions): use full variable name in cozy-installer report label Address review feedback from gemini-code-assist on hack/check-versions.sh: use the full `cozystack_chart_version` name (matching the actual key in roles/cozystack/defaults/main.yml) instead of the abbreviated `chart_version` so a DRIFT report points directly at the variable to inspect. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> * ci(versions): guard against silent empty yq extractions Address review feedback from coderabbitai on hack/check-versions.sh: without `inherit_errexit`, a failing yq in $(...) would silently become an empty string and pass every equality check, so drift could go undetected if a tracked key were renamed. - Enable `shopt -s inherit_errexit` so command-substitution failures propagate into outer assignments under `set -e`. - Add a defence-in-depth empty-value guard inside report() that fails loudly when the reference value is empty. - Extend test-check-versions.sh with a case that deletes `galaxy.yml:version` and asserts the script exits nonzero instead of reporting OK on an empty string. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> --------- Signed-off-by: Aleksei Sviridkin <f@lex.la>
1 parent d068844 commit 752d40b

4 files changed

Lines changed: 226 additions & 4 deletions

File tree

.github/workflows/test.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ jobs:
1515
- name: Checkout
1616
uses: actions/checkout@v6
1717

18+
- name: Check version invariants
19+
run: ./hack/check-versions.sh
20+
21+
- name: Self-test version-invariant check
22+
run: ./hack/test-check-versions.sh
23+
1824
- name: Set up Python
1925
uses: actions/setup-python@v6
2026
with:

CHANGELOG.rst

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22
cozystack.installer Release Notes
33
=================================
44

5+
Unreleased
6+
7+
- CI: new ``hack/check-versions.sh`` invariant check runs in the ``Lint``
8+
job and fails the build if version strings drift across the three
9+
tracked dependencies: the ``cozy-installer`` chart version must match
10+
in ``galaxy.yml``, ``roles/cozystack/defaults/main.yml``, and the three
11+
``examples/*/requirements.yml``; the ``k3s_version`` must match across
12+
all four inventory files; the ``k3s.orchestration`` collection version
13+
must match across ``tests/requirements.yml`` and the three
14+
``examples/*/requirements.yml``. A companion ``hack/test-check-versions.sh``
15+
self-test runs alongside in the same job and asserts the drift path
16+
correctly exits nonzero when any single tracked file is perturbed.
17+
- New variable ``cozystack_external_ips`` (list, default ``[]``): external
18+
IP addresses for ingress-nginx Service ``externalIPs``. Required on
19+
``isp-full-generic`` platform variant when nodes lack a native load
20+
balancer (cloud VMs, bare metal).
521

622
v1.4.0
723
======
@@ -147,10 +163,6 @@ Ubuntu 26.04 LTS support and namespace adoption.
147163
``cozy-system``) must remove the line. Replace any references in
148164
custom playbooks with the literal ``cozy-system``.
149165

150-
- New variable ``cozystack_external_ips`` (list, default ``[]``): external
151-
IP addresses for ingress-nginx Service ``externalIPs``. Required on
152-
``isp-full-generic`` platform variant when nodes lack a native load
153-
balancer (cloud VMs, bare metal).
154166
- New variable ``cozystack_tenant_root_ingress`` (bool, default ``false``):
155167
when enabled, patches the root Tenant CR to set ``spec.ingress: true``
156168
after Platform Package apply, creating the ``tenant-root`` IngressClass

hack/check-versions.sh

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#!/usr/bin/env bash
2+
# Enforce that version strings stay in sync across files that must agree.
3+
# Three independent invariants are checked; any drift fails the run.
4+
#
5+
# 1. cozy-installer chart version:
6+
# - galaxy.yml:version
7+
# - roles/cozystack/defaults/main.yml:cozystack_chart_version
8+
# - examples/{rhel,suse,ubuntu}/requirements.yml: cozystack.installer.version
9+
# (leading "v" normalised away before comparison so formats can vary)
10+
#
11+
# 2. k3s binary version:
12+
# - tests/ci-inventory.yml:k3s_version
13+
# - examples/{rhel,suse,ubuntu}/inventory.yml:k3s_version
14+
#
15+
# 3. k3s.orchestration collection version:
16+
# - tests/requirements.yml: k3s.orchestration.version
17+
# - examples/{rhel,suse,ubuntu}/requirements.yml: k3s.orchestration.version
18+
#
19+
# Requires mikefarah/yq (preinstalled on GitHub-hosted ubuntu runners).
20+
21+
set -euo pipefail
22+
# Propagate failures from command substitutions ($(...)) into the outer
23+
# assignment so a yq extraction error is not silently swallowed into an
24+
# empty value. Requires bash 4.4+; ubuntu-latest and macOS brew-bash both
25+
# qualify.
26+
shopt -s inherit_errexit
27+
28+
if ! command -v yq >/dev/null 2>&1; then
29+
echo "check-versions.sh: yq (mikefarah) is required but was not found on PATH" >&2
30+
exit 2
31+
fi
32+
33+
cd "$(dirname "$0")/.."
34+
35+
get_collection_version() {
36+
local file="$1" name="$2"
37+
NAME="$name" yq --exit-status \
38+
'(.collections[] | select(.name == strenv(NAME)) | .version)' "$file"
39+
}
40+
41+
strip_v() {
42+
printf '%s\n' "${1#v}"
43+
}
44+
45+
# Compare an arbitrary number of (label, value) pairs; returns 0 if all
46+
# values are equal, 1 otherwise. Prints OK/DRIFT report.
47+
report() {
48+
local label="$1"
49+
shift
50+
local -a pairs=("$@")
51+
local first="${pairs[1]}"
52+
if [ -z "$first" ]; then
53+
printf 'DRIFT in %s: reference value is empty (yq extraction failed?)\n' \
54+
"$label" >&2
55+
return 1
56+
fi
57+
local drift=0
58+
local i
59+
for ((i = 1; i < ${#pairs[@]}; i += 2)); do
60+
if [ "${pairs[i]}" != "$first" ]; then
61+
drift=1
62+
break
63+
fi
64+
done
65+
if [ "$drift" -eq 1 ]; then
66+
printf 'DRIFT in %s:\n' "$label" >&2
67+
for ((i = 0; i < ${#pairs[@]}; i += 2)); do
68+
printf ' %-48s = %s\n' "${pairs[i]}" "${pairs[i + 1]}" >&2
69+
done
70+
return 1
71+
fi
72+
printf 'OK %-20s = %s\n' "$label" "$first"
73+
return 0
74+
}
75+
76+
err=0
77+
78+
# 1. cozy-installer — normalise every value with strip_v so future format
79+
# choices (e.g. adding a "v" to galaxy.yml) stay equivalent.
80+
cozy_galaxy=$(strip_v "$(yq --exit-status '.version' galaxy.yml)")
81+
cozy_role=$(strip_v "$(yq --exit-status '.cozystack_chart_version' roles/cozystack/defaults/main.yml)")
82+
cozy_rhel=$(strip_v "$(get_collection_version examples/rhel/requirements.yml cozystack.installer)")
83+
cozy_suse=$(strip_v "$(get_collection_version examples/suse/requirements.yml cozystack.installer)")
84+
cozy_ubuntu=$(strip_v "$(get_collection_version examples/ubuntu/requirements.yml cozystack.installer)")
85+
86+
report "cozy-installer" \
87+
"galaxy.yml:version" "$cozy_galaxy" \
88+
"roles/cozystack/defaults/main.yml:cozystack_chart_version" "$cozy_role" \
89+
"examples/rhel/requirements.yml" "$cozy_rhel" \
90+
"examples/suse/requirements.yml" "$cozy_suse" \
91+
"examples/ubuntu/requirements.yml" "$cozy_ubuntu" \
92+
|| err=1
93+
94+
# 2. k3s binary — no strip_v: every inventory uses the v-prefixed
95+
# k3s_version form (e.g. "v1.35.3+k3s1"), so values are already
96+
# directly comparable. Adding a new inventory without the "v" prefix
97+
# would intentionally fail this check.
98+
k3s_ci=$(yq --exit-status '.cluster.vars.k3s_version' tests/ci-inventory.yml)
99+
k3s_rhel=$(yq --exit-status '.cluster.vars.k3s_version' examples/rhel/inventory.yml)
100+
k3s_suse=$(yq --exit-status '.cluster.vars.k3s_version' examples/suse/inventory.yml)
101+
k3s_ubuntu=$(yq --exit-status '.cluster.vars.k3s_version' examples/ubuntu/inventory.yml)
102+
103+
report "k3s" \
104+
"tests/ci-inventory.yml" "$k3s_ci" \
105+
"examples/rhel/inventory.yml" "$k3s_rhel" \
106+
"examples/suse/inventory.yml" "$k3s_suse" \
107+
"examples/ubuntu/inventory.yml" "$k3s_ubuntu" \
108+
|| err=1
109+
110+
# 3. k3s.orchestration
111+
orch_tests=$(get_collection_version tests/requirements.yml k3s.orchestration)
112+
orch_rhel=$(get_collection_version examples/rhel/requirements.yml k3s.orchestration)
113+
orch_suse=$(get_collection_version examples/suse/requirements.yml k3s.orchestration)
114+
orch_ubuntu=$(get_collection_version examples/ubuntu/requirements.yml k3s.orchestration)
115+
116+
report "k3s.orchestration" \
117+
"tests/requirements.yml" "$orch_tests" \
118+
"examples/rhel/requirements.yml" "$orch_rhel" \
119+
"examples/suse/requirements.yml" "$orch_suse" \
120+
"examples/ubuntu/requirements.yml" "$orch_ubuntu" \
121+
|| err=1
122+
123+
exit "$err"

hack/test-check-versions.sh

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#!/usr/bin/env bash
2+
# Tests for hack/check-versions.sh.
3+
#
4+
# Copies the repo tree to a tmpdir, runs the script once against the clean
5+
# tree (expect exit 0), then for each invariant perturbs a single file and
6+
# asserts exit 1 with the expected "DRIFT in <group>" label on stderr.
7+
# Ensures a future refactor of check-versions.sh does not silently stop
8+
# detecting drift — it must always detect and exit nonzero.
9+
10+
set -euo pipefail
11+
12+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
14+
15+
tmpdir=$(mktemp -d)
16+
trap 'rm -rf "$tmpdir"' EXIT
17+
18+
rsync --archive --exclude='.git' --exclude='*.bak' "$REPO_ROOT"/ "$tmpdir"/
19+
20+
fail() {
21+
echo "TEST FAIL: $*" >&2
22+
exit 1
23+
}
24+
25+
# Positive: clean tree reports no drift.
26+
echo "-- positive: clean tree must exit 0 --"
27+
if ! (cd "$tmpdir" && ./hack/check-versions.sh) >/dev/null; then
28+
fail "clean tree unexpectedly reported drift"
29+
fi
30+
31+
# Negative helper: mutate a single field in a copy of the repo, run the
32+
# script, expect nonzero exit and the right DRIFT label on stderr.
33+
run_negative() {
34+
local label="$1" file="$2" yq_expr="$3" new_value="$4"
35+
echo "-- negative: perturb ${file} (${label}) --"
36+
cp "$tmpdir/$file" "$tmpdir/$file.bak"
37+
NEW="$new_value" yq --inplace "$yq_expr" "$tmpdir/$file"
38+
local stderr_file="$tmpdir/stderr.log"
39+
local rc=0
40+
(cd "$tmpdir" && ./hack/check-versions.sh) 2>"$stderr_file" >/dev/null || rc=$?
41+
mv "$tmpdir/$file.bak" "$tmpdir/$file"
42+
if [ "$rc" -eq 0 ]; then
43+
cat "$stderr_file" >&2
44+
fail "perturbed ${file} but script returned 0 (expected nonzero)"
45+
fi
46+
if ! grep --quiet "DRIFT in ${label}" "$stderr_file"; then
47+
cat "$stderr_file" >&2
48+
fail "perturbed ${file} but stderr did not contain 'DRIFT in ${label}'"
49+
fi
50+
}
51+
52+
run_negative "cozy-installer" "galaxy.yml" \
53+
'.version = strenv(NEW)' "0.0.0-test"
54+
55+
run_negative "k3s" "tests/ci-inventory.yml" \
56+
'.cluster.vars.k3s_version = strenv(NEW)' "v0.0.0-test"
57+
58+
# Perturb a middle entry (not the first one paired to report()) to guard
59+
# against a future refactor of report()'s "reference value" logic silently
60+
# missing drift in anything but the reference file.
61+
run_negative "k3s" "examples/suse/inventory.yml" \
62+
'.cluster.vars.k3s_version = strenv(NEW)' "v0.0.0-test"
63+
64+
run_negative "k3s.orchestration" "tests/requirements.yml" \
65+
'(.collections[] | select(.name == "k3s.orchestration") | .version) = strenv(NEW)' "0.0.0-test"
66+
67+
# Silent-failure guard: if a tracked key is deleted (yq extraction yields
68+
# an error / empty), the script must still exit nonzero instead of
69+
# reporting OK on an empty string. Covers the inherit_errexit and the
70+
# empty-first guard in report().
71+
echo "-- negative: delete galaxy.yml:version key (must not silently report OK) --"
72+
cp "$tmpdir/galaxy.yml" "$tmpdir/galaxy.yml.bak"
73+
yq --inplace 'del(.version)' "$tmpdir/galaxy.yml"
74+
rc=0
75+
(cd "$tmpdir" && ./hack/check-versions.sh) >/dev/null 2>&1 || rc=$?
76+
mv "$tmpdir/galaxy.yml.bak" "$tmpdir/galaxy.yml"
77+
if [ "$rc" -eq 0 ]; then
78+
fail "deleted galaxy.yml:version but script returned 0"
79+
fi
80+
81+
echo "OK: all check-versions tests passed"

0 commit comments

Comments
 (0)