thermograph/CLAUDE.md
emi d138f00a20
Some checks failed
Sync infra to hosts / sync-beta (push) Has been skipped
Sync infra to hosts / sync-prod (push) Has been skipped
Sync infra to hosts / sync-dev (push) Failing after 6s
secrets-guard / encrypted (push) Successful in 6s
shell-lint / shellcheck (push) Successful in 13s
Validate observability stack / validate (push) Successful in 17s
PR build (required check) / changes (pull_request) Successful in 6s
secrets-guard / encrypted (pull_request) Successful in 5s
PR build (required check) / build-backend (pull_request) Has been skipped
shell-lint / shellcheck (pull_request) Successful in 6s
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Successful in 18s
PR build (required check) / gate (pull_request) Successful in 2s
infra: split the estate into vps1/vps2 — beta joins prod, dev gets a home (#103)
2026-07-26 06:56:38 +00:00

5.6 KiB

thermograph monorepo — agent instructions

One repo, four domains: backend/, frontend/, infra/, observability/. Reunified 2026-07-22 from four split repos, with history, via subtree merges. thermograph-docs is deliberately a separate repo — cross-cutting decision records and operator runbooks go there, not here.

Rule for this file and every domain CLAUDE.md: only statements that would break CI if they became false, or that name a file that exists. Background, history and rationale belong in thermograph-docs. These files are read before every change, so a stale one is a correctness bug, not a documentation bug.

Branches and environments

Branch Deploys to Workflow
feature branch nothing PR into dev
dev dev (vps1, mesh-only, own Postgres) deploy.yml
main beta (beta.thermograph.org, vps2) deploy.yml
release prod (thermograph.org, vps2) deploy.yml

dev, main and release are protected: everything is a PR, for humans and agents alike. Promotion is one PR per hop, devmainrelease.

dev is a first-class hosted environment, not just an integration branch. It runs on vps1 — the same box as Forgejo and the monitoring stack — with its own Postgres container, reachable only on the WireGuard mesh (10.10.0.2:8137): no public DNS record, no Caddy site, no TLS. It is deployed by CI like beta and prod. The desktop hosts no Thermograph environment at all; make dev-up there is a laptop convenience for running the stack locally, not a deployment target.

One workflow deploys everything. deploy.yml handles both services and all three environments: the branch selects the environment (dev → vps1, main → beta on vps2, release → prod on vps2), a matrix covers backend and frontend, and each leg checks whether this push actually touched its domain before rolling. build-push.yml is the same shape for images. THERMOGRAPH_ENV is the input that tells a leg which environment it's deploying — load-bearing on vps2, which runs beta and prod side by side and has no other way to tell them apart. See infra/deploy/env-topology.sh for the single source of truth on where each environment's checkout, branch, stack and ports live.

The deploy contract

One entry point, two modes, one contract:

SERVICE=backend|frontend|all  BACKEND_IMAGE_TAG=sha-<12hex>  FRONTEND_IMAGE_TAG=sha-<12hex> \
  /opt/thermograph/infra/deploy/deploy.sh

deploy.sh resets the host checkout, renders secrets from the SOPS vault, then either rolls compose services or — if infra/deploy/env-topology.sh says the environment's deploy mode is stack — execs infra/deploy/stack/deploy-stack.sh. The old host-wide /etc/thermograph/deploy-mode marker survives only as a fallback for a by-hand run with no environment resolvable any other way; it cannot describe vps2 alone, since vps2 runs beta and prod side by side.

  • prod and beta both run Swarm, as two separate stacks co-resident on vps2. Prod's is infra/deploy/stack/thermograph-stack.yml (db, web, worker, lake, daemon, frontend, autoscaler, autoscaler-lake). Beta's is the separate infra/deploy/stack/thermograph-beta-stack.yml — its services are prefixed (beta-web, beta-worker, …) because Swarm registers a service's short name as a DNS alias on every network it joins, and beta shares prod's data network to reach the database. Beta has no db service of its own: one TimescaleDB instance serves both, on separate databases and separate NOSUPERUSER roles. deploy-stack.sh also offers STACK_TEST=1: a full parallel rehearsal on throwaway volumes and ports that cannot touch live data.
  • dev runs compose, from infra/docker-compose.yml (db, backend, lake, daemon, frontend), on its own host (vps1) with its own Postgres container — the one environment not sharing a database with anything else.
  • Each service's live tag is persisted host-side, so a single-service roll never disturbs the sibling's running tag.

Images are emi/thermograph/backend and emi/thermograph/frontend, tagged sha-<12hex> on every push and by semver on v*.*.* tags. They build and deploy independently — a backend change ships without a frontend deploy and vice versa. That independence is the point of the FE/BE split and survived reunification.

Rules that bind across domains

  • CI lives only in root .forgejo/workflows/, path-filtered per domain. Never add workflows under a domain's own .forgejo/ — they are inert there and become a trap.
  • Secrets only via the SOPS vault (infra/deploy/secrets/). Never hand-edit /etc/thermograph.env on a host — it is a rendered artifact. secrets-guard CI rejects plaintext. seed-from-live.sh reads production secrets and is explicitly not for an agent to run.
  • FE and BE ship out of lockstep, so the /api/version contract and PAYLOAD_VER discipline are load-bearing — see backend/CLAUDE.md.
  • Anything named "prefetch" must never spend the Open-Meteo quota. Nominatim ≤ 1 req/s.
  • The compose project name is pinned (name: thermograph in infra/docker-compose.yml); dev (and a local make dev-up) overrides with COMPOSE_PROJECT_NAME=thermograph-dev. Don't remove either half — the pinned name is what makes the Swarm stack's external volume names line up.
  • CUTOVER-NOTES.md is the source of truth for what is and isn't live yet.

Commits & PRs

Describe only the substance of the change; concise and technical. Never mention AI, Claude, assistants or automated authorship anywhere — no trailers, co-authors, emoji, or "as requested" narration.