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:
@@ -75,18 +75,23 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
# The version is DERIVED, not read from the repo (milestone 271 step 4,
|
||||
# cut over 2026-08-27). `packaging.sh version` returns MAJOR.MINOR from
|
||||
# manifest.json plus a patch component that is the commit TIME of the
|
||||
# newest change to a PACKAGED extension file, in minutes since
|
||||
# 2020-01-01 — family rule 149, never a commit count, which orders by
|
||||
# cut over 2026-08-27). `packaging.sh version` returns `YYYY.M.D.HHMM`
|
||||
# UTC — the commit TIME of the newest change to a PACKAGED extension
|
||||
# file, per family rule 148/149. Never a commit count, which orders by
|
||||
# branch rather than by recency.
|
||||
#
|
||||
# The committed "version" in manifest.json / package.json no longer
|
||||
# decides anything: the stamp step below overwrites it in the working
|
||||
# tree before web-ext ever reads it. It is deliberately NOT committed
|
||||
# back — the commit carrying the bump would itself be a change to the
|
||||
# extension and would move the version again. The repo holds the source;
|
||||
# the build derives the label.
|
||||
# Unpadded, and only here: AMO's grammar rejects a leading zero, so the
|
||||
# extension renders rule 148's numbers without the family's padding
|
||||
# (milestone 318 step 8). Same value, one character narrower per segment;
|
||||
# ci.yml's extension-version lane checks the string against Mozilla's
|
||||
# published regex before this job ever calls AMO.
|
||||
#
|
||||
# The committed "version" in manifest.json / package.json decides NOTHING
|
||||
# — not even a MAJOR.MINOR prefix, which step 8 removed. The stamp step
|
||||
# below overwrites it in the working tree before web-ext ever reads it,
|
||||
# and it is deliberately NOT committed back: the commit carrying the bump
|
||||
# would itself be a change to the extension and would move the version
|
||||
# again. The repo holds the source; the build derives the label.
|
||||
- name: Derive extension version
|
||||
id: extver
|
||||
run: |
|
||||
|
||||
+35
-21
@@ -2,7 +2,7 @@ name: CI
|
||||
|
||||
# CI lanes per FabledRulebook/forgejo.md "CI philosophy":
|
||||
# - lint: ruff only, no dep install — fast-fail for the common lint bounce.
|
||||
# - extension-version: the derived version resolves and MAJOR.MINOR agrees.
|
||||
# - extension-version: the derived version resolves and is a shape AMO takes.
|
||||
# - backend-lint-and-test: `pytest -m "not integration"`, no service containers.
|
||||
# - frontend-build: vitest unit + vite build.
|
||||
# - integration: pgvector + redis service containers; alembic + `pytest -m integration`.
|
||||
@@ -58,9 +58,12 @@ jobs:
|
||||
# the extension.yml suite runs on node:24-slim, which is exactly why
|
||||
# version.spec.js sticks to packaging.sh's git-free subcommands.
|
||||
# 1. the derivation actually resolves on this commit
|
||||
# 2. MAJOR.MINOR agrees between the two files — the one part still hand-set,
|
||||
# and packaging.sh reads it from manifest.json ALONE, so a divergence
|
||||
# ships a version package.json disagrees with
|
||||
# 2. the derived string is one AMO will accept, checked against Mozilla's
|
||||
# own published grammar rather than a loose "digits and dots"
|
||||
#
|
||||
# The MAJOR.MINOR-agreement check that used to be (2) is gone with milestone
|
||||
# 318 step 8: the committed version no longer seeds anything, so there is no
|
||||
# hand-set part left for the two files to disagree about.
|
||||
#
|
||||
# Deliberately NOT checked here: that the derived value beats what has already
|
||||
# been signed. That guard belongs in build.yml, where it compares against the
|
||||
@@ -85,27 +88,38 @@ jobs:
|
||||
# busybox sh on the act_runner — no bashisms (family rule).
|
||||
VERSION=$(sh extension/scripts/packaging.sh version)
|
||||
echo "derived: $VERSION"
|
||||
# The shape AMO accepts, and the shape build.yml will stamp.
|
||||
if ! echo "$VERSION" | grep -qE '^[0-9]+(\.[0-9]+)*$'; then
|
||||
echo "ERROR: derived version '$VERSION' is not plain dotted-numeric."
|
||||
echo "AMO would reject it, and build.yml stamps it verbatim."
|
||||
|
||||
# Mozilla's published grammar for AMO, transcribed verbatim from
|
||||
# MDN's manifest.json/version page:
|
||||
#
|
||||
# ^(0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}$
|
||||
#
|
||||
# Not the looser `^[0-9]+(\.[0-9]+)*$` this lane used to carry. That
|
||||
# one passes `2026.08.29.0201`, which AMO REJECTS — a segment must be
|
||||
# the single digit 0 or start 1-9 — and it also passes five segments,
|
||||
# where AMO allows four. Both would surface as a failed sign with the
|
||||
# version already burned: AMO 409s on re-signing, so a rejected value
|
||||
# cannot be reclaimed and cannot be reused. This lane is the cheap
|
||||
# place to find out. (#3138, milestone 318 step 8.)
|
||||
if ! echo "$VERSION" | grep -qE '^(0|[1-9][0-9]{0,8})(\.(0|[1-9][0-9]{0,8})){0,3}$'; then
|
||||
echo "ERROR: derived version '$VERSION' is not a version AMO accepts."
|
||||
echo "AMO's grammar: ^(0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}$"
|
||||
echo "Most likely cause: a zero-padded segment (08, 0201). The rest"
|
||||
echo "of the family pads; the extension must not — see packaging.sh."
|
||||
exit 1
|
||||
fi
|
||||
mm() { grep -E '"version"' "$1" | head -1 | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([0-9]+\.[0-9]+).*/\1/'; }
|
||||
MAN=$(mm extension/manifest.json)
|
||||
PKG=$(mm extension/package.json)
|
||||
test -n "$MAN" || { echo "ERROR: no parseable version in extension/manifest.json"; exit 1; }
|
||||
test -n "$PKG" || { echo "ERROR: no parseable version in extension/package.json"; exit 1; }
|
||||
if [ "$MAN" != "$PKG" ]; then
|
||||
echo "ERROR: MAJOR.MINOR disagrees between the two files."
|
||||
echo " extension/manifest.json = $MAN <- packaging.sh reads MAJOR.MINOR from here"
|
||||
echo " extension/package.json = $PKG"
|
||||
echo "Only MAJOR.MINOR is hand-set. The patch component is derived from"
|
||||
echo "commit time and overwritten at build time, so the committed patch"
|
||||
echo "numbers are inert — but MAJOR.MINOR still ships. Set both the same."
|
||||
|
||||
# ...and the shape this project actually derives. AMO would happily
|
||||
# take `1.0.3500147` too, so the grammar check alone would not notice
|
||||
# a regression to the pre-318 shape — which orders BELOW everything
|
||||
# signed since, and is unrecoverable once Firefox has the higher one.
|
||||
if ! echo "$VERSION" | grep -qE '^20[0-9][0-9]\.[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,4}$'; then
|
||||
echo "ERROR: derived version '$VERSION' is not YYYY.M.D.HHMM."
|
||||
echo "Rule 148's CalVer is what build.yml signs; the old"
|
||||
echo "1.0.<minutes> shape would order below every ext-2026.* release."
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: MAJOR.MINOR $MAN, derived version $VERSION"
|
||||
echo "OK: derived version $VERSION"
|
||||
|
||||
backend-lint-and-test:
|
||||
runs-on: python-ci
|
||||
|
||||
+19
-6
@@ -71,12 +71,25 @@ per `docs/process.md`'s "add deps to the image when used by >1 project".
|
||||
published bytes?"*, not *"is this file copied in?"* — which is why the script
|
||||
keeps two lists rather than one.
|
||||
- **The shipped extension version is derived, not committed.** It is the commit
|
||||
TIME of the newest packaged-extension change (minutes since 2020-01-01, per
|
||||
family rule 149 — never a commit count, which orders by branch rather than by
|
||||
recency). `build.yml`'s `sign-extension` computes it and stamps it into
|
||||
`extension/manifest.json` + `package.json` in the working tree before signing;
|
||||
the stamp is never committed. Treat the version in the repo as a base: only
|
||||
its MAJOR.MINOR is read, and its patch component is inert.
|
||||
TIME of the newest packaged-extension change, rendered `YYYY.M.D.HHMM` UTC
|
||||
(family rules 148/149 — never a commit count, which orders by branch rather
|
||||
than by recency). `build.yml`'s `sign-extension` computes it and stamps it
|
||||
into `extension/manifest.json` + `package.json` in the working tree before
|
||||
signing; the stamp is never committed. The version in the repo is **wholly
|
||||
inert** — since milestone 318 step 8 there is no hand-set MAJOR.MINOR either.
|
||||
- **The extension is the one artifact that does not zero-pad, and that is not a
|
||||
drift** (#3138). Mozilla's grammar for AMO is
|
||||
`^(0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}$` — a segment is the single
|
||||
digit `0` or starts 1-9, and there are at most four. `2026.08.29.0201` is
|
||||
rejected; `2026.8.29.201` is the same value one character narrower per
|
||||
segment, and rule 148 defines comparison as numeric per segment, so nothing is
|
||||
reordered. `ci.yml`'s `extension-version` lane asserts the derived string
|
||||
against that exact regex, plus a `YYYY.M.D.HHMM` shape check that would catch
|
||||
a regression to the pre-318 `1.0.<minutes>` — which AMO would accept and which
|
||||
orders below everything already signed. Checking here is the whole point: AMO
|
||||
409s on re-signing, so a version it rejects is burned and cannot be reused.
|
||||
`scripts/artifacts.sh version extension` **delegates** to `packaging.sh` so
|
||||
the two cannot answer differently.
|
||||
- Every job that derives anything checks out with `fetch-depth: 0` — all four
|
||||
`build.yml` jobs, `ci.yml`'s `extension-version` and `backend-lint-and-test`
|
||||
(for `tests/test_artifact_paths.py` and `test_artifact_identity.py`), and
|
||||
|
||||
+24
-9
@@ -38,27 +38,42 @@ npm run build # unsigned XPI in web-ext-artifacts/
|
||||
- [ ] Subscriptions list: popup → "Sources" tab → list renders
|
||||
- [ ] Check now: click play icon on source row → no error toast
|
||||
|
||||
## Versioning — don't hand-edit the patch number
|
||||
## Versioning — the committed number decides nothing
|
||||
|
||||
The shipped version is **derived**, not committed. `scripts/packaging.sh
|
||||
version` returns `MAJOR.MINOR` from `manifest.json` plus a patch component
|
||||
that is the commit *time* of the newest change to a packaged extension file,
|
||||
in minutes since 2020-01-01. `build.yml` computes it and stamps it into both
|
||||
version` returns `YYYY.M.D.HHMM` in UTC: the commit *time* of the newest change
|
||||
to a packaged extension file. `build.yml` computes it and stamps it into both
|
||||
`manifest.json` and `package.json` at build time. The stamp is never
|
||||
committed — the commit carrying it would itself be a change to the extension,
|
||||
which would move the version again.
|
||||
|
||||
So:
|
||||
|
||||
- **Editing the patch number does nothing.** It is overwritten before web-ext
|
||||
ever reads it. There is no bump to make, and none to forget.
|
||||
- **MAJOR.MINOR is still yours.** It carries the deliberate meaning, it is read
|
||||
from `manifest.json` alone, and CI fails the `extension-version` lane if the
|
||||
two files disagree on it.
|
||||
- **Editing the version does nothing.** All of it is overwritten before web-ext
|
||||
ever reads it. There is no bump to make, and none to forget. There is no
|
||||
hand-set part left either: MAJOR.MINOR went away with milestone 318 step 8.
|
||||
- `npm run build` locally produces an XPI labelled with the *committed*
|
||||
version, since nothing stamped it. Fine for loading into a test profile; not
|
||||
what ships.
|
||||
|
||||
**Why the extension is the one artifact that does not zero-pad.** Every other
|
||||
FC artifact emits rule 148's `YYYY.MM.DD.HHMM`. AMO will not take it: Mozilla's
|
||||
grammar for addons.mozilla.org is
|
||||
|
||||
```
|
||||
^(0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}$
|
||||
```
|
||||
|
||||
— each segment is the single digit `0` or starts 1-9, so `08` and `0201` are
|
||||
rejected, and at most four segments are allowed. The extension therefore emits
|
||||
**the same numbers unpadded**: `2026.8.29.201` where the rest of the family
|
||||
says `2026.08.29.0201`. Rule 148 already defines comparison as numeric per
|
||||
segment, under which the two are equal, so nothing is reordered by the choice
|
||||
and left-padding each segment recovers the family string exactly. `ci.yml`'s
|
||||
`extension-version` lane checks the derived string against that regex on every
|
||||
push — the cheap place to find out, because AMO 409s on re-signing and a
|
||||
rejected version is burned for good.
|
||||
|
||||
Why commit time and not a commit count: a count is per-branch, so `dev` and
|
||||
`main` count different histories of the same code and their versions end up
|
||||
ordered by which branch accumulated more commits rather than by which is newer.
|
||||
|
||||
@@ -60,7 +60,7 @@ NOT_PACKAGED_BUILD='web-ext-artifacts node_modules'
|
||||
NOT_VERSION_RELEVANT='package.json package-lock.json README.md .gitignore vitest.config.js test test/**'
|
||||
|
||||
usage() {
|
||||
echo "usage: packaging.sh {ignore|pathspec|version|major-minor|patch}" >&2
|
||||
echo "usage: packaging.sh {ignore|pathspec|version}" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
@@ -86,36 +86,48 @@ cmd_pathspec() {
|
||||
echo
|
||||
}
|
||||
|
||||
# MAJOR.MINOR stays hand-set in manifest.json — it's the part that carries
|
||||
# deliberate meaning. Only the patch component is derived.
|
||||
cmd_major_minor() {
|
||||
root=$(git rev-parse --show-toplevel)
|
||||
grep -E '"version"' "$root/extension/manifest.json" \
|
||||
| head -1 \
|
||||
| sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([0-9]+)\.([0-9]+).*/\1.\2/'
|
||||
# Strip leading zeros from one segment, leaving at least one digit.
|
||||
#
|
||||
# This exists for AMO and nothing else. Mozilla's version grammar for
|
||||
# addons.mozilla.org is documented as
|
||||
#
|
||||
# ^(0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}$
|
||||
#
|
||||
# — each segment is either the single digit `0` or starts 1-9, so `08` and
|
||||
# `0201` are rejected outright, while `0` itself is fine. MDN states it in
|
||||
# prose too: "Non-zero numbers must not include a leading zero."
|
||||
#
|
||||
# POSIX sh has no trim-loop, hence the while.
|
||||
unpad() {
|
||||
s=$1
|
||||
while [ "${#s}" -gt 1 ]; do
|
||||
case "$s" in
|
||||
0*) s=${s#0} ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
# 2020-01-01T00:00:00Z — the anchor for the derived patch component. Fixed
|
||||
# forever; moving it would renumber every version downwards.
|
||||
VERSION_EPOCH=1577836800
|
||||
|
||||
# Minutes since VERSION_EPOCH of the LATEST commit that touched a PACKAGED
|
||||
# extension file.
|
||||
# The extension's version: `YYYY.M.D.HHMM`, UTC, derived from the commit TIME
|
||||
# of the newest change to a PACKAGED extension file.
|
||||
#
|
||||
# Time-derived, per family rule 149: an artifact's ordering key must never be a
|
||||
# commit count. A count is per-branch — `dev` and `main` count different
|
||||
# histories of the same code — so the moment BOTH channels publish, their
|
||||
# versions order by which branch accumulated more commits rather than by which
|
||||
# is newer. A squash-merge makes that permanent: main gains one commit where dev
|
||||
# gained five, so dev climbs away from main and a dev install can never cross
|
||||
# back. That is Roundtable's 2026-08-24 incident (`versionCode` was the branch's
|
||||
# commit count) in a different repo. Measured here on 2026-08-27: main=23,
|
||||
# dev=24 under the old formula — one apart, which is exactly how the inversion
|
||||
# stays invisible until it strands somebody.
|
||||
# THE ONE DELIBERATE DEPARTURE FROM THE FAMILY SHAPE, and it is a rendering
|
||||
# difference only. Rule 148 says `YYYY.MM.DD.HHMM` zero-padded, and every other
|
||||
# FC artifact emits exactly that. AMO's grammar (see unpad) forbids the padding,
|
||||
# and AMO is not negotiable: a rejected version is burned, since AMO 409s on
|
||||
# re-signing a version it has already seen. So the extension emits THE SAME
|
||||
# NUMBERS unpadded — 2026.08.29.0201 and 2026.8.29.201 are one value in two
|
||||
# renderings, and rule 148 already specifies comparison as numeric per segment,
|
||||
# under which they are equal. Nothing published is reordered by the choice, and
|
||||
# left-padding each segment recovers the family string exactly.
|
||||
#
|
||||
# HHMM is one segment, not two, because AMO allows at most FOUR. Unpadded that
|
||||
# reads oddly (00:14 -> `14`, midnight -> `0`) but stays strictly increasing
|
||||
# within a day, which is all the ordering needs.
|
||||
#
|
||||
# Why the commit's time and not the build's:
|
||||
# * MONOTONIC — max() over a set that only ever gains members. Verified
|
||||
# across all 24 extension-touching commits: zero non-monotonic steps.
|
||||
# * MONOTONIC — max() over a set that only ever gains members.
|
||||
# * STABLE while the extension is unchanged, so an unchanged extension keeps
|
||||
# its version, the ext-<version> signature cache still hits, and AMO is
|
||||
# called once per extension CHANGE rather than once per push. Build-time
|
||||
@@ -126,24 +138,34 @@ VERSION_EPOCH=1577836800
|
||||
# produced for byte-identical code. Same code, same version, one signing.
|
||||
# * REPRODUCIBLE — any checkout of a commit yields that commit's version.
|
||||
#
|
||||
# Never a commit count (family rule 149): a count is per-branch, so `dev` and
|
||||
# `main` count different histories of the same code and order by which branch
|
||||
# accumulated more commits rather than by which is newer. A squash-merge makes
|
||||
# that permanent. Roundtable's 2026-08-24 incident, in a different repo.
|
||||
#
|
||||
# Requires real history: a depth-1 clone sees one commit and will derive a wrong
|
||||
# (too low) value. Every consumer must check out with fetch-depth: 0.
|
||||
cmd_patch() {
|
||||
#
|
||||
# Formatted through git rather than date(1): busybox date does not reliably
|
||||
# accept `-d @<epoch>`, and git's --date=format-local is available wherever git
|
||||
# is. TZ=UTC so the value does not depend on the runner's timezone.
|
||||
cmd_version() {
|
||||
root=$(git rev-parse --show-toplevel)
|
||||
# Unquoted on purpose: the pathspec must word-split into separate args.
|
||||
# Globbing is already off script-wide (set -euf above).
|
||||
# shellcheck disable=SC2046
|
||||
ts=$(cd "$root" && git log --format=%ct HEAD -- extension/ $(cmd_pathspec) \
|
||||
| sort -n | tail -1)
|
||||
if [ -z "$ts" ]; then
|
||||
sha=$(cd "$root" && git log --format='%ct %H' HEAD -- extension/ $(cmd_pathspec) \
|
||||
| sort -n | tail -1 | cut -d' ' -f2)
|
||||
if [ -z "$sha" ]; then
|
||||
echo "packaging.sh: no commit touches a packaged extension file" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo $(( (ts - VERSION_EPOCH) / 60 ))
|
||||
}
|
||||
|
||||
cmd_version() {
|
||||
echo "$(cmd_major_minor).$(cmd_patch)"
|
||||
padded=$(cd "$root" && TZ=UTC git show -s --format=%cd \
|
||||
--date='format-local:%Y.%m.%d.%H%M' "$sha")
|
||||
# Rebinding the function's own positional params, which are unused here.
|
||||
# shellcheck disable=SC2046
|
||||
set -- $(echo "$padded" | tr '.' ' ')
|
||||
echo "$(unpad "$1").$(unpad "$2").$(unpad "$3").$(unpad "$4")"
|
||||
}
|
||||
|
||||
[ $# -ge 1 ] || usage
|
||||
@@ -151,7 +173,5 @@ case "$1" in
|
||||
ignore) cmd_ignore ;;
|
||||
pathspec) cmd_pathspec ;;
|
||||
version) cmd_version ;;
|
||||
major-minor) cmd_major_minor ;;
|
||||
patch) cmd_patch ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -170,6 +170,21 @@ cmd_revision() {
|
||||
# exists — at which point two lanes derive different answers for one source
|
||||
# and the shared-signature property is lost.
|
||||
cmd_version() {
|
||||
# The extension is the one artifact this script does not FORMAT, only route.
|
||||
# AMO's version grammar forbids leading zeros, so the extension emits the
|
||||
# same numbers unpadded (#3138) — a rendering exception, documented in
|
||||
# packaging.sh beside the signing step that has to obey it. Delegating keeps
|
||||
# one answer per artifact: `artifacts.sh version extension` and
|
||||
# `packaging.sh version` cannot drift into two.
|
||||
#
|
||||
# The direction is deliberate. artifacts.sh already asks packaging.sh for the
|
||||
# extension's PATH SET (ext_paths above), so the version has to flow the same
|
||||
# way; reversing it would have packaging.sh call back into this script, which
|
||||
# would call packaging.sh for the paths again.
|
||||
if [ "$1" = extension ]; then
|
||||
sh "$ROOT/extension/scripts/packaging.sh" version
|
||||
return
|
||||
fi
|
||||
sha=$(echo "$(newest "$1")" | cut -d' ' -f2)
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user