T1 deliverables for the v2 iteration:
- .gitignore — keep __pycache__, .smoke-*.log, and editor noise out
- docs/SMOKE.md — single source of truth for bringing up the v1 stack
from a fresh clone. Documents the 5-command path, expected output at
each step, the CI runner, and a troubleshooting table.
- scripts/ci-smoke.sh — CI smoke runner. Brings the stack up, waits for
all four services healthy, polls /healthz, idempotently seeds (skip
if data already present), runs test.sh. Exits 0 on pass. Supports
--keep-up and --skip-build for dev iteration. Shell-only because the
Gitea instance has no Actions runner wired up yet — the script is
the same shape a future Actions step will wrap.
Gitea milestones T1-T9 are created on kaykayyali/lore-engine-poc via
'tea milestones create'. See docs/SMOKE.md for the milestone → task
mapping (T7, T8 are deferred per the T9 integration task's body).
Verification:
- ./scripts/ci-smoke.sh --skip-build --keep-up → SMOKE PASSED
(12 v1 tools registered, 11/11 test.sh sections green)
- tea milestones list → all 9 milestones present
Co-located with the v1 baseline commit already on wt/t1-gitea-push
(commit 8e8503e adds the T3 consistency-skeleton plugin on top of v1;
that is T3's work, surfaced here only because T1, T2, T3 share a
single working dir). The T1 deliverables are additive: SMOKE.md,
ci-smoke.sh, .gitignore.
210 lines
7.8 KiB
Bash
Executable File
210 lines
7.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# lore-engine-poc — CI smoke runner
|
|
#
|
|
# Brings up the stack from a clean working tree, waits for all four services
|
|
# to be healthy, runs the seed, runs test.sh, and exits 0/1.
|
|
#
|
|
# Designed to be run identically on a developer laptop, in CI, or in a
|
|
# one-off cron. See docs/SMOKE.md for the full rationale + troubleshooting.
|
|
#
|
|
# Usage:
|
|
# ./scripts/ci-smoke.sh # full bring-up + test + teardown
|
|
# ./scripts/ci-smoke.sh --keep-up # leave the stack running on success
|
|
# ./scripts/ci-smoke.sh --skip-build # skip `docker compose build`
|
|
#
|
|
# Exit codes:
|
|
# 0 smoke passed
|
|
# 1 a service did not become healthy in time
|
|
# 2 seed.py failed
|
|
# 3 test.sh failed
|
|
# 4 usage / argument error
|
|
# 5 docker compose not available
|
|
|
|
set -euo pipefail
|
|
|
|
# ─── argument parsing ────────────────────────────────────────────────────────
|
|
KEEP_UP=0
|
|
SKIP_BUILD=0
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--keep-up) KEEP_UP=1 ;;
|
|
--skip-build) SKIP_BUILD=1 ;;
|
|
-h|--help)
|
|
sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//'
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "unknown arg: $arg" >&2
|
|
exit 4
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# ─── helpers ────────────────────────────────────────────────────────────────
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
cd "$SCRIPT_DIR/.." # repo root (scripts/ is a sibling of docker-compose.yml)
|
|
REPO_ROOT="$(pwd)"
|
|
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
echo "FATAL: docker not on PATH" >&2
|
|
exit 5
|
|
fi
|
|
if ! docker compose version >/dev/null 2>&1; then
|
|
echo "FATAL: 'docker compose' (v2 plugin) not available — install the compose plugin" >&2
|
|
exit 5
|
|
fi
|
|
|
|
# Timestamped log so concurrent runs (rare) don't trample each other and so
|
|
# CI can grep the timestamped output for the failure point.
|
|
LOG="$REPO_ROOT/.smoke-$(date -u +%Y%m%dT%H%M%SZ).log"
|
|
exec > >(tee -a "$LOG") 2>&1
|
|
echo "=== ci-smoke starting at $(date -u +%Y-%m-%dT%H:%M:%SZ) ==="
|
|
echo "=== repo: $REPO_ROOT"
|
|
echo "=== log: $LOG"
|
|
echo
|
|
|
|
cleanup() {
|
|
local exit_code=$?
|
|
echo
|
|
echo "=== ci-smoke exiting with code $exit_code at $(date -u +%Y-%m-%dT%H:%M:%SZ) ==="
|
|
if [ $KEEP_UP -eq 0 ] && [ $exit_code -ne 0 ]; then
|
|
echo
|
|
echo "stack left running for post-mortem. Tear down with:"
|
|
echo " docker compose down -v"
|
|
fi
|
|
if [ $KEEP_UP -eq 1 ] && [ $exit_code -eq 0 ]; then
|
|
echo
|
|
echo "stack left running (--keep-up). Tear down with:"
|
|
echo " docker compose down -v"
|
|
fi
|
|
exit $exit_code
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
# ─── step 1: build + start ───────────────────────────────────────────────────
|
|
if [ $SKIP_BUILD -eq 0 ]; then
|
|
echo ">>> [1/5] docker compose build"
|
|
docker compose build
|
|
fi
|
|
|
|
echo
|
|
echo ">>> [1/5] docker compose up -d"
|
|
docker compose up -d
|
|
|
|
# ─── step 2: wait for services healthy ───────────────────────────────────────
|
|
echo
|
|
echo ">>> [2/5] waiting for neo4j, postgres, minio to be healthy (60s deadline each)"
|
|
SERVICES=(lore-neo4j lore-postgres lore-minio)
|
|
DEADLINE_SECS=60
|
|
for svc in "${SERVICES[@]}"; do
|
|
elapsed=0
|
|
while [ $elapsed -lt $DEADLINE_SECS ]; do
|
|
status=$(docker inspect -f '{{.State.Health.Status}}' "$svc" 2>/dev/null || echo "missing")
|
|
if [ "$status" = "healthy" ]; then
|
|
echo " ✔ $svc healthy (after ${elapsed}s)"
|
|
break
|
|
fi
|
|
if [ "$status" = "unhealthy" ]; then
|
|
echo " ✖ $svc reported UNHEALTHY:" >&2
|
|
docker logs --tail 50 "$svc" >&2
|
|
exit 1
|
|
fi
|
|
sleep 2
|
|
elapsed=$((elapsed + 2))
|
|
done
|
|
if [ $elapsed -ge $DEADLINE_SECS ]; then
|
|
echo " ✖ $svc did not become healthy within ${DEADLINE_SECS}s" >&2
|
|
echo " last status: $status" >&2
|
|
docker logs --tail 50 "$svc" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# ─── step 3: wait for gateway /healthz ───────────────────────────────────────
|
|
echo
|
|
echo ">>> [3/5] waiting for gateway /healthz (60s deadline)"
|
|
elapsed=0
|
|
HEALTHZ_URL="${GATEWAY:-http://localhost:8765/healthz}"
|
|
while [ $elapsed -lt $DEADLINE_SECS ]; do
|
|
if response=$(curl -fsS "$HEALTHZ_URL" 2>/dev/null) && \
|
|
echo "$response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); assert d.get('status')=='ok'; assert isinstance(d.get('plugins'), list) and len(d['plugins'])>0" 2>/dev/null; then
|
|
tool_count=$(echo "$response" | python3 -c "import json,sys; print(len(json.loads(sys.stdin.read())['plugins']))")
|
|
echo " ✔ gateway healthy, $tool_count tools registered"
|
|
break
|
|
fi
|
|
sleep 2
|
|
elapsed=$((elapsed + 2))
|
|
done
|
|
if [ $elapsed -ge $DEADLINE_SECS ]; then
|
|
echo " ✖ gateway /healthz did not return 200+valid JSON within ${DEADLINE_SECS}s" >&2
|
|
docker logs --tail 50 lore-gateway >&2 || true
|
|
exit 1
|
|
fi
|
|
|
|
# ─── step 4: seed (idempotent — skip if data already present) ────────────────
|
|
echo
|
|
echo ">>> [4/5] seed: check if data is already loaded"
|
|
seeded_already=0
|
|
# Probe Neo4j for any Person node. If the count > 0, treat as already seeded.
|
|
# (Cheap, ~50ms.) seed.py is idempotent so re-running is safe, but skipping
|
|
# the seed keeps the smoke fast when the caller just wants to re-verify
|
|
# test.sh against a known-good DB.
|
|
person_count=$(docker exec lore-neo4j cypher-shell -u neo4j -p lore-dev-password \
|
|
"MATCH (p:Person) RETURN count(p) AS n" 2>/dev/null \
|
|
| awk '/^[0-9]+$/{print; exit}' || echo "0")
|
|
if [ "${person_count:-0}" -gt 0 ] 2>/dev/null; then
|
|
echo " ✔ already seeded (Person count = $person_count), skipping seed.py"
|
|
seeded_already=1
|
|
fi
|
|
|
|
if [ $seeded_already -eq 0 ]; then
|
|
echo " → running python3 seed.py (host)"
|
|
if ! python3 seed.py 2>/tmp/seed.err; then
|
|
echo " ⚠ host seed.py failed: $(head -1 /tmp/seed.err)" >&2
|
|
echo " → falling back to docker run via the gateway network"
|
|
# Run seed.py inside a sidecar container on the lore-engine-poc_default
|
|
# network. We use the gateway image because it has all the python deps,
|
|
# then bind-mount the repo so seed.py can find the mock-data dir.
|
|
if ! docker run --rm --network lore-engine-poc_default \
|
|
-v "$REPO_ROOT":/work -w /work \
|
|
-e NEO4J_URL='bolt://neo4j:7687' \
|
|
-e NEO4J_USER=neo4j -e NEO4J_PASSWORD=lore-dev-password \
|
|
-e POSTGRES_URL='postgresql://lore:***@postgres:5432/lore' \
|
|
-e MINIO_URL='http://minio:9000' \
|
|
-e MINIO_ACCESS_KEY=lorelore -e MINIO_SECRET_KEY=lore-dev-password \
|
|
-e MINIO_BUCKET=lore-images \
|
|
--entrypoint python3 \
|
|
lore-engine-poc-gateway \
|
|
seed.py 2>/tmp/seed-docker.err; then
|
|
echo " ✖ seed failed in both host and docker modes" >&2
|
|
echo " host stderr: $(cat /tmp/seed.err)" >&2
|
|
echo " docker stderr: $(cat /tmp/seed-docker.err)" >&2
|
|
exit 2
|
|
fi
|
|
fi
|
|
echo " ✔ seed complete"
|
|
fi
|
|
|
|
# ─── step 5: e2e test ────────────────────────────────────────────────────────
|
|
echo
|
|
echo ">>> [5/5] bash test.sh"
|
|
if ! bash test.sh; then
|
|
echo " ✖ test.sh failed" >&2
|
|
exit 3
|
|
fi
|
|
echo " ✔ test.sh passed"
|
|
|
|
# ─── optional teardown ──────────────────────────────────────────────────────
|
|
if [ $KEEP_UP -eq 0 ]; then
|
|
echo
|
|
echo ">>> tearing down stack (use --keep-up to leave it running)"
|
|
docker compose down -v
|
|
else
|
|
echo
|
|
echo ">>> --keep-up set, stack left running"
|
|
fi
|
|
|
|
echo
|
|
echo "=== SMOKE PASSED at $(date -u +%Y-%m-%dT%H:%M:%SZ) ==="
|
|
exit 0
|