feat: the whole application is what the image runs by default (4296)
CI / lint (push) Successful in 4s
CI / extension-version (push) Successful in 3s
Build images / sign-extension (push) Successful in 5s
Build images / build-agent (push) Successful in 7s
extension / lint (push) Successful in 19s
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 32s
Build images / build-web (push) Successful in 1m41s
CI / integration (push) Successful in 2m7s
Build images / smoke-web (push) Failing after 12m36s
Build images / promote (push) Skipped

Operator, 2026-09-23: *"I also want to see that we remove the need for the
command line of the configuration in the consolidated version."*

`CMD` was `web`, so the single-container layout only worked if you knew to
ask for it by name. A compose file that forgot `command: ["all"]` got a web
server with nothing processing its queues — a gallery that loads, accepts an
import, and never finishes one. Nothing errors; it just never progresses.

Now `docker run fabledcurator` with no command starts hypercorn plus every
lane under supervisord. `entrypoint.sh`'s own default moves with it, since
the two are doors to the same decision and a disagreement would only show up
as `--entrypoint` behaving differently from a plain run.
`docker-compose.single.yml` drops its `command:` line; `["all"]` still works
and still means the same thing.

The multi-service stack is untouched — every service there names its role
explicitly, which is what makes it the multi-service stack.

## And CI now actually boots it

This is the gap I should have named when I reported milestone 422 at 7/7 and
did not. Measured, not inferred: the smoke booted role `web` only
(build.yml:1454), nothing in CI ran `all`, `docker-compose.single.yml` was
read as TEXT by one test checking stop_grace_period and never run, and
test_gen_supervisord asserts the generated config against the lane table
without ever handing it to supervisord.

So the shape this milestone is NAMED for had started nowhere. Steps 5-7 were
marked done on evidence that did not cover it, and the operator is about to
collapse their production stack onto exactly that.

The smoke now boots the image with NO command — checking the Dockerfile CMD,
the entrypoint default and the role together, the way an adopter gets it —
and asserts `healthcheck_all`, which was itself never executed. That check
passes only when hypercorn answers AND every lane in the table answers the
broker; a web-only check goes green with every worker dead, which is the
failure mode consolidation creates. It then prints `supervisorctl status`, so
a lane that is merely restart-looping is visible rather than inferred.

Cheap because ml ships at 0 slots and disabled: nothing loads a model, and
the lane answers `inspect` with its consumers cancelled, which is what
healthy means for a disabled lane.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
2026-09-23 08:48:11 -04:00
co-authored by Claude Opus 5
parent 2677ce020c
commit 43ac737516
4 changed files with 71 additions and 10 deletions
+50 -4
View File
@@ -1374,15 +1374,16 @@ jobs:
# CID is empty until the app container exists, so this is safe to
# arm now and still covers a failure before that point.
CID=""
CID_ALL=""
cleanup() {
rc=$?
if [ -n "$CID" ]; then
for c in $CID $CID_ALL; do
# The log ONLY on failure — a boot that never answered must fail
# with the reason visible rather than as a bare timeout (rule
# 156), while a green run has nothing to say.
[ $rc -eq 0 ] || docker logs "$CID" 2>&1 | tail -40
docker rm -f "$CID" >/dev/null 2>&1 || true
fi
[ $rc -eq 0 ] || docker logs "$c" 2>&1 | tail -40
docker rm -f "$c" >/dev/null 2>&1 || true
done
docker network rm "$NET" >/dev/null 2>&1 || true
# Preserve the real status, which a trap ending on a successful
# `docker rm` would otherwise mask.
@@ -1511,6 +1512,51 @@ jobs:
docker run --rm --network "$NET" "$CANDIDATE" shell -c \
"python3 -c \"import urllib.request; print(urllib.request.urlopen('http://$WEB_IP:8080/api/health', timeout=5).read().decode())\""
# 4. THE DEFAULT ROLE. Everything above boots `web` explicitly, so
# until now nothing had ever started the shape the single-
# container layout actually runs — supervisord bringing up
# hypercorn plus one celery process per lane. The milestone is
# named for that shape and CI had never once executed it.
#
# Deliberately NO command: this is `docker run <image>` with
# nothing after it, so it checks the Dockerfile's CMD and the
# entrypoint's default together with the role itself. An adopter
# who writes no `command:` gets exactly this.
#
# `healthcheck_all` is the right assertion and was itself never
# run: it passes only when hypercorn answers AND every lane in
# the table is answering the broker. A web-only check would go
# green with every worker dead, which is the failure mode
# consolidation creates.
#
# Cheap because ml ships at 0 slots and disabled, so nothing
# loads a model — the lane answers `inspect` with its consumers
# cancelled, which is what "healthy" means for a disabled lane.
echo "smoke: the default role boots every lane under supervisord"
CID_ALL=$(docker run -d $ENVOPTS "$CANDIDATE")
lanes_up=""
for i in $(seq 1 60); do
if docker exec "$CID_ALL" python -m backend.app.scripts.healthcheck_all; then
lanes_up=1
break
fi
if [ "$(docker inspect -f '{{.State.Running}}' "$CID_ALL" 2>/dev/null)" != "true" ]; then
echo "smoke: FAILED — the all-role container exited during boot." >&2
exit 1
fi
sleep 3
done
if [ -z "$lanes_up" ]; then
echo "smoke: FAILED — supervisord came up but the composite" >&2
echo "smoke: healthcheck never passed: either hypercorn did not" >&2
echo "smoke: answer or a lane is not consuming. Its log follows." >&2
exit 1
fi
echo "smoke: every lane answered"
# Say WHICH processes supervisord is running, so a lane that is
# merely restart-looping is visible rather than inferred.
docker exec "$CID_ALL" supervisorctl -c "${SUPERVISOR_CONF:-/tmp/supervisord.conf}" status || true
echo "smoke: all checks passed against $CANDIDATE, with egress blocked"
# Move the channel tags — the whole point of the gate.
+12 -1
View File
@@ -127,4 +127,15 @@ ENV FC_VERSION=${FC_VERSION}
EXPOSE 8080
ENTRYPOINT ["./entrypoint.sh"]
CMD ["web"]
# The DEFAULT is the whole application, not one lane of it.
#
# `docker run fabledcurator` with no command starts hypercorn plus every
# worker lane under supervisord — the shape an adopter wants and the shape the
# consolidated stack runs. It was `web`, which meant the single-container
# layout only worked if you knew to ask for it by name, and a compose file
# that forgot `command:` got a web server with nothing processing its queues:
# a gallery that loads, accepts an import, and never finishes one.
#
# The multi-service stack is unaffected — every service there names its role
# explicitly, which is exactly what makes it the multi-service stack.
CMD ["all"]
+4 -4
View File
@@ -59,10 +59,10 @@ services:
fabledcurator:
image: git.fabledsword.com/bvandeusen/fabledcurator:latest
# Everything: hypercorn plus one celery process per lane, under
# supervisord, whose config is generated from the application's own lane
# table so the two cannot disagree.
command: ["all"]
# No `command:`. Everything hypercorn plus one celery process per lane
# under supervisord — is what the image does by default, and supervisord's
# config is generated from the application's own lane table so the two
# cannot disagree. `command: ["all"]` still works and means the same thing.
# tini as PID 1, in front of supervisord. supervisord reaps its own
# children, but a container's PID 1 also inherits orphans from anywhere
# below — celery's prefork pool and gallery-dl's subprocesses both make
+5 -1
View File
@@ -1,7 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
ROLE="${1:-web}"
# Defaults to the whole application (see the Dockerfile's CMD). Kept in step
# with that CMD deliberately: they are two doors to the same decision, and a
# disagreement between them would only show up as `docker run --entrypoint`
# behaving differently from `docker run`.
ROLE="${1:-all}"
shift || true
case "$ROLE" in