From 11b9d1891e195d4a807a97d98ac248ef496b285f Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Sat, 22 Aug 2026 15:31:50 -0700 Subject: [PATCH] fix(hrafn-ci): make the deploy converge instead of accrete 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. --- stacks/hrafn/ci/README.md | 9 +++++++ stacks/hrafn/ci/gitea-workflows-deploy.yaml | 15 +++++++---- stacks/hrafn/ci/playbooks-deploy.yaml | 30 +++++++++++++++++---- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/stacks/hrafn/ci/README.md b/stacks/hrafn/ci/README.md index 8039793..6cca569 100644 --- a/stacks/hrafn/ci/README.md +++ b/stacks/hrafn/ci/README.md @@ -38,6 +38,15 @@ then `docker compose build && up`. Two problems, both fixed here. - **`.env` is never deployed.** It is host-owned, `0600`, and holds the bearer token. The playbook *refuses to run* if it is missing or not `0600` — a guard added because the file arrived at `0644` on 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 and `rsync --delete` converges 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 `.env` and `.deployed`. ## Validation diff --git a/stacks/hrafn/ci/gitea-workflows-deploy.yaml b/stacks/hrafn/ci/gitea-workflows-deploy.yaml index 2e2020b..e7e87cc 100644 --- a/stacks/hrafn/ci/gitea-workflows-deploy.yaml +++ b/stacks/hrafn/ci/gitea-workflows-deploy.yaml @@ -67,14 +67,19 @@ jobs: chmod 600 ~/.ssh/config - name: Build the deploy context - # Only what the Dockerfile actually consumes, plus compose.yaml. - # Deliberately excludes tests/, docs/, persistent-memory.md, - # ROADMAP.md, CLAUDE.md, LICENSE and .env — none of them belong - # in a production compose directory. + # Only what the Dockerfile actually consumes, plus compose.yaml + # and .env.example. Deliberately excludes tests/, docs/, + # persistent-memory.md, ROADMAP.md, CLAUDE.md, LICENSE and .env — + # none of them belong in a production compose directory. + # + # This list is AUTHORITATIVE: the playbook converges the host + # directory onto this tarball with rsync --delete, so anything + # omitted here is removed from the host on the next deploy + # (except the host-owned .env and .deployed). run: | mkdir -p dist tar czf dist/hrafn-context.tgz \ - Dockerfile compose.yaml pyproject.toml README.md src + Dockerfile compose.yaml pyproject.toml README.md .env.example src echo "context contents:" tar tzf dist/hrafn-context.tgz diff --git a/stacks/hrafn/ci/playbooks-deploy.yaml b/stacks/hrafn/ci/playbooks-deploy.yaml index a8cbd2a..ef9f276 100644 --- a/stacks/hrafn/ci/playbooks-deploy.yaml +++ b/stacks/hrafn/ci/playbooks-deploy.yaml @@ -10,7 +10,11 @@ # 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 +# Dockerfile compose.yaml pyproject.toml README.md .env.example src +# +# The tarball must carry EVERYTHING that belongs in the compose dir: the +# converge step below deletes anything on the host that is not in it, +# except the host-owned .env and .deployed. # # DIFFERENCE 1 — tarball instead of per-file upload steps. nevermore # enumerates every source file as its own upload step. That is explicit, @@ -59,10 +63,26 @@ steps: 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 + - name: Unpack build context into a staging dir + shell: | + rm -rf {{ compose_dir }}/.stage && mkdir -p {{ compose_dir }}/.stage + tar xzf {{ compose_dir }}/.hrafn-context.tgz -C {{ compose_dir }}/.stage + rm -f {{ compose_dir }}/.hrafn-context.tgz + + - name: Converge the compose dir onto the build context + # The first version of this playbook unpacked in place, which + # ACCRETED: it 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. rsync --delete + # makes the directory CONVERGE on the build context, so a stray file + # — from an old deploy or a debugging session — cannot outlive the + # next deploy. Host-owned state is protected by name. + shell: | + rsync -a --delete \ + --exclude '.env' --exclude '.deployed' \ + {{ compose_dir }}/.stage/ {{ compose_dir }}/ + rm -rf {{ compose_dir }}/.stage # ── build + start ───────────────────────────────────────────────────