Files
vh c6864996f0 skaldsong playbook: verify container is Up, not 'healthy' (was racing start_period)
The verify step ran 0.09s after `docker compose up -d --force-recreate`,
well before the container's 30s start_period elapsed — so 'healthy'
isn't yet in docker ps's Status string and the grep failed. False
negative; container was operationally up (verify 1/2 already confirmed
/health 200).

Greping for '^Up' instead catches the case verify 2/2 actually wants
to catch (container exited entirely) without racing the healthcheck.
2026-05-18 23:47:51 -07:00

124 lines
5.5 KiB
YAML

# Deploy skaldsong (https://gitea.phasefinal.com/vh/skaldsong) to a
# Docker host following the PFI /opt/docker/ convention (ana-docker by
# default, but the playbook works against any host with Docker in place).
#
# Registry-pull pattern, NOT build-on-host: vh/skaldsong's CI builds and
# pushes `gitea.phasefinal.com/vh/skaldsong:<sha>` (+ :latest) to the
# gitea registry; this playbook just pulls + recreates the container.
# Differs from althing-chamber / asset-engine playbooks which build the
# image on-host from a git clone — those are local-build stacks.
#
# Idempotent: rerunning is safe. Creates-gates skip work that's already
# done; `docker compose up -d` is itself idempotent (no restart unless
# compose content, env, or image SHA changed).
#
# Usage:
# # CI passes the triggering commit SHA via --var ref=${{ github.sha }}:
# scripts/elway ana-docker --playbook playbooks/deploy-skaldsong.yaml \
# --var ref=<sha>
#
# # Manual runs can pass a branch or tag (will resolve to the registry
# # tag of that name — CI publishes :main and :v0.1.0 tags alongside
# # the SHA-pin):
# scripts/elway ana-docker --playbook playbooks/deploy-skaldsong.yaml \
# --var ref=main
#
# Prereqs on the target host:
# - Docker + docker compose plugin
# - Target user (lkraven) is in the `docker` group
# - `docker login gitea.phasefinal.com` has been done at least once
# (credentials persist in /home/lkraven/.docker/config.json). CI's
# workflow runs its own `docker login` step from REGISTRY_USER /
# REGISTRY_TOKEN secrets; manual runs assume the host's auth is
# already established.
vars:
registry_image: gitea.phasefinal.com/vh/skaldsong
ref: latest
compose_dir: /opt/docker/compose/skaldsong
db_dir: /opt/docker/conf/skaldsong/db
runs_dir: /opt/docker/conf/skaldsong/runs
host_port: "8300"
steps:
# ── compose + state dirs ────────────────────────────────────────────
- name: Ensure compose dir exists
shell: mkdir -p {{ compose_dir }}
creates: "{{ compose_dir }}"
- name: Ensure DB dir exists
# Created as lkraven (uid 1000 on these hosts), matching the
# container's app user — no chown dance needed.
shell: mkdir -p {{ db_dir }}
creates: "{{ db_dir }}"
- name: Ensure runs dir exists
# Separate from db_dir so runs/ can later move to a bigger volume
# without touching DB state.
shell: mkdir -p {{ runs_dir }}
creates: "{{ runs_dir }}"
# ── deploy compose files ────────────────────────────────────────────
- name: Upload compose.yaml
upload:
src: stacks/skaldsong/compose.yaml
dest: "{{ compose_dir }}/compose.yaml"
mode: "0644"
- name: Seed .env from template (only if absent)
upload:
src: stacks/skaldsong/.env.example
dest: "{{ compose_dir }}/.env"
mode: "0644"
when: "[ ! -f {{ compose_dir }}/.env ]"
# ── pin SKALDSONG_IMAGE to the requested ref ────────────────────────
- name: Set SKALDSONG_IMAGE pin to {{ ref }}
# Idempotent: strip any existing SKALDSONG_IMAGE= line and append
# the new one. CI passes --var ref=<sha>; manual runs pass branch /
# tag names (CI publishes those as registry tags too).
shell: |
sed -i '/^SKALDSONG_IMAGE=/d' {{ compose_dir }}/.env
echo "SKALDSONG_IMAGE={{ registry_image }}:{{ ref }}" >> {{ compose_dir }}/.env
# ── pull + bring up ─────────────────────────────────────────────────
- name: Pull image {{ registry_image }}:{{ ref }}
# Explicit pull step (rather than relying on compose up's pull) so
# any auth failure surfaces here, not buried in compose output.
shell: docker pull {{ registry_image }}:{{ ref }}
- name: docker compose up -d --force-recreate
# --force-recreate picks up env changes even if the image SHA is
# identical to what's running (rare but happens on env-only deploys).
# --pull never: we just pulled explicitly above, so no need for
# compose to re-check.
shell: cd {{ compose_dir }} && docker compose up -d --force-recreate --pull never
- name: Wait for skaldsong /health to respond
# Container's healthcheck is internal; this host-side poll confirms
# the published port is reachable + the FastAPI app finished booting.
# Generous retry budget — first-deploy bootstrapping (DB migrations,
# SPA asset indexing) can take 30s+.
shell: |
for i in $(seq 1 30); do
curl -sf -o /dev/null http://localhost:{{ host_port }}/health && exit 0
sleep 2
done
exit 1
changed_when: "false"
verify:
- name: skaldsong /health returns 200
shell: curl -sf -o /dev/null http://localhost:{{ host_port }}/health
changed_when: "false"
- name: skaldsong container running
# Grep for '^Up' (not 'healthy') — verify runs immediately after the
# deploy step finishes, which is well before the container's
# start_period (30s) elapses. The 'healthy' state from docker ps is
# a delayed echo of the same /health probe verify 1/2 already
# confirms, so racing the start_period here is bogus. Keep this
# check to catch the "container exited entirely" case.
shell: docker ps --filter name=^/skaldsong$ --format '{{.Status}}' | grep -q '^Up'
changed_when: "false"