diff --git a/tests/test_artifact_identity.py b/tests/test_artifact_identity.py index 60d086f..fe614bf 100644 --- a/tests/test_artifact_identity.py +++ b/tests/test_artifact_identity.py @@ -34,6 +34,15 @@ is genuinely the commit those paths last changed in. emit. Two nearly-identical formats are more dangerous than two obviously different ones, and the only thing keeping them identical is a test. +**The extension is the one exception, and it is a rendering exception only.** +AMO's version grammar forbids a leading zero, so the extension emits the same +numbers unpadded — `2026.8.29.201` where the family says `2026.08.29.0201` +(#3138, milestone 318 step 8). Rule 148 defines comparison as numeric per +dot-segment, under which the two are equal, so this is pinned in both +directions below: the extension must satisfy AMO's grammar, and every artifact +must derive the same NUMBERS its own commit stamps. An exception left as "the +extension is different" would drift into being differently different. + The identity-TAG tests this file used to hold are gone with the tag. There is no longer a `CHANNELLED` list to drift (the channel is which tag you inspect), and no `identity` subcommand to refuse an unqualified call. @@ -57,6 +66,26 @@ _REVISION = re.compile(r"^[0-9a-f]{12}$") # YYYY.MM.DD.HHMM, every segment zero-padded to its full width. _VERSION = re.compile(r"^\d{4}\.\d{2}\.\d{2}\.\d{4}$") +# The artifacts that cannot use the padded rendering. Exactly one, and the +# reason is external: `packaging.sh` derives the extension's version and AMO +# refuses to sign a padded one. +AMO_UNPADDED = frozenset({"extension"}) + +# Mozilla's published grammar for addons.mozilla.org, transcribed from MDN's +# manifest.json/version page. A segment is the single digit `0` or starts 1-9, +# and there are at most four. This is the constraint the exception exists for, +# so it is what the exception is tested against — `2026.08.29.0201` fails it. +_AMO = re.compile(r"^(0|[1-9][0-9]{0,8})(\.(0|[1-9][0-9]{0,8})){0,3}$") + +# YYYY.M.D.HHMM — four segments, none of them zero-padded. +_UNPADDED = re.compile(r"^\d{4}(\.(0|[1-9]\d*)){3}$") + + +def segments(value: str) -> tuple[int, ...]: + """A version as the numbers it denotes, which is how rule 148 says to + compare one. `2026.08.29.0201` and `2026.8.29.201` are one value here.""" + return tuple(int(part) for part in value.split(".")) + # Everything here goes through artifacts.sh rather than importing a sibling # test module. That is the interface build.yml actually calls, so the tests @@ -126,7 +155,7 @@ def test_revision_is_a_legal_label_value_and_is_stable(artifact): assert first == revision(artifact), "revision is not stable across calls" -@pytest.mark.parametrize("artifact", ARTIFACTS) +@pytest.mark.parametrize("artifact", sorted(set(ARTIFACTS) - AMO_UNPADDED)) def test_version_is_zero_padded_calver(artifact): """The family shape, pinned. @@ -148,6 +177,31 @@ def test_version_is_zero_padded_calver(artifact): ) +@pytest.mark.parametrize("artifact", sorted(AMO_UNPADDED)) +def test_the_unpadded_artifacts_derive_something_amo_will_sign(artifact): + """The other half of the family shape: the documented exception, tested + against the constraint that justifies it rather than against itself. + + A padded value passes `_UNPADDED` on any date with no leading zeros, so + that pattern alone would let a regression sit unnoticed until the first + single-digit month — at which point the failure is a burned AMO version, + not a red lane. AMO's grammar is the assertion that fires immediately. + """ + value = artifacts("version", artifact).strip() + assert _AMO.match(value), ( + f"{artifact} derives {value!r}, which AMO refuses: a segment must be " + f"the single digit `0` or start 1-9, and there are at most four. " + f"Almost certainly a zero-padded segment — the family pads and this " + f"artifact must not (#3138). AMO 409s on re-signing, so a version it " + f"rejects is burned." + ) + assert _UNPADDED.match(value), ( + f"{artifact} derives {value!r}, which is not YYYY.M.D.HHMM. AMO would " + f"also accept the pre-318 `1.0.`, and that orders below every " + f"ext-2026.* release already signed." + ) + + @pytest.mark.parametrize("artifact", ARTIFACTS) def test_version_and_revision_describe_the_same_commit(artifact): """They are derived independently and must not be able to disagree. @@ -162,5 +216,21 @@ def test_version_and_revision_describe_the_same_commit(artifact): capture_output=True, text=True, check=True, cwd=ROOT, env={"TZ": "UTC", "PATH": os.environ.get("PATH", "")}, ).stdout.strip() - assert artifacts("version", artifact).strip() == stamped + derived = artifacts("version", artifact).strip() + + # Compared as NUMBERS, which is how rule 148 defines comparison and the + # only way one assertion can cover both renderings. This is what makes the + # extension's exception cosmetic rather than semantic: it must denote + # exactly the value its own commit stamps, whatever the padding. + assert segments(derived) == segments(stamped), ( + f"{artifact} derives {derived!r}, but its newest shipped commit " + f"{sha[:12]} is {stamped!r}. The instance would name one commit while " + f"carrying another's bytes." + ) + if artifact not in AMO_UNPADDED: + assert derived == stamped, ( + f"{artifact} derives {derived!r} where the family shape is " + f"{stamped!r} — same numbers, wrong rendering. Only the artifacts " + f"in AMO_UNPADDED may differ here." + ) assert sha.startswith(revision(artifact))