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>
178 lines
8.1 KiB
Bash
Executable File
178 lines
8.1 KiB
Bash
Executable File
#!/bin/sh
|
|
# Single source of truth for "what ships inside the XPI", plus the version
|
|
# derived from it.
|
|
#
|
|
# Three consumers used to hand-maintain their own copy of this list, and
|
|
# keeping three copies of one fact in sync by hand is how issue #2397 happened:
|
|
#
|
|
# 1. web-ext's --ignore-files (extension/package.json's four scripts)
|
|
# 2. the :(exclude) pathspec (what moves the version — a WIDER
|
|
# set than the ignore list; see
|
|
# NOT_VERSION_RELEVANT)
|
|
# 3. the git-log pathspec (the derived version, below)
|
|
#
|
|
# They now all read from here. POSIX sh only — CI's run shell is busybox.
|
|
#
|
|
# -f (no pathname expansion) is set for the whole script and is load-bearing:
|
|
# the lists below are iterated with deliberate word-splitting, and without -f
|
|
# the shell would also GLOB them, expanding `test/**` into whatever files
|
|
# happen to exist and corrupting the output. A caller's own `set -f` does not
|
|
# help here — this runs as a separate sh process and does not inherit it.
|
|
# Callers still need their own `set -f` for the substituted result; the two
|
|
# guards protect different expansions.
|
|
set -euf
|
|
|
|
# Paths under extension/ that are NOT packaged into the XPI.
|
|
#
|
|
# Split by whether git tracks them: node_modules and web-ext-artifacts are
|
|
# build/dependency output that never appears in a commit, so they belong in
|
|
# web-ext's ignore list but would be meaningless in a git pathspec.
|
|
#
|
|
# Directories need BOTH forms. `test/**` matches the files inside, but not the
|
|
# directory entry itself — web-ext writes an entry for the directory too, so
|
|
# with only the glob the XPI ends up carrying empty `test/` and `scripts/`
|
|
# entries (caught by the XPI-content check on 2026-08-03). The bare name alone
|
|
# is not enough either: minimatch's `test` does not match `test/url.spec.js`,
|
|
# so dropping the glob would ship the contents. Keep both.
|
|
NOT_PACKAGED_TRACKED='package.json package-lock.json README.md .gitignore vitest.config.js scripts scripts/** test test/**'
|
|
NOT_PACKAGED_BUILD='web-ext-artifacts node_modules'
|
|
|
|
# Paths under extension/ that cannot change the SHIPPED BYTES, and so must not
|
|
# move the derived version.
|
|
#
|
|
# Deliberately NOT the same list as NOT_PACKAGED_TRACKED, and the whole
|
|
# difference is `scripts/`. packaging.sh is not packaged into the XPI — but it
|
|
# DECIDES the version string, and build.yml stamps that string into the
|
|
# manifest.json that is packaged. A change to how the version is computed is
|
|
# therefore a change to the shipped bytes.
|
|
#
|
|
# Excluding it was harmless only while every push rebuilt the web image.
|
|
# Milestone 313 step 4 made the rebuild conditional on the derived revision
|
|
# moving, which turned it into a silent failure: a packaging.sh change gives a
|
|
# NEW version, so sign-extension misses its ext-<version> cache and signs —
|
|
# while build-web sees an unmoved revision, reuses the published image, and
|
|
# ships the OLD XPI. An orphaned AMO signature, and an instance quietly serving
|
|
# code the registry says is current.
|
|
#
|
|
# The two directions are not symmetric, which is why this list is the narrower
|
|
# one. Too wide costs a re-sign and a rebuild for a change that ships nothing
|
|
# new. Too narrow serves stale bytes and says nothing.
|
|
NOT_VERSION_RELEVANT='package.json package-lock.json README.md .gitignore vitest.config.js test test/**'
|
|
|
|
usage() {
|
|
echo "usage: packaging.sh {ignore|pathspec|version}" >&2
|
|
exit 2
|
|
}
|
|
|
|
# web-ext --ignore-files values, space-separated.
|
|
#
|
|
# Callers MUST disable pathname expansion first (`set -f`), or the shell will
|
|
# glob `test/**` against the working tree before web-ext ever sees the pattern
|
|
# and silently narrow it to whatever happens to exist right now.
|
|
cmd_ignore() {
|
|
echo "$NOT_PACKAGED_TRACKED $NOT_PACKAGED_BUILD"
|
|
}
|
|
|
|
# git pathspec excluding the tracked files that cannot change the shipped
|
|
# bytes, e.g. :(exclude)extension/package.json :(exclude)extension/test/**
|
|
#
|
|
# This answers "what moves the version?", NOT "what goes in the XPI?" — see
|
|
# NOT_VERSION_RELEVANT for why those differ. cmd_ignore answers the other one.
|
|
# Same `set -f` requirement as above.
|
|
cmd_pathspec() {
|
|
for entry in $NOT_VERSION_RELEVANT; do
|
|
printf ':(exclude)extension/%s ' "$entry"
|
|
done
|
|
echo
|
|
}
|
|
|
|
# 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"
|
|
}
|
|
|
|
# The extension's version: `YYYY.M.D.HHMM`, UTC, derived from the commit TIME
|
|
# of the newest change to a PACKAGED extension file.
|
|
#
|
|
# 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.
|
|
# * 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
|
|
# minutes would re-sign on every push and never let two channels share a
|
|
# signature.
|
|
# * SHARED ACROSS CHANNELS — after a merge, `main` sees the same commit and
|
|
# derives the same number, so `:latest` reuses the signature `:dev` already
|
|
# 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.
|
|
#
|
|
# 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
|
|
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
|
|
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
|
|
case "$1" in
|
|
ignore) cmd_ignore ;;
|
|
pathspec) cmd_pathspec ;;
|
|
version) cmd_version ;;
|
|
*) usage ;;
|
|
esac
|