Merge pull request 'Promote dev to main (reconciled bot + hardening merge, test harness, in-image CI)' (#15) from dev into main
This commit is contained in:
commit
a4d7fcd1d7
7 changed files with 154 additions and 6 deletions
|
|
@ -9,9 +9,9 @@ name: Build check
|
||||||
# the runner's shared capacity=2 contention) without ever settling into a
|
# the runner's shared capacity=2 contention) without ever settling into a
|
||||||
# reliably green check. The image DOES boot correctly standalone --
|
# reliably green check. The image DOES boot correctly standalone --
|
||||||
# independently verified by hand: `docker run` + real alembic migrations +
|
# independently verified by hand: `docker run` + real alembic migrations +
|
||||||
# `/healthz` returning 200 + a real `/api/v2/place` 200. Full pytest-suite
|
# `/healthz` returning 200 + a real `/api/v2/place` 200. The full pytest
|
||||||
# migration is separate, deferred follow-up work too, same category as
|
# suite DOES run here now -- inside the built image (see the test step below),
|
||||||
# thermograph-copy's deferred vendoring.
|
# which sidesteps those runner quirks entirely.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# Reusable (uses: ./.forgejo/workflows/build.yml) by pr-build.yml and
|
# Reusable (uses: ./.forgejo/workflows/build.yml) by pr-build.yml and
|
||||||
|
|
@ -38,3 +38,21 @@ jobs:
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: docker build -t thermograph-backend:ci .
|
run: docker build -t thermograph-backend:ci .
|
||||||
|
|
||||||
|
# The suite is hermetic (sqlite/tmpdir; no network), and the image already
|
||||||
|
# contains tests/ + every runtime dep (COPY . /app/), so run pytest INSIDE
|
||||||
|
# the image we just built: the exact interpreter/deps that ship, none of
|
||||||
|
# the runner-environment quirks that sank the old host-side boot check.
|
||||||
|
# requirements-dev.txt only layers pytest on top, so the install is tiny.
|
||||||
|
# -u 0: the image's default user can't write to site-packages.
|
||||||
|
# --entrypoint sh: the image's entrypoint is alembic-migrate + uvicorn;
|
||||||
|
# without the override the test command is swallowed as entrypoint args
|
||||||
|
# and the container just boots the server forever. unset THERMOGRAPH_BASE:
|
||||||
|
# the image bakes the prod root-mount (/), which moves every route off the
|
||||||
|
# /thermograph prefix the tests are written against -- requests would fall
|
||||||
|
# through to the frontend-proxy catch-all and fail with ConnectError.
|
||||||
|
- name: Run tests in the built image
|
||||||
|
run: |
|
||||||
|
docker run --rm -u 0 --entrypoint sh thermograph-backend:ci \
|
||||||
|
-c "unset THERMOGRAPH_BASE; pip install -q --no-cache-dir pytest==8.4.1 && python -m pytest tests -q"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,8 @@ name: Deploy backend to beta VPS
|
||||||
# versa. thermograph-infra/deploy/deploy.sh persists each service's live tag
|
# versa. thermograph-infra/deploy/deploy.sh persists each service's live tag
|
||||||
# host-side, so a single-service roll never disturbs the other.
|
# host-side, so a single-service roll never disturbs the other.
|
||||||
#
|
#
|
||||||
# The SSH_HOST/USER/KEY/PORT secrets point at beta. Prod is NOT deployed by a
|
# The SSH_HOST/USER/KEY/PORT secrets point at beta. Prod deploys the same way
|
||||||
# workflow -- it's deployed with `terraform apply` on the `release` branch, so
|
# from deploy-prod.yml (release branch, PROD_SSH_* secrets). appleboy/ssh-action is
|
||||||
# there is deliberately no release-triggered workflow. appleboy/ssh-action is
|
|
||||||
# referenced by full GitHub URL because it isn't mirrored in Forgejo's default
|
# referenced by full GitHub URL because it isn't mirrored in Forgejo's default
|
||||||
# action registry.
|
# action registry.
|
||||||
#
|
#
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,4 +1,5 @@
|
||||||
.venv/
|
.venv/
|
||||||
|
.venv-test/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
data/cache/*.parquet
|
data/cache/*.parquet
|
||||||
|
|
|
||||||
14
Makefile
Normal file
14
Makefile
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# thermograph-backend — local dev/test targets.
|
||||||
|
.PHONY: help test smoke clean-venv
|
||||||
|
|
||||||
|
help: ## List targets
|
||||||
|
@grep -hE '^[a-z][a-zA-Z0-9_-]*:.*## ' $(MAKEFILE_LIST) | awk -F':.*## ' '{printf " %-14s %s\n", $$1, $$2}'
|
||||||
|
|
||||||
|
test: ## Build a py3.12 dev venv and run the hermetic pytest suite (pass ARGS=...)
|
||||||
|
./scripts/test.sh $(ARGS)
|
||||||
|
|
||||||
|
smoke: ## Boot the backend image + a throwaway db and assert /healthz + /api/version
|
||||||
|
./scripts/smoke.sh
|
||||||
|
|
||||||
|
clean-venv: ## Remove the test venv
|
||||||
|
rm -rf .venv-test
|
||||||
40
docker-compose.test.yml
Normal file
40
docker-compose.test.yml
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Boot-smoke harness: run the backend's OWN image against a throwaway TimescaleDB and
|
||||||
|
# assert it actually boots + serves its contract (build alone doesn't prove boot). Used
|
||||||
|
# by scripts/smoke.sh; not a deploy file. DB data is tmpfs (ephemeral) — no volume, no
|
||||||
|
# residue. See Makefile `smoke` and README.
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: timescale/timescaledb:${TIMESCALEDB_TAG:-latest-pg18}
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: thermograph
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-smoke}
|
||||||
|
POSTGRES_DB: thermograph
|
||||||
|
tmpfs:
|
||||||
|
# pg18 images place PGDATA in a major-version subdir under the mount, so mount at
|
||||||
|
# /var/lib/postgresql (matches the infra compose), not /var/lib/postgresql/data.
|
||||||
|
- /var/lib/postgresql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U thermograph -d thermograph"]
|
||||||
|
interval: 3s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
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.
|
||||||
|
image: ${REGISTRY_HOST:-git.thermograph.org}/${BACKEND_IMAGE_PATH:-emi/thermograph-backend/app}:${BACKEND_IMAGE_TAG:-local}
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
THERMOGRAPH_DATABASE_URL: postgresql+asyncpg://thermograph:${POSTGRES_PASSWORD:-smoke}@db:5432/thermograph
|
||||||
|
# Required at import (web/app.py). The smoke never exercises a proxied page, so an
|
||||||
|
# unreachable placeholder is fine — it only hits /healthz and {BASE}/api/version.
|
||||||
|
THERMOGRAPH_FRONTEND_BASE_INTERNAL: http://127.0.0.1:1
|
||||||
|
THERMOGRAPH_BASE: "/"
|
||||||
|
THERMOGRAPH_ENABLE_NOTIFIER: "0"
|
||||||
|
PORT: "8137"
|
||||||
|
WORKERS: "1"
|
||||||
|
ports:
|
||||||
|
# Host port defaults to 18137 to avoid colliding with a running dev server on 8137.
|
||||||
|
- "127.0.0.1:${SMOKE_HOST_PORT:-18137}:8137"
|
||||||
45
scripts/smoke.sh
Executable file
45
scripts/smoke.sh
Executable file
|
|
@ -0,0 +1,45 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Image boot-smoke: prove the backend IMAGE actually boots and serves its contract
|
||||||
|
# (a `docker build` succeeding does not prove the container starts). Builds the image
|
||||||
|
# locally (or uses a provided BACKEND_IMAGE_TAG), runs it + a throwaway TimescaleDB via
|
||||||
|
# docker-compose.test.yml, waits for /healthz, then asserts /healthz and the version
|
||||||
|
# endpoint. Always tears the stack down.
|
||||||
|
#
|
||||||
|
# ./scripts/smoke.sh # build :local and smoke it
|
||||||
|
# BACKEND_IMAGE_TAG=sha-abc123 ./scripts/smoke.sh # smoke a published image (CI)
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
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}"
|
||||||
|
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}"
|
||||||
|
|
||||||
|
cleanup() { echo "==> tearing down"; "${COMPOSE[@]}" down -v --remove-orphans >/dev/null 2>&1 || true; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo "==> pulling $IMG"
|
||||||
|
"${COMPOSE[@]}" pull backend
|
||||||
|
|
||||||
|
echo "==> starting backend + db ($IMG)"
|
||||||
|
"${COMPOSE[@]}" up -d
|
||||||
|
|
||||||
|
echo "==> waiting for /healthz (up to 90s)"
|
||||||
|
for i in $(seq 1 45); do
|
||||||
|
if curl -fsS -o /dev/null "$PORT_URL/healthz"; then ok=1; break; fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
[ "${ok:-}" = 1 ] || { echo "!! backend never became healthy"; "${COMPOSE[@]}" logs --tail=40 backend; exit 1; }
|
||||||
|
|
||||||
|
echo "==> asserting contract"
|
||||||
|
curl -fsS "$PORT_URL/healthz" >/dev/null && echo " /healthz 200 ok"
|
||||||
|
ver=$(curl -fsS "$PORT_URL/api/version")
|
||||||
|
echo " /api/version -> $ver"
|
||||||
|
echo "$ver" | grep -q '"backend_version"[[:space:]]*:[[:space:]]*"2"' \
|
||||||
|
|| { echo "!! /api/version did not report backend_version=2"; exit 1; }
|
||||||
|
|
||||||
|
echo "==> SMOKE PASSED"
|
||||||
31
scripts/test.sh
Executable file
31
scripts/test.sh
Executable file
|
|
@ -0,0 +1,31 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Reproducible local test runner: build a Python 3.12 dev venv, install the pinned
|
||||||
|
# dev deps, and run the hermetic pytest suite. Matches what CI should run.
|
||||||
|
#
|
||||||
|
# Why 3.12 explicitly: the runtime deps pin versions that need 3.12, and a common box
|
||||||
|
# python (pyenv 3.10) ships without the `_sqlite3` extension, which the suite's
|
||||||
|
# conftest imports — so we don't just use whatever `python3` is on PATH.
|
||||||
|
#
|
||||||
|
# ./scripts/test.sh # full suite
|
||||||
|
# ./scripts/test.sh tests/web # pass pytest args through
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
VENV="${VENV:-.venv-test}"
|
||||||
|
|
||||||
|
if [ ! -x "$VENV/bin/pytest" ]; then
|
||||||
|
echo "==> Building dev venv ($VENV) on Python 3.12"
|
||||||
|
if command -v uv >/dev/null 2>&1; then
|
||||||
|
uv venv --python 3.12 "$VENV"
|
||||||
|
VIRTUAL_ENV="$VENV" uv pip install -q -r requirements-dev.txt
|
||||||
|
else
|
||||||
|
PY="$(command -v python3.12 || echo "$HOME/.local/bin/python3.12")"
|
||||||
|
"$PY" -c 'import sqlite3' 2>/dev/null || { echo "!! $PY lacks the sqlite3 module — install a 3.12 with sqlite (e.g. via uv)"; exit 1; }
|
||||||
|
"$PY" -m venv "$VENV"
|
||||||
|
"$VENV/bin/pip" install -q --upgrade pip
|
||||||
|
"$VENV/bin/pip" install -q -r requirements-dev.txt
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> pytest ($("$VENV/bin/python" --version))"
|
||||||
|
exec "$VENV/bin/python" -m pytest "$@"
|
||||||
Loading…
Reference in a new issue