From 5bc7084a4d99a2221ab934194895fd12dd3892ef Mon Sep 17 00:00:00 2001 From: Bryan Date: Fri, 28 Aug 2026 15:34:46 +0000 Subject: [PATCH] ci: fingerprint the BIND build recipe --- .github/ci/bind920_profile.py | 13 ++++++++++++- .github/ci/ci-tests/test_bind920_reuse.py | 19 +++++++++++++++++++ .github/ci/ci-tests/test_reuse_bind920.py | 17 ++++++++++++++++- .github/ci/reuse_bind920.py | 9 ++++----- 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/.github/ci/bind920_profile.py b/.github/ci/bind920_profile.py index 0f33e97595..99d4eff575 100644 --- a/.github/ci/bind920_profile.py +++ b/.github/ci/bind920_profile.py @@ -24,7 +24,8 @@ SERIES_PATTERN = re.compile(r"[0-9]+\.[0-9]+") FREEBSD_RELEASE_PATTERN = re.compile(r"[0-9]+\.[0-9]+") ARCHITECTURE_PATTERN = re.compile(r"[a-z0-9_]+") -PROVENANCE_SCHEMA = 2 +PROVENANCE_SCHEMA = 3 +BUILD_RECIPE_PATH = Path(__file__).with_name("build-bind920.sh") PACKAGE_CREATOR_FIELDS = { "name", "version", @@ -102,6 +103,14 @@ def validate_package_creator(package_creator: object) -> dict[str, str]: return package_creator +def build_recipe_sha256() -> str: + """Identify the local build policy that produces the reusable BIND pair.""" + try: + return hashlib.sha256(BUILD_RECIPE_PATH.read_bytes()).hexdigest() + except OSError as error: + raise ValueError(f"cannot read BIND build recipe: {error}") from error + + def compatibility_fingerprint( profile: object, series: str, @@ -124,6 +133,7 @@ def compatibility_fingerprint( "freebsd_release": freebsd_release, "architecture": architecture, "bind_profile": profile, + "build_recipe_sha256": build_recipe_sha256(), "package_creator": package_creator, } encoded = json.dumps(inputs, sort_keys=True, separators=(",", ":")).encode("utf-8") @@ -167,6 +177,7 @@ def build_provenance( "series": series, "freebsd_release": freebsd_release, "architecture": architecture, + "build_recipe_sha256": build_recipe_sha256(), "package_creator": package_creator, "packages": validated_packages, } diff --git a/.github/ci/ci-tests/test_bind920_reuse.py b/.github/ci/ci-tests/test_bind920_reuse.py index 172f1f1a12..44b4092297 100644 --- a/.github/ci/ci-tests/test_bind920_reuse.py +++ b/.github/ci/ci-tests/test_bind920_reuse.py @@ -9,6 +9,7 @@ import tempfile import unittest from pathlib import Path +from unittest.mock import patch MODULE_PATH = Path(__file__).resolve().parents[1] / "bind920_profile.py" @@ -67,6 +68,22 @@ def test_fingerprint_rejects_different_compatibility_inputs(self) -> None: self.assertNotEqual(baseline, bind920_profile.compatibility_fingerprint(changed_profile, "26.1", "14.3", "x86_64", PACKAGE_CREATOR)) self.assertNotEqual(baseline, bind920_profile.compatibility_fingerprint(PROFILE, "26.1", "14.3", "x86_64", dict(PACKAGE_CREATOR, sha256="c" * 64))) + def test_fingerprint_changes_when_the_bind_build_recipe_changes(self) -> None: + """A build-policy change must force fresh BIND package archives.""" + with tempfile.TemporaryDirectory() as temporary_directory: + recipe = Path(temporary_directory) / "build-bind920.sh" + recipe.write_text("pkg install lmdb\n", encoding="utf-8") + with patch.object(bind920_profile, "BUILD_RECIPE_PATH", recipe): + baseline = bind920_profile.compatibility_fingerprint( + PROFILE, "26.1", "14.3", "x86_64", PACKAGE_CREATOR + ) + recipe.write_text("pkg install lmdb0\n", encoding="utf-8") + corrected = bind920_profile.compatibility_fingerprint( + PROFILE, "26.1", "14.3", "x86_64", PACKAGE_CREATOR + ) + + self.assertNotEqual(baseline, corrected) + def test_provenance_requires_exact_bind_package_identities(self) -> None: """A cache candidate must identify both BIND package archives exactly.""" provenance = bind920_profile.build_provenance( @@ -74,6 +91,8 @@ def test_provenance_requires_exact_bind_package_identities(self) -> None: ) self.assertEqual("dns/bind920", provenance["packages"]["bind920"]["origin"]) self.assertEqual(PACKAGE_CREATOR, provenance["package_creator"]) + self.assertEqual(3, provenance["schema"]) + self.assertRegex(provenance["build_recipe_sha256"], r"^[0-9a-f]{64}$") invalid = dict(PACKAGES) invalid["bind920"] = dict(PACKAGES["bind920"], origin="dns/bind918") with self.assertRaisesRegex(ValueError, "bind920 package"): diff --git a/.github/ci/ci-tests/test_reuse_bind920.py b/.github/ci/ci-tests/test_reuse_bind920.py index 726ba24bc0..4f1d0a4796 100644 --- a/.github/ci/ci-tests/test_reuse_bind920.py +++ b/.github/ci/ci-tests/test_reuse_bind920.py @@ -39,11 +39,12 @@ "pkg_static_sha256": "b" * 64, } PROVENANCE = { - "schema": 2, + "schema": reuse_bind920.bind920_profile.PROVENANCE_SCHEMA, "fingerprint": "", "series": "26.1", "freebsd_release": "14.3", "architecture": "x86_64", + "build_recipe_sha256": reuse_bind920.bind920_profile.build_recipe_sha256(), "package_creator": PACKAGE_CREATOR, "packages": { "bind-tools": { @@ -118,6 +119,20 @@ def test_old_or_different_creator_provenance_is_a_cache_miss(self) -> None: changed, PROFILE, "26.1", "14.3", "x86_64", PACKAGE_CREATOR ) + def test_schema_two_provenance_is_a_cache_miss_after_build_recipe_versioning(self) -> None: + legacy = dict( + PROVENANCE, + schema=2, + fingerprint=reuse_bind920.bind920_profile.compatibility_fingerprint( + PROFILE, "26.1", "14.3", "x86_64", PACKAGE_CREATOR + ), + ) + + with self.assertRaises(reuse_bind920.CacheMiss): + reuse_bind920.select_candidate( + legacy, PROFILE, "26.1", "14.3", "x86_64", PACKAGE_CREATOR + ) + def test_invalid_package_identity_is_rejected(self) -> None: """Malformed matching metadata must not silently become a cache miss.""" provenance = dict(PROVENANCE, packages=dict(PROVENANCE["packages"])) diff --git a/.github/ci/reuse_bind920.py b/.github/ci/reuse_bind920.py index f30b0df845..beaaae20c7 100644 --- a/.github/ci/reuse_bind920.py +++ b/.github/ci/reuse_bind920.py @@ -26,7 +26,7 @@ PACKAGE_FIELDS = {"name", "version", "origin", "filename"} PROVENANCE_FIELDS = { "schema", "fingerprint", "series", "freebsd_release", "architecture", - "package_creator", "packages", + "build_recipe_sha256", "package_creator", "packages", } @@ -45,10 +45,9 @@ def select_candidate( """Validate and select a compatible BIND package pair from provenance.""" if not isinstance(provenance, dict): raise ValueError("BIND provenance has an invalid schema") - if ( - provenance.get("schema") != bind920_profile.PROVENANCE_SCHEMA - or provenance.get("package_creator") != package_creator - ): + if provenance.get("schema") != bind920_profile.PROVENANCE_SCHEMA: + raise CacheMiss("stable BIND provenance schema differs") + if provenance.get("package_creator") != package_creator: raise CacheMiss("stable BIND package creator differs") if not isinstance(provenance, dict) or set(provenance) != PROVENANCE_FIELDS: raise ValueError("BIND provenance has an invalid schema")