The build pushes exactly one tag (the channel's) and the repoint step writes every other tag registry-side. This only operates on main, so it has never actually run: on dev there is one tag and nothing to repoint. This merge is its first real exercise.build-web will rebuild (its revision moved), push :latest, then read it back and write :c-<sha> from it.
The accepted cost, restated so it is not rediscovered: :c-<sha> becomes a manifest index, so fc.revision does not resolve through it. Harmless — only the channel tag is ever inspected, and the reuse path already produced :c-<sha> that way.
Leading zeros — rejected. A segment is the single digit 0 or starts 1-9.
So the fallback the task pre-specified applies, extension only: the same numbers unpadded. 2026.8.29.201 where the family says 2026.08.29.0201. Rule 148 defines comparison as numeric per dot-segment, under which the two are equal — nothing published is reordered, and left-padding each segment recovers the family string exactly.
MAJOR.MINOR is gone with it: cmd_major_minor, cmd_patch and VERSION_EPOCH deleted, the committed version wholly inert, and ci.yml's agreement check retired rather than left checking a fact that stopped existing.
What this merge still has to show is the other half of milestone 271's prize: main derives the same number and reuses dev's signature with no second AMO call.
1a941e9 — the assertions step 8 had to move
tests/test_artifact_identity.py pinned zero-padded CalVer for all four artifacts and failed exactly as written. The exception is pinned to the constraint that justifies it — the unpadded artifact is asserted against AMO's own grammar, so a regression to padding fires immediately rather than waiting for the first single-digit month and surfacing as a burned AMO version.
Expected on merge
build-web rebuilds — revision moved to 2e0124238156.
Three commits, and the merge itself is the test for two of them.
## `41f2bec` — #3190 mitigation, unexercised until now
The build pushes exactly one tag (the channel's) and the repoint step writes every other tag registry-side. This only operates on `main`, so it has never actually run: on `dev` there is one tag and nothing to repoint. **This merge is its first real exercise.** `build-web` will rebuild (its revision moved), push `:latest`, then read it back and write `:c-<sha>` from it.
The accepted cost, restated so it is not rediscovered: `:c-<sha>` becomes a manifest index, so `fc.revision` does not resolve through it. Harmless — only the channel tag is ever inspected, and the reuse path already produced `:c-<sha>` that way.
## `2e01242` — milestone 318 step 8 (Scribe #3138)
The hold was an unanswered question about AMO, not a decision. Mozilla publishes the answer:
```
^(0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}$
```
* Four all-numeric segments — **accepted**.
* Leading zeros — **rejected**. A segment is the single digit `0` or starts 1-9.
So the fallback the task pre-specified applies, extension only: the same numbers unpadded. `2026.8.29.201` where the family says `2026.08.29.0201`. Rule 148 defines comparison as numeric per dot-segment, under which the two are equal — nothing published is reordered, and left-padding each segment recovers the family string exactly.
`MAJOR.MINOR` is gone with it: `cmd_major_minor`, `cmd_patch` and `VERSION_EPOCH` deleted, the committed version wholly inert, and ci.yml's agreement check retired rather than left checking a fact that stopped existing.
**Already verified on `dev`:**
| | |
|---|---|
| AMO signed the derived value | `ext-2026.8.29.1743`, XPI cached (31,364 bytes) |
| a later push re-signs? | no — `Sign via AMO` **skipped**, cache hit |
| #3156 moves web with the extension | both → `2e0124238156` |
| ml / agent | untouched, both skipped |
What this merge still has to show is the other half of milestone 271's prize: **`main` derives the same number and reuses `dev`'s signature with no second AMO call.**
## `1a941e9` — the assertions step 8 had to move
`tests/test_artifact_identity.py` pinned zero-padded CalVer for all four artifacts and failed exactly as written. The exception is pinned to the constraint that justifies it — the unpadded artifact is asserted against **AMO's own grammar**, so a regression to padding fires immediately rather than waiting for the first single-digit month and surfacing as a burned AMO version.
## Expected on merge
* `build-web` rebuilds — revision moved to `2e0124238156`.
* `build-ml`, `build-agent` skip — `bce894ba2499` / `57e52433d03a` unchanged.
* `sign-extension` hits the `ext-2026.8.29.1743` cache; **no AMO call**.
* Plain merge commit, per rule 153, so the pathspec prunes it and no derived version moves.
Scribe #3138, #3190, #3156.
buildx on this runner pushes the first tag to the registry and then
re-pushes the remaining ones through the DOCKER driver, reading them out
of a local image store that a registry-direct build never populated:
#27 pushing …/fabledcurator:latest DONE 15.8s
#28 pushing …/fabledcurator:c-0e15c44 with docker
#28 ERROR: tag does not exist: …:c-0e15c44
It is intermittent — build-ml made the identical two-tag push seconds
later in the same run and succeeded — and the consequence is worse than
the red job suggests. `:latest` had already published, so production was
correct while the immutable rollback tag rule 145 requires of every main
push simply did not exist. Nothing else would ever have noticed: a
missing :c-<sha> has no consumer that fails, so it surfaces at the moment
somebody needs to roll back, which is the worst time to learn a rollback
target was never written.
So the build now pushes exactly one ref — the channel's — and the
existing repoint step, which already excluded the source tag and already
ran on every reuse, now runs on the build path too and owns every other
tag. `imagetools create` is a registry-side manifest copy: no local
daemon, nothing that can be absent. This adds no new code path; it puts
the build case onto the one that was already proven.
Chosen over the alternative of asserting each tag resolves after the
build, which would have made the failure loud without making it rarer.
The cost, accepted: `imagetools create` wraps its source in an index, so
:c-<sha> is an index rather than a plain image and fc.revision does not
resolve through it. Nothing reads that label off :c-<sha> — the reuse
check only ever inspects the CHANNEL tag — and the index names the same
manifest, so a pull is byte-identical. The reuse path already produced
:c-<sha> this way; this only makes it uniform.
`build_tags` goes with it — the tag list now has exactly one consumer.
`1.0.<minutes since 2020>` -> `YYYY.M.D.HHMM` UTC, from the commit time of
the newest change to a packaged extension file. Same clock and same commit as
before; readable instead of opaque, and the same value the rest of the family
derives.
The hold on this step was two questions about AMO, and Mozilla's own docs
answer both:
^(0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}$
1. four all-numeric segments -> ACCEPTED ({0,3} more after the first).
2. leading zeros -> REJECTED. A segment is the single digit
`0` or starts 1-9, so `08` and `0201` are refused. MDN says it in prose
too: "Non-zero numbers must not include a leading zero."
So the documented fallback applies, extension only: the same numbers rendered
without the family's zero-padding. `2026.08.29.0201` and `2026.8.29.201` are
one value in two renderings — rule 148 defines comparison as numeric per
segment, under which they are equal — so nothing already published is
reordered, and left-padding each segment recovers the family string exactly.
HHMM stays one segment because AMO allows at most four.
The transition is safe in the other direction too: 2026 > 1, so every CalVer
outranks every published 1.0.x. build.yml's downgrade guard confirms it.
Also in scope:
* MAJOR.MINOR is gone. `cmd_major_minor`, `cmd_patch` and VERSION_EPOCH go
with it, the committed version in manifest.json / package.json is now
wholly inert, and ci.yml's MAJOR.MINOR-agreement check is retired rather
than left running beside a fact that stopped existing (rule 22).
* ci.yml's `extension-version` lane now asserts Mozilla's regex verbatim
instead of a loose `^[0-9]+(\.[0-9]+)*$` — which would have passed the
padded shape. It also asserts YYYY.M.D.HHMM, because AMO would accept a
regression to `1.0.<minutes>` while that orders below everything signed
since. Checking here is the point: AMO 409s on re-signing, so a version it
rejects is burned and cannot be reused.
* `artifacts.sh version extension` delegates to packaging.sh, so the two
cannot answer differently. The direction matches the existing one —
artifacts.sh already asks packaging.sh for the extension's path set.
#3156 is what makes this commit safe to make: packaging.sh is in web's path
set, so the web revision moves with the extension version and build-web
rebuilds instead of republishing an image bundling the previous XPI.
Scribe #3138.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Missed in 2e01242. This module pinned zero-padded `YYYY.MM.DD.HHMM` for all
four artifacts, which is the family shape and was right until the extension
acquired a documented reason not to use it. Both assertions failed exactly as
written, on the value they were written to catch.
Rather than exempt the extension, the exception is pinned to the constraint
that justifies it:
* `test_version_is_zero_padded_calver` now covers the three padded artifacts.
* A new sibling covers the unpadded one against **AMO's own grammar** —
`2026.08.29.0201` fails it, so a regression to padding fires immediately.
Matching only `YYYY.M.D.HHMM` would not: on a date with no leading zeros the
two renderings are the same string, so a padding regression would sit unseen
until the first single-digit month, and surface as a burned AMO version
rather than a red lane.
* `test_version_and_revision_describe_the_same_commit` compares NUMBERS, per
rule 148's own definition of comparison — so one assertion covers both
renderings and says the real thing: whatever the padding, the extension must
denote exactly the value its own commit stamps. Exact-string equality is
still asserted for everything not in AMO_UNPADDED, so the exception cannot
quietly spread.
Scribe #3138.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Three commits, and the merge itself is the test for two of them.
41f2bec— #3190 mitigation, unexercised until nowThe build pushes exactly one tag (the channel's) and the repoint step writes every other tag registry-side. This only operates on
main, so it has never actually run: ondevthere is one tag and nothing to repoint. This merge is its first real exercise.build-webwill rebuild (its revision moved), push:latest, then read it back and write:c-<sha>from it.The accepted cost, restated so it is not rediscovered:
:c-<sha>becomes a manifest index, sofc.revisiondoes not resolve through it. Harmless — only the channel tag is ever inspected, and the reuse path already produced:c-<sha>that way.2e01242— milestone 318 step 8 (Scribe #3138)The hold was an unanswered question about AMO, not a decision. Mozilla publishes the answer:
0or starts 1-9.So the fallback the task pre-specified applies, extension only: the same numbers unpadded.
2026.8.29.201where the family says2026.08.29.0201. Rule 148 defines comparison as numeric per dot-segment, under which the two are equal — nothing published is reordered, and left-padding each segment recovers the family string exactly.MAJOR.MINORis gone with it:cmd_major_minor,cmd_patchandVERSION_EPOCHdeleted, the committed version wholly inert, and ci.yml's agreement check retired rather than left checking a fact that stopped existing.Already verified on
dev:ext-2026.8.29.1743, XPI cached (31,364 bytes)Sign via AMOskipped, cache hit2e0124238156What this merge still has to show is the other half of milestone 271's prize:
mainderives the same number and reusesdev's signature with no second AMO call.1a941e9— the assertions step 8 had to movetests/test_artifact_identity.pypinned zero-padded CalVer for all four artifacts and failed exactly as written. The exception is pinned to the constraint that justifies it — the unpadded artifact is asserted against AMO's own grammar, so a regression to padding fires immediately rather than waiting for the first single-digit month and surfacing as a burned AMO version.Expected on merge
build-webrebuilds — revision moved to2e0124238156.build-ml,build-agentskip —bce894ba2499/57e52433d03aunchanged.sign-extensionhits theext-2026.8.29.1743cache; no AMO call.Scribe #3138, #3190, #3156.
`1.0.<minutes since 2020>` -> `YYYY.M.D.HHMM` UTC, from the commit time of the newest change to a packaged extension file. Same clock and same commit as before; readable instead of opaque, and the same value the rest of the family derives. The hold on this step was two questions about AMO, and Mozilla's own docs answer both: ^(0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}$ 1. four all-numeric segments -> ACCEPTED ({0,3} more after the first). 2. leading zeros -> REJECTED. A segment is the single digit `0` or starts 1-9, so `08` and `0201` are refused. MDN says it in prose too: "Non-zero numbers must not include a leading zero." So the documented fallback applies, extension only: the same numbers rendered without the family's zero-padding. `2026.08.29.0201` and `2026.8.29.201` are one value in two renderings — rule 148 defines comparison as numeric per segment, under which they are equal — so nothing already published is reordered, and left-padding each segment recovers the family string exactly. HHMM stays one segment because AMO allows at most four. The transition is safe in the other direction too: 2026 > 1, so every CalVer outranks every published 1.0.x. build.yml's downgrade guard confirms it. Also in scope: * MAJOR.MINOR is gone. `cmd_major_minor`, `cmd_patch` and VERSION_EPOCH go with it, the committed version in manifest.json / package.json is now wholly inert, and ci.yml's MAJOR.MINOR-agreement check is retired rather than left running beside a fact that stopped existing (rule 22). * ci.yml's `extension-version` lane now asserts Mozilla's regex verbatim instead of a loose `^[0-9]+(\.[0-9]+)*$` — which would have passed the padded shape. It also asserts YYYY.M.D.HHMM, because AMO would accept a regression to `1.0.<minutes>` while that orders below everything signed since. Checking here is the point: AMO 409s on re-signing, so a version it rejects is burned and cannot be reused. * `artifacts.sh version extension` delegates to packaging.sh, so the two cannot answer differently. The direction matches the existing one — artifacts.sh already asks packaging.sh for the extension's path set. #3156 is what makes this commit safe to make: packaging.sh is in web's path set, so the web revision moves with the extension version and build-web rebuilds instead of republishing an image bundling the previous XPI. Scribe #3138. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>