# 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. **Branch drift**: the split repos' branch state as of 2026-07-22 evening is
   fully migrated (see "Branch migration status" below) — re-run the check
   (`git merge-tree --write-tree` per branch vs dev/main) if cutover slips and
   upstream moves again.

## Branch migration status (2026-07-22)

Checked every split-repo branch by dry-run merge; migrated everything real:

- **`dev` (this repo)** = `main` + backend/frontend `origin/dev` subtrees: the
  in-image test CI feature (ported into root `build.yml` — backend full suite,
  frontend unit tier; their subtree `build.yml` edits resolved as deleted) +
  backend test scripts/Makefile/compose.test + frontend two-tier suite with
  fixtures. Faithful to upstream: the feature is dev-only, so mono `main`
  doesn't have it; promote with dev→main like the split flow.
- **`feature/geocode-local` → `feature/history-meteostat-gusts` →
  `feature/nasa-primary-flip` → `feature/era5-seed`** — the unmerged backend
  stack, rebuilt stacked on mono `dev`. ⚠️ Two conflicts were resolved here
  that upstream has NOT resolved (the same conflicts exist merging those
  branches into split-repo dev): `data/climate.py` (dev's hardened `_request`
  signature combined with the gust-fill wrap) and `tests/data/test_climate.py`
  (kept both appended test blocks). **Unverified** — tests weren't runnable
  locally (Docker Hub blocked); run the in-image suite on these branches
  before trusting them.
- **Stale (absorbed upstream, nothing migrated — safe to delete in the split
  repos):** backend `ci-run-tests-in-image`, `perf/*`,
  `phase5/discord-gateway-bot`, `port-discord-gateway-bot`,
  `reconcile-dev-with-main`, `test-runner-and-smoke`; frontend
  `ci-run-tests-in-image`, `reconcile-dev-with-main`; all three infra branches
  (`ci-fix/render-secrets-owner`, `phase5/discord-bot-flag`,
  `terraform-two-image-tags` — all landed in infra `main`); both observability
  dashboard branches.
- **Dismissed, not stale-but-not-migrated:** frontend
  `perf/frontend-responsiveness` (its "new" content would resurrect test
  files dev reorganized into the unit tier) and `test-real-backend-harness`
  (an older revision of dev's `tests/unit/test_rendering.py`).

## 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.
