feat(extension): derive the version as unpadded CalVer (milestone 318 step 8)
Build images / build-ml (push) Successful in 4s
CI / lint (push) Successful in 4s
Build images / build-agent (push) Successful in 5s
CI / extension-version (push) Successful in 4s
CI / frontend-build (push) Successful in 22s
extension / lint (push) Successful in 22s
CI / backend-lint-and-test (push) Failing after 33s
Build images / sign-extension (push) Successful in 2m24s
Build images / build-web (push) Successful in 2m38s
CI / integration (push) Successful in 5m15s
Build images / build-ml (push) Successful in 4s
CI / lint (push) Successful in 4s
Build images / build-agent (push) Successful in 5s
CI / extension-version (push) Successful in 4s
CI / frontend-build (push) Successful in 22s
extension / lint (push) Successful in 22s
CI / backend-lint-and-test (push) Failing after 33s
Build images / sign-extension (push) Successful in 2m24s
Build images / build-web (push) Successful in 2m38s
CI / integration (push) Successful in 5m15s
`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>
This commit is contained in:
@@ -8,10 +8,10 @@ const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const read = (name) => JSON.parse(readFileSync(path.join(EXT_DIR, name), 'utf8'))
|
||||
const readText = (...seg) => readFileSync(path.join(EXT_DIR, ...seg), 'utf8')
|
||||
|
||||
// Only the git-free subcommands are exercised here: `version`/`patch` shell out
|
||||
// to git, and the extension lane runs on node:24-bookworm-slim which may not
|
||||
// ship it. Those two are covered where git is guaranteed — ci.yml and build.yml
|
||||
// run on ci-python.
|
||||
// Only the git-free subcommands are exercised here: `version` shells out to
|
||||
// git, and the extension lane runs on node:24-bookworm-slim which may not ship
|
||||
// it. That one is covered where git is guaranteed — ci.yml's extension-version
|
||||
// lane and build.yml both run on ci-python.
|
||||
const packaging = (cmd) =>
|
||||
execFileSync('sh', [path.join(EXT_DIR, 'scripts', 'packaging.sh'), cmd], {
|
||||
cwd: EXT_DIR,
|
||||
@@ -145,28 +145,33 @@ describe('consumers delegate rather than keeping their own copy', () => {
|
||||
})
|
||||
|
||||
describe('extension version', () => {
|
||||
const majorMinor = (v) => v.split('.').slice(0, 2).join('.')
|
||||
// Mozilla's published grammar for addons.mozilla.org, transcribed from MDN's
|
||||
// manifest.json/version page. Each segment is the single digit 0 or starts
|
||||
// 1-9 — so no leading zeros — and there are at most four of them.
|
||||
const AMO = /^(0|[1-9][0-9]{0,8})(\.(0|[1-9][0-9]{0,8})){0,3}$/
|
||||
|
||||
it('keeps the hand-set MAJOR.MINOR in lockstep across both files', () => {
|
||||
// Narrowed from full-string equality at milestone 271 step 5. Since step 4
|
||||
// the patch component is derived from commit time and stamped into both
|
||||
// files at build time, so the committed patch numbers are inert — nothing
|
||||
// reads them and they are not what ships. Asserting on them would fail for
|
||||
// a difference that changes nothing.
|
||||
it('keeps a committed version AMO would accept, though it ships nothing', () => {
|
||||
// The committed value is wholly inert since milestone 318 step 8: there is
|
||||
// no hand-set MAJOR.MINOR left for packaging.sh to read, and build.yml
|
||||
// stamps the derived string over both files before web-ext sees them.
|
||||
//
|
||||
// MAJOR.MINOR is the opposite: still hand-set, still shipped, and
|
||||
// packaging.sh reads it from manifest.json ALONE. Let the two diverge and
|
||||
// the extension ships a version package.json disagrees with, with no other
|
||||
// signal.
|
||||
expect(majorMinor(read('manifest.json').version))
|
||||
.toBe(majorMinor(read('package.json').version))
|
||||
// It is still asserted, for one reason: `npm run build` locally packages
|
||||
// whatever is committed, so a value AMO would reject turns a local build
|
||||
// into a confusing failure with no CI signal ahead of it. ci.yml checks
|
||||
// the same grammar against the DERIVED value, which is the one AMO sees.
|
||||
for (const file of ['manifest.json', 'package.json']) {
|
||||
expect(read(file).version, `${file} version is not AMO-shaped`).toMatch(AMO)
|
||||
}
|
||||
})
|
||||
|
||||
it('uses a plain dotted numeric version AMO will accept', () => {
|
||||
// The committed value seeds MAJOR.MINOR, so it still has to parse even
|
||||
// though its patch component never ships. ci.yml asserts the same shape on
|
||||
// the DERIVED value, which is the one AMO actually sees.
|
||||
expect(read('package.json').version).toMatch(/^\d+(\.\d+)*$/)
|
||||
it('rejects the zero-padded family shape, which is why the extension unpads', () => {
|
||||
// Guards the reason for the exception, not just its result. If this ever
|
||||
// starts passing, someone has loosened the pattern and the next sign burns
|
||||
// an AMO version to find out. (#3138.)
|
||||
expect('2026.08.29.0201').not.toMatch(AMO)
|
||||
expect('2026.8.29.201').toMatch(AMO)
|
||||
// Five segments: AMO allows four.
|
||||
expect('2026.8.29.2.1').not.toMatch(AMO)
|
||||
})
|
||||
|
||||
it('declares manifest v3', () => {
|
||||
|
||||
Reference in New Issue
Block a user