Release: dev → main (first public release) #258
@@ -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.
|
||||
|
||||
@@ -27,6 +27,23 @@ COPY fc_agent ./fc_agent
|
||||
# imgutils ONNX models + the transformers SigLIP weights both cache here; mount
|
||||
# a volume to persist them across restarts (the SigLIP download is ~3.5 GB once).
|
||||
ENV HF_HOME=/models
|
||||
|
||||
# Declared LAST on purpose, exactly as the web Dockerfile does: an ARG/ENV
|
||||
# invalidates every layer below it, and these are the only values that differ
|
||||
# between builds of otherwise identical source. Any earlier and the ~6.3 GB
|
||||
# CUDA + torch layers could never be shared between the dev and main builds of
|
||||
# one commit — which is the cost #3114 measured at 9m26s cold.
|
||||
#
|
||||
# Three values, never folded together (rule 149) — the NAME a person reads, the
|
||||
# CHANNEL it came from, and the REVISION that identifies the content. See
|
||||
# fc_agent/build_info.py; CI derives all three from scripts/artifacts.sh.
|
||||
ARG FC_CHANNEL=""
|
||||
ENV FC_CHANNEL=${FC_CHANNEL}
|
||||
ARG FC_VERSION=""
|
||||
ENV FC_VERSION=${FC_VERSION}
|
||||
ARG FC_REVISION=""
|
||||
ENV FC_REVISION=${FC_REVISION}
|
||||
|
||||
EXPOSE 8770
|
||||
|
||||
# The control UI; the worker is started from it (or POST /start).
|
||||
|
||||
+27
-8
@@ -12,16 +12,21 @@ from fastapi import FastAPI, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
|
||||
from . import logbuf
|
||||
from .build_info import FC_CHANNEL, FC_REVISION, FC_VERSION, build_id, display_version
|
||||
from .config import Config
|
||||
from .gpu import read_gpu
|
||||
from .worker import Worker
|
||||
|
||||
log = logging.getLogger("fc_agent.app")
|
||||
|
||||
# Bump on every agent change. The page embeds this and /status reports it; the UI
|
||||
# warns to reload when they differ — so a stale browser-cached page can't be
|
||||
# mistaken for "the new image didn't deploy". (Belt-and-braces with no-store.)
|
||||
VERSION = "2026-07-17.1 · idle model-unload: after ~5 min idle the GPU models release their VRAM and reload on the next job (env IDLE_UNLOAD_SECONDS, 0=off) · sleep mode sheds to one downloader"
|
||||
# DERIVED at image build time, not hand-maintained — see build_info. This was a
|
||||
# literal an author was asked to bump on every agent change, and the September
|
||||
# image printed the same "2026-07-17.1" as the July one, so the surface meant to
|
||||
# answer "did my pull work?" answered the same either way.
|
||||
#
|
||||
# Two values with two jobs, kept apart (rule 149): the page SHOWS the version
|
||||
# and COMPARES the build id. /status reports both, plus the raw fields, so a
|
||||
# reader never has to take a formatted string apart to get at one of them.
|
||||
|
||||
logbuf.install()
|
||||
cfg = Config.from_env()
|
||||
@@ -52,7 +57,14 @@ def _maybe_autostart() -> None:
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
def index() -> str:
|
||||
return _PAGE.replace("__BUILD__", VERSION)
|
||||
# Two substitutions, not one: `__VERSION__` is what a person reads in the
|
||||
# meta line, `__BUILD_ID__` is what the script compares against /status to
|
||||
# notice the page is a cached copy from a previous build.
|
||||
return (
|
||||
_PAGE
|
||||
.replace("__VERSION__", display_version())
|
||||
.replace("__BUILD_ID__", build_id())
|
||||
)
|
||||
|
||||
|
||||
@app.post("/start")
|
||||
@@ -117,7 +129,14 @@ def status():
|
||||
s["fc_url"] = cfg.fc_url
|
||||
s["configured"] = bool(cfg.token)
|
||||
s["queue"] = worker.latest_queue()
|
||||
s["build"] = VERSION
|
||||
# `build` is the comparison token the page checks — see build_info.
|
||||
# `version`/`channel`/`revision` ride BESIDE it rather than inside it, so a
|
||||
# reader wanting the version never has to parse it back out of something
|
||||
# else. Absent rather than empty when the image carries no stamp.
|
||||
s["build"] = build_id()
|
||||
s["version"] = FC_VERSION or None
|
||||
s["channel"] = FC_CHANNEL or None
|
||||
s["revision"] = FC_REVISION or None
|
||||
return JSONResponse(s)
|
||||
|
||||
|
||||
@@ -203,7 +222,7 @@ _PAGE = """<!doctype html><html><head><meta charset=utf-8>
|
||||
<div class=brand><span class=logo>◆</span> FabledCurator <span class=sub>GPU agent</span></div>
|
||||
<div class=conn><span class="dot" id=dot></span><span id=connlbl>—</span></div>
|
||||
</header>
|
||||
<p class=meta>Server <code id=fc>—</code> · token <code id=cfg>—</code> · build <code id=build>__BUILD__</code></p>
|
||||
<p class=meta>Server <code id=fc>—</code> · token <code id=cfg>—</code> · build <code id=build>__VERSION__</code></p>
|
||||
|
||||
<div id=verbanner class=banner style="display:none;background:#3a1212;border-color:#5a1717;color:#ffb3b3">
|
||||
a newer agent version is running — reload this page (Ctrl+Shift+R) to update the controls
|
||||
@@ -262,7 +281,7 @@ _PAGE = """<!doctype html><html><head><meta charset=utf-8>
|
||||
</section>
|
||||
</div>
|
||||
<script>
|
||||
const PAGE_BUILD="__BUILD__"
|
||||
const PAGE_BUILD="__BUILD_ID__"
|
||||
let CAP=8
|
||||
// Optimistic transitional state on click, then apply the POST's own status
|
||||
// response (it returns worker.status()) for instant feedback — don't wait on the
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""What this agent build IS — stamped at image build time, not configurable.
|
||||
|
||||
The mirror of `backend/app/build_info.py`, for the same reasons and with the
|
||||
same posture. Kept as its own module rather than as constants in `app.py`
|
||||
because it is stdlib-only and therefore importable by the test suite, which
|
||||
cannot import `app` (torch, transformers and ultralytics are not in the CI
|
||||
image — see build.yml's "Agent syntax check").
|
||||
|
||||
## Why this replaced a hand-written string
|
||||
|
||||
`app.VERSION` used to be a literal an author was asked to bump, carrying a
|
||||
version AND a changelog in one string:
|
||||
|
||||
VERSION = "2026-07-17.1 · idle model-unload: after ~5 min idle ..."
|
||||
|
||||
Nobody bumped it. The September image printed the identical string to the July
|
||||
one, so the one surface that was supposed to answer *"did my pull work?"*
|
||||
answered *"2026-07-17.1"* either way. An artifact that cannot identify itself
|
||||
is worse than one that says nothing, because the stale value reads as an
|
||||
answer.
|
||||
|
||||
The values are now derived by `scripts/artifacts.sh` from the commit its
|
||||
shipped files last changed in — the same derivation the web image has used
|
||||
since milestone 313, and the same one the reuse check already ran for the
|
||||
agent and discarded.
|
||||
|
||||
## Three values, never folded together (rule 149)
|
||||
|
||||
* `FC_VERSION` — the NAME, `YYYY.MM.DD.HHMM` UTC, derived from COMMIT time.
|
||||
For people to read and quote. Identical on `dev` and `main` for the same
|
||||
source, which is the property that makes "am I running the same code as
|
||||
production?" answerable at a glance.
|
||||
* `FC_CHANNEL` — a SIBLING field, never a suffix inside the name.
|
||||
* `FC_REVISION` — the 12-char commit sha, the artifact's IDENTITY. This is
|
||||
what the reuse check already keys on as the `fc.revision` image label.
|
||||
|
||||
**Absent rather than empty when unknown.** A locally built image has no
|
||||
stamp, and neither does any image predating this module. One spelling of
|
||||
"cannot say" instead of two.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
FC_VERSION = os.environ.get("FC_VERSION", "").strip()
|
||||
FC_CHANNEL = os.environ.get("FC_CHANNEL", "").strip()
|
||||
FC_REVISION = os.environ.get("FC_REVISION", "").strip()
|
||||
|
||||
|
||||
def display_version() -> str:
|
||||
"""The build, as a line for a human: `2026.09.24.1052 (dev)`.
|
||||
|
||||
`unknown` rather than a blank when unstamped — an empty slot in the meta
|
||||
line reads as "no version", which is a different and false claim from "this
|
||||
build does not carry one".
|
||||
"""
|
||||
if not FC_VERSION:
|
||||
return "unknown"
|
||||
return f"{FC_VERSION} ({FC_CHANNEL})" if FC_CHANNEL else FC_VERSION
|
||||
|
||||
|
||||
def build_id() -> str:
|
||||
"""The token the control page compares against `/status` to notice it is
|
||||
showing a CACHED page from a previous build.
|
||||
|
||||
Deliberately NOT `display_version()`. That is the value for reading; this
|
||||
is the value for deciding, and folding the two is what rule 149 is about.
|
||||
The revision is the better discriminator of the two — two builds of the
|
||||
same commit ARE the same agent and should not prompt a reload, and two
|
||||
different commits always differ here even when they land in the same
|
||||
minute and derive one version name.
|
||||
|
||||
A locally built image falls through to a constant, so the reload banner
|
||||
cannot fire for it. That is honest rather than a gap: nothing in an
|
||||
unstamped image knows what source it was built from, and a per-process
|
||||
nonce would make every ordinary container RESTART claim a new version had
|
||||
arrived — a false positive on the exact surface the banner exists to keep
|
||||
trustworthy.
|
||||
"""
|
||||
return FC_REVISION or FC_VERSION or "local"
|
||||
+22
-9
@@ -55,20 +55,33 @@ WEB_PATHS='Dockerfile requirements.txt requirements-ml.txt backend alembic alemb
|
||||
# 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/`.
|
||||
#
|
||||
# $DERIVER is appended in cmd_paths, not here, so the two artifacts that stamp
|
||||
# a version pick it up the same way rather than one of them carrying it in its
|
||||
# literal — see DERIVER below for why it belongs to both.
|
||||
AGENT_PATHS='agent/Dockerfile agent/requirements.txt agent/fc_agent'
|
||||
|
||||
# This file. It is copied into no image and it is still part of what the web
|
||||
# image is built from, because it DECIDES the FC_VERSION baked into that image
|
||||
# (#3202). Same finding as #3156 about packaging.sh, one level up.
|
||||
#
|
||||
# Why web and nothing else. Every artifact stamps `fc.revision`, but only web
|
||||
# also stamps a version (build.yml line ~488 feeds `version web` to the
|
||||
# FC_VERSION build arg; the agent asks for `revision` alone, and the
|
||||
# extension takes its version from packaging.sh). For a revision-only artifact
|
||||
# this file needs no entry: any change to how the revision is COMPUTED changes
|
||||
# the derived value, which then disagrees with the label on the published image
|
||||
# and forces a rebuild. That mechanism is self-correcting because it compares
|
||||
# against a string stamped into a real artifact.
|
||||
# Web AND agent, since 2026-09-24. Both stamp a VERSION into the image they
|
||||
# build (FC_VERSION), and the extension takes its version from packaging.sh.
|
||||
#
|
||||
# The agent was deliberately absent from this list until then, and the reason
|
||||
# it was correct to be absent is the reason it no longer is: it asked this
|
||||
# script for `revision` alone. For a revision-only artifact this file needs no
|
||||
# entry — any change to how the revision is COMPUTED changes the derived value,
|
||||
# which then disagrees with the label on the published image and forces a
|
||||
# rebuild. That mechanism is self-correcting because it compares against a
|
||||
# string stamped into a real artifact.
|
||||
#
|
||||
# Giving the agent a self-reported version took that backstop away from it, so
|
||||
# it inherits web's exposure and has to be listed here in the SAME change that
|
||||
# introduced the version. Left out, the failure is the one recorded below: a
|
||||
# change to `cmd_version` alone would move no revision, the reuse check would
|
||||
# hit, the build would be skipped, and the published agent would go on
|
||||
# reporting the old format — with nothing anywhere to disagree with it.
|
||||
#
|
||||
# The version is compared against nothing, so it has no such backstop. Before
|
||||
# this entry, a change to cmd_version alone left every artifact's revision
|
||||
@@ -100,7 +113,7 @@ ext_paths() {
|
||||
cmd_paths() {
|
||||
case "$1" in
|
||||
web) echo "$WEB_PATHS $DERIVER $(ext_paths)" ;;
|
||||
agent) echo "$AGENT_PATHS" ;;
|
||||
agent) echo "$AGENT_PATHS $DERIVER" ;;
|
||||
extension) ext_paths ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""The agent's self-report: a derived version, and a build id kept apart from it.
|
||||
|
||||
The agent's version used to be a literal in `app.py` that an author was asked
|
||||
to bump on every change. Nobody did, so the September image printed the same
|
||||
`2026-07-17.1` as the July one — the one surface meant to answer *"did my pull
|
||||
work?"* gave the same answer either way.
|
||||
|
||||
These pin the two properties that make the replacement worth having: the
|
||||
displayed string degrades honestly when a build carries no stamp, and the value
|
||||
the page COMPARES is not the value it SHOWS (rule 149).
|
||||
|
||||
`build_info` is stdlib-only on purpose, so the suite can import it — `app` is
|
||||
unimportable here (torch, transformers, ultralytics are not in the CI image,
|
||||
which is why build.yml lints the agent and then only `compileall`s it).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from agent.fc_agent import build_info
|
||||
|
||||
APP = Path(__file__).resolve().parents[1] / "agent" / "fc_agent" / "app.py"
|
||||
|
||||
|
||||
def _stamp(monkeypatch, *, version="", channel="", revision=""):
|
||||
monkeypatch.setattr(build_info, "FC_VERSION", version)
|
||||
monkeypatch.setattr(build_info, "FC_CHANNEL", channel)
|
||||
monkeypatch.setattr(build_info, "FC_REVISION", revision)
|
||||
|
||||
|
||||
# --- what a person reads -----------------------------------------------------
|
||||
|
||||
|
||||
def test_an_unstamped_build_says_unknown_rather_than_nothing(monkeypatch):
|
||||
"""A blank in the meta line reads as "no version", which is a different and
|
||||
false claim from "this build does not carry one". Same posture the web
|
||||
image's build_info takes, for the same reason."""
|
||||
_stamp(monkeypatch)
|
||||
|
||||
assert build_info.display_version() == "unknown"
|
||||
|
||||
|
||||
def test_the_channel_rides_beside_the_version_not_inside_it(monkeypatch):
|
||||
"""Rule 149. The channel is a sibling field; a `-dev` folded into the name
|
||||
is issue #2993's exact failure. Rendering it in parentheses is display, not
|
||||
encoding — `FC_VERSION` itself never carries it, which the next test is
|
||||
what actually holds."""
|
||||
_stamp(monkeypatch, version="2026.09.24.1052", channel="dev")
|
||||
|
||||
assert build_info.display_version() == "2026.09.24.1052 (dev)"
|
||||
|
||||
|
||||
def test_a_version_with_no_channel_is_the_bare_version(monkeypatch):
|
||||
_stamp(monkeypatch, version="2026.09.24.1052")
|
||||
|
||||
assert build_info.display_version() == "2026.09.24.1052"
|
||||
|
||||
|
||||
# --- what the page decides on ------------------------------------------------
|
||||
|
||||
|
||||
def test_the_build_id_prefers_the_revision(monkeypatch):
|
||||
"""Two builds of the same commit ARE the same agent and must not prompt a
|
||||
reload; two different commits always differ here, even when they land in
|
||||
the same minute and derive one version name. The revision is the only one
|
||||
of the three that is true of the content rather than of the clock."""
|
||||
_stamp(monkeypatch, version="2026.09.24.1052", revision="a3071a754907")
|
||||
|
||||
assert build_info.build_id() == "a3071a754907"
|
||||
|
||||
|
||||
def test_the_build_id_falls_back_to_the_version_then_to_a_constant(monkeypatch):
|
||||
_stamp(monkeypatch, version="2026.09.24.1052")
|
||||
assert build_info.build_id() == "2026.09.24.1052"
|
||||
|
||||
_stamp(monkeypatch)
|
||||
# A constant, deliberately — see build_info.build_id. A per-process nonce
|
||||
# would make every ordinary container RESTART claim a new version had
|
||||
# arrived, a false positive on the surface the banner exists to keep
|
||||
# trustworthy.
|
||||
assert build_info.build_id() == "local"
|
||||
|
||||
|
||||
def test_the_displayed_string_is_never_the_compared_one(monkeypatch):
|
||||
"""The property both of the above serve, stated once on its own. Folding
|
||||
these together is how the version acquires a second job and then cannot be
|
||||
changed without breaking the reload banner."""
|
||||
_stamp(monkeypatch, version="2026.09.24.1052", channel="dev",
|
||||
revision="a3071a754907")
|
||||
|
||||
assert build_info.display_version() != build_info.build_id()
|
||||
|
||||
|
||||
# --- the page wires them to the right places ---------------------------------
|
||||
#
|
||||
# Read as SOURCE, not imported: `app` pulls the agent's GPU stack. Asserting on
|
||||
# what is PRESENT rather than on what is absent, deliberately — a guard that
|
||||
# greps for the absence of a pattern trips over the comment explaining why the
|
||||
# pattern is gone (lesson #4365), and app.py's new comment names the old
|
||||
# literal it replaced.
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"marker, why",
|
||||
[
|
||||
(
|
||||
'id=build>__VERSION__',
|
||||
"the meta line must show the human-readable version",
|
||||
),
|
||||
(
|
||||
'const PAGE_BUILD="__BUILD_ID__"',
|
||||
"the staleness check must compare the build id, not the version",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_the_control_page_shows_one_value_and_compares_the_other(marker, why):
|
||||
assert marker in APP.read_text(), why
|
||||
@@ -131,14 +131,21 @@ DERIVERS = [
|
||||
# so the reuse path republishes the old image and the fresh signature is
|
||||
# orphaned. Guarded for web too, since web bundles what the extension makes.
|
||||
("extension/scripts/packaging.sh", ("extension", "web")),
|
||||
# artifacts.sh decides the FC_VERSION baked into the web image (#3202).
|
||||
# Web only, and deliberately: the agent asks this script for `revision`
|
||||
# alone, so they are covered by the self-correcting path above, and the
|
||||
# extension takes its version from packaging.sh. Milestone 318 step 5 is
|
||||
# the worked instance — b3989d0 and 5771fd5 share revision fb2c4d5b80be
|
||||
# while the version moved 2026.8.28.1249 -> 2026.08.28.1249. It was
|
||||
# harmless only because FC_VERSION did not exist until one commit later.
|
||||
("scripts/artifacts.sh", ("web",)),
|
||||
# artifacts.sh decides the FC_VERSION baked into the web image (#3202)
|
||||
# and, since 2026-09-24, the agent image too.
|
||||
#
|
||||
# It was web-only before that, and correctly so: the agent asked this
|
||||
# script for `revision` alone, which the self-correcting path above
|
||||
# covers. Giving the agent a self-reported version is what moved it into
|
||||
# this list, and the two had to happen in the SAME change — a version with
|
||||
# no deriver entry is the exposure this list exists for, not a smaller
|
||||
# version of it. The extension still takes its version from packaging.sh.
|
||||
#
|
||||
# Milestone 318 step 5 is the worked instance — b3989d0 and 5771fd5 share
|
||||
# revision fb2c4d5b80be while the version moved 2026.8.28.1249 ->
|
||||
# 2026.08.28.1249. It was harmless only because FC_VERSION did not exist
|
||||
# until one commit later.
|
||||
("scripts/artifacts.sh", ("web", "agent")),
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user