Compare commits
2 commits
df409f88b3
...
e75b46cba8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e75b46cba8 | ||
|
|
e60bc66448 |
37 changed files with 237 additions and 93 deletions
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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}$" \
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
117
infra/deploy/secrets/verify-age-quorum.sh
Executable file
117
infra/deploy/secrets/verify-age-quorum.sh
Executable 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"
|
||||
|
|
@ -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}$" \
|
||||
|
|
|
|||
|
|
@ -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`)
|
||||
|
|
|
|||
|
|
@ -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) --
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
>
|
||||
|
|
|
|||
Loading…
Reference in a new issue