All checks were successful
PR build (required check) / changes (pull_request) Successful in 6s
secrets-guard / encrypted (pull_request) Successful in 4s
shell-lint / shellcheck (pull_request) Successful in 6s
PR build (required check) / validate-observability (pull_request) Successful in 20s
PR build (required check) / build-frontend (pull_request) Successful in 37s
PR build (required check) / build-backend (pull_request) Successful in 51s
PR build (required check) / gate (pull_request) Successful in 1s
Repos moved to the Jinemi org; the container packages did not follow, since
Forgejo does not transfer packages with a repo. The deploy path still resolved
`emi/thermograph/*` — a user_redirect to admin_emi — while build-push.yml
derives its push path from ${github.repository}, now jinemi/thermograph. The
next backend or frontend build would have published somewhere no deploy looks.
Point the image paths at jinemi/thermograph/* (lowercase: OCI references admit
no uppercase, which is why build-push.yml already pipes through tr), the clone
URLs at Jinemi/thermograph, and the registry logins at admin_emi — the account
that actually owns the tokens, rather than the redirect.
The live tags and both ci-runner tags were copied into the Jinemi namespace
first, so the switch has something to pull. thermograph-infra,
thermograph-observability and the retired */app packages stay under admin_emi;
they did not move.
109 lines
5.8 KiB
Markdown
109 lines
5.8 KiB
Markdown
# 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, `dev` → `main` → `release`.
|
|
|
|
`.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 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 `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.
|