hrafn was handed to infra-ops for uptime ownership with no CI deploy and no commit provenance -- the image was always local/hrafn:v1 and the whole working tree lived in the compose directory. These two files fix both. They are authored here because infra-ops owns hrafn's uptime, but they belong in vh/hrafn; claude-bot is not a collaborator there, so they are handed to the repo holder rather than committed directly. - playbooks-deploy.yaml -> vh/hrafn playbooks/deploy.yaml - gitea-workflows-deploy.yaml -> vh/hrafn .gitea/workflows/deploy.yaml Design calls recorded in the README: the build context travels as one tarball rather than per-file upload steps (nevermore's pattern fails open when a new source file has no matching step), and the playbook refuses to deploy unless .env exists at 0600 -- a guard prompted by it arriving 0644 with a live bearer token in it. Validated with `elway --dry-run`, which caught a real interpolation bug during authoring. No new Actions secrets needed.
110 lines
5.0 KiB
YAML
110 lines
5.0 KiB
YAML
# Deploy hrafn (fleet browser-fetch service) to a Docker host.
|
|
#
|
|
# Single source of truth for "how hrafn is deployed" — CI and manual runs
|
|
# take the same path. Modelled on nevermore's playbook, with two deliberate
|
|
# differences noted below.
|
|
#
|
|
# Usage (from this repo's root):
|
|
# elway <host> --playbook playbooks/deploy.yaml
|
|
#
|
|
# Requires a build-context tarball at dist/hrafn-context.tgz. The CI
|
|
# workflow builds it; for a manual run, build it the same way:
|
|
# mkdir -p dist && tar czf dist/hrafn-context.tgz \
|
|
# Dockerfile compose.yaml pyproject.toml README.md src
|
|
#
|
|
# DIFFERENCE 1 — tarball instead of per-file upload steps. nevermore
|
|
# enumerates every source file as its own upload step. That is explicit,
|
|
# but it fails OPEN: add a new src/hrafn/*.py and forget the matching
|
|
# step, and the deploy silently ships without it. hrafn's build context
|
|
# is a whole package, so it travels as one archive and cannot go partial.
|
|
#
|
|
# DIFFERENCE 2 — the image is tagged with the commit SHA, not a fixed
|
|
# `v1`. Before this, nothing on the host could answer "what is running";
|
|
# now the image tag IS the answer, and rolling back is retagging.
|
|
#
|
|
# NOT DEPLOYED BY THIS PLAYBOOK: .env. It is host-owned, 0600, and holds
|
|
# the bearer token. Uploading it would either clobber the live token or
|
|
# leak it into the repo. It is provisioned once by hand from the vault
|
|
# item `ana-docker/hrafn/bearer-token`.
|
|
|
|
vars:
|
|
compose_dir: /opt/docker/compose/hrafn
|
|
# Overridden by CI with the triggering commit. `manual` marks a
|
|
# hand-run deploy so an un-provenanced image is obvious on the host.
|
|
hrafn_sha: manual
|
|
|
|
steps:
|
|
# ── preconditions ───────────────────────────────────────────────────
|
|
|
|
- name: Verify compose dir exists + writable
|
|
shell: test -w {{ compose_dir }}
|
|
changed_when: "false"
|
|
|
|
- name: Verify .env is present and 0600 (deploy must not create it)
|
|
shell: |
|
|
test -f {{ compose_dir }}/.env || { echo "MISSING .env — provision it from vault item ana-docker/hrafn/bearer-token"; exit 1; }
|
|
perms=$(stat -c %a {{ compose_dir }}/.env)
|
|
test "$perms" = "600" || { echo "REFUSING: .env is $perms, expected 600 (it holds the bearer token)"; exit 1; }
|
|
changed_when: "false"
|
|
|
|
- name: Verify traefik-net exists (consumers reach hrafn on it)
|
|
shell: docker network inspect traefik-net >/dev/null 2>&1
|
|
changed_when: "false"
|
|
|
|
# ── ship the build context ──────────────────────────────────────────
|
|
|
|
- name: Upload build context
|
|
upload:
|
|
src: dist/hrafn-context.tgz
|
|
dest: "{{ compose_dir }}/.hrafn-context.tgz"
|
|
mode: "0600"
|
|
|
|
- name: Unpack build context
|
|
# Overwrites tracked files in place; leaves .env and any host-only
|
|
# state alone because the archive does not contain them.
|
|
shell: tar xzf {{ compose_dir }}/.hrafn-context.tgz -C {{ compose_dir }} && rm -f {{ compose_dir }}/.hrafn-context.tgz
|
|
|
|
# ── build + start ───────────────────────────────────────────────────
|
|
|
|
- name: Build image tagged with the commit
|
|
shell: cd {{ compose_dir }} && HRAFN_TAG={{ hrafn_sha }} docker compose build
|
|
|
|
- name: Start/replace the container on the new image
|
|
shell: cd {{ compose_dir }} && HRAFN_TAG={{ hrafn_sha }} docker compose up -d
|
|
|
|
- name: Record what is deployed
|
|
# The provenance fix. Anyone on the box can now answer "what commit
|
|
# is this" without asking the person who last rsync'd.
|
|
shell: |
|
|
printf 'sha=%s\ndeployed_at=%s\n' "{{ hrafn_sha }}" "$(date -Is)" > {{ compose_dir }}/.deployed
|
|
chmod 644 {{ compose_dir }}/.deployed
|
|
|
|
verify:
|
|
- name: Container reports healthy
|
|
# start_period is 20s and the interval is 30s, so allow two cycles
|
|
# before calling it a failure rather than a slow start.
|
|
shell: |
|
|
# elway's variable regex only matches an identifier starting with a
|
|
# letter or underscore, so docker's .State Go template below passes
|
|
# through untouched and needs no escaping.
|
|
for i in $(seq 1 20); do
|
|
s=$(docker inspect hrafn --format '{{.State.Health.Status}}' 2>/dev/null || echo missing)
|
|
[ "$s" = "healthy" ] && { echo "healthy after ${i}0s"; exit 0; }
|
|
sleep 10
|
|
done
|
|
echo "still $s after 200s"; docker logs --tail 40 hrafn; exit 1
|
|
changed_when: "false"
|
|
|
|
- name: Readiness endpoint answers on the shared network
|
|
# Proves consumers can actually reach it, not just that the process
|
|
# is up — hrafn has no published host port, so this has to run from
|
|
# inside traefik-net.
|
|
shell: >
|
|
docker run --rm --network traefik-net curlimages/curl:latest
|
|
-sf -m 10 -o /dev/null -w '%{http_code}\n' http://hrafn:8080/readyz
|
|
changed_when: "false"
|
|
|
|
- name: Deployed SHA matches what we just shipped
|
|
shell: grep -q "^sha={{ hrafn_sha }}$" {{ compose_dir }}/.deployed
|
|
changed_when: "false"
|