ci: guard the extension publish path against a missed version bump
build.yml's sign-extension keys its AMO-signing cache purely on the version string in extension/package.json. If an ext-<version> release already has an XPI, signing is skipped and build-web bakes that OLD signed XPI into :latest. Nothing in that path inspects whether extension/ actually changed, so a forgotten bump ships a stale extension on a fully green build -- silently, and as the default outcome of forgetting. AMO can't backstop it either: it 409s on re-signing a version, which is precisely why the cache exists. New extension-version job, pure git + text, no deps or services: 1. Unconditional consistency check. manifest.json and package.json versions must match. web-ext sign reads manifest.json (package.json is in --ignore-files and isn't even inside the XPI), so AMO signs the manifest version; build.yml keys its cache, release tag, XPI filename -- and so the version /api/extension/manifest reports to the update prompt -- on package.json. Divergence either 409s at AMO or ships an XPI whose update prompt lies about what's installed. 2. Changed-without-bump check. If any PACKAGED file under extension/ differs, the version must have moved. Exclusions mirror --ignore-files so a Renovate web-ext devDep bump in package.json doesn't falsely demand one. Compared against main rather than the previous push: the publish decision is made at merge-to-main against whatever ext-<version> exists, so "differs from main" is the question that matters. Diffing against the previous dev push would demand a fresh bump on every iteration, inflating the version to buy nothing. Bumping stays manual -- making it automatic requires rewriting the version in CI and committing back to a protected branch, which this workflow deliberately avoided. This only ensures a missed bump can no longer be silent. Refs #2393 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ name: CI
|
|||||||
|
|
||||||
# CI lanes per FabledRulebook/forgejo.md "CI philosophy":
|
# CI lanes per FabledRulebook/forgejo.md "CI philosophy":
|
||||||
# - lint: ruff only, no dep install — fast-fail for the common lint bounce.
|
# - lint: ruff only, no dep install — fast-fail for the common lint bounce.
|
||||||
|
# - extension-version: guards the extension publish path (see the job).
|
||||||
# - backend-lint-and-test: `pytest -m "not integration"`, no service containers.
|
# - backend-lint-and-test: `pytest -m "not integration"`, no service containers.
|
||||||
# - frontend-build: vitest unit + vite build.
|
# - frontend-build: vitest unit + vite build.
|
||||||
# - integration: pgvector + redis service containers; alembic + `pytest -m integration`.
|
# - integration: pgvector + redis service containers; alembic + `pytest -m integration`.
|
||||||
@@ -41,6 +42,116 @@ jobs:
|
|||||||
# catching syntax errors before the image build.
|
# catching syntax errors before the image build.
|
||||||
run: python -m compileall -q agent/fc_agent
|
run: python -m compileall -q agent/fc_agent
|
||||||
|
|
||||||
|
# Guards the extension publish path, which has no self-correcting behavior.
|
||||||
|
#
|
||||||
|
# build.yml's sign-extension job keys its AMO-signing cache purely on the
|
||||||
|
# version string in extension/package.json: if an `ext-<version>` Forgejo
|
||||||
|
# release already carries an XPI, signing is SKIPPED and that old signed XPI
|
||||||
|
# is what build-web bakes into `:latest`. Nothing in that path inspects
|
||||||
|
# whether extension/ actually changed — so a forgotten version bump ships a
|
||||||
|
# stale extension on a fully green build, silently. (AMO can't help: it 409s
|
||||||
|
# on re-signing a version, which is exactly why the cache exists.)
|
||||||
|
#
|
||||||
|
# This job makes that case loud, on the dev push, instead of invisible at
|
||||||
|
# merge-to-main. It is pure git + text work — no deps, no services.
|
||||||
|
extension-version:
|
||||||
|
runs-on: python-ci
|
||||||
|
container:
|
||||||
|
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# Full history: the check diffs against the push's `before` SHA (or
|
||||||
|
# the PR base), which a depth-1 clone wouldn't contain.
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Extension version guard
|
||||||
|
env:
|
||||||
|
BEFORE: ${{ github.event.before }}
|
||||||
|
PR_BASE: ${{ github.event.pull_request.base.sha }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
# busybox sh on the act_runner — no bashisms (family rule).
|
||||||
|
ver() { grep -E '"version"' "$1" | head -1 | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/'; }
|
||||||
|
PKG=$(ver extension/package.json)
|
||||||
|
MAN=$(ver extension/manifest.json)
|
||||||
|
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; }
|
||||||
|
|
||||||
|
# (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.
|
||||||
|
# build.yml keys its cache, release tag, XPI filename — and therefore
|
||||||
|
# the version /api/extension/manifest reports to the update prompt —
|
||||||
|
# on PKG. Divergence either hard-fails at AMO or ships a mislabelled
|
||||||
|
# XPI whose update prompt lies about what's installed.
|
||||||
|
if [ "$MAN" != "$PKG" ]; then
|
||||||
|
echo "ERROR: extension version mismatch."
|
||||||
|
echo " extension/manifest.json = $MAN <- what AMO signs / Firefox installs"
|
||||||
|
echo " extension/package.json = $PKG <- what CI caches, names, and reports"
|
||||||
|
echo "Set both to the same value."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# (2) If the SHIPPED extension changed, the version must have moved.
|
||||||
|
#
|
||||||
|
# Compare against MAIN, not against the previous push. The publish
|
||||||
|
# decision is made at merge-to-main against whatever ext-<version>
|
||||||
|
# already exists, so "differs from main" is the question that matters.
|
||||||
|
# Diffing against the previous dev push instead would demand a fresh
|
||||||
|
# bump on every iteration — push, tweak the extension again, and CI
|
||||||
|
# would insist on a second bump that buys nothing, inflating the
|
||||||
|
# version for no reason. On a main push there is no "main to compare
|
||||||
|
# to" yet, so fall back to that push's own before-SHA.
|
||||||
|
if [ "${GITHUB_REF##*/}" = "main" ]; then
|
||||||
|
BASE="${BEFORE:-}"
|
||||||
|
else
|
||||||
|
BASE=$(git rev-parse --verify -q origin/main 2>/dev/null || git rev-parse --verify -q main 2>/dev/null || echo "")
|
||||||
|
# PR base is the fallback when main isn't in the clone at all.
|
||||||
|
[ -n "$BASE" ] || BASE="${PR_BASE:-}"
|
||||||
|
fi
|
||||||
|
case "$BASE" in
|
||||||
|
''|0000000000000000000000000000000000000000)
|
||||||
|
echo "No usable base ref (no main in clone / first push) — skipping the bump check."
|
||||||
|
echo "OK: extension version $PKG"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then
|
||||||
|
echo "Base commit $BASE not in this clone — skipping the bump check."
|
||||||
|
echo "OK: extension version $PKG"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# Exclusions mirror --ignore-files in extension/package.json's web-ext
|
||||||
|
# scripts: these files are not packaged into the XPI, so touching them
|
||||||
|
# (e.g. Renovate bumping the web-ext devDep) changes nothing shipped
|
||||||
|
# and must not demand a version bump.
|
||||||
|
CHANGED=$(git diff --name-only "$BASE" HEAD -- extension/ \
|
||||||
|
':(exclude)extension/package.json' \
|
||||||
|
':(exclude)extension/package-lock.json' \
|
||||||
|
':(exclude)extension/README.md' \
|
||||||
|
':(exclude)extension/.gitignore')
|
||||||
|
if [ -z "$CHANGED" ]; then
|
||||||
|
echo "No packaged extension files changed since $BASE — nothing to guard."
|
||||||
|
echo "OK: extension version $PKG"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Packaged extension files changed since $BASE:"
|
||||||
|
echo "$CHANGED" | sed 's/^/ /'
|
||||||
|
PKG_OLD=$(git show "$BASE:extension/package.json" 2>/dev/null | grep -E '"version"' | head -1 | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
|
||||||
|
if [ -z "$PKG_OLD" ]; then
|
||||||
|
echo "Could not read the base version — skipping the bump check."
|
||||||
|
echo "OK: extension version $PKG"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [ "$PKG_OLD" = "$PKG" ]; then
|
||||||
|
echo "ERROR: packaged extension files changed but the version is still $PKG."
|
||||||
|
echo "build.yml would find the existing ext-$PKG release, skip AMO signing,"
|
||||||
|
echo "and bake the OLD signed XPI into :latest — a green build shipping stale code."
|
||||||
|
echo "Bump the version in BOTH extension/package.json and extension/manifest.json."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "OK: extension version $PKG_OLD -> $PKG"
|
||||||
|
|
||||||
backend-lint-and-test:
|
backend-lint-and-test:
|
||||||
runs-on: python-ci
|
runs-on: python-ci
|
||||||
container:
|
container:
|
||||||
|
|||||||
Reference in New Issue
Block a user