From d11708ab4d56c3676b0c79795b2d39f72392f1bb Mon Sep 17 00:00:00 2001 From: SoundMindsAI Date: Mon, 18 May 2026 08:53:54 -0500 Subject: [PATCH 1/4] fix(install): rebuild every Compose service, not just api+worker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit install.sh hardcoded `docker compose build api worker`, silently skipping the `ui` service after it joined Compose. Frontend code changes stayed invisible across `make up` invocations because `docker compose up -d` reuses the cached `relyloop/ui:dev` image and never rebuilds on its own. Switch to `docker compose build` (no args) so every service with a `build:` block — api, worker, migrate, ui, and any future addition — gets rebuilt without a maintenance burden on this list. Also corrects CLAUDE.md's stale "not yet a Compose service" claim about the UI; it has been a Compose service since `chore_tutorial_polish`. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 2 +- scripts/install.sh | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0c690b79..d52ab14c 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 Compose `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/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 From 8cc64f6be3cfababfc39c29831afeb1c5e3d5a22 Mon Sep 17 00:00:00 2001 From: SoundMindsAI Date: Mon, 18 May 2026 09:00:35 -0500 Subject: [PATCH 2/4] =?UTF-8?q?test(ci):=20regression=20gate=20for=20insta?= =?UTF-8?q?ll.sh=20=E2=86=94=20Compose=20service=20drift?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds scripts/ci/verify_install_builds_all_services.sh — parses docker-compose.yml for services with a `build:` block, parses scripts/install.sh for the `docker compose build` invocation, and fails when an explicit service list misses any buildable service. The no-args form (current state) passes trivially. Wired into pr.yml's backend job next to the enum source-of-truth verifier. Catches the failure mode the smoke job can't: on a fresh runner the `relyloop/ui:dev` image doesn't exist, so `docker compose up -d` auto-builds it regardless of whether install.sh listed `ui`. The stale-image bug only surfaces on a second `make up` after a code change — a path CI never exercises but every operator does. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/pr.yml | 9 ++ .../ci/verify_install_builds_all_services.sh | 83 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100755 scripts/ci/verify_install_builds_all_services.sh 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/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)" From b488505812f6ec79d7a2f95517a66467d6b71f3b Mon Sep 17 00:00:00 2001 From: SoundMindsAI Date: Mon, 18 May 2026 09:06:17 -0500 Subject: [PATCH 3/4] fix(make): make down removes containers; make up + make down is the canonical lifecycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous behavior: - `make down` ran `docker compose stop` — containers stayed around in Exited state. Operators expecting `make down && make up` to do a clean recycle got a half-measure (the same containers + cached images were just resumed). New behavior: - `make down` runs `docker compose down` — removes containers + network, preserves named volumes (postgres-data, redis-data, es-data, opensearch-data) so application state survives. `make reset` remains the destructive option (down -v + rm -rf ./data). Combined with the install.sh fix in this PR, the operator contract is: - `make up` → rebuild every buildable service + start - `make down` → stop and remove containers (data preserved) - `make reset` → wipe everything including data (prompted) Updated local-dev.md and deployment.md to match. Co-Authored-By: Claude Opus 4.7 (1M context) --- Makefile | 4 ++-- docs/01_architecture/deployment.md | 4 ++-- docs/03_runbooks/local-dev.md | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) 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) | From e03fd68512c6475f3c3d9bfe473e6521b5675688 Mon Sep 17 00:00:00 2001 From: SoundMindsAI Date: Mon, 18 May 2026 09:16:49 -0500 Subject: [PATCH 4/4] docs: explicit docker compose stop ui command per Gemini review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Gemini PR #146 review — replace the vague "stop the Compose ui" phrasing with the explicit `docker compose stop ui` command so contributors switching to the pnpm-dev hot-reload path don't have to guess the syntax. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index d52ab14c..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: `127.0.0.1:3000` (Compose service `ui`, rebuilt by `make up`; for hot-reload during frontend work, stop the Compose `ui` and run `cd ui && pnpm dev` instead) +- 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`