diff --git a/scripts/artifacts.sh b/scripts/artifacts.sh index 0b19cb2..234b09d 100755 --- a/scripts/artifacts.sh +++ b/scripts/artifacts.sh @@ -100,13 +100,6 @@ fmt() { (cd "$ROOT" && TZ=UTC git show -s --format=%cd --date="format-local:$2" "$1") } -# Leading zeros stripped so every segment is a plain integer — some version -# validators reject `08`, and a leading zero buys nothing. `0000` (midnight) -# must survive as `0`, not as the empty string. -strip0() { - printf '%s' "$1" | sed -e 's/^0*//' -e 's/^$/0/' -} - # The IDENTITY of an artifact's content: the commit its shipped files last # changed in. This is what decides whether a build can be skipped. # @@ -124,17 +117,32 @@ cmd_revision() { echo "$(newest "$1")" | cut -d' ' -f2 | cut -c1-12 } -# The ORDERING KEY: full precision, YYYY.M.D.HHMM. Used by the extension, -# where the value is what Firefox compares to decide whether an update exists -# — two same-day builds MUST be distinguishable or the second never reaches -# anyone. +# The VERSION: `YYYY.MM.DD.HHMM`, zero-padded, UTC. One shape across the whole +# family (note #3127 §1, rule 148) — the number an instance reports about +# itself, and, with a `v` in front, the release tag naming the same build. +# +# Zero-padded since 2026-08-28. This stripped leading zeros until then, on the +# reasoning that every segment should read as a plain integer — which never +# held, since comparison strips them on parse anyway. Padding costs nothing, +# sorts lexically as well as numerically, and keeps this project emitting the +# same string as its siblings: unpadded, a `2026.8.28.1432` here sits beside a +# `2026.08.28.1432` there, two shapes one character apart. Two obviously +# different formats are safer than two nearly identical ones. +# +# Comparison is numeric per dot-segment, so `08` and `8` are equal and nothing +# already published is reordered by the change. +# +# HHMM is not decoration: it is what makes the value unique per build with no +# lookup. A date alone collides on the second build of a day, and resolving +# that needs a `.N` suffix, which needs asking the registry what already +# exists — at which point two lanes derive different answers for one source +# and the shared-signature property is lost. cmd_version() { sha=$(echo "$(newest "$1")" | cut -d' ' -f2) - printf '%s.%s.%s.%s\n' \ - "$(fmt "$sha" %Y)" \ - "$(strip0 "$(fmt "$sha" %m)")" \ - "$(strip0 "$(fmt "$sha" %d)")" \ - "$(strip0 "$(fmt "$sha" %H%M)")" + # One git call for the whole string rather than four and a sed. git's + # format-local takes the complete format, and doing it in pieces was only + # ever there to strip the padding between them. + fmt "$sha" '%Y.%m.%d.%H%M' } [ $# -ge 2 ] || usage diff --git a/tests/test_artifact_identity.py b/tests/test_artifact_identity.py index e234ab2..60d086f 100644 --- a/tests/test_artifact_identity.py +++ b/tests/test_artifact_identity.py @@ -1,4 +1,10 @@ -"""`artifacts.sh revision` is what decides whether a build gets skipped. +"""The two values `artifacts.sh` derives, and what each of them promises. + +`revision` decides whether a build gets skipped; `version` is what an instance +reports about itself and what a release tag is named after. Neither has a +consumer that would notice it going subtly wrong. + +## revision Milestone 318 step 3: each image carries its revision as an `fc.revision` label, and build.yml reads that label back off the moving channel tag. Equal @@ -21,12 +27,20 @@ Both ways of getting it wrong are silent: This module owns the narrower claim: whatever the path sets say, the revision is genuinely the commit those paths last changed in. +## version + +`YYYY.MM.DD.HHMM`, zero-padded, UTC — one shape across the family (note #3127 +§1, rule 148), so the string this project emits is the same string its siblings +emit. Two nearly-identical formats are more dangerous than two obviously +different ones, and the only thing keeping them identical is a test. + 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. """ from __future__ import annotations +import os import re import subprocess from pathlib import Path @@ -40,6 +54,9 @@ ARTIFACTS = ("web", "ml", "agent", "extension") # 12 hex chars — the prefix build.yml stamps and compares. _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}$") + # Everything here goes through artifacts.sh rather than importing a sibling # test module. That is the interface build.yml actually calls, so the tests @@ -107,3 +124,43 @@ def test_revision_is_a_legal_label_value_and_is_stable(artifact): first = revision(artifact) assert _REVISION.match(first), f"{first!r} is not a 12-char hex revision" assert first == revision(artifact), "revision is not stable across calls" + + +@pytest.mark.parametrize("artifact", ARTIFACTS) +def test_version_is_zero_padded_calver(artifact): + """The family shape, pinned. + + Padding was stripped until 2026-08-28 on the reasoning that each segment + should read as a plain integer — which never held, since comparison strips + leading zeros on parse anyway. What it did do was make this project emit + `2026.8.28.1432` while a sibling emitted `2026.08.28.1432`: two shapes one + character apart, which is the hard kind of difference to notice. + + Also catches the midnight case. A `%H%M` of `0322` must survive as `0322`; + the old strip-leading-zeros helper turned it into `322`, silently changing + a four-digit field into three. + """ + value = artifacts("version", artifact).strip() + assert _VERSION.match(value), ( + f"{artifact} derives {value!r}, which is not zero-padded " + f"YYYY.MM.DD.HHMM. Note #3127 §1 and rule 148 both specify the padded " + f"form, and a release tag is this string with a `v` in front." + ) + + +@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. + + A build reports the version and skips on the revision, so a divergence + would mean an instance naming one commit while carrying another's bytes — + unfalsifiable from outside, since both values look perfectly well-formed. + """ + sha = newest_by_commit_time(artifact) + stamped = subprocess.run( + ["git", "show", "-s", "--format=%cd", "--date=format-local:%Y.%m.%d.%H%M", sha], + 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 + assert sha.startswith(revision(artifact))