diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 7329d956..407e98df 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -224,6 +224,15 @@ jobs: # via the project venv that uv-sync set up above. run: bash scripts/ci/verify_enum_source_of_truth.sh + - name: Verify install.sh builds every buildable Compose service (bug_install_skip_ui_rebuild) + # Fails when scripts/install.sh's `docker compose build` line uses an + # explicit service list that doesn't cover every service with a + # `build:` block in docker-compose.yml. Catches the silent-drift class + # of bug where adding a service to Compose leaves operators running a + # stale image on the second `make up` after a code change (the + # smoke-test job runs on a fresh runner so it never sees that path). + run: bash scripts/ci/verify_install_builds_all_services.sh + frontend: name: frontend (lint + typecheck + tests + build) runs-on: ubuntu-latest diff --git a/CLAUDE.md b/CLAUDE.md index 0c690b79..a851bc03 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -176,7 +176,7 @@ make migrate-create name= # alembic revision --autogenerate -m "" **Ports (MVP1):** - API: `127.0.0.1:8000` -- UI dev server: `127.0.0.1:3000` (run via `cd ui && pnpm dev`; not yet a Compose service) +- UI: `127.0.0.1:3000` (Compose service `ui`, rebuilt by `make up`; for hot-reload during frontend work, stop the service with `docker compose stop ui` and run `cd ui && pnpm dev` instead) - Postgres: internal only (`postgres:5432` on the Compose network; not bound to host) - Redis: internal only (`redis:6379`) - Elasticsearch: `127.0.0.1:9200` diff --git a/Makefile b/Makefile index 1b3e07ab..3d6898d9 100644 --- a/Makefile +++ b/Makefile @@ -105,8 +105,8 @@ ui-dev: ## Start the Next.js dev server (http://localhost:3000) — uses .nvmrc up: ## Generate secrets if missing, then docker compose up -d (auto-bootstrap) bash scripts/install.sh -down: ## docker compose stop (preserves data volumes) - docker compose stop +down: ## docker compose down (removes containers + network; preserves data volumes) + docker compose down restart: ## docker compose restart api + worker (fast bounce when something wedges) docker compose restart api worker diff --git a/docs/01_architecture/deployment.md b/docs/01_architecture/deployment.md index 573221b9..9a7b29ae 100644 --- a/docs/01_architecture/deployment.md +++ b/docs/01_architecture/deployment.md @@ -218,9 +218,9 @@ echo "" > ./secrets/openai_key make up # Daily use -make up # docker compose up -d (builds ui image on first run) +make up # docker compose build (all services) + up -d make logs # docker compose logs -f api worker -make down # docker compose stop +make down # docker compose down (containers removed; data volumes preserved) make migrate # alembic upgrade head + optuna_schema — idempotent (also runs automatically via the migrate init container at boot) make seed-clusters # populate local-es + local-opensearch as cluster rows make seed-es # seed local-es 'products' index from samples/products.json diff --git a/docs/03_runbooks/local-dev.md b/docs/03_runbooks/local-dev.md index 5970be7b..d99ffbdd 100644 --- a/docs/03_runbooks/local-dev.md +++ b/docs/03_runbooks/local-dev.md @@ -104,8 +104,8 @@ If you want the round-trip test to actually run from your shell, you'd need to e | Target | What it does | |---|---| -| `make up` | Generate secrets if missing → `docker compose up -d` | -| `make down` | `docker compose stop` (preserves data volumes) | +| `make up` | Generate secrets if missing → `docker compose build` (every buildable service) → `docker compose up -d` | +| `make down` | `docker compose down` (removes containers + network; preserves data volumes) | | `make logs` | `docker compose logs -f api worker` | | `make migrate` | `alembic upgrade head` + initialize Optuna RDB schema (idempotent — also runs automatically via the `migrate` init container at boot) | | `make migrate-create name=` | New Alembic revision (autogenerate) | diff --git a/scripts/ci/verify_install_builds_all_services.sh b/scripts/ci/verify_install_builds_all_services.sh new file mode 100755 index 00000000..46cfd40e --- /dev/null +++ b/scripts/ci/verify_install_builds_all_services.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# bug_install_skip_ui_rebuild regression gate. +# +# Asserts that scripts/install.sh's `docker compose build` invocation covers +# every Compose service that declares a `build:` block. Catches the silent-drift +# class of bug where a service is added to docker-compose.yml but the build +# step in install.sh isn't updated, leaving `make up` with a stale image on +# subsequent runs (the smoke job runs on a fresh runner so it never sees this +# failure mode — only the second-run-after-edit operator does). +# +# Accepts two forms: +# 1. `docker compose build` — no args, builds everything; OK +# 2. `docker compose build a b c` — explicit list; must include every +# service with a `build:` block + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +COMPOSE_FILE="${REPO_ROOT}/docker-compose.yml" +INSTALL_FILE="${REPO_ROOT}/scripts/install.sh" + +if [[ ! -f "${COMPOSE_FILE}" ]]; then + echo "verify_install_builds_all_services: ${COMPOSE_FILE} not found" >&2 + exit 2 +fi +if [[ ! -f "${INSTALL_FILE}" ]]; then + echo "verify_install_builds_all_services: ${INSTALL_FILE} not found" >&2 + exit 2 +fi + +# Extract services with a `build:` block. Service headers are top-level keys +# at 2-space indent; build configs sit at 4-space indent inside them. +buildable=$(awk ' + /^[a-z][a-z0-9_-]*:$/ { in_services = ($0 == "services:") ? 1 : 0; next } + in_services && /^ [a-z][a-z0-9_-]*:$/ { + svc = $1; sub(/:$/, "", svc); next + } + in_services && /^ build:/ { print svc } +' "${COMPOSE_FILE}" | sort -u) + +if [[ -z "${buildable}" ]]; then + echo "verify_install_builds_all_services: no buildable services found in ${COMPOSE_FILE}" >&2 + exit 2 +fi + +# Extract the `docker compose build [args...]` line from install.sh. +# Match the bare command line (no pipes, no &&) — we want the operative build +# step, not commentary or shell-substitution variants. +build_line=$(grep -E '^docker compose build( .*)?$' "${INSTALL_FILE}" || true) + +if [[ -z "${build_line}" ]]; then + echo "verify_install_builds_all_services: no 'docker compose build' line found in ${INSTALL_FILE}" >&2 + echo " Expected a top-level invocation that builds images before 'up -d'." >&2 + exit 1 +fi + +# Strip the prefix to get the args (if any). +args=$(echo "${build_line}" | sed -E 's/^docker compose build *//') + +if [[ -z "${args}" ]]; then + echo "verify_install_builds_all_services: OK (no-args = builds all)" + exit 0 +fi + +# Explicit list — every buildable service must appear in args. +missing=() +for svc in ${buildable}; do + if ! grep -qE "(^| )${svc}( |$)" <<<"${args}"; then + missing+=("${svc}") + fi +done + +if (( ${#missing[@]} > 0 )); then + echo "verify_install_builds_all_services: FAIL" >&2 + echo " install.sh build invocation: 'docker compose build ${args}'" >&2 + echo " Missing services: ${missing[*]}" >&2 + echo " Fix: either add the missing services, or drop all args so the line" >&2 + echo " becomes 'docker compose build' (which builds every service that" >&2 + echo " declares a build: block — preferred, drift-proof)." >&2 + exit 1 +fi + +echo "verify_install_builds_all_services: OK (explicit list covers all buildable services)" diff --git a/scripts/install.sh b/scripts/install.sh index b1903feb..b53a2ff6 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -70,7 +70,10 @@ docker compose config --quiet # explicit build step, contributors who pull new code and re-run `make up` # keep running the stale image (PR #4 first-run testing surfaced exactly # this — a stale image missing newly-added Python deps). -docker compose build api worker +# No-args = build every service that declares a `build:` block. The earlier +# hardcoded `api worker` list silently skipped the `ui` service after it +# joined Compose, leaving frontend changes invisible until manual rebuild. +docker compose build # 7. Bring the stack up. `docker compose up -d` is itself idempotent. exec docker compose up -d