|
All checks were successful
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 7s
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 2s
CENTRALIS_TOKENS has a parse-and-prove check in both renderers because it is JSON, full of double quotes, living in a file bash expands, and because Centralis fails closed on it. CENTRALIS_TEAM is the same kind of value with none of the same guard. It is the allowlist for Google sign-in. Centralis' buildOAuth() requires a Google client AND at least one team member, so an empty or malformed team does not fail loudly -- it turns OAuth off and leaves the server bearer-token-only, which looks exactly like never having configured it. The sharper failure is a partial one. parseTeam() (centralis src/config.ts) SKIPS an unparseable entry with a console.error and carries on, so an address with a typo'd domain drops exactly one person, behind a log line on a container start nobody watches. Entries parseTeam would skip are therefore refused here, where a human is reading the output. Accepts all three shapes parseTeam accepts -- email->role, email->object, and an array of objects -- and matches its case-insensitive duplicate detection. Rejects invalid JSON, a non-object/array, an entry without an @, a duplicate address, and an empty team. Prints member addresses and roles on success, the same argument the adjacent CENTRALIS_TOKENS line makes for printing subjects: seeing the list is how an operator notices somebody is missing. Added to BOTH renderers with identical logic. The parity argument this migration rests on is that the two backends produce the same file; a validation present on only one side means one backend can write a file the other would have refused. Verified end to end against the live vault with verify-centralis-render.sh: 9 keys, 2 subjects, VERDICT PASS, compose model digests identical. |
||
|---|---|---|
| .claude | ||
| .forgejo/workflows | ||
| backend | ||
| docs/onboarding | ||
| frontend | ||
| infra | ||
| observability | ||
| .gitignore | ||
| CLAUDE.md | ||
| CUTOVER-NOTES.md | ||
| README.md | ||
thermograph
The Thermograph monorepo — the split repos reunified (2026-07-22) with full history via subtree merges, while keeping everything the split was actually for: per-domain images, per-domain deploys, and an async FE/BE contract.
Domains
| Dir | What | CI |
|---|---|---|
backend/ |
FastAPI graded-climate API, accounts, notifications (Discord bot, push, mail), data pipeline | build-push → image emi/thermograph/backend; deploy |
frontend/ |
Public client: static JS/CSS + SSR pages | same build-push / deploy workflows, matrixed by domain; image emi/thermograph/frontend |
infra/ |
Compose (dev, on vps1) + two Swarm stacks co-resident on vps2 (beta, prod), deploy scripts, terraform, SOPS secrets vault, ops cron | infra-sync (host checkout + secrets render), secrets-guard, ops-cron |
observability/ |
Loki + Grafana + Alloy stack | observability-validate |
thermograph-docs deliberately stays its own repo (ADRs + runbooks, no
build artifacts, different change cadence).
New here?
docs/onboarding/ is the developer onboarding
path: orientation, verified local-setup recipes, a per-domain deep dive, the
cross-service contracts that break silently, the release flow, and a list of
which docs in this repo are currently stale.
How CI stays decoupled
Every workflow in .forgejo/workflows/ is path-filtered to its domain: a
push touching only frontend/** builds/deploys nothing else. Images stay
separate (emi/thermograph/backend, emi/thermograph/frontend, each tagged
sha-<12hex>), deploys stay per-service (infra/deploy/deploy.sh SERVICE=backend|frontend|all), and the API version contract
(GET /api/version, PAYLOAD_VER) still lets FE and BE ship out of lockstep.
The one intentionally coupled piece is pr-build.yml: a single always-running
gate required check that builds only the domains a PR touches (a
path-filtered required check would deadlock auto-merge).
Branch model (unchanged from the split era): PRs → dev, main → beta,
release → prod. Infra isn't environment-staged the same way app images are:
beta's and prod's checkouts (both on vps2) track main; dev's checkout (on
vps1) tracks dev itself, since it's the one environment that isn't a
rehearsal for something downstream.
Before pointing anything live at this repo, read CUTOVER-NOTES.md.