# Monorepo cutover notes (assembled 2026-07-22) Scratch assembly for inspection — **nothing live points here yet**. Built by subtree-merging each split repo's `origin/main` (full history, 380 commits): - `backend/` ← thermograph-backend `83c2e05` (Discord gateway bot landed, #7) - `frontend/` ← thermograph-frontend `3a98146` (responsiveness hardening, #4) - `infra/` ← thermograph-infra `99b4b3f` (mail docs, #12) - `observability/` ← thermograph-observability `19e74af` (validator CI, #3) ## What changed vs. the split repos **CI (all in root `.forgejo/workflows/`; the subtree copies were deleted — Forgejo only reads root workflows, but dead copies are a trap):** - `backend-build-push.yml` / `frontend-build-push.yml` — the old per-repo build-push, path-filtered (`paths: ['backend/**']` etc.), image paths now **explicit**: `emi/thermograph/backend|frontend` (the old `${{ github.repository }}/app` derivation collides in a monorepo). Per-domain concurrency groups. Caveat: `v*.*.*` tag pushes carry no paths context, so a version tag builds **both** images (intended: a release names the set). - `backend|frontend-deploy[.yml|-prod.yml|-dev.yml]` — ports of the SSH deploy workflows, path-filtered, script path now `/opt/thermograph/infra/deploy/deploy.sh`. Same secret names (`SSH_*` beta, `PROD_SSH_*` prod) — but now **one** secret set on **one** repo. - `build.yml` — the build-only sanity check, now a single reusable workflow taking a `domain` input (context = the domain dir, so Dockerfiles see the same tree as in the split era). - `pr-build.yml` — one `changes` job (git-diff domain detection) + per-domain gated builds + a single always-reporting **`gate`** required check. Deliberately not path-filtered: a required check that never fires blocks auto-merge forever. - `infra-sync.yml` — **new** (the infra domain's own pipeline): push to `main` touching `infra/**` → git-sync `/opt/thermograph` on beta+prod and re-render secrets. Rolls no services (image tags are the app domains' axis). - `observability-validate.yml` — port with `observability/` prefixes + `workflow_call` so pr-build reuses it. - `secrets-guard.yml` — glob is now `infra/deploy/secrets/*.yaml`; still runs on everything (cheap backstop). - `ops-cron.yml` — compose invocations `cd /opt/thermograph/infra`. **Deploy scripts / compose (hosts see a monorepo checkout at `/opt/thermograph`):** - `deploy.sh`: `INFRA_DIR="$APP_DIR/infra"`; git ops at the checkout root, `cd "$INFRA_DIR"` for all compose work; lock/tags/render paths under `infra/deploy/`; image-path defaults renamed. - `docker-compose.yml`: **`name: thermograph` pinned.** Critical: without it, running compose from `…/infra` derives project "infra" — a silently NEW project that would recreate the whole stack beside the running one. `deploy-dev.sh` exports `COMPOSE_PROJECT_NAME=thermograph-dev` (env wins over the file key), preserving LAN dev's separate project. - `deploy/stack/deploy-stack.sh` + `thermograph-stack.yml`: same path/image renames (prod is stack-mode; `docker stack deploy` takes an explicit stack name, so no project-name concern there). **Verified here:** every workflow + compose file parses (PyYAML); all three deploy scripts pass `bash -n`; no `emi/thermograph-{backend,frontend}/app` stragglers in sh/yml. **Not verified (impossible locally):** Forgejo actually honoring `on.push.paths` and `workflow_call` inputs on our version; any live deploy. ## Before cutover (in order) 1. **Run the pending prod network-recreate runbook first.** Don't rebase the deploy path on a prod that wedges on its next deploy. 2. **Prove Forgejo path filters + reusable-workflow inputs** with a throwaway repo: one push touching only `backend/**` must fire only backend workflows; a `uses: ./.forgejo/workflows/build.yml` + `with:` job must pass its input. If paths aren't honored, fall back to the pr-build pattern (diff job + gated jobs) for the push workflows too. 3. **Land the in-flight split-repo branches** (backend/frontend `reconcile-dev-with-main`, infra `phase5/discord-bot-flag`, …) in the split repos, then re-subtree or graft the deltas — this assembly is `origin/main` as of 2026-07-22 and goes stale as those merge. ## Cutover runbook 1. Create `emi/thermograph` on Forgejo (a **new** repo — do not resurrect the archived monorepo) and push `main`. 2. Actions config, once: secrets `REGISTRY_TOKEN`, `SSH_HOST/USER/KEY/PORT` (beta), `PROD_SSH_*` (prod); variable `REGISTRY_HOST=git.thermograph.org`. 3. Branch protection on `dev` + `main`: required check `gate`, squash merges, auto-merge on green. 4. Hosts (beta, then prod): clone the monorepo to `/opt/thermograph.new`; **copy `deploy/.image-tags.env` from the old checkout into `infra/deploy/`** (its path moved — losing it makes the next single-service deploy roll the sibling onto `local`); swap directories (running containers are untouched — they're images, not the checkout); `/etc/thermograph*` files stay as-is. 5. LAN box: re-point `~/thermograph-dev` at the monorepo the same way. 6. First push builds the **new** image paths — old `emi/thermograph-{backend,frontend}/app` packages stay in the registry for rollback; GC after burn-in. deploy.sh's tag prune already targets the new paths only. 7. One `workflow_dispatch` of `backend-deploy.yml` after its build-push completes = the live end-to-end validation, same as the split-era wiring test. 8. After burn-in: archive the four split repos (leave `thermograph-docs` alone), update its runbooks to the monorepo paths. ## Known gaps / deferred - **Discord/notifier extraction (phase 2):** discord still ships inside the backend image (`backend/notifications/`); a discord tweak = a backend deploy. True `notifier/` domain (own image + compose service, singleton lock retired) is a refactor for after the monorepo settles. - Domain `CLAUDE.md`s still speak repo-era ("this repo", sibling-repo links) — correct them as touched; root `CLAUDE.md` points the right way. - `backend/MONOREPO-CUTOVER-PLAN.md` references (the *split* plan) are now historical. - Infra `Makefile` local-image targets still assume sibling checkouts — update when first needed locally.