build: zero-pad the derived version to YYYY.MM.DD.HHMM (318 step 5)
Build images / sign-extension (push) Successful in 4s
CI / lint (push) Successful in 4s
CI / extension-version (push) Successful in 5s
Build images / build-ml (push) Successful in 4s
Build images / build-agent (push) Successful in 5s
Build images / build-web (push) Successful in 4s
CI / frontend-build (push) Successful in 17s
CI / backend-lint-and-test (push) Successful in 32s
CI / integration (push) Successful in 3m50s

`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.
This commit is contained in:
2026-08-28 16:35:26 -04:00
parent b3989d0224
commit 5771fd5770
2 changed files with 82 additions and 17 deletions
+24 -16
View File
@@ -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
+58 -1
View File
@@ -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))