Extension packaging: one source of truth, shadow-mode versioning, real XPI verification #235
@@ -40,6 +40,11 @@ jobs:
|
||||
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Full history: the shadow-mode step below derives a version from a
|
||||
# commit count, which a depth-1 clone cannot produce. Harmless for
|
||||
# everything else in this job.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Resolve extension version
|
||||
id: extver
|
||||
@@ -48,6 +53,25 @@ jobs:
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Resolved extension version: $VERSION"
|
||||
|
||||
# --- shadow mode (milestone #271, step 2) ---------------------------
|
||||
# Informational ONLY — nothing downstream reads this, and it must never
|
||||
# fail the build. This is THE place the derived formula gets validated:
|
||||
# `sign-extension` only runs on main, so main pushes are the sole source
|
||||
# of truth for whether the derived version moves exactly when the shipped
|
||||
# extension changes. Compare these lines across several main builds
|
||||
# before step 4 lets the derived value control publishing.
|
||||
- name: Shadow — derived version (informational)
|
||||
run: |
|
||||
set -u
|
||||
DERIVED=$(sh extension/scripts/packaging.sh version 2>&1 || echo "UNAVAILABLE")
|
||||
MANUAL=${{ steps.extver.outputs.version }}
|
||||
echo "shadow: manual=$MANUAL derived=$DERIVED sha=$GITHUB_SHA"
|
||||
if [ "$MANUAL" = "$DERIVED" ]; then
|
||||
echo "shadow: manual and derived agree"
|
||||
else
|
||||
echo "shadow: DIVERGENT — expected until step 4 cuts over; derived is authoritative-to-be"
|
||||
fi
|
||||
|
||||
- name: Check Forgejo release-asset cache
|
||||
id: cache
|
||||
env:
|
||||
|
||||
@@ -77,6 +77,17 @@ jobs:
|
||||
test -n "$PKG" || { echo "ERROR: no version found in extension/package.json"; exit 1; }
|
||||
test -n "$MAN" || { echo "ERROR: no version found in extension/manifest.json"; exit 1; }
|
||||
|
||||
# --- shadow mode (milestone #271, step 2) -------------------------
|
||||
# Informational ONLY: nothing below reads DERIVED, and this must never
|
||||
# fail the job. `web-ext sign` is one-shot per version (AMO 409s on a
|
||||
# repeat), so a wrong formula would burn a real version number that
|
||||
# can't be reclaimed. Logging it against real pushes first is the only
|
||||
# way to validate it at zero cost.
|
||||
# Placed before every early-exit path so it reports on all runs.
|
||||
DERIVED=$(sh extension/scripts/packaging.sh version 2>&1 || echo "UNAVAILABLE")
|
||||
echo "shadow: manual=$PKG derived=$DERIVED"
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
# (1) Unconditional: the two version strings must agree. `web-ext sign`
|
||||
# reads manifest.json (package.json sits in --ignore-files and isn't
|
||||
# even inside the XPI), so AMO signs MAN and Firefox installs MAN.
|
||||
|
||||
@@ -38,3 +38,45 @@ jobs:
|
||||
# package version-consistency checks. No browser, no network.
|
||||
- name: Unit tests
|
||||
run: cd extension && npm run test:unit
|
||||
|
||||
# Everything else about packaging is asserted against our own declaration
|
||||
# of what ships. This is the only check that asks web-ext what it ACTUALLY
|
||||
# put in the archive. Until now that was an unverified assumption about
|
||||
# glob semantics — and a fragile one: `test/**` reaches web-ext intact
|
||||
# only because callers `set -f` first, so losing that quoting would
|
||||
# silently start shipping dev files with no other signal.
|
||||
- name: Verify XPI contents
|
||||
run: |
|
||||
set -eu
|
||||
command -v unzip >/dev/null 2>&1 || { apt-get update -qq && apt-get install -y -qq unzip; }
|
||||
cd extension
|
||||
npm run build
|
||||
ZIP=$(ls web-ext-artifacts/*.zip | head -1)
|
||||
echo "=== packaged entries in $ZIP ==="
|
||||
unzip -Z1 "$ZIP" | sort
|
||||
echo "=== end ==="
|
||||
ENTRIES=$(unzip -Z1 "$ZIP")
|
||||
fail=0
|
||||
# Must NOT ship: repo infrastructure with no business in a user's browser.
|
||||
for pat in 'test/' 'scripts/' 'vitest.config.js' 'package.json' 'package-lock.json' 'README.md' 'node_modules/' 'web-ext-artifacts/'; do
|
||||
if echo "$ENTRIES" | grep -q "^$pat"; then
|
||||
echo "ERROR: '$pat' was packaged into the XPI but must not be"
|
||||
fail=1
|
||||
fi
|
||||
done
|
||||
# Must ship: if an exclusion pattern ever over-matches, the extension
|
||||
# breaks at runtime rather than at build time, so assert presence too.
|
||||
for req in 'manifest.json' 'lib/url.js' 'lib/api.js' 'lib/platforms.js' 'lib/cookies.js'; do
|
||||
if ! echo "$ENTRIES" | grep -q "^$req$"; then
|
||||
echo "ERROR: '$req' is missing from the XPI"
|
||||
fail=1
|
||||
fi
|
||||
done
|
||||
for dir in 'background/' 'popup/' 'options/' 'content/' 'icons/'; do
|
||||
if ! echo "$ENTRIES" | grep -q "^$dir"; then
|
||||
echo "ERROR: nothing from '$dir' was packaged"
|
||||
fail=1
|
||||
fi
|
||||
done
|
||||
[ "$fail" -eq 0 ] || exit 1
|
||||
echo "XPI contents verified."
|
||||
|
||||
@@ -27,6 +27,10 @@ per `docs/process.md`'s "add deps to the image when used by >1 project".
|
||||
- `pip install -r requirements.txt pytest pytest-asyncio` — in `backend-lint-and-test` and `integration` jobs
|
||||
- `npm install --no-audit --no-fund` — in `frontend-build` job
|
||||
- `npm install --no-audit --no-fund` — in `extension.yml`'s `lint` job (web-ext + vitest)
|
||||
- `unzip` — in `extension.yml`'s "Verify XPI contents" step, installed via apt
|
||||
only when absent (`node:24-bookworm-slim` may or may not carry it). Debian
|
||||
package, ~2s. Not worth baking into a shared image for a single consumer, per
|
||||
`docs/process.md`'s ">1 project" rule.
|
||||
|
||||
## Notes
|
||||
|
||||
@@ -55,6 +59,8 @@ per `docs/process.md`'s "add deps to the image when used by >1 project".
|
||||
pathspec in `ci.yml`'s `extension-version` guard, and the commit count that
|
||||
derives the extension version. Three hand-kept copies of that one fact is
|
||||
what allowed issue #2397.
|
||||
- `build.yml`'s `sign-extension` checks out with `fetch-depth: 0` — the derived
|
||||
extension version is a commit count, which a shallow clone cannot produce.
|
||||
- Callers MUST `set -f` before substituting the script's output. Without it the
|
||||
shell expands `test/**` against the working tree and silently narrows the
|
||||
pattern to whatever files exist at that moment — a failure that looks like
|
||||
|
||||
Reference in New Issue
Block a user