diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6991b07f..ae756585 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,6 +167,9 @@ jobs: echo "registryctl_tutorial=${registryctl_tutorial}" } >> "${GITHUB_OUTPUT}" + - name: Check Debian 13 image contract + run: python3 release/scripts/check-debian13-images.py + secrets: name: Secret scan runs-on: ubuntu-24.04 @@ -644,9 +647,7 @@ jobs: path: | target/registryctl-tutorial-cargo-home target/registryctl-tutorial-linux-amd64 - key: registryctl-tutorial-${{ runner.os }}-rust-1.95.0-${{ hashFiles('Cargo.lock') }} - restore-keys: | - registryctl-tutorial-${{ runner.os }}-rust-1.95.0- + key: registryctl-tutorial-${{ runner.os }}-${{ hashFiles('docs/site/scripts/check-registryctl-tutorials.sh', 'Cargo.lock') }} - name: Execute registryctl tutorials from source working-directory: docs/site diff --git a/docs/site/scripts/check-registryctl-tutorials.sh b/docs/site/scripts/check-registryctl-tutorials.sh index 77e70304..de8c556b 100644 --- a/docs/site/scripts/check-registryctl-tutorials.sh +++ b/docs/site/scripts/check-registryctl-tutorials.sh @@ -15,7 +15,7 @@ REPO_ROOT="$(cd "$SITE_ROOT/../.." && pwd)" HELPER="$SITE_ROOT/scripts/registryctl-tutorial.mjs" RELAY_TUTORIAL="$SITE_ROOT/src/content/docs/tutorials/publish-spreadsheet-secured-registry-api.mdx" NOTARY_TUTORIAL="$SITE_ROOT/src/content/docs/tutorials/verify-claim-registry-api.mdx" -BUILDER_IMAGE="rust:1.95-bookworm@sha256:4c2fd73ef19c5ef9d54bee03b06b2839a392604fbfcd578ed948b71b37c1d7fb" +BUILDER_IMAGE="rust:1.95-trixie@sha256:f49565f188ee00bc2a18dd418183f2c5f23ef7d6e691890517ed341a598f67c3" LINUX_TARGET="$REPO_ROOT/target/registryctl-tutorial-linux-amd64" CARGO_HOME_DIR="$REPO_ROOT/target/registryctl-tutorial-cargo-home" NATIVE_TARGET="$REPO_ROOT/target/registryctl-tutorial-native" diff --git a/release/scripts/check-debian13-images.py b/release/scripts/check-debian13-images.py index b6aac87c..d59bce37 100755 --- a/release/scripts/check-debian13-images.py +++ b/release/scripts/check-debian13-images.py @@ -44,6 +44,7 @@ Path("crates/registry-relay/docs/security-assurance.md"), Path("crates/registry-relay/scripts/check_docker_build_contract.py"), Path("crates/registry-relay/scripts/run-live-consultation-journey.sh"), + Path("docs/site/scripts/check-registryctl-tutorials.sh"), Path("products/notary/docs/security-assurance.md"), ) @@ -61,6 +62,10 @@ FROM_RE = re.compile(r"^FROM\s+(?:--platform=\S+\s+)?(\S+)", re.MULTILINE) DIGEST_PIN_RE = re.compile(r"@sha256:[0-9a-f]{64}$") +RETIRED_DEBIAN_RE = re.compile( + r"\b(?:bookworm|debian[\s_:-]*v?[\s_:-]*12)\b", + re.IGNORECASE, +) def read(root: Path, relative: Path, failures: list[str]) -> str: @@ -96,14 +101,11 @@ def check_repository(root: Path = ROOT) -> list[str]: for relative in MAINTAINED_TEXT_PATHS } - retired_markers = ("book" + "worm", "debian" + "12") for relative, text in texts.items(): - lowered = text.casefold() - for marker in retired_markers: - if marker in lowered: - failures.append( - f"{relative}: retired Debian image generation marker remains: {marker}" - ) + if RETIRED_DEBIAN_RE.search(text): + failures.append( + f"{relative}: retired Debian image generation marker remains" + ) for relative in DOCKERFILES: text = texts[relative] @@ -274,6 +276,15 @@ def check_repository(root: Path = ROOT) -> list[str]: failures, ) + tutorial = texts[Path("docs/site/scripts/check-registryctl-tutorials.sh")] + require( + tutorial, + f'BUILDER_IMAGE="{RUST_BUILDER}"', + Path("docs/site/scripts/check-registryctl-tutorials.sh"), + "pinned Debian 13 tutorial builder", + failures, + ) + return failures diff --git a/release/scripts/check-gates-inventory.py b/release/scripts/check-gates-inventory.py index 3d4837d6..331000dd 100644 --- a/release/scripts/check-gates-inventory.py +++ b/release/scripts/check-gates-inventory.py @@ -32,6 +32,10 @@ "Other workflow path classification", ".github/workflows/*)\n mark_all\n ;;", ), + ( + "Debian 13 image contract", + "run: python3 release/scripts/check-debian13-images.py", + ), ("Cargo metadata", "run: cargo metadata --locked --format-version 1"), ( "Manifest profile validation", diff --git a/release/scripts/test_check_gates_inventory.py b/release/scripts/test_check_gates_inventory.py index a050580f..8b641ebb 100644 --- a/release/scripts/test_check_gates_inventory.py +++ b/release/scripts/test_check_gates_inventory.py @@ -121,6 +121,13 @@ def test_missing_relay_exposure_gate_is_reported(self) -> None: text = self.workflow.replace("name: Relay exposure check", "name: Relay exposure") self.assertIn("Relay exposure check", self.module.missing_gates(text)) + def test_missing_debian13_image_contract_is_reported(self) -> None: + text = self.workflow.replace( + "run: python3 release/scripts/check-debian13-images.py", + "run: true", + ) + self.assertIn("Debian 13 image contract", self.module.missing_gates(text)) + def test_missing_pull_request_concurrency_group_is_reported(self) -> None: text = self.workflow.replace( "format('pr-{0}', github.event.pull_request.number)", diff --git a/release/scripts/test_registry_release.py b/release/scripts/test_registry_release.py index 19e0ffab..76485126 100755 --- a/release/scripts/test_registry_release.py +++ b/release/scripts/test_registry_release.py @@ -62,6 +62,34 @@ def test_debian13_contract_rejects_retired_base_and_unpinned_base(self) -> None: self.assertTrue( any("not pinned by immutable digest" in failure for failure in failures) ) + + def test_debian13_contract_rejects_other_tutorial_builders(self) -> None: + module = load_debian13_image_check() + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for relative in module.MAINTAINED_TEXT_PATHS: + destination = root / relative + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_text( + (ROOT / relative).read_text(encoding="utf-8"), + encoding="utf-8", + ) + + tutorial = root / "docs/site/scripts/check-registryctl-tutorials.sh" + text = tutorial.read_text(encoding="utf-8") + tutorial.write_text( + text.replace(module.RUST_BUILDER, "rust:1.95-debian-12", 1), + encoding="utf-8", + ) + + failures = module.check_repository(root) + self.assertTrue( + any("retired Debian image generation marker" in failure for failure in failures) + ) + self.assertTrue( + any("pinned Debian 13 tutorial builder" in failure for failure in failures) + ) + def test_contributing_documents_major_functionality_test_policy(self) -> None: text = (ROOT / "CONTRIBUTING.md").read_text(encoding="utf-8")