From 77e4211afe914a12b7bda99049a48d75e86a234c Mon Sep 17 00:00:00 2001 From: mrbobbytables Date: Sun, 13 Sep 2026 23:22:59 +0000 Subject: [PATCH 1/3] refactor(arch): split release-version into installer and Flatcar axes Split the single release-version axis in project.conf into two independent axes: - installer-version: FSDK point release describing the installer - flatcar-version: Flatcar LTS release describing the OS payload Update .github/scripts/check-release-version.py to independently validate both axes against their respective pins (freedesktop-sdk.bst junction ref for FSDK, include/flatcar.yml for Flatcar). Update unit tests in tests/unit/test_release_version.py to cover the two-axis contract, and update element asset naming and os-release metadata. Closes #125 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: mrbobbytables --- .github/scripts/check-release-version.py | 108 ++++++--- .pre-commit-config.yaml | 4 +- .../bluefin-server/os-release-flatcar.bst | 8 +- elements/oci/bluefin-server-ddi.bst | 2 +- elements/oci/bluefin-server-installer.bst | 7 +- project.conf | 11 +- tests/unit/test_release_version.py | 223 ++++++++++++++---- 7 files changed, 268 insertions(+), 95 deletions(-) diff --git a/.github/scripts/check-release-version.py b/.github/scripts/check-release-version.py index dd9ed25..c6c5b48 100755 --- a/.github/scripts/check-release-version.py +++ b/.github/scripts/check-release-version.py @@ -1,29 +1,29 @@ #!/usr/bin/env python3 """Enforce the release-version invariant for Bluefin Server. -project.conf declares: +project.conf declares the two release version axes: variables: - release-version: "X.Y.Z" # must match the FSDK point release + installer-version: "X.Y.Z" # must match the FSDK point release + flatcar-version: "X.Y.Z" # must match the Flatcar LTS release -That value names every published *OS* release asset -(`bluefin-server-ddi-.raw.zst`, `bluefin-server-.efi`, -`bluefin-server-installer-.raw.zst`) and is the version +The installer-version axis names the offline installer disk image and PXE boot +inputs (bluefin-server-installer-.raw.zst, bluefin-server-pxe-*), which +compose their userspace from freedesktop-sdk. + +The flatcar-version axis names the OS payload release assets +(bluefin-server-ddi-.raw.zst, bluefin-server-.efi) and is the version systemd-sysupdate extracts from those filenames via `@v`. -The k0s sysext is deliberately *not* on this axis: it is an -independently-pinned third-party payload versioned from `include/k0s.yml` -and enforced separately by `.github/scripts/check-k0s-version.py`. +The k0s sysext is on its own axis: an independently-pinned third-party +payload versioned from `include/k0s.yml` and enforced separately by +`.github/scripts/check-k0s-version.py`. -The release *tag* is derived independently by the Justfile -(`fsdk_version`), which greps the point release out of the pinned -`elements/freedesktop-sdk.bst` junction ref. Renovate bumps that ref -automatically; nothing bumps `release-version`. When the two drift, CI -publishes a new tag containing assets that still carry the old version -string, so `systemd-sysupdate` sees no version change and the fleet -silently stops updating. +This script validates both axes independently against their pins: + * installer-version against elements/freedesktop-sdk.bst + * flatcar-version against include/flatcar.yml -This script fails closed on that drift. +This script fails closed on any drift. """ import re @@ -33,11 +33,18 @@ ROOT = Path(__file__).resolve().parents[2] PROJECT_CONF = ROOT / "project.conf" FSDK_JUNCTION = ROOT / "elements" / "freedesktop-sdk.bst" +FLATCAR_PIN = ROOT / "include" / "flatcar.yml" -RELEASE_VERSION_RE = re.compile( - r"^\s*release-version:\s*[\"']?([0-9]+\.[0-9]+\.[0-9]+)[\"']?\s*$", re.MULTILINE +INSTALLER_VERSION_RE = re.compile( + r"^\s*installer-version:\s*[\"']?([0-9]+\.[0-9]+\.[0-9]+)[\"']?\s*$", re.MULTILINE +) +FLATCAR_VERSION_RE = re.compile( + r"^\s*flatcar-version:\s*[\"']?([0-9]+\.[0-9]+\.[0-9]+)[\"']?\s*$", re.MULTILINE ) FSDK_REF_RE = re.compile(r"freedesktop-sdk-([0-9]+\.[0-9]+\.[0-9]+)") +FLATCAR_PIN_RE = re.compile( + r"^\s*flatcar-version:\s*[\"']?([0-9]+\.[0-9]+\.[0-9]+)[\"']?\s*$", re.MULTILINE +) def read(path): @@ -49,39 +56,68 @@ def read(path): def main(): conf = read(PROJECT_CONF) junction = read(FSDK_JUNCTION) + flatcar = read(FLATCAR_PIN) - conf_match = RELEASE_VERSION_RE.search(conf) - if not conf_match: + installer_match = INSTALLER_VERSION_RE.search(conf) + if not installer_match: + sys.exit( + "ERROR: project.conf does not declare an " + "'installer-version: X.Y.Z' variable." + ) + installer_declared = installer_match.group(1) + + flatcar_match = FLATCAR_VERSION_RE.search(conf) + if not flatcar_match: sys.exit( "ERROR: project.conf does not declare a " - "'release-version: X.Y.Z' variable." + "'flatcar-version: X.Y.Z' variable." ) - declared = conf_match.group(1) + flatcar_declared = flatcar_match.group(1) - ref_match = FSDK_REF_RE.search(junction) - if not ref_match: + fsdk_match = FSDK_REF_RE.search(junction) + if not fsdk_match: sys.exit( "ERROR: elements/freedesktop-sdk.bst has no " "'freedesktop-sdk-X.Y.Z' point release in its ref." ) - pinned = ref_match.group(1) + fsdk_pinned = fsdk_match.group(1) + + flatcar_pin_match = FLATCAR_PIN_RE.search(flatcar) + if not flatcar_pin_match: + sys.exit( + "ERROR: include/flatcar.yml does not declare a " + "'flatcar-version: X.Y.Z' variable." + ) + flatcar_pinned = flatcar_pin_match.group(1) + + if installer_declared != fsdk_pinned: + sys.exit( + "ERROR: installer-version drift.\n" + f" project.conf installer-version : {installer_declared}\n" + f" elements/freedesktop-sdk.bst pinned ref: {fsdk_pinned}\n" + "\n" + "The installer release tag and installer assets are derived from the\n" + "junction ref while project.conf declares installer-version.\n" + "\n" + f"Fix: set installer-version to \"{fsdk_pinned}\" in project.conf." + ) - if declared != pinned: + if flatcar_declared != flatcar_pinned: sys.exit( - "ERROR: release-version drift.\n" - f" project.conf release-version : {declared}\n" - f" elements/freedesktop-sdk.bst pinned ref: {pinned}\n" + "ERROR: flatcar-version drift.\n" + f" project.conf flatcar-version : {flatcar_declared}\n" + f" include/flatcar.yml pinned release : {flatcar_pinned}\n" "\n" - "The release tag is derived from the junction ref while asset\n" - "filenames are derived from release-version. While these differ,\n" - "a new GitHub Release publishes assets still named with the old\n" - "version, systemd-sysupdate reads the old version from '@v', and\n" - "deployed hosts never see an update.\n" + "The OS payload version and systemd-sysupdate assets are derived from\n" + "the Flatcar LTS pin while project.conf declares flatcar-version.\n" "\n" - f"Fix: set release-version to \"{pinned}\" in project.conf." + f"Fix: set flatcar-version to \"{flatcar_pinned}\" in project.conf." ) - print(f"OK: release-version {declared} matches the pinned FSDK point release.") + print( + f"OK: installer-version {installer_declared} matches pinned FSDK point release.\n" + f"OK: flatcar-version {flatcar_declared} matches pinned Flatcar release." + ) if __name__ == "__main__": diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 95cf310..3f992c9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,11 +13,11 @@ repos: language: system files: ^\.github/workflows/.*\.(yml|yaml)$ - id: check-release-version - name: release-version matches pinned FSDK point release + name: release versions match pinned FSDK and Flatcar releases entry: python .github/scripts/check-release-version.py language: system pass_filenames: false - files: ^(project\.conf|elements/freedesktop-sdk\.bst|\.github/scripts/check-release-version\.py)$ + files: ^(project\.conf|elements/freedesktop-sdk\.bst|include/flatcar\.yml|\.github/scripts/check-release-version\.py)$ - id: check-k0s-version name: k0s version derived from include/k0s.yml, not restated entry: python .github/scripts/check-k0s-version.py diff --git a/elements/bluefin-server/os-release-flatcar.bst b/elements/bluefin-server/os-release-flatcar.bst index e21ed19..9566b2a 100644 --- a/elements/bluefin-server/os-release-flatcar.bst +++ b/elements/bluefin-server/os-release-flatcar.bst @@ -18,8 +18,8 @@ config: install-commands: - mkdir -p "%{install-root}/usr/lib" - | - # %{release-version} is declared in project.conf and enforced against - # the junction ref by check-release-version.py — no re-parsing needed. + # %{flatcar-version} is declared in project.conf and enforced against + # include/flatcar.yml by check-release-version.py — no re-parsing needed. FLATCAR_VERSION="%{flatcar-version}" cat < "%{install-root}/usr/lib/os-release" @@ -28,13 +28,13 @@ config: ID_LIKE=fedora VERSION="${FLATCAR_VERSION}-fsdk" VERSION_ID=${FLATCAR_VERSION} - PRETTY_NAME="Bluefin Server %{release-version}" + PRETTY_NAME="Bluefin Server %{flatcar-version}" ANSI_COLOR="0;38;5;208" HOME_URL="https://github.com/projectbluefin/server" BUG_REPORT_URL="https://github.com/projectbluefin/server/issues" FLATCAR_BOARD="%{flatcar-board}" CPE_NAME="cpe:/o:flatcar-linux:flatcar_linux:${FLATCAR_VERSION}" - IMAGE_VERSION=%{release-version} + IMAGE_VERSION=%{flatcar-version} EOF - | mkdir -p "%{install-root}/etc" diff --git a/elements/oci/bluefin-server-ddi.bst b/elements/oci/bluefin-server-ddi.bst index 9f80157..3b7ecf4 100644 --- a/elements/oci/bluefin-server-ddi.bst +++ b/elements/oci/bluefin-server-ddi.bst @@ -19,7 +19,7 @@ build-depends: location: /layer variables: - ddi-version: "%{release-version}" + ddi-version: "%{flatcar-version}" config: commands: diff --git a/elements/oci/bluefin-server-installer.bst b/elements/oci/bluefin-server-installer.bst index 44d9f30..8321051 100644 --- a/elements/oci/bluefin-server-installer.bst +++ b/elements/oci/bluefin-server-installer.bst @@ -62,9 +62,6 @@ build-depends: config: location: /k0s -variables: - installer-version: "%{release-version}" - config: commands: # ── Step 1a: Prepare rootfs directories and mount points ──────────── @@ -443,7 +440,7 @@ config: zstd --rm -T0 -19 -q "${FNAME}" -o "${FNAME}.zst" # Also export the target UKI (.efi) for GitHub Releases and systemd-sysupdate - cp /layer/usr/lib/bluefin-server/bluefin-server.efi "bluefin-server-%{installer-version}.efi" + cp /layer/usr/lib/bluefin-server/bluefin-server.efi "bluefin-server-%{flatcar-version}.efi" # Export raw PXE inputs alongside the UKI. PXE users supply their own # kernel command line, so these remain separate from the fixed-cmdline UKI. INSTALLER_KVER="$(basename "$(find /layer/usr/lib/modules -mindepth 1 -maxdepth 1 -type d | head -n1)")" @@ -451,7 +448,7 @@ config: cp /installer.cpio.gz "bluefin-server-pxe-initrd-%{installer-version}.cpio.gz" sha256sum --binary "${FNAME}.zst" > SHA256SUMS - sha256sum --binary "bluefin-server-%{installer-version}.efi" >> SHA256SUMS + sha256sum --binary "bluefin-server-%{flatcar-version}.efi" >> SHA256SUMS sha256sum --binary "bluefin-server-pxe-vmlinuz-%{installer-version}" >> SHA256SUMS sha256sum --binary "bluefin-server-pxe-initrd-%{installer-version}.cpio.gz" >> SHA256SUMS ls -lh diff --git a/project.conf b/project.conf index f369f67..e44d7a3 100644 --- a/project.conf +++ b/project.conf @@ -23,10 +23,15 @@ sandbox: build-arch: "%{arch}" variables: - # Single source of truth for asset versioning; must match the FSDK point - # release pinned in elements/freedesktop-sdk.bst. Enforced by + # Installer axis (FSDK): must match the point release pinned in + # elements/freedesktop-sdk.bst. Enforced by # .github/scripts/check-release-version.py. - release-version: "26.08.0" + installer-version: "26.08.0" + + # OS payload axis (Flatcar): must match the Flatcar LTS release pinned in + # include/flatcar.yml. Enforced by + # .github/scripts/check-release-version.py. + flatcar-version: "4593.2.5" # Pull-only: read from the shared GNOME + Bluefin BuildStream CAS caches. artifacts: diff --git a/tests/unit/test_release_version.py b/tests/unit/test_release_version.py index 4f61fc4..ee633ae 100644 --- a/tests/unit/test_release_version.py +++ b/tests/unit/test_release_version.py @@ -1,10 +1,13 @@ """Unit coverage for .github/scripts/check-release-version.py. -The script is the only gate protecting against release-version drift between -``project.conf`` and the pinned ``elements/freedesktop-sdk.bst`` junction ref. -When it regresses, CI publishes release assets carrying a stale version string -and systemd-sysupdate stops seeing updates, so every branch of ``read()``, -``main()`` and both module-level regexes is exercised here. +The script is the gate protecting against version drift between ``project.conf`` +and the two release axes: the installer axis pinned in +``elements/freedesktop-sdk.bst`` and the Flatcar OS payload axis pinned in +``include/flatcar.yml``. + +When either regresses, CI publishes release assets carrying stale version +strings and systemd-sysupdate or operators see version mismatches, so every +branch of ``read()``, ``main()`` and all module-level regexes is exercised here. """ import importlib.util @@ -29,25 +32,43 @@ def _load_module(): def checker(tmp_path): """Fresh module instance with its path constants rooted at tmp_path. - ``ROOT``/``PROJECT_CONF``/``FSDK_JUNCTION`` are module-level globals derived - from the script's own location, so each test gets a re-imported copy pointed - at an isolated tree. + ``ROOT``/``PROJECT_CONF``/``FSDK_JUNCTION``/``FLATCAR_PIN`` are module-level + globals derived from the script's own location, so each test gets a + re-imported copy pointed at an isolated tree. """ module = _load_module() module.ROOT = tmp_path module.PROJECT_CONF = tmp_path / "project.conf" module.FSDK_JUNCTION = tmp_path / "elements" / "freedesktop-sdk.bst" - module.FSDK_JUNCTION.parent.mkdir(parents=True) + module.FLATCAR_PIN = tmp_path / "include" / "flatcar.yml" + module.FSDK_JUNCTION.parent.mkdir(parents=True, exist_ok=True) + module.FLATCAR_PIN.parent.mkdir(parents=True, exist_ok=True) yield module sys.modules.pop("check_release_version", None) -def _write(checker, declared="26.08.0", pinned="26.08.0"): +def _write( + checker, + installer_declared="26.08.0", + fsdk_pinned="26.08.0", + flatcar_declared="4593.2.5", + flatcar_pinned="4593.2.5", +): checker.PROJECT_CONF.write_text( - f'variables:\n release-version: "{declared}"\n', encoding="utf-8" + "variables:\n" + f' installer-version: "{installer_declared}"\n' + f' flatcar-version: "{flatcar_declared}"\n', + encoding="utf-8", ) checker.FSDK_JUNCTION.write_text( - f"junction:\n ref: freedesktop-sdk-{pinned}-0-gdb97cce\n", encoding="utf-8" + f"junction:\n ref: freedesktop-sdk-{fsdk_pinned}-0-gdb97cce\n", + encoding="utf-8", + ) + checker.FLATCAR_PIN.write_text( + "variables:\n" + f' flatcar-version: "{flatcar_pinned}"\n' + ' flatcar-kver: "6.12.102-flatcar"\n', + encoding="utf-8", ) @@ -73,22 +94,22 @@ def test_read_exits_when_path_is_a_directory(checker, tmp_path): assert "expected file not found" in str(excinfo.value) -# --- RELEASE_VERSION_RE --------------------------------------------------- +# --- INSTALLER_VERSION_RE ------------------------------------------------- @pytest.mark.parametrize( "line", [ - ' release-version: "26.08.0"', - " release-version: '26.08.0'", - " release-version: 26.08.0", - "release-version: 26.08.0", - "\trelease-version: 26.08.0", - ' release-version: "26.08.0" ', + ' installer-version: "26.08.0"', + " installer-version: '26.08.0'", + " installer-version: 26.08.0", + "installer-version: 26.08.0", + "\tinstaller-version: 26.08.0", + ' installer-version: "26.08.0" ', ], ) -def test_release_version_re_accepts_supported_spellings(checker, line): - match = checker.RELEASE_VERSION_RE.search(f"variables:\n{line}\n") +def test_installer_version_re_accepts_supported_spellings(checker, line): + match = checker.INSTALLER_VERSION_RE.search(f"variables:\n{line}\n") assert match is not None assert match.group(1) == "26.08.0" @@ -96,14 +117,47 @@ def test_release_version_re_accepts_supported_spellings(checker, line): @pytest.mark.parametrize( "line", [ - " release-version: 26.08", - " release-version:", - " other-release-version-thing: 1.2.3", - " # release-version: 1.2.3", + " installer-version: 26.08", + " installer-version:", + " other-installer-version-thing: 1.2.3", + " # installer-version: 1.2.3", ], ) -def test_release_version_re_rejects_malformed_declarations(checker, line): - assert checker.RELEASE_VERSION_RE.search(f"variables:\n{line}\n") is None +def test_installer_version_re_rejects_malformed_declarations(checker, line): + assert checker.INSTALLER_VERSION_RE.search(f"variables:\n{line}\n") is None + + +# --- FLATCAR_VERSION_RE --------------------------------------------------- + + +@pytest.mark.parametrize( + "line", + [ + ' flatcar-version: "4593.2.5"', + " flatcar-version: '4593.2.5'", + " flatcar-version: 4593.2.5", + "flatcar-version: 4593.2.5", + "\tflatcar-version: 4593.2.5", + ' flatcar-version: "4593.2.5" ', + ], +) +def test_flatcar_version_re_accepts_supported_spellings(checker, line): + match = checker.FLATCAR_VERSION_RE.search(f"variables:\n{line}\n") + assert match is not None + assert match.group(1) == "4593.2.5" + + +@pytest.mark.parametrize( + "line", + [ + " flatcar-version: 4593.2", + " flatcar-version:", + " other-flatcar-version-thing: 1.2.3", + " # flatcar-version: 1.2.3", + ], +) +def test_flatcar_version_re_rejects_malformed_declarations(checker, line): + assert checker.FLATCAR_VERSION_RE.search(f"variables:\n{line}\n") is None # --- FSDK_REF_RE ---------------------------------------------------------- @@ -120,17 +174,38 @@ def test_fsdk_ref_re_ignores_two_component_track_glob(checker): assert checker.FSDK_REF_RE.search(" track: freedesktop-sdk-26.08*\n") is None +# --- FLATCAR_PIN_RE ------------------------------------------------------- + + +def test_flatcar_pin_re_extracts_version_from_pin(checker): + match = checker.FLATCAR_PIN_RE.search(' flatcar-version: "4593.2.5"\n') + assert match.group(1) == "4593.2.5" + + +def test_flatcar_pin_re_rejects_malformed(checker): + assert checker.FLATCAR_PIN_RE.search(" flatcar-version: 4593.2\n") is None + + # --- main() --------------------------------------------------------------- def test_main_passes_when_versions_match(checker, capsys): - _write(checker, declared="26.08.0", pinned="26.08.0") + _write( + checker, + installer_declared="26.08.0", + fsdk_pinned="26.08.0", + flatcar_declared="4593.2.5", + flatcar_pinned="4593.2.5", + ) checker.main() - assert "OK: release-version 26.08.0" in capsys.readouterr().out + out = capsys.readouterr().out + assert "OK: installer-version 26.08.0" in out + assert "OK: flatcar-version 4593.2.5" in out def test_main_exits_when_project_conf_missing(checker): checker.FSDK_JUNCTION.write_text("ref: freedesktop-sdk-26.08.0\n", encoding="utf-8") + checker.FLATCAR_PIN.write_text('flatcar-version: "4593.2.5"\n', encoding="utf-8") with pytest.raises(SystemExit) as excinfo: checker.main() assert "expected file not found" in str(excinfo.value) @@ -138,50 +213,108 @@ def test_main_exits_when_project_conf_missing(checker): def test_main_exits_when_junction_missing(checker): checker.PROJECT_CONF.write_text( - 'variables:\n release-version: "26.08.0"\n', encoding="utf-8" + 'variables:\n installer-version: "26.08.0"\n flatcar-version: "4593.2.5"\n', + encoding="utf-8", + ) + checker.FLATCAR_PIN.write_text('flatcar-version: "4593.2.5"\n', encoding="utf-8") + with pytest.raises(SystemExit) as excinfo: + checker.main() + assert "expected file not found" in str(excinfo.value) + + +def test_main_exits_when_flatcar_pin_missing(checker): + checker.PROJECT_CONF.write_text( + 'variables:\n installer-version: "26.08.0"\n flatcar-version: "4593.2.5"\n', + encoding="utf-8", ) + checker.FSDK_JUNCTION.write_text("ref: freedesktop-sdk-26.08.0\n", encoding="utf-8") with pytest.raises(SystemExit) as excinfo: checker.main() assert "expected file not found" in str(excinfo.value) -def test_main_exits_when_release_version_not_declared(checker): - checker.PROJECT_CONF.write_text("variables:\n other: 1\n", encoding="utf-8") +def test_main_exits_when_installer_version_not_declared(checker): + checker.PROJECT_CONF.write_text( + 'variables:\n flatcar-version: "4593.2.5"\n', encoding="utf-8" + ) checker.FSDK_JUNCTION.write_text("ref: freedesktop-sdk-26.08.0\n", encoding="utf-8") + checker.FLATCAR_PIN.write_text('flatcar-version: "4593.2.5"\n', encoding="utf-8") with pytest.raises(SystemExit) as excinfo: checker.main() - assert "does not declare a" in str(excinfo.value) + assert "does not declare an 'installer-version" in str(excinfo.value) + + +def test_main_exits_when_flatcar_version_not_declared(checker): + checker.PROJECT_CONF.write_text( + 'variables:\n installer-version: "26.08.0"\n', encoding="utf-8" + ) + checker.FSDK_JUNCTION.write_text("ref: freedesktop-sdk-26.08.0\n", encoding="utf-8") + checker.FLATCAR_PIN.write_text('flatcar-version: "4593.2.5"\n', encoding="utf-8") + with pytest.raises(SystemExit) as excinfo: + checker.main() + assert "does not declare a 'flatcar-version" in str(excinfo.value) def test_main_exits_when_junction_has_no_point_release(checker): checker.PROJECT_CONF.write_text( - 'variables:\n release-version: "26.08.0"\n', encoding="utf-8" + 'variables:\n installer-version: "26.08.0"\n flatcar-version: "4593.2.5"\n', + encoding="utf-8", ) checker.FSDK_JUNCTION.write_text( "junction:\n track: freedesktop-sdk-26.08*\n", encoding="utf-8" ) + checker.FLATCAR_PIN.write_text('flatcar-version: "4593.2.5"\n', encoding="utf-8") with pytest.raises(SystemExit) as excinfo: checker.main() - assert "no" in str(excinfo.value) - assert "point release" in str(excinfo.value) + assert "no 'freedesktop-sdk-X.Y.Z' point release" in str(excinfo.value) -def test_main_exits_on_drift_and_names_both_versions(checker): - _write(checker, declared="26.08.0", pinned="26.08.1") +def test_main_exits_when_flatcar_pin_has_no_version(checker): + checker.PROJECT_CONF.write_text( + 'variables:\n installer-version: "26.08.0"\n flatcar-version: "4593.2.5"\n', + encoding="utf-8", + ) + checker.FSDK_JUNCTION.write_text("ref: freedesktop-sdk-26.08.0\n", encoding="utf-8") + checker.FLATCAR_PIN.write_text("variables:\n other: 1\n", encoding="utf-8") + with pytest.raises(SystemExit) as excinfo: + checker.main() + assert "does not declare a 'flatcar-version: X.Y.Z'" in str(excinfo.value) + + +def test_main_exits_on_installer_drift_and_names_both_versions(checker): + _write(checker, installer_declared="26.08.0", fsdk_pinned="26.08.1") with pytest.raises(SystemExit) as excinfo: checker.main() message = str(excinfo.value) - assert "release-version drift" in message + assert "installer-version drift" in message assert "26.08.0" in message assert "26.08.1" in message - assert 'set release-version to "26.08.1"' in message + assert 'set installer-version to "26.08.1"' in message + + +def test_main_installer_drift_is_detected_across_minor_lines(checker): + _write(checker, installer_declared="25.08.0", fsdk_pinned="26.08.0") + with pytest.raises(SystemExit) as excinfo: + checker.main() + assert "installer-version drift" in str(excinfo.value) + + +def test_main_exits_on_flatcar_drift_and_names_both_versions(checker): + _write(checker, flatcar_declared="4593.2.5", flatcar_pinned="4593.2.6") + with pytest.raises(SystemExit) as excinfo: + checker.main() + message = str(excinfo.value) + assert "flatcar-version drift" in message + assert "4593.2.5" in message + assert "4593.2.6" in message + assert 'set flatcar-version to "4593.2.6"' in message -def test_main_drift_is_detected_across_minor_lines(checker): - _write(checker, declared="25.08.0", pinned="26.08.0") +def test_main_flatcar_drift_is_detected_across_minor_lines(checker): + _write(checker, flatcar_declared="4593.1.0", flatcar_pinned="4593.2.5") with pytest.raises(SystemExit) as excinfo: checker.main() - assert "release-version drift" in str(excinfo.value) + assert "flatcar-version drift" in str(excinfo.value) # --- live repository invariant ------------------------------------------- @@ -194,4 +327,6 @@ def test_real_repository_has_no_release_version_drift(capsys): module.main() finally: sys.modules.pop("check_release_version", None) - assert "OK: release-version" in capsys.readouterr().out + out = capsys.readouterr().out + assert "OK: installer-version" in out + assert "OK: flatcar-version" in out From 83ed7f56351c0c07944ccac95ede905237f3308b Mon Sep 17 00:00:00 2001 From: mrbobbytables Date: Tue, 15 Sep 2026 03:04:45 +0000 Subject: [PATCH 2/3] docs: update stale release-version references and clarify flatcar-version declaration Updates avoid-over-engineering.md and ddi-installer-build.md to reference the split axes (installer-version and flatcar-version), noting that release tags track the installer axis while the DDI and UKI track Flatcar. Also adds a comment in project.conf explaining why flatcar-version is deliberately declared there in addition to include/flatcar.yml. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/skills/avoid-over-engineering.md | 4 ++-- docs/skills/ddi-installer-build.md | 3 ++- project.conf | 3 +++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/skills/avoid-over-engineering.md b/docs/skills/avoid-over-engineering.md index ff1702e..424b264 100644 --- a/docs/skills/avoid-over-engineering.md +++ b/docs/skills/avoid-over-engineering.md @@ -67,8 +67,8 @@ existing repo convention already does the job. ## Verification - [ ] `just validate` passes before and after the change. -- [ ] No hardcoded version duplicates remain; `release-version` in `project.conf` - is the single source of truth. +- [ ] No hardcoded version duplicates remain; `installer-version` and `flatcar-version` + in `project.conf` define the installer and OS payload axes. - [ ] Removed build dependencies are not used by any command in the element. - [ ] For `manual`/`script` elements, the sandbox still has `/bin/sh` and any coreutils the commands need after a dep cut. - [ ] For `script` elements, build the element with `just bst build `; transitive tools (`dracut`, `ukify`, etc.) may fail silently if their own runtime deps are missing from the sandbox. diff --git a/docs/skills/ddi-installer-build.md b/docs/skills/ddi-installer-build.md index 0916d10..d0973b6 100644 --- a/docs/skills/ddi-installer-build.md +++ b/docs/skills/ddi-installer-build.md @@ -91,7 +91,8 @@ The release process is driven by `.github/workflows/build.yml`: - CI builds the DDI payload, installer, target UKI, k0s sysext, and standalone PXE boot inputs (`bluefin-server-pxe-vmlinuz-*`, `bluefin-server-pxe-initrd-*.cpio.gz`). - CI uploads the versioned release assets to the corresponding - `installer-v` GitHub Release. + `installer-v` GitHub Release. The release tag tracks the + installer axis, while the DDI and UKI inside it track the Flatcar payload axis (`flatcar-version`). - CI also produces a combined `dist/release/SHA256SUMS` manifest and signs it to create `SHA256SUMS.gpg` for `systemd-sysupdate` verification. The PXE inputs are included in this manifest, per `docs/skills/ddi-installer.md`. diff --git a/project.conf b/project.conf index e44d7a3..bb7b97d 100644 --- a/project.conf +++ b/project.conf @@ -31,6 +31,9 @@ variables: # OS payload axis (Flatcar): must match the Flatcar LTS release pinned in # include/flatcar.yml. Enforced by # .github/scripts/check-release-version.py. + # flatcar-version is deliberately declared here as well as in include/flatcar.yml + # because elements such as bluefin-server-ddi.bst do not include (@) include/flatcar.yml + # and resolve flatcar-version from project.conf. The checker enforces they stay identical. flatcar-version: "4593.2.5" # Pull-only: read from the shared GNOME + Bluefin BuildStream CAS caches. From a2257f2e34bf18adc4449164622591f918cee941 Mon Sep 17 00:00:00 2001 From: Jorge Castro Date: Fri, 18 Sep 2026 13:13:49 -0400 Subject: [PATCH 3/3] refactor(arch): declare flatcar-version only in include/flatcar.yml project.conf restated flatcar-version alongside include/flatcar.yml, whose own header declares it the single source of truth for the Flatcar axis. Add include/flatcar.yml to project.conf's existing (@) include list instead, so %{flatcar-version} and %{flatcar-kver} resolve project-wide from one declaration, matching the k0s axis precedent. check-release-version.py no longer compares two copies of the same fact: it validates project.conf's installer-version against the freedesktop-sdk junction ref and that include/flatcar.yml declares a well-formed flatcar-version. Stale standing fact in docs/skills/index.md replaced with the two-axis description this refactor introduces. Assisted-by: Claude Opus 4.6 via GitHub Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/scripts/check-release-version.py | 42 +++------ docs/skills/avoid-over-engineering.md | 5 +- docs/skills/index.md | 2 +- .../bluefin-server/os-release-flatcar.bst | 4 +- project.conf | 12 +-- tests/unit/test_release_version.py | 86 ++----------------- 6 files changed, 29 insertions(+), 122 deletions(-) diff --git a/.github/scripts/check-release-version.py b/.github/scripts/check-release-version.py index c6c5b48..04101bb 100755 --- a/.github/scripts/check-release-version.py +++ b/.github/scripts/check-release-version.py @@ -1,11 +1,10 @@ #!/usr/bin/env python3 """Enforce the release-version invariant for Bluefin Server. -project.conf declares the two release version axes: +The two release version axes are each declared in exactly one place: - variables: - installer-version: "X.Y.Z" # must match the FSDK point release - flatcar-version: "X.Y.Z" # must match the Flatcar LTS release + project.conf installer-version: "X.Y.Z" # FSDK point release + include/flatcar.yml flatcar-version: "X.Y.Z" # Flatcar LTS release The installer-version axis names the offline installer disk image and PXE boot inputs (bluefin-server-installer-.raw.zst, bluefin-server-pxe-*), which @@ -13,15 +12,17 @@ The flatcar-version axis names the OS payload release assets (bluefin-server-ddi-.raw.zst, bluefin-server-.efi) and is the version -systemd-sysupdate extracts from those filenames via `@v`. +systemd-sysupdate extracts from those filenames via `@v`. project.conf pulls +include/flatcar.yml in via its `(@)` list, so %{flatcar-version} resolves +project-wide without being restated. The k0s sysext is on its own axis: an independently-pinned third-party payload versioned from `include/k0s.yml` and enforced separately by `.github/scripts/check-k0s-version.py`. -This script validates both axes independently against their pins: - * installer-version against elements/freedesktop-sdk.bst - * flatcar-version against include/flatcar.yml +This script validates both axes against their pins: + * project.conf installer-version against elements/freedesktop-sdk.bst + * include/flatcar.yml flatcar-version is well-formed This script fails closed on any drift. """ @@ -38,9 +39,6 @@ INSTALLER_VERSION_RE = re.compile( r"^\s*installer-version:\s*[\"']?([0-9]+\.[0-9]+\.[0-9]+)[\"']?\s*$", re.MULTILINE ) -FLATCAR_VERSION_RE = re.compile( - r"^\s*flatcar-version:\s*[\"']?([0-9]+\.[0-9]+\.[0-9]+)[\"']?\s*$", re.MULTILINE -) FSDK_REF_RE = re.compile(r"freedesktop-sdk-([0-9]+\.[0-9]+\.[0-9]+)") FLATCAR_PIN_RE = re.compile( r"^\s*flatcar-version:\s*[\"']?([0-9]+\.[0-9]+\.[0-9]+)[\"']?\s*$", re.MULTILINE @@ -66,14 +64,6 @@ def main(): ) installer_declared = installer_match.group(1) - flatcar_match = FLATCAR_VERSION_RE.search(conf) - if not flatcar_match: - sys.exit( - "ERROR: project.conf does not declare a " - "'flatcar-version: X.Y.Z' variable." - ) - flatcar_declared = flatcar_match.group(1) - fsdk_match = FSDK_REF_RE.search(junction) if not fsdk_match: sys.exit( @@ -102,21 +92,9 @@ def main(): f"Fix: set installer-version to \"{fsdk_pinned}\" in project.conf." ) - if flatcar_declared != flatcar_pinned: - sys.exit( - "ERROR: flatcar-version drift.\n" - f" project.conf flatcar-version : {flatcar_declared}\n" - f" include/flatcar.yml pinned release : {flatcar_pinned}\n" - "\n" - "The OS payload version and systemd-sysupdate assets are derived from\n" - "the Flatcar LTS pin while project.conf declares flatcar-version.\n" - "\n" - f"Fix: set flatcar-version to \"{flatcar_pinned}\" in project.conf." - ) - print( f"OK: installer-version {installer_declared} matches pinned FSDK point release.\n" - f"OK: flatcar-version {flatcar_declared} matches pinned Flatcar release." + f"OK: flatcar-version {flatcar_pinned} declared in include/flatcar.yml." ) diff --git a/docs/skills/avoid-over-engineering.md b/docs/skills/avoid-over-engineering.md index 424b264..7b25c18 100644 --- a/docs/skills/avoid-over-engineering.md +++ b/docs/skills/avoid-over-engineering.md @@ -67,8 +67,9 @@ existing repo convention already does the job. ## Verification - [ ] `just validate` passes before and after the change. -- [ ] No hardcoded version duplicates remain; `installer-version` and `flatcar-version` - in `project.conf` define the installer and OS payload axes. +- [ ] No hardcoded version duplicates remain; `installer-version` in `project.conf` + and `flatcar-version` in `include/flatcar.yml` define the installer and OS + payload axes. - [ ] Removed build dependencies are not used by any command in the element. - [ ] For `manual`/`script` elements, the sandbox still has `/bin/sh` and any coreutils the commands need after a dep cut. - [ ] For `script` elements, build the element with `just bst build `; transitive tools (`dracut`, `ukify`, etc.) may fail silently if their own runtime deps are missing from the sandbox. diff --git a/docs/skills/index.md b/docs/skills/index.md index 02c5b0a..d04fe69 100644 --- a/docs/skills/index.md +++ b/docs/skills/index.md @@ -43,4 +43,4 @@ This is the lazy-load routing table for agent skills. Keep this file in memory w - **Publish registry:** factory OCI registry at `:30500` (or configured by operator). - **Cluster build workflow:** `bluefin-server-build-pipeline` in the downstream factory CI repository. - **Cluster boot-test workflow:** `bluefin-server-boot-test` in the downstream factory CI repository. -- **Version scheme:** FSDK-derived only; no separate application version axis. +- **Version scheme:** two axes — `installer-version` (FSDK-derived, `project.conf`) and `flatcar-version` (OS payload, `include/flatcar.yml`); no application version axis. diff --git a/elements/bluefin-server/os-release-flatcar.bst b/elements/bluefin-server/os-release-flatcar.bst index 9566b2a..cc1c29d 100644 --- a/elements/bluefin-server/os-release-flatcar.bst +++ b/elements/bluefin-server/os-release-flatcar.bst @@ -18,8 +18,8 @@ config: install-commands: - mkdir -p "%{install-root}/usr/lib" - | - # %{flatcar-version} is declared in project.conf and enforced against - # include/flatcar.yml by check-release-version.py — no re-parsing needed. + # %{flatcar-version} comes from include/flatcar.yml, the single source of + # truth for the Flatcar axis — no re-parsing needed. FLATCAR_VERSION="%{flatcar-version}" cat < "%{install-root}/usr/lib/os-release" diff --git a/project.conf b/project.conf index bb7b97d..d395f19 100644 --- a/project.conf +++ b/project.conf @@ -9,6 +9,10 @@ element-path: elements (@): - gnome-build-meta.bst:freedesktop-sdk.bst:include/runtime.yml - include/aliases.yml + # OS payload axis (Flatcar): single source of truth for %{flatcar-version} + # and %{flatcar-kver}. Included here so both resolve project-wide without + # being restated. Enforced by .github/scripts/check-release-version.py. + - include/flatcar.yml options: arch: @@ -28,14 +32,6 @@ variables: # .github/scripts/check-release-version.py. installer-version: "26.08.0" - # OS payload axis (Flatcar): must match the Flatcar LTS release pinned in - # include/flatcar.yml. Enforced by - # .github/scripts/check-release-version.py. - # flatcar-version is deliberately declared here as well as in include/flatcar.yml - # because elements such as bluefin-server-ddi.bst do not include (@) include/flatcar.yml - # and resolve flatcar-version from project.conf. The checker enforces they stay identical. - flatcar-version: "4593.2.5" - # Pull-only: read from the shared GNOME + Bluefin BuildStream CAS caches. artifacts: - url: https://gbm.gnome.org:11003 diff --git a/tests/unit/test_release_version.py b/tests/unit/test_release_version.py index ee633ae..ed010d8 100644 --- a/tests/unit/test_release_version.py +++ b/tests/unit/test_release_version.py @@ -1,8 +1,8 @@ """Unit coverage for .github/scripts/check-release-version.py. -The script is the gate protecting against version drift between ``project.conf`` -and the two release axes: the installer axis pinned in -``elements/freedesktop-sdk.bst`` and the Flatcar OS payload axis pinned in +The script is the gate protecting against version drift on the two release +axes: the installer axis declared in ``project.conf`` and pinned in +``elements/freedesktop-sdk.bst``, and the Flatcar OS payload axis declared in ``include/flatcar.yml``. When either regresses, CI publishes release assets carrying stale version @@ -51,13 +51,10 @@ def _write( checker, installer_declared="26.08.0", fsdk_pinned="26.08.0", - flatcar_declared="4593.2.5", flatcar_pinned="4593.2.5", ): checker.PROJECT_CONF.write_text( - "variables:\n" - f' installer-version: "{installer_declared}"\n' - f' flatcar-version: "{flatcar_declared}"\n', + f'variables:\n installer-version: "{installer_declared}"\n', encoding="utf-8", ) checker.FSDK_JUNCTION.write_text( @@ -127,39 +124,6 @@ def test_installer_version_re_rejects_malformed_declarations(checker, line): assert checker.INSTALLER_VERSION_RE.search(f"variables:\n{line}\n") is None -# --- FLATCAR_VERSION_RE --------------------------------------------------- - - -@pytest.mark.parametrize( - "line", - [ - ' flatcar-version: "4593.2.5"', - " flatcar-version: '4593.2.5'", - " flatcar-version: 4593.2.5", - "flatcar-version: 4593.2.5", - "\tflatcar-version: 4593.2.5", - ' flatcar-version: "4593.2.5" ', - ], -) -def test_flatcar_version_re_accepts_supported_spellings(checker, line): - match = checker.FLATCAR_VERSION_RE.search(f"variables:\n{line}\n") - assert match is not None - assert match.group(1) == "4593.2.5" - - -@pytest.mark.parametrize( - "line", - [ - " flatcar-version: 4593.2", - " flatcar-version:", - " other-flatcar-version-thing: 1.2.3", - " # flatcar-version: 1.2.3", - ], -) -def test_flatcar_version_re_rejects_malformed_declarations(checker, line): - assert checker.FLATCAR_VERSION_RE.search(f"variables:\n{line}\n") is None - - # --- FSDK_REF_RE ---------------------------------------------------------- @@ -194,7 +158,6 @@ def test_main_passes_when_versions_match(checker, capsys): checker, installer_declared="26.08.0", fsdk_pinned="26.08.0", - flatcar_declared="4593.2.5", flatcar_pinned="4593.2.5", ) checker.main() @@ -213,7 +176,7 @@ def test_main_exits_when_project_conf_missing(checker): def test_main_exits_when_junction_missing(checker): checker.PROJECT_CONF.write_text( - 'variables:\n installer-version: "26.08.0"\n flatcar-version: "4593.2.5"\n', + 'variables:\n installer-version: "26.08.0"\n', encoding="utf-8", ) checker.FLATCAR_PIN.write_text('flatcar-version: "4593.2.5"\n', encoding="utf-8") @@ -224,7 +187,7 @@ def test_main_exits_when_junction_missing(checker): def test_main_exits_when_flatcar_pin_missing(checker): checker.PROJECT_CONF.write_text( - 'variables:\n installer-version: "26.08.0"\n flatcar-version: "4593.2.5"\n', + 'variables:\n installer-version: "26.08.0"\n', encoding="utf-8", ) checker.FSDK_JUNCTION.write_text("ref: freedesktop-sdk-26.08.0\n", encoding="utf-8") @@ -234,9 +197,7 @@ def test_main_exits_when_flatcar_pin_missing(checker): def test_main_exits_when_installer_version_not_declared(checker): - checker.PROJECT_CONF.write_text( - 'variables:\n flatcar-version: "4593.2.5"\n', encoding="utf-8" - ) + checker.PROJECT_CONF.write_text("variables:\n other: 1\n", encoding="utf-8") checker.FSDK_JUNCTION.write_text("ref: freedesktop-sdk-26.08.0\n", encoding="utf-8") checker.FLATCAR_PIN.write_text('flatcar-version: "4593.2.5"\n', encoding="utf-8") with pytest.raises(SystemExit) as excinfo: @@ -244,20 +205,9 @@ def test_main_exits_when_installer_version_not_declared(checker): assert "does not declare an 'installer-version" in str(excinfo.value) -def test_main_exits_when_flatcar_version_not_declared(checker): - checker.PROJECT_CONF.write_text( - 'variables:\n installer-version: "26.08.0"\n', encoding="utf-8" - ) - checker.FSDK_JUNCTION.write_text("ref: freedesktop-sdk-26.08.0\n", encoding="utf-8") - checker.FLATCAR_PIN.write_text('flatcar-version: "4593.2.5"\n', encoding="utf-8") - with pytest.raises(SystemExit) as excinfo: - checker.main() - assert "does not declare a 'flatcar-version" in str(excinfo.value) - - def test_main_exits_when_junction_has_no_point_release(checker): checker.PROJECT_CONF.write_text( - 'variables:\n installer-version: "26.08.0"\n flatcar-version: "4593.2.5"\n', + 'variables:\n installer-version: "26.08.0"\n', encoding="utf-8", ) checker.FSDK_JUNCTION.write_text( @@ -271,7 +221,7 @@ def test_main_exits_when_junction_has_no_point_release(checker): def test_main_exits_when_flatcar_pin_has_no_version(checker): checker.PROJECT_CONF.write_text( - 'variables:\n installer-version: "26.08.0"\n flatcar-version: "4593.2.5"\n', + 'variables:\n installer-version: "26.08.0"\n', encoding="utf-8", ) checker.FSDK_JUNCTION.write_text("ref: freedesktop-sdk-26.08.0\n", encoding="utf-8") @@ -299,24 +249,6 @@ def test_main_installer_drift_is_detected_across_minor_lines(checker): assert "installer-version drift" in str(excinfo.value) -def test_main_exits_on_flatcar_drift_and_names_both_versions(checker): - _write(checker, flatcar_declared="4593.2.5", flatcar_pinned="4593.2.6") - with pytest.raises(SystemExit) as excinfo: - checker.main() - message = str(excinfo.value) - assert "flatcar-version drift" in message - assert "4593.2.5" in message - assert "4593.2.6" in message - assert 'set flatcar-version to "4593.2.6"' in message - - -def test_main_flatcar_drift_is_detected_across_minor_lines(checker): - _write(checker, flatcar_declared="4593.1.0", flatcar_pinned="4593.2.5") - with pytest.raises(SystemExit) as excinfo: - checker.main() - assert "flatcar-version drift" in str(excinfo.value) - - # --- live repository invariant -------------------------------------------