|
All checks were successful
Sync infra to hosts / sync-beta (push) Has been skipped
Sync infra to hosts / sync-dev (push) Successful in 5s
secrets-guard / encrypted (push) Successful in 4s
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 1s
Sync infra to hosts / sync-prod (push) Has been skipped
shell-lint / shellcheck (push) Successful in 7s
secrets-guard / encrypted (pull_request) Successful in 5s
PR build (required check) / changes (pull_request) Successful in 7s
shell-lint / shellcheck (pull_request) Successful in 7s
Centralis could not migrate at all. `seed-from-sops.sh --all` refused
centralis.prod with `CENTRALIS_TOKENS: contains a shell metacharacter`, and
render-secrets-openbao.sh had no Centralis path whatsoever — so the seeded data
would have had nothing to read it.
Both halves come from applying the app stack's rules to a file that does not share
its shape. /etc/thermograph.env is raw unquoted KEY=value. /etc/centralis.env is
consumed by exactly one thing and that thing is a shell:
sudo bash -c 'set -a && . /etc/centralis.env && set +a && docker compose up'
CENTRALIS_TOKENS is JSON, so it can never satisfy a no-metacharacters rule, and
rendering it unquoted is precisely the 2026-07-24 failure: bash strips the quotes,
Centralis fails closed on the malformed registry and serves a single `shared`
identity, indistinguishable from the file never being written.
So:
- seed-from-sops.sh validates against the renderer that will consume the path. The
dotenv rules still apply to common and env/*; centralis/* is exempt because it is
single-quoted downstream.
- render-secrets-openbao.sh gains thermograph_render_openbao_centralis, mirroring
render_centralis_secrets: fetch as JSON, emit POSIX single-quoted assignments,
then PROVE the file by sourcing it under `env -i` with `set -a` and comparing
every value back before anything touches the destination. Mirrored rather than
shared, per the existing note above thermograph_render_openbao — consolidate when
the SOPS path is deleted.
- render_centralis_secrets gains the same early-return backend dispatch the app
path already uses, so the new function is reachable. Without it the SOPS path was
the only path regardless of TG_SECRETS_BACKEND.
Verified against a synthetic registry containing a JSON token map, an apostrophe,
and `$(...)`/backtick/backslash/double-quote payloads: 5 keys round-trip
byte-for-byte through `set -a; . file`, CENTRALIS_TOKENS parses to the right
subjects, and the command substitution does not execute. The negative case is
covered too — fed the unquoted 2026-07-24 shape, the round-trip check reports the
value as changed (by length, never content) and refuses to write.
Also corrects the claim in render-secrets-openbao.sh that the SOPS path never
tripped the metacharacter hazard "because every current value happens to be
alphanumeric". CENTRALIS_TOKENS is the counter-example; it never tripped those
paths because it was never in them.
|
||
|---|---|---|
| .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.