Completes milestone 318 apart from step 8, which is held on AMO.
What lands
5771fd5
Step 5 — zero-pad the derived version to YYYY.MM.DD.HHMM. One shape across the family (rule 148); also fixes a midnight case where an HHMM of 0322 became 322.
bce894b
Step 6 — the instance reports which build it is. FC_VERSION build arg → /api/health → a dim line at the foot of Settings.
b6b9fd8
Step 7 — a v* tag publishes a changelog and builds nothing.
a3071a7
#3202 — scripts/artifacts.sh decides FC_VERSION and was in no artifact's path set, so a change to the derivation could not invalidate the image carrying it.
d38585e
Step 9 — docs, workflow comments and note #3127 trued up against what the pipeline actually publishes.
Two things worth reading before merging
The version footer becomes the only answer to "which build is this?" With no version image tags (step 4, already on main), nothing in the registry carries the number. That is why step 6 keeps three display states distinct — unasked renders nothing, asked-and-unknown renders unknown, and a failed health call is neither. A blank footer would read as "this build has no version", which is a different and false claim with no second source to contradict it.
#3202 is #3156 one level up, and the asymmetry is the interesting part. Every artifact stamps fc.revision, but only web also stamps a version. A revision has a backstop — change how it is computed and the derived value stops matching the published label, forcing a rebuild. A version is compared against nothing. So the deriver goes in web's path set and nowhere else; adding ml and agent would be the "too wide" error artifacts.sh warns about in its own header.
Verification
All five commits green on dev (runs 4783–4785, 4798–4800, 4815–4816, 4827–4829).
Measured behaviour rather than asserted:
Run 4783 stamped FC_CHANNEL=dev FC_VERSION=2026.08.28.2208 --label fc.revision=bce894ba2499 in one command line, and in the same run web rebuilt, ml rebuilt and agent skipped — per-artifact derivation working on one commit.
Run 4815 read fc.revision=bce894ba2499 off :dev against a derived a3071a754907, missed, and rebuilt web only — #3202's fix demonstrating itself, with ml and agent reusing in 5s and 6s.
Run 4827, the docs commit, re-versioned nothing and reused all three in ~10 seconds.
Not covered
The Settings footer has not been eyeballed in a browser. Store logic and API shape are tested (6 frontend, 3 backend); the template binding is not.
release.yml's publish path has never run — no v* tag has been pushed since it landed. Everything up to the HTTP call is exercised by --dry-run in CI (7 tests). The first real tag is the first exercise of the last step.
After this
Milestone 318 is 8 of 9. Step 8 (extension CalVer) stays held until AMO is confirmed to accept a four-part all-numeric version with leading zeros — checked via web-ext lint or a throwaway addon id, never by attempting the real sign.
Completes milestone 318 apart from step 8, which is held on AMO.
## What lands
| | |
|---|---|
| `5771fd5` | **Step 5** — zero-pad the derived version to `YYYY.MM.DD.HHMM`. One shape across the family (rule 148); also fixes a midnight case where an `HHMM` of `0322` became `322`. |
| `bce894b` | **Step 6** — the instance reports which build it is. `FC_VERSION` build arg → `/api/health` → a dim line at the foot of Settings. |
| `b6b9fd8` | **Step 7** — a `v*` tag publishes a changelog and builds nothing. |
| `a3071a7` | **#3202** — `scripts/artifacts.sh` decides `FC_VERSION` and was in no artifact's path set, so a change to the derivation could not invalidate the image carrying it. |
| `d38585e` | **Step 9** — docs, workflow comments and note #3127 trued up against what the pipeline actually publishes. |
## Two things worth reading before merging
**The version footer becomes the only answer to "which build is this?"** With no version image tags (step 4, already on `main`), nothing in the registry carries the number. That is why step 6 keeps three display states distinct — unasked renders nothing, asked-and-unknown renders `unknown`, and a failed health call is neither. A blank footer would read as "this build has no version", which is a different and false claim with no second source to contradict it.
**#3202 is #3156 one level up, and the asymmetry is the interesting part.** Every artifact stamps `fc.revision`, but only web also stamps a version. A revision has a backstop — change how it is computed and the derived value stops matching the published label, forcing a rebuild. A version is compared against nothing. So the deriver goes in web's path set and nowhere else; adding ml and agent would be the "too wide" error `artifacts.sh` warns about in its own header.
## Verification
All five commits green on `dev` (runs 4783–4785, 4798–4800, 4815–4816, 4827–4829).
Measured behaviour rather than asserted:
- Run 4783 stamped `FC_CHANNEL=dev FC_VERSION=2026.08.28.2208 --label fc.revision=bce894ba2499` in one command line, and in the same run web rebuilt, ml rebuilt and agent skipped — per-artifact derivation working on one commit.
- Run 4815 read `fc.revision=bce894ba2499` off `:dev` against a derived `a3071a754907`, missed, and rebuilt **web only** — #3202's fix demonstrating itself, with ml and agent reusing in 5s and 6s.
- Run 4827, the docs commit, re-versioned nothing and reused all three in ~10 seconds.
## Not covered
- The Settings footer has not been eyeballed in a browser. Store logic and API shape are tested (6 frontend, 3 backend); the template binding is not.
- `release.yml`'s publish path has never run — no `v*` tag has been pushed since it landed. Everything up to the HTTP call is exercised by `--dry-run` in CI (7 tests). The first real tag is the first exercise of the last step.
## After this
Milestone 318 is 8 of 9. Step 8 (extension CalVer) stays held until AMO is confirmed to accept a four-part all-numeric version with leading zeros — checked via `web-ext lint` or a throwaway addon id, never by attempting the real sign.
`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.
A dim line at the foot of Settings: `FabledCurator 2026.08.28.1249 · dev`.
This is no longer a convenience. Milestone 318 stopped publishing version
image tags, so an instance's own report is the ONLY answer to "which build is
this?" — there is no registry name left to check it against. Note #3127 §5
says it directly: a wrong answer here has no second source to contradict it.
Three states, kept distinct because collapsing any two of them lies:
not asked yet render nothing
asked, no version render "unknown"
asked, has a version render it
A blank footer reads as "no version", which is a different claim from "I
cannot say". And a failed health call deliberately does NOT mark the build
loaded — a network blip says nothing about the image, and presenting it as
"unknown" would look like a defective build.
Carried on /api/health rather than a new route: it answers at the same cost
(two module constants, no I/O) and TopNav already fetches it app-wide, so a
separate endpoint would mean a second request for two strings.
Both fields are OMITTED when unset rather than sent empty. Absence already
means "cannot say" — an image predating the field says exactly that by not
having the key — so a second spelling would make every reader special-case
it. The pre-existing test asserting the body is EXACTLY {"status": "ok"} is
what keeps a well-meaning `or ""` default from creeping in.
FC_CHANNEL now has one definition. It was read from the environment in
extension.py and would have been read again here; the new build_info module
holds both, and extension.py binds it as a module-level name so existing
tests monkeypatch it exactly as before. Separate from config.py on purpose:
those are operator settings meant to be changed, these describe the artifact.
Channel sits beside the version, never inside it (rule 149), asserted from
both ends. A `-dev` suffix would read as a 0 segment to the extension's
parseInt comparator and make every dev build compare equal — #2993 exactly.
Not hidden, per the operator and §7: the JS bundle and asset hashes
fingerprint the build anyway, and "I'm on 2026.08.28.1249" is the single most
useful line in a bug report.
Step 2 took the build consequence away from a `v*` tag — `main` has already
built and published the commit by the time anyone tags it, and rebuilding
would re-push `:c-<sha>`, which rule 145 forbids even when the source
matches. That left the tag with nothing to do at all.
This is the job it has instead. Step 6 put the derived version in the
Settings footer, so an operator can say WHICH build they are running; this
says what is in it that was not in the one they ran last month. Both halves
of one question (note #3127 §5).
The previous release is found by walking ANCESTRY, not by sorting a list.
That is load-bearing here specifically: rule 148 moved the tag shape from
`v26.05.22.0` to `v2026.08.28.2208`, and lexicographically `v2026...` sorts
BEFORE `v26...` — the third character is `0` against `6`. A sorted
implementation would reach back past every new-shape tag to the newest
old-shape one and publish months of commits as "changes since", looking
entirely correct while doing it. `git describe --exclude` is immune to the
shape change, and reachability is the more honest question anyway.
The publisher GETs and PATCHes rather than POSTing and recovering the id
from a 409 — note #3127 §6.7, which is ThoughtSync #2182's bug. A `v*` tag
is created once so the conflict path is rare, but "rare" is how that one
survived to be found somewhere else.
Cross-checks are reported on the release, not enforced. The tag is already
pushed by the time this runs, so failing would leave the operator with a tag,
no release, and a red lane to explain it — while the release is still the
useful object. It says so at the top when the tag names a version the web
image does not report, or when the commit is not on `main` and the `:c-`
rollback refs it lists were never published.
Nothing runs on a schedule and nothing auto-tags on merge. Release tags are
bookmarks (note #3127 §0); FC went twelve weeks without one and nothing was
wrong.
Also here:
- `scripts/` joins the ruff lane. release_notes.py runs only on a tag push,
so a syntax error there would otherwise surface at the one moment nobody
wants to be debugging a workflow.
- version.spec.js reads the workflow directory instead of listing three
files by hand. Its own comment says the assertion should survive consumers
coming and going; the hardcoded list was the part that could not, and
release.yml would have joined the directory without joining the check.
Tests build a synthetic history spanning the tag-shape change rather than
leaning on this repo's tags, so the span assertion holds whether or not a
checkout brought the tags along — a span test that quietly skips is worse
than one that fails.
`scripts/artifacts.sh` decides both values the web image carries — the
`fc.revision` label the reuse check compares and the `FC_VERSION` baked into
the image — and was in no artifact's path set. So a change to `cmd_version`
alone left every revision untouched, the reuse check hit, the build was
skipped, and the published image went on reporting the OLD version format,
indefinitely, until some unrelated commit forced a rebuild. Nothing goes red;
the footer just shows a well-formed string of the wrong shape.
Milestone 318 step 5 is the worked instance:
b3989d0 -> rev=fb2c4d5b80be ver=2026.8.28.1249
5771fd5 -> rev=fb2c4d5b80be ver=2026.08.28.1249
bce894b -> rev=bce894ba2499 ver=2026.08.28.2208
Same revision across the zero-pad commit, so web's build was skipped. It cost
nothing only by timing: FC_VERSION did not exist until step 6 landed one
commit later.
Web only, and that is the interesting part. Every artifact stamps a revision,
but only web also stamps a version. A revision-only artifact needs no entry
here, because changing how a revision is COMPUTED changes the derived value,
which then disagrees with the label on the published image and forces a
rebuild — the mechanism self-corrects, since it compares against a string
stamped into a real artifact. Nothing compares a version to anything. That
asymmetry is why this was invisible and is now written down in both files.
Named as a file rather than `scripts`: release_notes.py sits beside it and
only reads derived values, so it decides nothing and must not re-version web.
This is #3156 one level up — packaging.sh excluded from the version it
derives — so the guard is generalised rather than duplicated: one DERIVERS
table naming each deriver and the artifacts whose identity it decides. The
too-wide test gains a note saying where the line is, since "copied into no
image" no longer settles it on its own.
A doc describing a tag scheme the pipeline stopped using is worse than no
doc — it is a confident wrong answer, and #3159 named it as the drift that
had just bitten rule 148.
build.yml (each edit ×3, once per build job):
* The derived-values block claimed three values including a `tag`
subcommand that no longer exists, and said "on dev the date tag is
computed nowhere else". There are two values and no date tag.
* The BASE-IMAGE FRESHNESS paragraph argued from a pin that no longer
exists — "a date tag has to keep serving the bytes it served
(fabledcurator:2026.7.17 still resolves to July's image)". Milestone 318
removed the argument's premise rather than answering it: with no version
tags there is no immutable name a refresh could contradict, and rule 145
already allows a moving tag to be republished. Deleted the argument,
kept a line saying why there is nothing to argue about (#3159's phrasing:
the workflow becomes obviously correct instead of carefully correct).
* The repoint comment said ":c-<sha> and the date pin". There is no date pin.
* "its set is too narrow — the direction that serves stale bytes on a pin"
now names the failure that actually exists: the reuse check hits and the
channel serves a web image bundling the previous XPI (#3156).
README gains a "Versions and tags" section — the three tags, why there is
no fourth, and the fact that with no registry name carrying it, the
instance's own report is the only answer to "which build is this?". Also
fixes two stale claims: three workflows (four since release.yml) and "AMO
signing runs on main only", which has been false since milestone 271
step 6 signed on both channels.
ci-requirements.md was missing milestones 313 and 318 entirely: no
artifacts.sh, no label-keyed reuse, no FC_VERSION. Added those, plus the
#3156/#3202 membership test (can changing this file change the published
bytes — not is it copied in), and corrected packaging.sh's "two consumers"
to three now that artifacts.sh reads it.
docker-compose.yml needed nothing — it names `:dev` directly and never
offered a pin target.
Note #3127 and rule 145's relation notes are updated in Scribe.
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.
Completes milestone 318 apart from step 8, which is held on AMO.
What lands
5771fd5YYYY.MM.DD.HHMM. One shape across the family (rule 148); also fixes a midnight case where anHHMMof0322became322.bce894bFC_VERSIONbuild arg →/api/health→ a dim line at the foot of Settings.b6b9fd8v*tag publishes a changelog and builds nothing.a3071a7scripts/artifacts.shdecidesFC_VERSIONand was in no artifact's path set, so a change to the derivation could not invalidate the image carrying it.d38585eTwo things worth reading before merging
The version footer becomes the only answer to "which build is this?" With no version image tags (step 4, already on
main), nothing in the registry carries the number. That is why step 6 keeps three display states distinct — unasked renders nothing, asked-and-unknown rendersunknown, and a failed health call is neither. A blank footer would read as "this build has no version", which is a different and false claim with no second source to contradict it.#3202 is #3156 one level up, and the asymmetry is the interesting part. Every artifact stamps
fc.revision, but only web also stamps a version. A revision has a backstop — change how it is computed and the derived value stops matching the published label, forcing a rebuild. A version is compared against nothing. So the deriver goes in web's path set and nowhere else; adding ml and agent would be the "too wide" errorartifacts.shwarns about in its own header.Verification
All five commits green on
dev(runs 4783–4785, 4798–4800, 4815–4816, 4827–4829).Measured behaviour rather than asserted:
FC_CHANNEL=dev FC_VERSION=2026.08.28.2208 --label fc.revision=bce894ba2499in one command line, and in the same run web rebuilt, ml rebuilt and agent skipped — per-artifact derivation working on one commit.fc.revision=bce894ba2499off:devagainst a deriveda3071a754907, missed, and rebuilt web only — #3202's fix demonstrating itself, with ml and agent reusing in 5s and 6s.Not covered
release.yml's publish path has never run — nov*tag has been pushed since it landed. Everything up to the HTTP call is exercised by--dry-runin CI (7 tests). The first real tag is the first exercise of the last step.After this
Milestone 318 is 8 of 9. Step 8 (extension CalVer) stays held until AMO is confirmed to accept a four-part all-numeric version with leading zeros — checked via
web-ext lintor a throwaway addon id, never by attempting the real sign.