Build images / sign-extension (push) Successful in 4s
CI / lint (push) Successful in 4s
CI / extension-version (push) Successful in 4s
Build images / build-agent (push) Successful in 6s
CI / frontend-build (push) Successful in 19s
extension / lint (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 30s
Build images / build-web (push) Successful in 1m57s
Build images / build-ml (push) Successful in 2m35s
CI / integration (push) Successful in 3m46s
The narrowing itself. Rule 145, narrowed 2026-08-28 once it was verified that
nothing pins: "a third name for the same thing is upkeep for a model we do
not run."
Gone:
:2026.8.28 the per-artifact date tag from milestone 313 step 3. It shipped,
it was verified on both branches, and its premise is gone. This
is a withdrawal, not a correction — nothing about it was wrong.
:main a second moving name for whatever :latest already pointed at,
justified by nothing but symmetry with :dev.
Kept, and note the inversion: :c-<sha> on main. Milestone 313 step 6 planned
to retire it on "haven't used, not important" — amended rule 145 makes it the
rollback unit and, with version tags gone, the only immutable identifier
left. #3139 is cancelled as superseded rather than quietly dropped.
No retention job is built. One already runs (#3157): a nightly Gitea
package_cleanup_rule with keep_count=10. Two consequences worth writing down.
Its keep_pattern protects any dotted-numeric tag, so the date tags this
commit stops minting would have been exempt from retention and accumulated
without bound — silently, in the direction of unbounded growth. That trap is
now dissolved rather than needing the pattern change #3139 planned. And
:c-<sha> IS prunable under that pattern, so the honest statement is that it
is the rollback unit for roughly the last 10 builds, not for every main
commit ever.
cmd_tag goes with its last consumer rather than being left as a dead
subcommand for a later step to notice. The report step logs version and
revision, which is the same information — the date is the first three fields
of the version.
Docs needed no change: every consumer already tracks a moving tag
(docker-compose.yml on :dev, agent/README.md on :latest), which is the
evidence the whole stance rests on.
147 lines
6.1 KiB
Bash
Executable File
147 lines
6.1 KiB
Bash
Executable File
#!/bin/sh
|
|
# Single definition of WHAT EACH PUBLISHED ARTIFACT IS BUILT FROM, and the
|
|
# version derived from it. Milestone 313; generalises the shape
|
|
# extension/scripts/packaging.sh established for the extension alone.
|
|
#
|
|
# Four artifacts, four independent versions. An artifact whose shipped files
|
|
# did not change keeps its version and does not rebuild — that is the whole
|
|
# point, and it is why each path set must match its Dockerfile rather than
|
|
# being a plausible guess. Getting a set wrong is quiet in BOTH directions:
|
|
#
|
|
# too narrow -> a pin serves stale bytes, because the version did not move
|
|
# when the content did. This is the dangerous one.
|
|
# too wide -> the artifact re-versions and rebuilds for a change it does
|
|
# not ship. Merely wasteful.
|
|
#
|
|
# tests/test_artifact_paths.py asserts every COPY source in each Dockerfile is
|
|
# covered here, so adding a COPY without updating this file fails CI.
|
|
#
|
|
# POSIX sh only — CI's run shell is busybox on some paths.
|
|
#
|
|
# -f (no pathname expansion) is load-bearing for the whole script: the lists
|
|
# below are iterated with deliberate word-splitting, and without it the shell
|
|
# would glob `frontend/test/**` against the working tree and silently narrow
|
|
# the pattern. Callers substituting the output need their own `set -f` too;
|
|
# the two guards protect different expansions.
|
|
set -euf
|
|
|
|
ROOT=$(git rev-parse --show-toplevel)
|
|
|
|
# --- what each artifact ships ------------------------------------------------
|
|
#
|
|
# Each set includes its own Dockerfile and requirements: changing a base image
|
|
# or a pin changes the artifact just as surely as changing a source file.
|
|
#
|
|
# web (Dockerfile, context `.`) — the runtime stage copies backend/, alembic/,
|
|
# alembic.ini, entrypoint.sh and requirements.txt; the frontend-builder stage
|
|
# copies frontend/ and the runtime takes its `dist` output.
|
|
#
|
|
# frontend/test is excluded: `npm run build` is vite, which builds from src/,
|
|
# index.html and public/ and never reads test/. It lands in the builder layer
|
|
# but not in `dist`, so it cannot reach the shipped image.
|
|
#
|
|
# The web image ALSO bundles the signed XPI (build.yml downloads it into
|
|
# frontend/public/extension/ before the docker build), so an extension change
|
|
# changes the web image. The extension's packaged set is appended in cmd_paths
|
|
# rather than restated — one definition, per #2397.
|
|
WEB_PATHS='Dockerfile requirements.txt backend alembic alembic.ini entrypoint.sh frontend :(exclude)frontend/test :(exclude)frontend/test/**'
|
|
|
|
# ml (Dockerfile.ml, context `.`) — no frontend, no extension. Note it copies
|
|
# BOTH requirements-ml.txt and requirements.txt.
|
|
ML_PATHS='Dockerfile.ml requirements-ml.txt requirements.txt backend alembic alembic.ini entrypoint.sh'
|
|
|
|
# agent (agent/Dockerfile, context `agent`) — copies requirements.txt and
|
|
# fc_agent only. agent/README.md, agent/docker-compose.yml and agent/ruff.toml
|
|
# live in the directory but never reach the image, so they must not re-version
|
|
# it: this is deliberately NOT `agent/`.
|
|
AGENT_PATHS='agent/Dockerfile agent/requirements.txt agent/fc_agent'
|
|
|
|
|
|
usage() {
|
|
echo "usage: artifacts.sh {paths|revision|version} {web|ml|agent|extension}" >&2
|
|
exit 2
|
|
}
|
|
|
|
# The extension's packaged set, read from its own definition rather than
|
|
# copied. packaging.sh emits `:(exclude)extension/...` entries, so the bare
|
|
# `extension` include has to come with them.
|
|
ext_paths() {
|
|
echo "extension $(sh "$ROOT/extension/scripts/packaging.sh" pathspec)"
|
|
}
|
|
|
|
cmd_paths() {
|
|
case "$1" in
|
|
web) echo "$WEB_PATHS $(ext_paths)" ;;
|
|
ml) echo "$ML_PATHS" ;;
|
|
agent) echo "$AGENT_PATHS" ;;
|
|
extension) ext_paths ;;
|
|
*) usage ;;
|
|
esac
|
|
}
|
|
|
|
# "<unix ts> <sha>" of the newest commit touching this artifact's shipped set.
|
|
# Unquoted on purpose: the pathspec must word-split into separate args.
|
|
# Globbing is already off script-wide.
|
|
newest() {
|
|
# shellcheck disable=SC2046
|
|
set -- "$(cd "$ROOT" && git log --format='%ct %H' HEAD -- $(cmd_paths "$1") \
|
|
| sort -n | tail -1)"
|
|
if [ -z "$1" ]; then
|
|
echo "artifacts.sh: no commit touches this artifact's shipped files" >&2
|
|
exit 1
|
|
fi
|
|
echo "$1"
|
|
}
|
|
|
|
# Formatted through git rather than date(1): busybox date does not reliably
|
|
# accept `-d @<epoch>`, and git's own --date=format-local is available wherever
|
|
# git is. TZ=UTC so the value does not depend on the runner's timezone.
|
|
fmt() {
|
|
(cd "$ROOT" && TZ=UTC git show -s --format=%cd --date="format-local:$2" "$1")
|
|
}
|
|
|
|
# Leading zeros stripped so every segment is a plain integer — some version
|
|
# validators reject `08`, and a leading zero buys nothing. `0000` (midnight)
|
|
# must survive as `0`, not as the empty string.
|
|
strip0() {
|
|
printf '%s' "$1" | sed -e 's/^0*//' -e 's/^$/0/'
|
|
}
|
|
|
|
# The IDENTITY of an artifact's content: the commit its shipped files last
|
|
# changed in. This is what decides whether a build can be skipped.
|
|
#
|
|
# It is published as the `fc.revision` LABEL on the image itself, and read
|
|
# back off the moving channel tag — not as a tag of its own (milestone 318
|
|
# step 3). A tag would be a name minted per build that only one thing reads,
|
|
# which is what rule 145 narrowed against; it would also be prunable under the
|
|
# registry's keep_pattern (#3157), so the cache would silently expire.
|
|
#
|
|
# A published image with no such label reads as a MISS and rebuilds. That is
|
|
# the migration path, not a fault: `imagetools create` copies a manifest and
|
|
# config labels are not manifest annotations, so the reuse path cannot stamp
|
|
# one and there is nothing to backfill. Each artifact pays one rebuild, once.
|
|
cmd_revision() {
|
|
echo "$(newest "$1")" | cut -d' ' -f2 | cut -c1-12
|
|
}
|
|
|
|
# The ORDERING KEY: full precision, YYYY.M.D.HHMM. Used by the extension,
|
|
# where the value is what Firefox compares to decide whether an update exists
|
|
# — two same-day builds MUST be distinguishable or the second never reaches
|
|
# anyone.
|
|
cmd_version() {
|
|
sha=$(echo "$(newest "$1")" | cut -d' ' -f2)
|
|
printf '%s.%s.%s.%s\n' \
|
|
"$(fmt "$sha" %Y)" \
|
|
"$(strip0 "$(fmt "$sha" %m)")" \
|
|
"$(strip0 "$(fmt "$sha" %d)")" \
|
|
"$(strip0 "$(fmt "$sha" %H%M)")"
|
|
}
|
|
|
|
[ $# -ge 2 ] || usage
|
|
case "$1" in
|
|
paths) cmd_paths "$2" ;;
|
|
revision) cmd_revision "$2" ;;
|
|
version) cmd_version "$2" ;;
|
|
*) usage ;;
|
|
esac
|