Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ jobs:
- name: Checkout
uses: actions/checkout@v6

- name: Check version invariants
run: ./hack/check-versions.sh

- name: Self-test version-invariant check
run: ./hack/test-check-versions.sh

- name: Set up Python
uses: actions/setup-python@v6
with:
Expand Down
11 changes: 10 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
cozystack.installer Release Notes
=================================


Unreleased
==========

Expand Down Expand Up @@ -50,6 +49,16 @@ Ubuntu 26.04 LTS support and namespace adoption.
``cozy-system``) must remove the line. Replace any references in
custom playbooks with the literal ``cozy-system``.

- CI: new ``hack/check-versions.sh`` invariant check runs in the ``Lint``
job and fails the build if version strings drift across the three
tracked dependencies: the ``cozy-installer`` chart version must match
in ``galaxy.yml``, ``roles/cozystack/defaults/main.yml``, and the three
``examples/*/requirements.yml``; the ``k3s_version`` must match across
all four inventory files; the ``k3s.orchestration`` collection version
must match across ``tests/requirements.yml`` and the three
``examples/*/requirements.yml``. A companion ``hack/test-check-versions.sh``
self-test runs alongside in the same job and asserts the drift path
correctly exits nonzero when any single tracked file is perturbed.
- New variable ``cozystack_external_ips`` (list, default ``[]``): external
IP addresses for ingress-nginx Service ``externalIPs``. Required on
``isp-full-generic`` platform variant when nodes lack a native load
Expand Down
123 changes: 123 additions & 0 deletions hack/check-versions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env bash
# Enforce that version strings stay in sync across files that must agree.
# Three independent invariants are checked; any drift fails the run.
#
# 1. cozy-installer chart version:
# - galaxy.yml:version
# - roles/cozystack/defaults/main.yml:cozystack_chart_version
# - examples/{rhel,suse,ubuntu}/requirements.yml: cozystack.installer.version
# (leading "v" normalised away before comparison so formats can vary)
#
# 2. k3s binary version:
# - tests/ci-inventory.yml:k3s_version
# - examples/{rhel,suse,ubuntu}/inventory.yml:k3s_version
#
# 3. k3s.orchestration collection version:
# - tests/requirements.yml: k3s.orchestration.version
# - examples/{rhel,suse,ubuntu}/requirements.yml: k3s.orchestration.version
#
# Requires mikefarah/yq (preinstalled on GitHub-hosted ubuntu runners).

set -euo pipefail
# Propagate failures from command substitutions ($(...)) into the outer
# assignment so a yq extraction error is not silently swallowed into an
# empty value. Requires bash 4.4+; ubuntu-latest and macOS brew-bash both
# qualify.
shopt -s inherit_errexit

if ! command -v yq >/dev/null 2>&1; then
echo "check-versions.sh: yq (mikefarah) is required but was not found on PATH" >&2
exit 2
fi

cd "$(dirname "$0")/.."

get_collection_version() {
local file="$1" name="$2"
NAME="$name" yq --exit-status \
'(.collections[] | select(.name == strenv(NAME)) | .version)' "$file"
}

strip_v() {
printf '%s\n' "${1#v}"
}

# Compare an arbitrary number of (label, value) pairs; returns 0 if all
# values are equal, 1 otherwise. Prints OK/DRIFT report.
report() {
local label="$1"
shift
local -a pairs=("$@")
local first="${pairs[1]}"
if [ -z "$first" ]; then
printf 'DRIFT in %s: reference value is empty (yq extraction failed?)\n' \
"$label" >&2
return 1
fi
local drift=0
local i
for ((i = 1; i < ${#pairs[@]}; i += 2)); do
if [ "${pairs[i]}" != "$first" ]; then
drift=1
break
fi
done
if [ "$drift" -eq 1 ]; then
printf 'DRIFT in %s:\n' "$label" >&2
for ((i = 0; i < ${#pairs[@]}; i += 2)); do
printf ' %-48s = %s\n' "${pairs[i]}" "${pairs[i + 1]}" >&2
done
return 1
fi
printf 'OK %-20s = %s\n' "$label" "$first"
return 0
}

err=0

# 1. cozy-installer — normalise every value with strip_v so future format
# choices (e.g. adding a "v" to galaxy.yml) stay equivalent.
cozy_galaxy=$(strip_v "$(yq --exit-status '.version' galaxy.yml)")
cozy_role=$(strip_v "$(yq --exit-status '.cozystack_chart_version' roles/cozystack/defaults/main.yml)")
cozy_rhel=$(strip_v "$(get_collection_version examples/rhel/requirements.yml cozystack.installer)")
cozy_suse=$(strip_v "$(get_collection_version examples/suse/requirements.yml cozystack.installer)")
cozy_ubuntu=$(strip_v "$(get_collection_version examples/ubuntu/requirements.yml cozystack.installer)")

report "cozy-installer" \
"galaxy.yml:version" "$cozy_galaxy" \
"roles/cozystack/defaults/main.yml:cozystack_chart_version" "$cozy_role" \
"examples/rhel/requirements.yml" "$cozy_rhel" \
"examples/suse/requirements.yml" "$cozy_suse" \
"examples/ubuntu/requirements.yml" "$cozy_ubuntu" \
|| err=1

# 2. k3s binary — no strip_v: every inventory uses the v-prefixed
# k3s_version form (e.g. "v1.35.3+k3s1"), so values are already
# directly comparable. Adding a new inventory without the "v" prefix
# would intentionally fail this check.
k3s_ci=$(yq --exit-status '.cluster.vars.k3s_version' tests/ci-inventory.yml)
k3s_rhel=$(yq --exit-status '.cluster.vars.k3s_version' examples/rhel/inventory.yml)
k3s_suse=$(yq --exit-status '.cluster.vars.k3s_version' examples/suse/inventory.yml)
k3s_ubuntu=$(yq --exit-status '.cluster.vars.k3s_version' examples/ubuntu/inventory.yml)

report "k3s" \
"tests/ci-inventory.yml" "$k3s_ci" \
"examples/rhel/inventory.yml" "$k3s_rhel" \
"examples/suse/inventory.yml" "$k3s_suse" \
"examples/ubuntu/inventory.yml" "$k3s_ubuntu" \
|| err=1

# 3. k3s.orchestration
orch_tests=$(get_collection_version tests/requirements.yml k3s.orchestration)
orch_rhel=$(get_collection_version examples/rhel/requirements.yml k3s.orchestration)
orch_suse=$(get_collection_version examples/suse/requirements.yml k3s.orchestration)
orch_ubuntu=$(get_collection_version examples/ubuntu/requirements.yml k3s.orchestration)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

report "k3s.orchestration" \
"tests/requirements.yml" "$orch_tests" \
"examples/rhel/requirements.yml" "$orch_rhel" \
"examples/suse/requirements.yml" "$orch_suse" \
"examples/ubuntu/requirements.yml" "$orch_ubuntu" \
|| err=1

exit "$err"
81 changes: 81 additions & 0 deletions hack/test-check-versions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Tests for hack/check-versions.sh.
#
# Copies the repo tree to a tmpdir, runs the script once against the clean
# tree (expect exit 0), then for each invariant perturbs a single file and
# asserts exit 1 with the expected "DRIFT in <group>" label on stderr.
# Ensures a future refactor of check-versions.sh does not silently stop
# detecting drift — it must always detect and exit nonzero.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"

tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT

rsync --archive --exclude='.git' --exclude='*.bak' "$REPO_ROOT"/ "$tmpdir"/

fail() {
echo "TEST FAIL: $*" >&2
exit 1
}

# Positive: clean tree reports no drift.
echo "-- positive: clean tree must exit 0 --"
if ! (cd "$tmpdir" && ./hack/check-versions.sh) >/dev/null; then
fail "clean tree unexpectedly reported drift"
fi

# Negative helper: mutate a single field in a copy of the repo, run the
# script, expect nonzero exit and the right DRIFT label on stderr.
run_negative() {
local label="$1" file="$2" yq_expr="$3" new_value="$4"
echo "-- negative: perturb ${file} (${label}) --"
cp "$tmpdir/$file" "$tmpdir/$file.bak"
NEW="$new_value" yq --inplace "$yq_expr" "$tmpdir/$file"
local stderr_file="$tmpdir/stderr.log"
local rc=0
(cd "$tmpdir" && ./hack/check-versions.sh) 2>"$stderr_file" >/dev/null || rc=$?
mv "$tmpdir/$file.bak" "$tmpdir/$file"
if [ "$rc" -eq 0 ]; then
cat "$stderr_file" >&2
fail "perturbed ${file} but script returned 0 (expected nonzero)"
fi
if ! grep --quiet "DRIFT in ${label}" "$stderr_file"; then
cat "$stderr_file" >&2
fail "perturbed ${file} but stderr did not contain 'DRIFT in ${label}'"
fi
}

run_negative "cozy-installer" "galaxy.yml" \
'.version = strenv(NEW)' "0.0.0-test"

run_negative "k3s" "tests/ci-inventory.yml" \
'.cluster.vars.k3s_version = strenv(NEW)' "v0.0.0-test"

# Perturb a middle entry (not the first one paired to report()) to guard
# against a future refactor of report()'s "reference value" logic silently
# missing drift in anything but the reference file.
run_negative "k3s" "examples/suse/inventory.yml" \
'.cluster.vars.k3s_version = strenv(NEW)' "v0.0.0-test"

run_negative "k3s.orchestration" "tests/requirements.yml" \
'(.collections[] | select(.name == "k3s.orchestration") | .version) = strenv(NEW)' "0.0.0-test"

# Silent-failure guard: if a tracked key is deleted (yq extraction yields
# an error / empty), the script must still exit nonzero instead of
# reporting OK on an empty string. Covers the inherit_errexit and the
# empty-first guard in report().
echo "-- negative: delete galaxy.yml:version key (must not silently report OK) --"
cp "$tmpdir/galaxy.yml" "$tmpdir/galaxy.yml.bak"
yq --inplace 'del(.version)' "$tmpdir/galaxy.yml"
rc=0
(cd "$tmpdir" && ./hack/check-versions.sh) >/dev/null 2>&1 || rc=$?
mv "$tmpdir/galaxy.yml.bak" "$tmpdir/galaxy.yml"
if [ "$rc" -eq 0 ]; then
fail "deleted galaxy.yml:version but script returned 0"
fi

echo "OK: all check-versions tests passed"