From 5771fd5770159a4291a157d083981d28496e5d0e Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 28 Aug 2026 16:35:26 -0400 Subject: [PATCH] build: zero-pad the derived version to YYYY.MM.DD.HHMM (318 step 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `2026.8.28.1249` becomes `2026.08.28.1249`. Note #3127 §1 and rule 148 both specify the padded form. The old reasoning was that each segment should read as a plain integer, and it never held — comparison strips leading zeros on parse anyway, which the same paragraph said. What stripping actually bought was this project emitting `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. Two obviously different formats would be safer than two nearly identical ones, and identical is safer still. Nothing already published is reordered: comparison is numeric per dot-segment, so `08` and `8` are equal. strip0 goes, and with it three of the four git calls per version — git's format-local takes the whole format string, and splitting it into pieces only ever existed to strip the padding between them. It also fixes a real edge the old helper mangled. A commit at 03:22 UTC derived `322` for its HHMM field, silently turning a four-digit field into three; it now derives `0322`. Verified against a real commit rather than reasoned about. Checked before relying on it, since step 8 feeds this to Firefox: the extension's comparator is `parseInt(n, 10)` with an explicit radix, so `08` reads as 8 and there is no octal hazard (rule 150). Two tests added. One pins the padded shape — the only thing keeping the family's projects emitting one string is an assertion that they do. The other asserts version and revision describe the same commit: they are derived independently, and a divergence would mean an instance naming one commit while carrying another's bytes, which is unfalsifiable from outside because both values still look well-formed. --- scripts/artifacts.sh | 40 +++++++++++++--------- tests/test_artifact_identity.py | 59 ++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 17 deletions(-) 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))