diff --git a/infra/deploy/deploy-dev.sh b/infra/deploy/deploy-dev.sh index 83de807..f4e3d70 100755 --- a/infra/deploy/deploy-dev.sh +++ b/infra/deploy/deploy-dev.sh @@ -2,7 +2,7 @@ # LAN-dev deploy: roll the per-service registry-pull stack (see deploy.sh) onto # the ~/thermograph-dev overlay instead of prod/beta's loopback-only stack. # -# This script assumes $APP_DIR is a checkout of THIS repo (thermograph-infra), +# This script assumes $APP_DIR is a checkout of the monorepo (deploy assets under infra/), # the same way /opt/thermograph is on prod/beta. That is the live state: the LAN # box's ~/thermograph-dev was reprovisioned as an infra checkout during the # 2026-07-22 cutover (the app monorepo is archived), provision-dev-lan.sh's @@ -44,6 +44,12 @@ export APP_DIR BRANCH # path-list separator it expects (';' only on Windows). export COMPOSE_FILE="docker-compose.yml:docker-compose.dev.yml" +# Pin the dev project name: docker-compose.yml pins `name: thermograph` for +# prod/beta, but dev's stack has always been the separate "thermograph-dev" +# project (originally derived from this checkout's directory name). The env +# var wins over the compose-file `name:` key, preserving that split. +export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-thermograph-dev}" + # deploy.sh needs POSTGRES_PASSWORD to interpolate the compose file (both to init # the db container and to build backend's THERMOGRAPH_DATABASE_URL) -- normally # supplied by /etc/thermograph.env via render-secrets.sh's SOPS render, but dev has diff --git a/infra/deploy/deploy.sh b/infra/deploy/deploy.sh index 192761b..b4a56ce 100755 --- a/infra/deploy/deploy.sh +++ b/infra/deploy/deploy.sh @@ -6,27 +6,30 @@ # and you can run it by hand too. # # # roll just the backend onto a specific image: -# ssh deploy@vps 'SERVICE=backend BACKEND_IMAGE_TAG=sha-<12hex> /opt/thermograph/deploy/deploy.sh' +# ssh deploy@vps 'SERVICE=backend BACKEND_IMAGE_TAG=sha-<12hex> /opt/thermograph/infra/deploy/deploy.sh' # # roll just the frontend: -# ssh deploy@vps 'SERVICE=frontend FRONTEND_IMAGE_TAG=sha-<12hex> /opt/thermograph/deploy/deploy.sh' +# ssh deploy@vps 'SERVICE=frontend FRONTEND_IMAGE_TAG=sha-<12hex> /opt/thermograph/infra/deploy/deploy.sh' # # bring the whole stack up (both tags required): -# ssh deploy@vps 'SERVICE=all BACKEND_IMAGE_TAG=sha- FRONTEND_IMAGE_TAG=sha- /opt/thermograph/deploy/deploy.sh' +# ssh deploy@vps 'SERVICE=all BACKEND_IMAGE_TAG=sha- FRONTEND_IMAGE_TAG=sha- /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/app, emi/thermograph-frontend/app), +# separate images (emi/thermograph/backend, emi/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 # single-service roll re-renders compose with BOTH services' real tags and # never accidentally recreates or downgrades the sibling. # -# This checkout is thermograph-infra, not an app repo: BRANCH is this repo's +# This checkout is the monorepo (compose + deploy live under infra/), not an app-only checkout: BRANCH is this repo's # branch (compose files, db init, the secrets vault, this script itself); # the *_IMAGE_TAG values are the separately-published app images to run -- the # two axes are independent and rarely change together. set -euo pipefail APP_DIR="${APP_DIR:-/opt/thermograph}" +# Monorepo layout: git operations act on the checkout root ($APP_DIR); all +# compose files and deploy assets live under infra/. +INFRA_DIR="$APP_DIR/infra" BRANCH="${BRANCH:-main}" # Which service this deploy rolls: backend | frontend | all. Defaults to `all` # (a full-stack bring-up) so a by-hand run with both tags still works; the @@ -49,7 +52,7 @@ esac # inherited across the self re-exec below, so the lock spans the whole run; # -w 600 bounds the wait (a deploy holding the lock >10 min is already # broken), after which this exits non-zero and the CI job fails loudly. -DEPLOY_LOCK="$APP_DIR/deploy/.deploy.lock" +DEPLOY_LOCK="$INFRA_DIR/deploy/.deploy.lock" if [ -z "${DEPLOY_SH_FLOCKED:-}" ]; then export DEPLOY_SH_FLOCKED=1 exec flock -w 600 "$DEPLOY_LOCK" "$0" "$@" @@ -66,10 +69,10 @@ fi # after which deploy.sh re-execs), so a missing helper simply falls back to the # existing /etc/thermograph.env. Then source it so a by-hand run interpolates the # same as the systemd unit does. See deploy/render-secrets.sh + deploy/secrets/. -if [ -f "$APP_DIR/deploy/render-secrets.sh" ]; then +if [ -f "$INFRA_DIR/deploy/render-secrets.sh" ]; then # shellcheck source=deploy/render-secrets.sh - . "$APP_DIR/deploy/render-secrets.sh" - render_thermograph_secrets "$APP_DIR" + . "$INFRA_DIR/deploy/render-secrets.sh" + render_thermograph_secrets "$INFRA_DIR" fi set -a; . /etc/thermograph.env 2>/dev/null || true; set +a @@ -120,9 +123,15 @@ fi # freshly-pulled one, and the SERVICE/tag contract passes through unchanged -- # the app repos' workflows never need to know which mode a host runs. if [ "$(cat /etc/thermograph/deploy-mode 2>/dev/null || true)" = "stack" ]; then - exec bash "$APP_DIR/deploy/stack/deploy-stack.sh" + exec bash "$INFRA_DIR/deploy/stack/deploy-stack.sh" fi +# Monorepo: run every docker compose command from infra/ (the git operations +# above ran at the checkout root). The compose project name is pinned in the +# file itself (`name: thermograph`), so moving the working directory does NOT +# rename the project and recreate the stack under a second name. +cd "$INFRA_DIR" + # Registry-pull cutover: pull the image each app repo's build-push.yml already # built and pushed, instead of building in place. This checkout is # thermograph-infra, not an app repo, so there's no "current commit" to derive @@ -137,7 +146,7 @@ export REGISTRY_HOST BACKEND_IMAGE_PATH FRONTEND_IMAGE_PATH # bare `local` that would recreate/downgrade it). The incoming env for the # service being deployed then overrides its line below. This file is untracked # (see .gitignore), so `git reset --hard` above leaves it in place. -TAGS_FILE="$APP_DIR/deploy/.image-tags.env" +TAGS_FILE="$INFRA_DIR/deploy/.image-tags.env" # Capture the tags the caller explicitly passed BEFORE sourcing -- they must # WIN. The file only supplies the *sibling's* last-known tag; sourcing it # unconditionally would clobber an incoming tag (e.g. a frontend deploy whose @@ -277,8 +286,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/app}" -_fe_repo="${REGISTRY_HOST}/${FRONTEND_IMAGE_PATH:-emi/thermograph-frontend/app}" +_be_repo="${REGISTRY_HOST}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}" +_fe_repo="${REGISTRY_HOST}/${FRONTEND_IMAGE_PATH:-emi/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}$" \ diff --git a/infra/deploy/stack/deploy-stack.sh b/infra/deploy/stack/deploy-stack.sh index cdaa5a0..8833aec 100755 --- a/infra/deploy/stack/deploy-stack.sh +++ b/infra/deploy/stack/deploy-stack.sh @@ -48,10 +48,10 @@ export STACK_NAME # Render (SOPS) + source /etc/thermograph.env exactly like deploy.sh, then # install the uid-10001-readable copy the tasks' env-entrypoint shim sources. # 10001 = the app images' `thermograph` user; the file is 0400 to that uid. -if [ -f "$APP_DIR/deploy/render-secrets.sh" ]; then +if [ -f "$APP_DIR/infra/deploy/render-secrets.sh" ]; then # shellcheck source=deploy/render-secrets.sh - . "$APP_DIR/deploy/render-secrets.sh" - render_thermograph_secrets "$APP_DIR" + . "$APP_DIR/infra/deploy/render-secrets.sh" + render_thermograph_secrets "$APP_DIR/infra" fi set -a; . /etc/thermograph.env 2>/dev/null || true; set +a sudo install -o 10001 -g 0 -m 0400 /etc/thermograph.env /etc/thermograph/stack.env \ @@ -62,7 +62,7 @@ sudo install -o 10001 -g 0 -m 0400 /etc/thermograph.env /etc/thermograph/stack.e # supplies the sibling. Stack mode keeps its own file so test/real never mix. REGISTRY_HOST="${REGISTRY_HOST:-git.thermograph.org}" export REGISTRY_HOST -TAGS_FILE="$APP_DIR/deploy/.stack-image-tags.env" +TAGS_FILE="$APP_DIR/infra/deploy/.stack-image-tags.env" _incoming_backend="${BACKEND_IMAGE_TAG:-}" _incoming_frontend="${FRONTEND_IMAGE_TAG:-}" if [ -f "$TAGS_FILE" ]; then set -a; . "$TAGS_FILE"; set +a; fi @@ -77,8 +77,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/app}:$BACKEND_IMAGE_TAG" -FRONTEND_IMAGE="$REGISTRY_HOST/${FRONTEND_IMAGE_PATH:-emi/thermograph-frontend/app}:$FRONTEND_IMAGE_TAG" +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" # --- timescale image pin --------------------------------------------------------- # Hazard #7: the db image under an existing volume must never drift. Resolve @@ -138,7 +138,7 @@ FIRST_DEPLOY_MIGRATE=0 docker service inspect "${STACK_NAME}_db" >/dev/null 2>&1 || FIRST_DEPLOY_MIGRATE=1 if [ "$SERVICE" = "all" ] || ! docker service inspect "${STACK_NAME}_web" >/dev/null 2>&1; then echo "==> docker stack deploy ($STACK_NAME)" - docker stack deploy --with-registry-auth -c "$APP_DIR/deploy/stack/thermograph-stack.yml" "$STACK_NAME" + docker stack deploy --with-registry-auth -c "$APP_DIR/infra/deploy/stack/thermograph-stack.yml" "$STACK_NAME" # First-ever deploy ran no migrate above (db didn't exist): wait for db, # migrate, then force web/worker to restart cleanly against the schema. if [ "${FIRST_DEPLOY_MIGRATE:-0}" = "1" ]; then @@ -179,7 +179,7 @@ if ! docker ps --format '{{.Names}}' | grep -qx "$LB_NAME"; then docker run -d --name "$LB_NAME" --restart unless-stopped \ --network "$NET" \ -p "127.0.0.1:${LB_HTTP_PORT}:8137" -p "127.0.0.1:${LB_FE_PORT}:8080" \ - -v "$APP_DIR/deploy/stack/lb/Caddyfile:/etc/caddy/Caddyfile:ro" \ + -v "$APP_DIR/infra/deploy/stack/lb/Caddyfile:/etc/caddy/Caddyfile:ro" \ caddy:2-alpine >/dev/null fi @@ -205,8 +205,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/app}" -_fe_repo="$REGISTRY_HOST/${FRONTEND_IMAGE_PATH:-emi/thermograph-frontend/app}" +_be_repo="$REGISTRY_HOST/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}" +_fe_repo="$REGISTRY_HOST/${FRONTEND_IMAGE_PATH:-emi/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}$" \ diff --git a/infra/deploy/stack/thermograph-stack.yml b/infra/deploy/stack/thermograph-stack.yml index a3b7c22..c8bdd51 100644 --- a/infra/deploy/stack/thermograph-stack.yml +++ b/infra/deploy/stack/thermograph-stack.yml @@ -64,7 +64,7 @@ services: condition: on-failure web: - image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph-backend/app}:${BACKEND_IMAGE_TAG:?required} + image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required} entrypoint: ["/host/env-entrypoint.sh"] environment: THERMOGRAPH_DATABASE_URL: postgresql+asyncpg://thermograph:${POSTGRES_PASSWORD}@db:5432/thermograph @@ -105,7 +105,7 @@ services: failure_action: rollback worker: - image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph-backend/app}:${BACKEND_IMAGE_TAG:?required} + image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:?required} entrypoint: ["/host/env-entrypoint.sh"] environment: THERMOGRAPH_DATABASE_URL: postgresql+asyncpg://thermograph:${POSTGRES_PASSWORD}@db:5432/thermograph @@ -139,7 +139,7 @@ services: condition: on-failure frontend: - image: ${REGISTRY_HOST:-git.thermograph.org}/${FRONTEND_IMAGE_PATH:-emi/thermograph-frontend/app}:${FRONTEND_IMAGE_TAG:?required} + image: ${REGISTRY_HOST:-git.thermograph.org}/${FRONTEND_IMAGE_PATH:-emi/thermograph/frontend}:${FRONTEND_IMAGE_TAG:?required} entrypoint: ["/host/env-entrypoint.sh"] environment: THERMOGRAPH_BASE: / diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index c866273..4de4659 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -25,6 +25,13 @@ # so `docker compose up` can interpolate it. It is used BOTH to initialize the db # container and to build backend's THERMOGRAPH_DATABASE_URL below. +# Pin the compose project name: the monorepo layout runs compose from +# /opt/thermograph/infra, and without an explicit name the project would be +# derived from that directory ("infra") -- silently a NEW project, recreating +# the whole stack beside the running one. LAN dev overrides this via +# COMPOSE_PROJECT_NAME=thermograph-dev (the env var always wins over this key). +name: thermograph + services: db: # TimescaleDB on PostgreSQL 18 (the stock image already sets @@ -90,10 +97,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/app:local` from the backend repo (see + # builds `emi/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/app}:${BACKEND_IMAGE_TAG:-local} + image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph/backend}:${BACKEND_IMAGE_TAG:-local} depends_on: db: condition: service_healthy @@ -155,7 +162,7 @@ services: # from its own Dockerfile (which starts `uvicorn app:app` directly -- no # THERMOGRAPH_SERVICE_ROLE process-picking anymore). 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/app}:${FRONTEND_IMAGE_TAG:-local} + image: ${REGISTRY_HOST:-git.thermograph.org}/${FRONTEND_IMAGE_PATH:-emi/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: