# [2026-08-23] hrafn adopted; its CI deploy reported green while deploying nothing `hrafn` — genuine-Chromium browser-fetch behind a REST API, for bot-gated sites (Reddit first). Built by nevermore-claude on ana-docker, handed to infra-ops for uptime ownership. Internal-only on `traefik-net`, no host port; consumers reach `http://hrafn:8080`. Canonical at `stacks/hrafn/`. ## Intake found a live credential exposure `/opt/docker/compose/hrafn/.env` was mode **0644 with a live 57-char bearer token** — verified as real exposure by reading it as `nobody` on a box with four interactive accounts. Tightened to 0600. That triggered the wider sweep (see [[2026-08-23-ana-docker-env-perms-sweep]]). ## The CI defect — the one worth remembering I authored the deploy (elway playbook + gitea workflow) to replace a hand-rsync, tagging the image with the commit SHA for provenance. nevermore-claude later found v1.0.0 deploying "green" while the host still served 0.1.0. **Root cause was mine and nastier than either hypothesis.** The staging dir was `$compose_dir/.stage` — **inside** the rsync target. So `rsync -a --delete $compose_dir/.stage/ $compose_dir/` deleted `.stage` from the destination (absent from the source listing) **during** the transfer, destroying its own source mid-copy. Reproduced exactly: ``` before: app.py="OLD" leftover.txt .stage/app.py="NEW" after: app.py="OLD" leftover.txt GONE, .stage GONE ``` Deletion succeeded, the copy silently did not, rsync exited 0. So the directory *looked* converged while host source stayed frozen at the first manual rsync — and because the build's `COPY` inputs never changed, Docker full-cache-hit and every SHA tag aliased one image. **The provenance the tagging existed to provide was false for the pipeline's entire life.** **The real failure is the verification.** The verify steps asserted the marker, container health, and a 200 from `/readyz` — all of which pass against a completely frozen host. None measured *content*. A deploy that reports success without asserting the bytes changed is verifying an **uptime**, not a deploy. ## Fixes - stage at `/tmp/hrafn-deploy-stage`, outside the target - CI computes `context_sha256` over the shipped file list; the playbook recomputes it **on the host after the converge** and fails on mismatch - compare the running container's `src/**/*.py` against the host's, so a SHA tag cannot name layers the image lacks - **compare `*.py` only** — `pip install .` generates `src/*.egg-info/*` inside the image and `__pycache__` appears at runtime, so a naive `find src -type f` compare false-fails on every healthy deploy. Verified against a known-good container before shipping (12 host files, 18 in container, 0 content differences). - declined `--no-cache`: a cache hit is *correct* when the context is genuinely unchanged; assert the property rather than brute-force it. ## Access Operator granted claude-bot **write** on `vh/hrafn`, so infra-ops maintains the pipeline it owns instead of routing patches through the repo holder. `vh/hrafn` is canonical; `stacks/hrafn/ci/` is a verified mirror. Commits `b6924de`, `b001d0c`, `11b9d18`, `b38c369`, `9642952`.