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
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:
@@ -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
@@ -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"]
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user