feat: the agent derives its version instead of being asked to remember it (4386)
CI and images / lint (push) Successful in 3s
CI and images / extension-version (push) Successful in 2s
CI and images / frontend-build (push) Successful in 19s
extension / lint (push) Successful in 21s
CI and images / backend-lint-and-test (push) Successful in 33s
CI and images / integration (push) Successful in 2m25s
CI and images / sign-extension (push) Successful in 4s
CI and images / build-web (push) Successful in 2m1s
CI and images / smoke-web (push) Successful in 1m2s
CI and images / build-agent (push) Successful in 10m7s
CI and images / promote (push) Skipped

Operator: the agent's build string could not identify the agent. VERSION was a
literal in app.py an author was meant to bump, and nobody did — the September
image printed the same "2026-07-17.1" as the July one, so the one surface
meant to answer "did my pull work?" answered the same either way.

Nothing new was needed. scripts/artifacts.sh has derived a version per
artifact since milestone 313, and build-agent has been computing the agent's
on every run and printing it to the log. The image just never carried it.

Three values, never folded together (rule 149):

  FC_VERSION   YYYY.MM.DD.HHMM from the COMMIT its shipped files last changed
               in — identical on dev and main for the same source, which is
               what makes "am I running production's code?" answerable.
  FC_CHANNEL   a sibling field, never a suffix inside the name.
  FC_REVISION  the 12-char sha; the same string as the fc.revision LABEL, so
               the image and the registry cannot disagree about which commit
               this is.

The page SHOWS the version and COMPARES the revision. Those were one value
before, which is how a version acquires a second job and then cannot be
changed without breaking the reload banner. An unstamped local build reads
`unknown` and compares `local` — absent rather than empty, one spelling of
"cannot say".

scripts/artifacts.sh joins the AGENT path set in the same commit, and it had
to: a version has no backstop. A revision that is computed differently stops
matching the published label and forces a rebuild, so it self-corrects; a
version is compared against nothing, so a change to cmd_version alone would
leave the agent publishing the old format with nothing to contradict it. That
is #3202's finding, and the agent was rightly exempt only while it had no
version of its own. tests/test_artifact_paths.py pins it.

Also corrects two build.yml comments claiming agent/ had not changed since
2026-07-17. Both were already false — it changed 2026-09-23 — and one of them
is the stated rationale for the force_build escape hatch. Rewritten without
dates: how long an artifact has been quiet is a `git log` question, and its
answer in a comment is wrong the next time anyone commits (lesson #4383).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
2026-09-24 07:07:31 -04:00
co-authored by Claude Opus 5
parent 970d17f98a
commit e1cd629196
7 changed files with 325 additions and 37 deletions
+45 -12
View File
@@ -41,10 +41,17 @@ on:
# produce a changelog, not an image.
# The escape hatch for the one thing skip-if-exists makes untestable: a
# build that WOULD be skipped. `agent/` has not changed since 2026-07-17, so
# every push since has correctly declined to build it — which also means the
# agent build path has not run in six weeks and cannot be exercised on
# demand. #3190 lives on exactly that path.
# build that WOULD be skipped. An artifact whose source is quiet has its
# build path correctly declined on every push — which also means that path
# is not exercised, and cannot be exercised on demand, for as long as the
# quiet lasts. #3190 lives on exactly such a path.
#
# This used to name the agent and a date ("unchanged since 2026-07-17"). It
# was already false when read on 2026-09-24 — the agent had changed the day
# before — and a rationale resting on a stale fact reads as settled
# reasoning forever after (lesson #4383). Which artifact is quiet is a
# question for `git log`, not for a comment; the hatch exists because ANY of
# them can be.
#
# Editing build.yml does not force one either, and that is deliberate: the
# workflow is not shipped bytes, so it is in no artifact's path set. Putting
@@ -67,11 +74,18 @@ on:
# The base-image refresh (milestone 326 step 4, #3154).
#
# Skip-if-exists is keyed on OUR source, so an artifact whose source stops
# moving stops picking up base-image updates. `agent/` last changed
# 2026-07-17; every push since has correctly declined to rebuild it, which
# also means it will serve that day's `nvidia/cuda` layers forever. Nothing
# is wrong until it has been unchanged for months, which is precisely why
# this is a calendar trigger and not a condition on the push path.
# moving stops picking up base-image updates — it will serve the base layers
# of its last build forever. The agent is the standing example because its
# base is the heaviest (`nvidia/cuda`, ~6.3 GB) and its source the quietest:
# 48 of 1111 commits touch it, about 4% (#3114).
#
# Deliberately no date here. Nothing is wrong until an artifact has been
# unchanged for months, and "how long has it been" is a `git log` question
# whose answer in a comment is wrong the next time anyone commits — which is
# exactly how the previous version of this paragraph came to claim the agent
# had not moved since 2026-07-17 while it had moved the day before. That is
# precisely why this is a CALENDAR trigger and not a condition on the push
# path: the calendar cannot be wrong about how much time has passed.
#
# Weekly, Sunday 06:00 UTC. Away from CI-runner's Monday security sweep so
# the two are never diagnosing each other, and on the quietest day so a
@@ -1352,9 +1366,11 @@ jobs:
# :dev than to the :2026.8.28 tags milestone 318 deleted. (#3114.)
cache-from: type=registry,ref=git.fabledsword.com/bvandeusen/fabledcurator:buildcache
cache-to: type=registry,ref=git.fabledsword.com/bvandeusen/fabledcurator:buildcache,mode=max
# Only the web image carries these: it is the one with a UI and an
# HTTP surface to report them on. The ml and agent images have
# nothing to tell.
# The agent carries its own copy of these (plus FC_REVISION) since
# 2026-09-24. This used to say the agent "has nothing to tell" — true
# of the ml image, which no longer exists (#4311), and never true of
# the agent, which has a control page and a /status endpoint and was
# reporting a hand-written literal on both.
build-args: |
FC_CHANNEL=${{ steps.tag.outputs.channel }}
FC_VERSION=${{ steps.reuse.outputs.version }}
@@ -2200,6 +2216,13 @@ jobs:
set -eu
DERIVED=$(sh scripts/artifacts.sh revision agent)
echo "revision=$DERIVED" >> "$GITHUB_OUTPUT"
# Baked into the agent image as FC_VERSION and reported by its
# control page and /status. A pure function of the revision — same
# commit, same string — so it adds no variability the reuse check
# would have to account for. Added 2026-09-24: the agent's version
# used to be a hand-written literal in app.py that nobody bumped, so
# the September image reported the same string as the July one.
echo "version=$(sh scripts/artifacts.sh version agent)" >> "$GITHUB_OUTPUT"
# The build clock, pinned to the same commit (#3265). Without it
# buildkit stamps the image config with the wall clock of the build,
@@ -2353,6 +2376,16 @@ jobs:
# decoration — an unstamped image is one that will always rebuild.
labels: |
fc.revision=${{ steps.reuse.outputs.revision }}
# What the agent reports about ITSELF, as opposed to the label above,
# which is what the registry reports about it. Three values kept
# apart (rule 149). FC_REVISION is the same string as the label, so
# the image and the registry cannot disagree about which commit this
# is — a build whose self-report names a different commit from the
# tag it was published under is unrollbackable in practice.
build-args: |
FC_CHANNEL=${{ steps.tag.outputs.channel }}
FC_VERSION=${{ steps.reuse.outputs.version }}
FC_REVISION=${{ steps.reuse.outputs.revision }}
# LOAD-BEARING, not a preference. On the default docker driver these
# were no-ops; on the docker-container driver above,
# build-push-action@v5 defaults provenance to TRUE when pushing.