feat(images): check declared architectures against the registry, and make a pin say why

Nothing held a role's architectures to reality: test_image_reachable proves a tag exists, test_arm64_images proves four named roles publish arm64, and the closure lint only proves the declarations do not intersect to nothing. An amd64-only bump became a scheduling failure on an arm64 row rather than a red test.

A new external test sweeps every pinned image and judges a role by the intersection over its services, because a role runs only where all of its images run; judging per image would demand arm64 from an amd64-only role's nginx and call a narrowing stale because nginx publishes arm64 while the application does not. A role declaring nothing must cover every architecture the matrix rotates; a role that narrows must narrow to exactly its capability, which is what retires its TODO by itself. An unread image downgrades the stale verdict to a warning, since the image a probe could not read may be the one the narrowing exists for. The probe reads the mirror first and caches on disk, because the sweep is ~160 manifests and Docker Hub counts its allowance per IP, shared.

A new lint requires every architectures declaration to carry a nocheck marker and a Reason. bigbluebutton and opentalk keep their pins with measured reasons; decidim, pretix and taiga lose theirs, because each builds its own image and their upstream Dockerfiles are arch-neutral. Their source builds are not converted yet, so until that lands the matrix may hand them an arm64 row that fails at FROM.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-10-05 22:11:48 +02:00
1 parent 03ed2f1ca1
commit 0bb437d9ce
14 files changed
+653 -27

No files matched your search

@@ -5,6 +5,7 @@ from __future__ import annotations
import unittest
from utils.annotations.suppress import (
has_rule_with_reason,
is_suppressed_anywhere,
is_suppressed_at,
is_suppressed_in_head,
@@ -152,5 +153,69 @@ class TestSuppressedLineNumbers(unittest.TestCase):
self.assertEqual(suppressed_line_numbers(lines, "shared"), {4})
class TestHasRuleWithReason(unittest.TestCase):
"""An exemption that has to say why, written above the value it exempts."""
def test_both_markers_on_the_value_line(self) -> None:
lines = [" min_storage: 20GB # nocheck: rule-x Reason: the volume"]
self.assertTrue(has_rule_with_reason(lines, 1, "rule-x"))
def test_both_markers_in_the_comment_block_above(self) -> None:
lines = [
" # nocheck: rule-x",
" # Reason: the data volume dominates",
" min_storage: 20GB",
]
self.assertTrue(has_rule_with_reason(lines, 3, "rule-x"))
def test_the_marker_order_does_not_matter(self) -> None:
lines = [
" # Reason: the data volume dominates",
" # nocheck: rule-x",
" min_storage: 20GB",
]
self.assertTrue(has_rule_with_reason(lines, 3, "rule-x"))
def test_a_marker_without_a_reason_is_refused(self) -> None:
lines = [" # nocheck: rule-x", " min_storage: 20GB"]
self.assertFalse(has_rule_with_reason(lines, 2, "rule-x"))
def test_a_reason_without_the_marker_is_refused(self) -> None:
lines = [" # Reason: the data volume dominates", " min_storage: 20GB"]
self.assertFalse(has_rule_with_reason(lines, 2, "rule-x"))
def test_an_empty_reason_is_refused(self) -> None:
lines = [" # nocheck: rule-x Reason:", " min_storage: 20GB"]
self.assertFalse(has_rule_with_reason(lines, 2, "rule-x"))
def test_another_rules_marker_does_not_count(self) -> None:
lines = [" # nocheck: rule-y Reason: something else", " min_storage: 20GB"]
self.assertFalse(has_rule_with_reason(lines, 2, "rule-x"))
def test_a_blank_line_ends_the_comment_block(self) -> None:
lines = [
" # nocheck: rule-x Reason: the data volume dominates",
"",
" min_storage: 20GB",
]
self.assertFalse(
has_rule_with_reason(lines, 3, "rule-x"),
"a marker separated by a blank line belongs to the construct above it",
)
def test_a_line_number_outside_the_file_is_refused(self) -> None:
for line_no in (0, 5):
with self.subTest(line_no=line_no):
self.assertFalse(has_rule_with_reason(["x"], line_no, "rule-x"))
if __name__ == "__main__":
unittest.main()