The first CI run shipped clean but revealed a design gap in the playbook: unpacking the context tarball in place overwrites tracked files and never removes anything. Leftovers from the pre-CI hand-rsync (tests/, docs/, ROADMAP.md, persistent-memory.md, CLAUDE.md, LICENSE) survived the deploy and had to be cleaned off ana-docker by hand. That is the same failure class that produced the mess in the first place: a deploy that only ever adds cannot return the host to a known state. - unpack to a staging dir, then rsync --delete onto the compose dir - protect host-owned .env and .deployed by name - add .env.example to the context tarball so converge does not delete it - record in the workflow that the tar list is now AUTHORITATIVE: anything omitted is removed from the host on the next deploy Re-validated with `elway --dry-run` (9 steps, 3 verify, parses clean). Not yet in vh/hrafn -- infra-ops has no write access there, so this is offered to the repo holder rather than pushed.
hrafn CI deploy — authored here, lands in vh/hrafn
These two files replace hrafn's hand-rsync deploy. They are authored and
version-controlled here because infra-ops owns hrafn's uptime, but they
belong in the vh/hrafn repo — infra-ops has no write access to it
(claude-bot is not a collaborator), so they are handed to the repo holder
rather than committed directly.
| file here | destination in vh/hrafn |
|---|---|
playbooks-deploy.yaml |
playbooks/deploy.yaml |
gitea-workflows-deploy.yaml |
.gitea/workflows/deploy.yaml |
Keep this copy in sync if the deployed version changes, or delete it once
infra-ops has write access to vh/hrafn and the repo copy is canonical.
What the change buys
The pre-CI shape was: rsync a working tree into /opt/docker/compose/hrafn/,
then docker compose build && up. Two problems, both fixed here.
- No provenance. The image was always
local/hrafn:v1, so nothing on the box could answer "what commit is running". The image is now tagged with the commit SHA, and/opt/docker/compose/hrafn/.deployedrecords the SHA and timestamp. Rollback becomes a retag. - The whole repo lived in the compose directory —
tests/,docs/,ROADMAP.md,persistent-memory.md,CLAUDE.md. Only the build context ships now (Dockerfile,compose.yaml,pyproject.toml,README.md,src/).
Two design calls worth knowing
- Tarball, not per-file upload steps. nevermore's playbook enumerates
every source file as its own
upload:step. That is explicit, but it fails open: addsrc/hrafn/newthing.py, forget the matching step, and the deploy silently ships without it. hrafn's build context travels as one archive so it cannot go partial. .envis never deployed. It is host-owned,0600, and holds the bearer token. The playbook refuses to run if it is missing or not0600— a guard added because the file arrived at0644on handoff.- The deploy converges, it does not accrete. The first version unpacked
the tarball in place, which overwrote tracked files but never removed
anything — so leftovers from the pre-CI hand-rsync (
tests/,docs/,ROADMAP.md,persistent-memory.md,CLAUDE.md,LICENSE) survived the first CI deploy and had to be cleaned off the host by hand. Now the tarball unpacks to a staging dir andrsync --deleteconverges the compose directory onto it, so a stray file cannot outlive the next deploy. The consequence: the tar list in the workflow is authoritative — anything omitted from it is deleted from the host, except.envand.deployed.
Validation
The playbook parses and interpolates clean under elway's own parser:
scripts/elway ana-docker --playbook stacks/hrafn/ci/playbooks-deploy.yaml \
--var hrafn_sha=abc123def456 --dry-run
That dry-run is worth running after any edit — it caught a real bug during
authoring, where a comment containing a literal {{ ... }} identifier was
picked up by elway's variable substitution and failed the run.
No new Actions secrets are required: DEPLOY_SSH_KEY and MGMT_REPO_TOKEN
already exist at user scope on vh from the nevermore/task-board wiring.