thermograph/CLAUDE.md
Emi Griffith ea613e33ca
All checks were successful
PR build (required check) / changes (pull_request) Successful in 8s
shell-lint / shellcheck (pull_request) Successful in 7s
secrets-guard / encrypted (pull_request) Successful in 10s
PR build (required check) / build-backend (pull_request) Has been skipped
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / gate (pull_request) Successful in 3s
flow: squash into dev, fast-forward the promotions
Forgejo cannot scope merge styles per branch — they live on the repo's
pull-request unit, and BranchProtection carries no merge-style or linear-history
field in 9.0.3. So the chain is enforced repo-wide by allowing exactly two
styles: squash and fast-forward-only. Merge commits and rebase merges are off.

Squash belongs on the short-lived hop. Squashing dev into main would put a
commit on main that does not have dev's commits as ancestors, freezing
merge-base(dev, main); every later promotion would then list all commits since
that stale point and re-apply changes main already has. Fast-forwarding the
promotions instead means a promotion creates no commit at all, so the three
branches keep identical history rather than merely identical trees, and
`release ⊆ main ⊆ dev` holds permanently.

Rebase-updates stay enabled. They are a different setting from rebase merges,
and BRANCHING.md is right that the merge queue 403s without them.

Also adds `release` to pr-build.yml's pull_request branches. This is step one of
the order BRANCHING.md prescribes for closing the missing check on release:
teach the workflow to fire there, confirm on a real PR that the context appears,
and only then require it. Requiring a context nothing produces is what makes a
production promotion unmergeable with a 405 that names no cause.
2026-08-01 17:14:45 -07:00

6.8 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.

Merge style is part of the contract, and Forgejo enforces it repo-wide — it cannot scope styles per branch, so these are the only two enabled:

Hop Style
feat/*dev squash
devmain, mainrelease fast-forward only

Merge commits and rebase merges are disabled. A promotion therefore never creates a commit, which keeps release ⊆ main ⊆ dev true permanently: the three branches hold identical history, never merely identical trees. Squashing into main instead would strand merge-base(dev, main) and make every later promotion re-apply work already there. Rebase-updates stay enabled — the merge queue 403s without them.

.claude/BRANCHING.md is the orchestrator's runbook for that flow — the serialised merge queue, promotions, hotfix down-merges — and .claude/ownership.md is the module map saying which tasks may run concurrently. Read both before dispatching parallel work.

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 from vps1 itself (127.0.0.1:8137infra/docker-compose.yml binds the port to loopback, so the mesh does not reach it either; use an SSH tunnel): 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; the root Makefile (make install once, then make up / make down, wrapping infra's dev-up/dev-down) is a laptop convenience for running the stack locally, not a deployment target. install is what builds the :local image tags those compose files default to — nothing publishes them.

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 jinemi/thermograph/backend and jinemi/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.