Compare commits

...

2 commits

Author SHA1 Message Date
Emi Griffith
e75b46cba8 registry: move image and repo references to the Jinemi namespace
Repos moved to the Jinemi org; the container packages did not follow, since
Forgejo does not transfer packages with a repo. The deploy path still resolved
`emi/thermograph/*` — a user_redirect to admin_emi — while build-push.yml
derives its push path from ${github.repository}, now jinemi/thermograph. The
next backend or frontend build would have published somewhere no deploy looks.

Point the image paths at jinemi/thermograph/* (lowercase: OCI references admit
no uppercase, which is why build-push.yml already pipes through tr), the clone
URLs at Jinemi/thermograph, and the registry logins at admin_emi — the account
that actually owns the tokens, rather than the redirect.

The live tags and both ci-runner tags were copied into the Jinemi namespace
first, so the switch has something to pull. thermograph-infra,
thermograph-observability and the retired */app packages stay under admin_emi;
they did not move.
2026-08-01 09:07:11 -07:00
Emi Griffith
e60bc66448 secrets: make the two-host age key quorum a checked policy
All checks were successful
PR build (required check) / build-backend (pull_request) Has been skipped
PR build (required check) / gate (pull_request) Successful in 1s
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / changes (pull_request) Successful in 6s
secrets-guard / encrypted (pull_request) Successful in 5s
shell-lint / shellcheck (pull_request) Successful in 6s
PR build (required check) / build-frontend (pull_request) Has been skipped
The copies of /etc/thermograph/age.key on vps1 and vps2 have always read as
provisioning residue — a side effect of provision-secrets.sh rather than a
decision. They are in fact the estate's recovery quorum: that key is the single
recovery root for all five SOPS vaults AND for every off-box backup, since
ops-cron.yml:142,197 pipe those dumps through `age -r` to the same recipient.

On 2026-07-30 the operator's copy was shredded from the desktop before it had been
copied to its replacement machine, leaving zero operator-side copies. These two were
the only surviving material and the key was restored from vps2. Documenting the
ordering rule that would have prevented it, and adding the check that makes the
quorum verifiable rather than assumed.

verify-age-quorum.sh asserts presence, permissions and hash equality across both
hosts. Divergence is treated as seriously as absence: two hosts holding different
keys means one renders secrets the other cannot read, and a restore silently picks
the wrong one. Prints SHA-256 digests and modes only — a digest of a 32-byte random
key is not reversible, so it is safe in a CI log, and the key is never read or
copied anywhere. Exit status makes it usable as a cron gate.

Also replaces the "wasn't re-verified for this pass" hedge in the README with what
was actually measured: vps1 0440 root:deploy, vps2 0400 root:root, digests equal.
The dev render does not need vps1's group bit — it runs as `agent`, which is not in
group `deploy` and reaches the key through passwordless sudo, with
/opt/thermograph-dev owned agent:agent. Tightening vps1 to 0400 root:root is
therefore expected to be safe, and is called out as the follow-up rather than done
here because it changes a live host on the deploy path.

The check reports that group read as an advisory rather than a failure. It is the
current provisioned state, and a check that fails on day one is a check that is
ignored by day three.
2026-07-30 20:26:37 -07:00
37 changed files with 237 additions and 93 deletions

View file

@ -4,8 +4,8 @@ name: Build + push images (Forgejo registry)
# They were identical apart from the domain string: the paths filter, the
# concurrency group, IMAGE_PATH, the tag key and the build context.
#
# Images stay separate and independently deployable -- emi/thermograph/backend
# and emi/thermograph/frontend -- exactly as before. Build-once,
# Images stay separate and independently deployable -- jinemi/thermograph/backend
# and jinemi/thermograph/frontend -- exactly as before. Build-once,
# deploy-everywhere; nothing about the published artefacts changes here.
#
# SEMVER EXCEPTION, preserved: a v*.*.* tag push carries no paths context, so a
@ -116,7 +116,7 @@ jobs:
# independent of any permission setting). Needs a manually provisioned
# PAT with write:package scope, stored as REGISTRY_TOKEN.
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "${{ steps.image.outputs.host }}" \
--username emi --password-stdin
--username admin_emi --password-stdin
- name: Build
if: steps.image.outputs.changed == 'true'

View file

@ -77,7 +77,7 @@ cannot describe vps2 alone, since vps2 runs beta and prod side by side.
- Each service's live tag is persisted host-side, so a single-service roll never
disturbs the sibling's running tag.
Images are `emi/thermograph/backend` and `emi/thermograph/frontend`, tagged
Images are `jinemi/thermograph/backend` and `jinemi/thermograph/frontend`, tagged
`sha-<12hex>` on every push and by semver on `v*.*.*` tags. They build and deploy
**independently** — a backend change ships without a frontend deploy and vice
versa. That independence is the point of the FE/BE split and survived

View file

@ -25,7 +25,7 @@ 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
**explicit**: `jinemi/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).
@ -65,7 +65,7 @@ Forgejo only reads root workflows, but dead copies are a trap):**
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`
deploy scripts pass `bash -n`; no `admin_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.
@ -118,7 +118,7 @@ Checked every split-repo branch by dry-run merge; migrated everything real:
## Cutover runbook
1. Create `emi/thermograph` on Forgejo (a **new** repo — do not resurrect the
1. Create `Jinemi/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`.
@ -132,7 +132,7 @@ Checked every split-repo branch by dry-run merge; migrated everything real:
`/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
`admin_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

View file

@ -8,8 +8,8 @@ for: **per-domain images, per-domain deploys, and an async FE/BE contract**.
| 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` |
| `backend/` | FastAPI graded-climate API, accounts, notifications (Discord bot, push, mail), data pipeline | `build-push` → image `jinemi/thermograph/backend`; `deploy` |
| `frontend/` | Public client: static JS/CSS + SSR pages | same `build-push` / `deploy` workflows, matrixed by domain; image `jinemi/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` |
@ -27,7 +27,7 @@ which docs in this repo are currently stale.
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
separate (`jinemi/thermograph/backend`, `jinemi/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.

View file

@ -3,7 +3,7 @@
The Thermograph API service: grades recent local weather against ~45 years of
climate history, and hosts the accounts, notification, and SSR-content
back-end that the rest of the split Thermograph stack (`thermograph-frontend`)
talks to over HTTP. Split from the `emi/thermograph` monorepo — this repo owns
talks to over HTTP. Split from the `Jinemi/thermograph` monorepo — this repo owns
the API/DB/accounts/notifications layer only; it renders no HTML/CSS/JS of its
own.
@ -67,7 +67,7 @@ cities_flavor.json bundled reference data (generated by gen_cities.py /
`/content/...`; it negotiates compatibility via `GET /api/version`.
- **`thermograph-infra`** owns the deploy/Compose/Terraform layer — this repo
only builds and publishes its own container image
(`git.thermograph.org/emi/thermograph-backend/app`) and hands infra a tag to
(`git.thermograph.org/admin_emi/thermograph-backend/app`) and hands infra a tag to
roll out (`SERVICE=backend` + `BACKEND_IMAGE_TAG` into infra's
`deploy/deploy.sh`).
- **`thermograph-docs`** holds the cross-repo architecture/runbook docs; this

View file

@ -22,11 +22,11 @@ services:
backend:
# Defaults to a locally-built `:local` image (scripts/smoke.sh builds it). In CI,
# set BACKEND_IMAGE_TAG=sha-<12hex> to smoke the exact published image.
# The path must match what build-push.yml publishes -- emi/thermograph/backend.
# It read emi/thermograph-backend/app (the retired split-era path) until this
# The path must match what build-push.yml publishes -- jinemi/thermograph/backend.
# It read admin_emi/thermograph-backend/app (the retired split-era path) until this
# was corrected; harmless for the default `:local` build, wrong the moment
# BACKEND_IMAGE_TAG names a real published tag.
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:-local}
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-jinemi/thermograph/backend}:${BACKEND_IMAGE_TAG:-local}
depends_on:
db:
condition: service_healthy

View file

@ -14,7 +14,7 @@ export POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-smoke}"
# The image to smoke, pulled from the registry (no local build). CI passes the just-
# pushed sha (BACKEND_IMAGE_TAG=sha-<12hex>); locally it defaults to a published tag.
export BACKEND_IMAGE_TAG="${BACKEND_IMAGE_TAG:-v0.0.2-split-ci}"
IMG="${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph-backend/app}:${BACKEND_IMAGE_TAG}"
IMG="${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-admin_emi/thermograph-backend/app}:${BACKEND_IMAGE_TAG}"
export SMOKE_HOST_PORT="${SMOKE_HOST_PORT:-18137}"
COMPOSE=(docker compose -f docker-compose.test.yml)
PORT_URL="http://127.0.0.1:${SMOKE_HOST_PORT}"

View file

@ -12,8 +12,8 @@ thermograph/
├── README.md domain table + how CI stays decoupled
├── .forgejo/workflows/ ALL CI. Nine files. Never add workflows elsewhere.
├── .claude/ settings.json + enforcement hooks
├── backend/ FastAPI API + Go daemon → emi/thermograph/backend
├── frontend/ Go SSR + static assets → emi/thermograph/frontend
├── backend/ FastAPI API + Go daemon → jinemi/thermograph/backend
├── frontend/ Go SSR + static assets → jinemi/thermograph/frontend
├── infra/ compose, Swarm, deploy, secrets, terraform, ops
├── observability/ Loki + Grafana + Alloy
└── docs/onboarding/ you are here

View file

@ -102,7 +102,7 @@ alembic + `/healthz` 200 + a real `/api/v2/place` 200.
Triggers on pushes to `dev`/`main`/`release` touching `backend/**` or
`frontend/**`, and on `v*.*.*` tags. Publishes
`git.thermograph.org/emi/thermograph/{backend,frontend}:sha-<12hex>`.
`git.thermograph.org/Jinemi/thermograph/{backend,frontend}:sha-<12hex>`.
- **Serialised** (`max-parallel: 1`): one physical runner, and two whole image
builds racing each other buys nothing.

View file

@ -148,7 +148,7 @@ pages nobody. **The only proof is a message actually arriving in
> ⚠️ **Merging is not deploying — and this one has already bitten.**
> `/opt/observability` on vps1 and vps2 is a checkout of the **archived**
> `emi/thermograph-observability` repo. It can never `git pull` again; the
> `admin_emi/thermograph-observability` repo. It can never `git pull` again; the
> content now lives here under `observability/`. A previous observability PR
> merged and had **zero live effect** for exactly this reason.

View file

@ -77,7 +77,7 @@ mentally translate host names.
### `backend/README.md` is written for the split-repo era
It describes `thermograph-backend` as its own repo with sibling repos, uses the
retired image path `git.thermograph.org/emi/thermograph-backend/app`, and says
retired image path `git.thermograph.org/admin_emi/thermograph-backend/app`, and says
*"there is no Makefile in this repo yet"* — there is (`make test`, `make smoke`,
`make clean-venv`).
@ -106,7 +106,7 @@ it — that's the standing instruction in the root `CLAUDE.md`.
### `observability/README.md`: merging is not deploying
`/opt/observability` on vps1 and vps2 is a checkout of the **archived**
`emi/thermograph-observability` repo. It can never `git pull` again. A previous
`admin_emi/thermograph-observability` repo. It can never `git pull` again. A previous
observability PR merged and had **zero live effect** for exactly this reason.
The README documents the hand-ship procedure; until the checkouts are re-pointed
at the monorepo, that's the only path. Flagged again in
@ -118,7 +118,7 @@ at the monorepo, that's the only path. Flagged again in
### ~~The `docker-compose.test.yml` files pull retired image paths~~ — fixed
**Fixed.** Both now use `emi/thermograph/backend`, the path `build-push.yml`
**Fixed.** Both now use `jinemi/thermograph/backend`, the path `build-push.yml`
actually publishes.
The frontend harness had the worse half of this: it also pinned

View file

@ -1,6 +1,6 @@
# Onboarding — becoming a full contributor to Thermograph
This is the developer onboarding path for the `emi/thermograph` monorepo: what
This is the developer onboarding path for the `Jinemi/thermograph` monorepo: what
the product is, how the code is shaped, how to run it, what will break silently
if you get it wrong, and how a change actually travels from your editor to
`thermograph.org`.
@ -35,10 +35,10 @@ readings.
- **`backend/`** — Python 3.12 / FastAPI. Owns all climate data, grading, the
API, accounts, notifications, plus a Go daemon (`backend/daemon/`) that owns
the Discord gateway and recurring timers. Ships as `emi/thermograph/backend`.
the Discord gateway and recurring timers. Ships as `jinemi/thermograph/backend`.
- **`frontend/`** — **Go** SSR service (`frontend/server/`) plus every static
asset. No climate data, no database, no compute. Ships as
`emi/thermograph/frontend`. (The Python implementation at `frontend/*.py` is
`jinemi/thermograph/frontend`. (The Python implementation at `frontend/*.py` is
the superseded original — see [traps](11-traps.md).)
- **`infra/`** — compose and Swarm files, deploy scripts, the SOPS secrets
vault, Terraform, ops query tooling.

View file

@ -10,7 +10,7 @@ names.
## The live service is Go — `server/`
`server/` (module `thermograph/frontend`, Go 1.26) is what builds, tests, ships
and runs. It is the only thing in the `emi/thermograph/frontend` image.
and runs. It is the only thing in the `jinemi/thermograph/frontend` image.
The Python files at this level — `app.py`, `content.py`, `api_client.py`,
`format.py`, `content_loader.py`, `paths.py`, `templates/*.html.j2` — are the

View file

@ -5,7 +5,7 @@ static asset for [Thermograph](https://thermograph.org) — grades recent local
weather against ~45 years of climate history. This domain holds **no climate
data and does no compute**; it renders pages and serves the JS/CSS/image assets
that call the backend's API from the browser. It builds its own image
(`emi/thermograph/frontend`) and deploys independently of the backend.
(`jinemi/thermograph/frontend`) and deploys independently of the backend.
See [`CLAUDE.md`](CLAUDE.md) for the agent-facing detail on the deploy contract
and the cross-service contracts, and [`DESIGN.md`](DESIGN.md) for the visual

View file

@ -27,9 +27,9 @@ services:
# domain-keyed rule build-push.yml and deploy.yml both use -- so the harness
# tracks the tree instead of drifting behind a hardcoded pin. (It was pinned
# to the split-era v0.0.2-split-ci, under the retired
# emi/thermograph-backend/app path, long after both were superseded.)
# admin_emi/thermograph-backend/app path, long after both were superseded.)
# Override THERMOGRAPH_BACKEND_TEST_TAG to pin a specific build.
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${THERMOGRAPH_BACKEND_TEST_TAG:?set it, or run via scripts/backend-for-tests.sh which derives it}
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-jinemi/thermograph/backend}:${THERMOGRAPH_BACKEND_TEST_TAG:?set it, or run via scripts/backend-for-tests.sh which derives it}
depends_on:
db:
condition: service_healthy

View file

@ -55,5 +55,5 @@ binary; static assets and the YAML copy are read from disk.
go build ./... && go vet ./... && go test ./...
The deployed binary is `/usr/local/bin/thermograph-frontend` inside the
`emi/thermograph/frontend` image; the image name, `frontend-*` CI workflows and
`jinemi/thermograph/frontend` image; the image name, `frontend-*` CI workflows and
deploy path are unchanged from the Python service.

View file

@ -58,7 +58,7 @@ THERMOGRAPH_INTERNAL_TOKEN=
# :local, which is the default here -- only set these to pull a specific
# published build instead of building locally.
# REGISTRY_HOST=git.thermograph.org
# BACKEND_IMAGE_PATH=emi/thermograph-backend/app
# BACKEND_IMAGE_PATH=admin_emi/thermograph-backend/app
# BACKEND_IMAGE_TAG=local
# FRONTEND_IMAGE_PATH=emi/thermograph-frontend/app
# FRONTEND_IMAGE_PATH=admin_emi/thermograph-frontend/app
# FRONTEND_IMAGE_TAG=local

View file

@ -3,7 +3,7 @@
Infrastructure for [Thermograph](https://thermograph.org): Terraform host
provisioning, the SOPS+age secrets vault, WireGuard/Swarm networking, Forgejo,
Caddy, mail, and the deploy scripts that run the already-built app images on each
host. This is a domain of the `emi/thermograph` monorepo — a host's checkout
host. This is a domain of the `Jinemi/thermograph` monorepo — a host's checkout
(`/opt/thermograph`, `/opt/thermograph-beta`, or `/opt/thermograph-dev`) is a
checkout of the whole monorepo, and `infra/` never builds app source; it only
runs published images.

View file

@ -79,7 +79,7 @@ or 8 is done wrong, which is what the verification lines are for.
```
ssh agent@169.58.46.181
sudo git clone --branch main http://10.10.0.2:3080/emi/thermograph.git /opt/thermograph-beta
sudo git clone --branch main http://10.10.0.2:3080/Jinemi/thermograph.git /opt/thermograph-beta
sudo chown -R agent:agent /opt/thermograph-beta
```

View file

@ -13,7 +13,7 @@
# ssh deploy@vps 'SERVICE=all BACKEND_IMAGE_TAG=sha-<a> FRONTEND_IMAGE_TAG=sha-<b> /opt/thermograph/infra/deploy/deploy.sh'
#
# FE/BE CI-CD split: backend and frontend are published from separate repos as
# separate images (emi/thermograph/backend, emi/thermograph/frontend),
# separate images (jinemi/thermograph/backend, jinemi/thermograph/frontend),
# so a deploy targets ONE service and leaves the other's running container +
# tag untouched. Each service's live tag is persisted host-side in
# deploy/.image-tags.env (untracked -- survives the git reset below) so a
@ -282,7 +282,7 @@ esac
# host's existing cred; a genuine auth problem then fails loudly at `pull`.
if [ -n "${REGISTRY_TOKEN:-}" ]; then
echo "==> Logging in to the registry ($REGISTRY_HOST)"
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" --username emi --password-stdin
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" --username admin_emi --password-stdin
else
echo "==> No REGISTRY_TOKEN in env; relying on the host's existing docker login to $REGISTRY_HOST"
fi
@ -328,7 +328,7 @@ case " ${TARGETS[*]} " in
# then always found no daemon binary in a Postgres image and dropped
# daemon from EVERY backend deploy, regardless of what the real backend
# image contained -- reproduced and confirmed against beta directly.
daemon_img="${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG}"
daemon_img="${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-jinemi/thermograph/backend}:${BACKEND_IMAGE_TAG}"
if ! docker run --rm --entrypoint sh "$daemon_img" -c 'test -x /usr/local/bin/thermograph-daemon' 2>/dev/null; then
echo "==> $daemon_img predates the daemon binary; rolling without the daemon service this run"
kept=()
@ -408,8 +408,8 @@ fi
# roll never garbage-collects the image a rollback would need; docker also
# refuses to remove an image any container still uses.
echo "==> Pruning old app-image tags"
_be_repo="${REGISTRY_HOST}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}"
_fe_repo="${REGISTRY_HOST}/${FRONTEND_IMAGE_PATH:-emi/thermograph/frontend}"
_be_repo="${REGISTRY_HOST}/${BACKEND_IMAGE_PATH:-jinemi/thermograph/backend}"
_fe_repo="${REGISTRY_HOST}/${FRONTEND_IMAGE_PATH:-jinemi/thermograph/frontend}"
docker images --format '{{.Repository}}:{{.Tag}}' \
| grep -E "^(${_be_repo}|${_fe_repo}):" \
| grep -v -e "^${_be_repo}:${BACKEND_IMAGE_TAG}$" -e "^${_fe_repo}:${FRONTEND_IMAGE_TAG}$" \

View file

@ -143,14 +143,14 @@ bug hit during the frontend Go rewrite). `ci-runner` adds `docker-ce-cli` +
`git`/`python3`/`python3-yaml` for the other jobs that need them
(`shell-lint`, `observability-validate`).
Current tag: `git.thermograph.org/emi/thermograph/ci-runner:v2` (`v1` is
Current tag: `git.thermograph.org/jinemi/thermograph/ci-runner:v2` (`v1` is
broken — do not register any runner against it). Rebuild/push (requires a PAT
with `write:package` scope — the embedded git-remote token lacks it, same
requirement documented in `build-push.yml`):
```bash
docker build -t git.thermograph.org/emi/thermograph/ci-runner:vN deploy/forgejo/ci-runner
docker push git.thermograph.org/emi/thermograph/ci-runner:vN
docker build -t git.thermograph.org/jinemi/thermograph/ci-runner:vN deploy/forgejo/ci-runner
docker push git.thermograph.org/jinemi/thermograph/ci-runner:vN
```
`register-lan-runner.sh`'s `LABELS` default points at the current tag, so

View file

@ -19,9 +19,9 @@
# class, and skips the per-job install entirely.
#
# Build/push (manual — see ../README.md for when to rebuild):
# docker build -t git.thermograph.org/emi/thermograph/ci-runner:vN \
# docker build -t git.thermograph.org/jinemi/thermograph/ci-runner:vN \
# infra/deploy/forgejo/ci-runner
# docker push git.thermograph.org/emi/thermograph/ci-runner:vN
# docker push git.thermograph.org/jinemi/thermograph/ci-runner:vN
#
# Cutting over the live runner to a new tag means editing the `labels` array
# in ~/forgejo-runner/.runner on the runner host directly (same runner

View file

@ -35,7 +35,7 @@ set -euo pipefail
FORGEJO_URL="${1:?usage: $0 <forgejo_url> <registration_token>}"
TOKEN="${2:?}"
RUNNER_DIR="${RUNNER_DIR:-$HOME/forgejo-runner}"
LABELS="${LABELS:-docker:docker://git.thermograph.org/emi/thermograph/ci-runner:v2,thermograph-lan}"
LABELS="${LABELS:-docker:docker://git.thermograph.org/jinemi/thermograph/ci-runner:v2,thermograph-lan}"
echo "==> Stopping and disabling the old GitHub Actions runner service, if present"
systemctl --user stop github-actions-runner 2>/dev/null || true

View file

@ -26,7 +26,7 @@ SELF_DIR=$(cd "$(dirname "$0")" && pwd)
. "$SELF_DIR/env-topology.sh"
thermograph_topology dev
REPO_URL="${REPO_URL:-http://10.10.0.2:3080/emi/thermograph.git}"
REPO_URL="${REPO_URL:-http://10.10.0.2:3080/Jinemi/thermograph.git}"
APP_DIR="${APP_DIR:-$TG_APP_DIR}"
BRANCH="${BRANCH:-$TG_BRANCH}"

View file

@ -251,17 +251,44 @@ files), so layering would only push the VAPID private key, both S3 keypairs and
- `/etc/thermograph/age.key` (`0400`) on each VPS that renders at deploy time.
- **Back it up** in the password manager. The public recipient is in `.sops.yaml`.
On **vps1** (dev) those two can end up being the same file. `render-secrets.sh`
falls back to `sudo cat` for a root-owned key, and `deploy-dev.sh` still assumes
the account driving a CI-triggered dev deploy has no passwordless sudo and no
tty to answer a `sudo` prompt (a `systemd --user` Forgejo-runner service) —
inherited from the old desktop setup, where that was true of the whole box.
Whether it's still true of vps1's CI-runner account specifically (as opposed
to the `agent` login, which does have passwordless sudo there per `ACCESS.md`)
wasn't re-verified for this pass; `deploy-dev.sh` points `THERMOGRAPH_AGE_KEY`
at the operator's keyring as a fallback either way, so the box keeps **one**
copy of the recovery root rather than two regardless of which account ends up
mattering.
### The two on-host copies are a deliberate quorum
`/etc/thermograph/age.key` on **vps1** and **vps2** is not provisioning residue and
must not be tidied away. Together they are the recovery quorum for the entire
estate: five SOPS vaults, and every off-box backup, since `ops-cron.yml:142,197`
pipe those dumps through `age -r` to this same recipient.
That is not theoretical. On **2026-07-30** the operator's copy was shredded from the
desktop *before* it had been copied to its replacement machine, leaving zero
operator-side copies. These two were the only surviving material, and the key was
restored from vps2. The ordering rule below exists because of that hour.
- **Never remove the last operator-side copy** until its replacement has been
verified against all five vaults (`sops -d … | grep -c '='` → 16 / 12 / 8 / 16 / 9).
Deleting first and verifying second is unrecoverable if the copy is bad.
- **Both hosts must hold byte-identical copies.** Divergence is as bad as absence:
two different keys means one host renders secrets the other cannot read, and a
restore silently picks the wrong one.
- **Verify it, don't assume it**`deploy/secrets/verify-age-quorum.sh` asserts
presence, permissions and hash equality across both hosts. It prints SHA-256
digests and modes only, never the key, so it is safe in a CI log and suitable as
a cron gate. Deletion and divergence are both silent failures; nothing else in the
estate would notice either until a restore.
Verified 2026-07-30: vps1 `0440 root:deploy`, vps2 `0400 root:root`, digests equal.
Note the asymmetry — on vps1 the group `deploy` can read the key that decrypts
`common.yaml` and `prod.yaml`, on the box that also runs Forgejo, its CI runner and
dev's unreviewed branches. The dev render does not need that group bit: it runs as
`agent`, which is not in group `deploy` and reaches the key through its passwordless
`sudo` instead (`/opt/thermograph-dev` is `agent:agent`). Tightening vps1 to `0400
root:root` is therefore expected to be safe and is the obvious follow-up; it is
called out rather than done here because it changes a live host on the deploy path.
This weakens — in practice, not in intent — the rule in `infra/CLAUDE.md` that "vps1
must never hold prod credentials". Withholding `common.yaml` from dev's *render* does
not withhold the key that decrypts it from the *box*, because every vault is
encrypted to a single recipient. Fixing that properly means a second age identity for
dev, not a policy line.
## Prerequisites (once per machine)

View file

@ -0,0 +1,117 @@
#!/usr/bin/env bash
# Assert that the age recovery quorum is intact.
#
# infra/deploy/secrets/verify-age-quorum.sh
#
# The age private key is the single recovery root for all five SOPS vaults AND for
# every off-box backup (ops-cron.yml:142,197 pipe dumps through `age -r` to the same
# recipient). The copies at /etc/thermograph/age.key on vps1 and vps2 are a DELIBERATE
# two-host quorum, not provisioning residue — see the "age key" section of
# deploy/secrets/README.md. This script is what makes that policy checkable instead of
# merely stated.
#
# It fails if the key is missing on either host, if the two copies have diverged, or
# if either is more permissive than 0440. Divergence matters as much as absence: two
# hosts holding different keys means one of them renders secrets nobody else can
# decrypt, and a restore from backup silently picks the wrong one.
#
# OUTPUT DISCIPLINE: prints SHA-256 digests and file modes only. A digest of a
# 32-byte random key is not reversible, so it is safe in a CI log; the key itself is
# never read, echoed or transferred. Nothing here copies the key anywhere.
#
# Exit status: 0 = quorum intact; 1 = something needs a human. Suitable as a cron gate.
set -euo pipefail
VPS1="${TG_VPS1_SSH:-vps1}"
VPS2="${TG_VPS2_SSH:-vps2}"
KEY_PATH="${TG_AGE_KEY_PATH:-/etc/thermograph/age.key}"
SSH_OPTS="${TG_SSH_OPTS:--o BatchMode=yes -o ConnectTimeout=10}"
# Reads the digest and mode of the key on one host. Uses sudo when the key is not
# directly readable, mirroring how render-secrets.sh lifts it.
probe() {
local target="$1" out
# The remote script is a QUOTED heredoc and the key path is passed as $1 to
# `bash -s`, so nothing expands on this side. Interpolating the path into the
# command string would work too, but it is the shape that quietly breaks the day
# a path contains a space, and shellcheck is right to flag it (SC2029).
# shellcheck disable=SC2086 # SSH_OPTS is a deliberate word-split option list
out=$(ssh $SSH_OPTS "$target" bash -s -- "$KEY_PATH" 2>/dev/null <<'REMOTE'
key="$1"
if [ ! -e "$key" ]; then echo MISSING; exit 0; fi
mode=$(stat -c %a "$key")
owner=$(stat -c %U:%G "$key")
if [ -r "$key" ]; then
digest=$(sha256sum "$key" | cut -d' ' -f1)
else
digest=$(sudo sha256sum "$key" 2>/dev/null | cut -d' ' -f1)
fi
[ -n "$digest" ] || { echo UNREADABLE; exit 0; }
echo "$digest $mode $owner"
REMOTE
) || { echo UNREACHABLE; return 0; }
printf '%s\n' "$out"
}
rc=0
declare -A DIGEST
for pair in "vps1:$VPS1" "vps2:$VPS2"; do
name="${pair%%:*}"
target="${pair#*:}"
result="$(probe "$target")"
case "$result" in
MISSING)
echo "!! ${name}: no key at ${KEY_PATH} — the quorum is DOWN TO ONE COPY" >&2
rc=1
;;
UNREADABLE)
echo "!! ${name}: key present but unreadable even via sudo" >&2
rc=1
;;
UNREACHABLE)
echo "!! ${name}: host unreachable — quorum NOT verified (this is not a pass)" >&2
rc=1
;;
*)
digest="${result%% *}"
rest="${result#* }"
DIGEST["$name"]="$digest"
printf ' %-5s %s mode=%s\n' "$name" "${digest:0:16}" "$rest"
mode="${rest%% *}"
# 0400 or 0440 only. Anything wider means a non-root account on that box can
# read the key that decrypts prod — and vps1 also runs Forgejo and its CI runner.
case "$mode" in
400|440) ;;
*) echo "!! ${name}: mode ${mode} is wider than 0440" >&2; rc=1 ;;
esac
# Advisory, not a failure: 0440 with a non-root group means that group can read
# the key that decrypts prod. On vps1 that is doubly worth knowing, because the
# same box runs Forgejo, its CI runner and dev's unreviewed branches. Left
# non-fatal because it is the current provisioned state — a check that fails on
# day one is a check that gets ignored by day three. See README.md.
group="${rest#* }"; group="${group#*:}"
if [ "$mode" = 440 ] && [ "$group" != root ]; then
printf ' note: group %s can read this key\n' "$group"
fi
;;
esac
done
if [ -n "${DIGEST[vps1]:-}" ] && [ -n "${DIGEST[vps2]:-}" ]; then
if [ "${DIGEST[vps1]}" = "${DIGEST[vps2]}" ]; then
echo " quorum: 2 copies, identical"
else
echo "!! the two copies have DIVERGED — they are different keys" >&2
echo "!! do not 'fix' this by overwriting one. Establish which decrypts the" >&2
echo "!! vaults (sops -d on any deploy/secrets/*.yaml) before touching either." >&2
rc=1
fi
fi
if [ "$rc" -eq 0 ]; then
echo " OK — recovery quorum intact"
else
echo "!! recovery quorum degraded; see deploy/secrets/README.md" >&2
fi
exit "$rc"

View file

@ -124,8 +124,8 @@ case "$SERVICE" in
esac
export BACKEND_IMAGE_TAG="${BACKEND_IMAGE_TAG:-local}"
export FRONTEND_IMAGE_TAG="${FRONTEND_IMAGE_TAG:-local}"
BACKEND_IMAGE="$REGISTRY_HOST/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:$BACKEND_IMAGE_TAG"
FRONTEND_IMAGE="$REGISTRY_HOST/${FRONTEND_IMAGE_PATH:-emi/thermograph/frontend}:$FRONTEND_IMAGE_TAG"
BACKEND_IMAGE="$REGISTRY_HOST/${BACKEND_IMAGE_PATH:-jinemi/thermograph/backend}:$BACKEND_IMAGE_TAG"
FRONTEND_IMAGE="$REGISTRY_HOST/${FRONTEND_IMAGE_PATH:-jinemi/thermograph/frontend}:$FRONTEND_IMAGE_TAG"
# --- timescale image pin ---------------------------------------------------------
# Hazard #7: the db image under an existing volume must never drift. Resolve
@ -158,7 +158,7 @@ fi
# --- registry ------------------------------------------------------------------
if [ -n "${REGISTRY_TOKEN:-}" ]; then
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" --username emi --password-stdin
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" --username admin_emi --password-stdin
fi
echo "==> Pulling images"
pull_ok=0
@ -290,8 +290,8 @@ FRONTEND_IMAGE_TAG=$FRONTEND_IMAGE_TAG
EOF
# GC superseded app-image tags (keep the running pair), same as deploy.sh.
_be_repo="$REGISTRY_HOST/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}"
_fe_repo="$REGISTRY_HOST/${FRONTEND_IMAGE_PATH:-emi/thermograph/frontend}"
_be_repo="$REGISTRY_HOST/${BACKEND_IMAGE_PATH:-jinemi/thermograph/backend}"
_fe_repo="$REGISTRY_HOST/${FRONTEND_IMAGE_PATH:-jinemi/thermograph/frontend}"
docker images --format '{{.Repository}}:{{.Tag}}' \
| grep -E "^(${_be_repo}|${_fe_repo}):" \
| grep -v -e "^${_be_repo}:${BACKEND_IMAGE_TAG}$" -e "^${_fe_repo}:${FRONTEND_IMAGE_TAG}$" \

View file

@ -62,7 +62,7 @@
services:
beta-web:
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required}
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-jinemi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required}
entrypoint: ["/host/env-entrypoint.sh"]
environment:
# Beta's OWN role and OWN database on the shared instance. `db` resolves
@ -108,7 +108,7 @@ services:
failure_action: rollback
beta-worker:
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required}
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-jinemi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required}
entrypoint: ["/host/env-entrypoint.sh"]
environment:
THERMOGRAPH_DATABASE_URL: postgresql+asyncpg://thermograph_beta:${POSTGRES_PASSWORD}@db:5432/thermograph_beta
@ -144,7 +144,7 @@ services:
condition: on-failure
beta-lake:
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required}
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-jinemi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required}
entrypoint: ["/host/env-entrypoint.sh"]
environment:
THERMOGRAPH_ROLE: lake
@ -176,7 +176,7 @@ services:
failure_action: rollback
beta-daemon:
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required}
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-jinemi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required}
# Same reasoning as prod's daemon: NOT env-entrypoint.sh, because that shim
# execs the image's own entrypoint (Alembic + uvicorn) and migrations belong
# to the one-shot task. Source the host-rendered env and exec the binary.
@ -218,7 +218,7 @@ services:
failure_action: rollback
beta-frontend:
image: ${REGISTRY_HOST:-git.thermograph.org}/${FRONTEND_IMAGE_PATH:-emi/thermograph/frontend}:${FRONTEND_IMAGE_TAG:?required}
image: ${REGISTRY_HOST:-git.thermograph.org}/${FRONTEND_IMAGE_PATH:-jinemi/thermograph/frontend}:${FRONTEND_IMAGE_TAG:?required}
entrypoint: ["/host/env-entrypoint.sh"]
# REQUIRED: overriding `entrypoint:` with no `command:` drops the image's
# CMD entirely, and env-entrypoint.sh's fallback (`exec uvicorn app:app`)

View file

@ -64,7 +64,7 @@ services:
condition: on-failure
web:
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required}
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-jinemi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required}
entrypoint: ["/host/env-entrypoint.sh"]
environment:
THERMOGRAPH_DATABASE_URL: postgresql+asyncpg://thermograph:${POSTGRES_PASSWORD}@db:5432/thermograph
@ -109,7 +109,7 @@ services:
failure_action: rollback
worker:
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required}
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-jinemi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required}
entrypoint: ["/host/env-entrypoint.sh"]
environment:
THERMOGRAPH_DATABASE_URL: postgresql+asyncpg://thermograph:${POSTGRES_PASSWORD}@db:5432/thermograph
@ -153,7 +153,7 @@ services:
# healthy and answers 503/404, and web falls through to NASA — the lake is
# an accelerator, never a point of failure.
lake:
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required}
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-jinemi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required}
entrypoint: ["/host/env-entrypoint.sh"]
environment:
THERMOGRAPH_ROLE: lake
@ -186,7 +186,7 @@ services:
# (/usr/local/bin/thermograph-daemon, built into the backend image) and the
# app share the /internal/* API contract, so one BACKEND_IMAGE_TAG rolls
# them together and they can never skew.
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required}
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-jinemi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required}
# Not env-entrypoint.sh: that shim's handoff execs the image's
# deploy/entrypoint.sh (Alembic + uvicorn) whenever it exists, which is
# exactly what this service must never run — migrations belong to the
@ -243,7 +243,7 @@ services:
failure_action: rollback
frontend:
image: ${REGISTRY_HOST:-git.thermograph.org}/${FRONTEND_IMAGE_PATH:-emi/thermograph/frontend}:${FRONTEND_IMAGE_TAG:?required}
image: ${REGISTRY_HOST:-git.thermograph.org}/${FRONTEND_IMAGE_PATH:-jinemi/thermograph/frontend}:${FRONTEND_IMAGE_TAG:?required}
entrypoint: ["/host/env-entrypoint.sh"]
# REQUIRED, not cosmetic: overriding `entrypoint:` with no `command:` drops
# the image's own CMD entirely (Docker/Swarm semantics, not merged) --

View file

@ -6,7 +6,7 @@
# build-push.yml -- backend = ${BACKEND_IMAGE_PATH}, frontend =
# ${FRONTEND_IMAGE_PATH}, tagged independently by BACKEND_IMAGE_TAG /
# FRONTEND_IMAGE_TAG. This replaces the earlier Stage-4 model where both
# containers shared the single emi/thermograph/app image and
# containers shared the single admin_emi/thermograph/app image and
# THERMOGRAPH_SERVICE_ROLE picked the process; the split Dockerfiles now start
# the right process directly (frontend the thermograph-frontend Go binary,
# backend entrypoint.sh), so a backend deploy and a frontend deploy are fully
@ -99,10 +99,10 @@ services:
# No `build:` here -- infra holds no Dockerfile; each service's Dockerfile
# lives in its own app repo. deploy.sh sets BACKEND_IMAGE_TAG to the SHA
# build-push.yml pushed for the backend commit being deployed; local dev
# builds `emi/thermograph/backend:local` from the backend repo (see
# builds `jinemi/thermograph/backend:local` from the backend repo (see
# infra Makefile) and this pulls/uses it. BACKEND_IMAGE_PATH/TAG are
# independent of the frontend's, so the two services deploy separately.
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:-local}
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-jinemi/thermograph/backend}:${BACKEND_IMAGE_TAG:-local}
depends_on:
db:
condition: service_healthy
@ -180,7 +180,7 @@ services:
# without them the service stays healthy and every read falls through to
# NASA. Never published on a host port: only the backend talks to it.
lake:
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:-local}
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-jinemi/thermograph/backend}:${BACKEND_IMAGE_TAG:-local}
environment:
THERMOGRAPH_ROLE: lake
PORT: 8141
@ -207,7 +207,7 @@ services:
# share the /internal/* API contract, so they must roll together — a
# separate tag could skew them. It owns the Discord gateway websocket and
# the recurring-job timers; anything needing data calls back into backend.
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:-local}
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-jinemi/thermograph/backend}:${BACKEND_IMAGE_TAG:-local}
# Bypass deploy/entrypoint.sh entirely: that script runs Alembic, and the
# backend service's boot already owns migrations — two containers racing
# `alembic upgrade head` against one database is a real hazard, not a
@ -262,7 +262,7 @@ services:
# frontend/Dockerfile (which starts the thermograph-frontend Go binary
# directly -- no THERMOGRAPH_SERVICE_ROLE process-picking). Independent
# FRONTEND_IMAGE_TAG so a frontend deploy never disturbs the backend's tag.
image: ${REGISTRY_HOST:-git.thermograph.org}/${FRONTEND_IMAGE_PATH:-emi/thermograph/frontend}:${FRONTEND_IMAGE_TAG:-local}
image: ${REGISTRY_HOST:-git.thermograph.org}/${FRONTEND_IMAGE_PATH:-jinemi/thermograph/frontend}:${FRONTEND_IMAGE_TAG:-local}
# Its own register() fetches the IndexNow key from backend at boot -- must
# wait for a real, healthy backend, not just a started container.
depends_on:

View file

@ -50,7 +50,7 @@ new network exposure and no interactive steps.
**As originally written** (pre vps1/vps2 split; kept for history — see the
note at the top of this document):
- Monorepo `emi/thermograph` (domain dirs: `backend/ frontend/ infra/
- Monorepo `Jinemi/thermograph` (domain dirs: `backend/ frontend/ infra/
observability/`). Ops tooling lives in `infra/ops/`.
- Environments: **dev** = LAN compose stack on the dev machine; **beta** =
`75.119.132.91`; **prod** = `169.58.46.181`. SSH as `agent` with

View file

@ -42,7 +42,7 @@ infra repo**, not the app repo — the "branch" column above is which app-repo
tag an environment is meant to track conceptually; the actual pinned versions
are `var.hosts[*].environments[*].backend_image_tag` / `frontend_image_tag`
(e.g. `"sha-<12 hex>"` each — the app is two separately-published images,
`emi/thermograph-backend/app` and `emi/thermograph-frontend/app`), since the
`admin_emi/thermograph-backend/app` and `admin_emi/thermograph-frontend/app`), since the
host has no app checkout to derive a tag from. `app_dir` is required with no
default specifically so vps2's two environments can never collide on the same
checkout path (prod: `/opt/thermograph`, beta: `/opt/thermograph-beta`). The

View file

@ -239,7 +239,7 @@ resource "null_resource" "host" {
# then bring the stack up on the EXPLICIT per-service app image tags there's no
# app checkout on this host to derive a tag from (see var.backend_image_tag /
# frontend_image_tag). The compose file reads BACKEND_IMAGE_TAG / FRONTEND_IMAGE_TAG
# (for emi/thermograph-backend/app and emi/thermograph-frontend/app), so we export
# (for admin_emi/thermograph-backend/app and admin_emi/thermograph-frontend/app), so we export
# those, not the old single IMAGE_PATH/IMAGE_TAG. docker runs as root (sources both
# env files in the same shell) so it never depends on the docker group membership
# taking effect in this session.
@ -257,7 +257,7 @@ resource "null_resource" "host" {
render_thermograph_secrets ${var.app_dir}
set -a; . /etc/thermograph-topology.env; . /etc/thermograph.env; set +a
export REGISTRY_HOST="git.thermograph.org" BACKEND_IMAGE_TAG="${var.backend_image_tag}" FRONTEND_IMAGE_TAG="${var.frontend_image_tag}"
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" --username emi --password-stdin
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" --username admin_emi --password-stdin
docker compose ${local.compose_flags} pull backend frontend
docker compose ${local.compose_flags} up -d --remove-orphans
'

View file

@ -31,12 +31,12 @@ variable "git_branch" {
}
variable "backend_image_tag" {
description = "Backend image tag to pull (emi/thermograph-backend/app), e.g. \"sha-<12 hex>\" (build-push.yml's tag for the backend-repo commit) or a semver tag. The host has no app-repo checkout to derive this from, so it's always explicit."
description = "Backend image tag to pull (admin_emi/thermograph-backend/app), e.g. \"sha-<12 hex>\" (build-push.yml's tag for the backend-repo commit) or a semver tag. The host has no app-repo checkout to derive this from, so it's always explicit."
type = string
}
variable "frontend_image_tag" {
description = "Frontend image tag to pull (emi/thermograph-frontend/app), e.g. \"sha-<12 hex>\" (build-push.yml's tag for the frontend-repo commit) or a semver tag. Always explicit, same as backend_image_tag."
description = "Frontend image tag to pull (admin_emi/thermograph-frontend/app), e.g. \"sha-<12 hex>\" (build-push.yml's tag for the frontend-repo commit) or a semver tag. Always explicit, same as backend_image_tag."
type = string
}

View file

@ -151,13 +151,13 @@ hosts = {
# }
# Optional overrides (shown with their defaults):
# repo_url = "https://git.thermograph.org/emi/thermograph-infra.git"
# repo_url = "https://git.thermograph.org/admin_emi/thermograph-infra.git"
# app_port = 8137
# frontend_port = 8080
#
# thermograph-infra is a PRIVATE repo, so a host cloning it for the first time
# needs read credentials embedded in repo_url, e.g. a Forgejo deploy token:
# repo_url = "https://deploy:REPLACE_WITH_TOKEN@git.thermograph.org/emi/thermograph-infra.git"
# repo_url = "https://deploy:REPLACE_WITH_TOKEN@git.thermograph.org/admin_emi/thermograph-infra.git"
# ---------------------------------------------------------------------------------
# Self-hosted Open-Meteo archive (only used by environments with openmeteo = true)

View file

@ -54,8 +54,8 @@ variable "hosts" {
git_branch = string # this INFRA repo's branch the checkout is reset to
# Which app images to run, e.g. "sha-<12 hex>" (each matching build-push.yml's tag
# for the commit that app repo built) or a semver tag on a release push. The app is
# TWO separately-published images now emi/thermograph-backend/app and
# emi/thermograph-frontend/app pinned independently. Required, no default: the
# TWO separately-published images now admin_emi/thermograph-backend/app and
# admin_emi/thermograph-frontend/app pinned independently. Required, no default: the
# host's checkout is this infra repo, not the app repos, so there's no "current
# commit" to derive a tag from; both must be explicit. Bump these (via a normal
# tfvars edit + apply) whenever an app repo ships a commit you want this
@ -127,9 +127,9 @@ variable "gcp_hosts" {
}
variable "repo_url" {
description = "Git remote to clone from when a host has no checkout yet. Points at THIS repo (thermograph-infra) now, not the app repo -- the app's own source is never checked out on a host; only its published registry images are pulled (see var.hosts[*].backend_image_tag / frontend_image_tag). thermograph-infra is a private repo, so this typically needs embedded read credentials, e.g. a Forgejo deploy token: \"https://<token-name>:<token>@git.thermograph.org/emi/thermograph-infra.git\"."
description = "Git remote to clone from when a host has no checkout yet. Points at THIS repo (thermograph-infra) now, not the app repo -- the app's own source is never checked out on a host; only its published registry images are pulled (see var.hosts[*].backend_image_tag / frontend_image_tag). thermograph-infra is a private repo, so this typically needs embedded read credentials, e.g. a Forgejo deploy token: \"https://<token-name>:<token>@git.thermograph.org/admin_emi/thermograph-infra.git\"."
type = string
default = "https://git.thermograph.org/emi/thermograph-infra.git"
default = "https://git.thermograph.org/admin_emi/thermograph-infra.git"
sensitive = true
}

View file

@ -62,7 +62,7 @@ persistent, queryable, fleet-wide log store and UI.
> ### ⚠️ Merging is not deploying
>
> `/opt/observability` on vps1 (and vps2) is a checkout of the **archived**
> `emi/thermograph-observability` repo. It can never `git pull` again — the
> `admin_emi/thermograph-observability` repo. It can never `git pull` again — the
> content now lives here, in the mono repo, under `observability/`. A previous
> observability PR merged and had **zero live effect** for exactly this reason.
>