Files
FabledCurator/scripts/artifacts.sh
T
bvandeusen cd0b0ff04a
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
ci: publish :latest + :c-<sha> on main, :dev on dev — nothing else (318 step 4)
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.
2026-08-28 14:45:11 -04:00

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