All checks were successful
PR build (required check) / changes (pull_request) Successful in 9s
PR build (required check) / build-backend (pull_request) Has been skipped
PR build (required check) / build-frontend (pull_request) Has been skipped
secrets-guard / encrypted (pull_request) Successful in 7s
shell-lint / shellcheck (pull_request) Successful in 9s
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / gate (pull_request) Successful in 2s
The six deploy workflows were one file written six times, differing only in a branch name, a paths filter, a concurrency group, the service name, its *_IMAGE_TAG variable and a secret prefix. The two build-push workflows were the same file twice, differing only in the domain string. The contract into infra/deploy/deploy.sh (SERVICE + BACKEND_IMAGE_TAG/FRONTEND_IMAGE_TAG) was already fully parameterised, so the duplication bought nothing and cost eight files to keep in step. deploy.yml: branch selects the environment (main -> beta, release -> prod), a matrix covers backend and frontend, and each leg decides whether this push actually touched its domain before rolling anything. The workflow-level paths filter only says backend OR frontend moved; without the per-leg refinement a backend-only push would also roll the frontend and lose the independent-deploy property the FE/BE split exists for. build-push.yml: the same shape for images. Images stay separate and independently deployable; nothing about the published artefacts changes. The two *-deploy-dev.yml workflows are deleted rather than ported. They were already documented as inert -- they call a monorepo path on the LAN box whose ~/thermograph-dev is still a split-era thermograph-infra checkout. LAN dev is a local `make dev-up` concern, not a CI environment. Deliberately boring expressions throughout. No dynamic matrix (fromJSON), and the *_IMAGE_TAG selection is done in shell rather than with a `matrix.service == 'x' && a || b` ternary. Those are GitHub idioms a Forgejo/act runner may evaluate differently, and the failure mode is silent: an empty *_IMAGE_TAG makes deploy.sh fall back to the tag already running, so the job goes green having deployed nothing. Beta and prod get two explicit, mutually exclusive steps rather than a ternary over secrets, where an empty host would be worse still. Everything load-bearing is preserved: fetch-depth 0 and the domain-keyed 12-hex tag (the branch tip is often another domain's commit), per-service per-ref concurrency with cancel-in-progress false, separate PROD_SSH_* credentials, the v*.*.* both-images exception, and appleboy/ssh-action by full URL. pr-build.yml is untouched. Its workflow name and `gate` job are the required status check branch protection is configured against, and renaming that context makes every PR unmergeable with fully green CI. secrets-guard.yml and shell-lint.yml are likewise left alone: they are the only other consolidation candidates that run on pull_request, and the branch-protection API needs credentials this could not read, so merging them was not worth the risk for two files. Logic verified by replaying the plan step against real history: an infra-only push rolls nothing, a backend-only push rolls backend and leaves frontend alone, and a run with no usable before-sha defaults to deploying rather than silently skipping. The computed frontend tag for a backend-only push is sha-ca84e0ce95f0 -- exactly the tag live on beta -- so the derivation matches what the old workflows produced. 15 workflows -> 9, 1365 lines -> 1019. Docs naming the deleted files are updated in the same commit, per the rule the root CLAUDE.md now carries.
34 lines
1.9 KiB
Markdown
34 lines
1.9 KiB
Markdown
# 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 (beta, LAN dev) + the Swarm stack (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).
|
|
|
|
## 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`.**
|