Release: dev → main (first public release) #258

Merged
bvandeusen merged 94 commits from dev into main 2026-09-25 10:02:40 -04:00
4 changed files with 71 additions and 10 deletions
Showing only changes of commit 43ac737516 - Show all commits
+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