Thermograph monorepo: graded-climate API + SSR frontend + infra, domain-specific containerized deploys
Find a file
Emi Griffith 5418628989
Some checks failed
shell-lint / shellcheck (pull_request) Failing after 14s
secrets-guard / encrypted (pull_request) Successful in 16s
PR build (required check) / changes (pull_request) Successful in 18s
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 4s
guardrails: enforce live-host and secrets policy with hooks
CLAUDE.md can only ask; nothing enforced it. The estate's widest privilege is
that `agent` has passwordless sudo on prod and beta while global settings allow
`Bash(ssh prod:*)` outright, so any session in any directory could restart,
roll or delete production with no prompt. CI cannot see this: nothing about
`ssh prod 'docker service rm ...'` goes through a pull request.

Adds three PreToolUse/PostToolUse hooks, checked in so they travel with the
repo rather than living in one machine's global settings.

prod-guard.sh — reads stay friction-free, mutations ask first. Covers Bash ssh
to prod/beta (by alias, public IP or mesh IP) plus Centralis run_on_host,
sql_query(write:true), rollback_to, promote and secrets_rotate. sql_query's own
description notes it escalates to the app role with no confirmation of its own;
this supplies one.

Classification is an allowlist of read-only commands, not a blocklist of
dangerous ones. A blocklist is wrong by construction — the first destructive
verb nobody thought of sails through. Compound commands are split on &&, ||, ;
and | and every segment must be recognised, so `docker ps; rm -rf /` asks.
Redirection to a file, command substitution, `sed -i`, and mutating
docker/git/systemctl subcommands all count as writes.

Beta is guarded as strictly as prod: it serves beta.thermograph.org and hosts
Forgejo, so one destructive command there takes out git, CI and the registry
together. LAN dev is deliberately unguarded.

secrets-guard.sh — denies any direct Write/Edit of infra/deploy/secrets/*.yaml.
All six vault files are SOPS-encrypted and secrets-guard.yml fails the build on
a plaintext one, but only after the secret is already on disk and probably
committed. Denies rather than asks: `sops edit` is the only correct path, so a
prompt would just be an invitation to click through.

lint-after-edit.sh — shellchecks an edited *.sh and feeds findings back in the
same turn instead of after a five-minute CI round trip. These scripts run as
root over SSH against live hosts with no test suite in front of them. It exits
quietly when shellcheck is absent, so it is inert until installed; shell-lint
CI remains the backstop.

All three fail toward the prompt: exit 0 with no output means "no opinion" and
the normal permission flow proceeds, which is also what happens if jq is
missing or a script errors.

Verified by piping tool payloads directly to each hook — 21 cases covering
reads, mutations, compound smuggling and out-of-scope calls. That testing
caught two silent bypasses in the first draft: an unescaped `[` in a case
pattern list, and a missing trailing newline that made `read` return non-zero
on the only line so the loop body never ran and every command classified as
read-only. Both are called out in .claude/hooks/README.md.
2026-07-24 21:09:49 -07:00
.claude guardrails: enforce live-host and secrets policy with hooks 2026-07-24 21:09:49 -07:00
.forgejo/workflows observability: add the estate's first alerting; supervise Postfix 2026-07-24 13:19:19 -07:00
backend Retire NASA POWER from every serving path (#72) 2026-07-24 23:56:34 +00:00
frontend Frontend QA batch: date/TZ, https origin, date-422, VAPID rotation, trace-precip, partial-day gate (#70) 2026-07-24 23:13:36 +00:00
infra secrets: silence two SC2016s that are the intended behaviour 2026-07-24 18:13:08 -07:00
observability observability: add the estate's first alerting; supervise Postfix 2026-07-24 13:19:19 -07:00
.gitignore Drop accidentally-committed worktrees; ignore .claude/worktrees 2026-07-24 15:57:34 -07:00
CLAUDE.md docs: monorepo README, cutover runbook, root agent instructions 2026-07-22 22:11:33 -07:00
CUTOVER-NOTES.md docs: record the 2026-07-22 branch-migration sweep in cutover notes 2026-07-22 22:27:09 -07:00
README.md docs: monorepo README, cutover runbook, root agent instructions 2026-07-22 22:11:33 -07:00

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 backend-build-push → image emi/thermograph/backend; backend-deploy[-prod|-dev]
frontend/ Public client: static JS/CSS + SSR pages frontend-* mirrors of the above; image emi/thermograph/frontend
infra/ Compose, 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).

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 tracked via main on all hosts.

Before pointing anything live at this repo, read CUTOVER-NOTES.md.