diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index ce6007c..b09b440 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -42,7 +42,7 @@ These rules govern every task. Any deviation must be discussed and documented as ### Phase 1 — Slice 1 schema + deploy pipeline -**Status:** 🟨 In progress (1.1, 1.2, 1.3 done; 1.4 next) +**Status:** 🟨 In progress (1.1, 1.2, 1.3, 1.6, 1.7 done; 1.4, 1.5, 1.8, 1.9 remaining) **Outcome:** A Directus instance with the org-level catalog (orgs, users, organization_users, vehicles, devices and their org junctions) and event-participation collections (events, classes, entries, entry_crew, entry_devices) live and snapshot-tracked. `db-init/` covers the TimescaleDB extension, the `positions` hypertable, and the `faulty` column. Image builds via Gitea Actions with a CI dry-run that catches snapshot drift before deploy. Rally Albania 2026 is registered as the first event in admin UI to dogfood the registration workflow. **This is what Rally Albania 2026 needs.** [**See `phase-1-slice-1-schema/README.md`**](./phase-1-slice-1-schema/README.md) @@ -54,8 +54,8 @@ These rules govern every task. Any deviation must be discussed and documented as | 1.3 | [Initial migrations (extensions, positions hypertable, faulty column)](./phase-1-slice-1-schema/03-initial-migrations.md) | 🟩 | pending user commit | | 1.4 | [Org-level catalog collections](./phase-1-slice-1-schema/04-org-catalog-collections.md) | ⬜ | — | | 1.5 | [Event-participation collections](./phase-1-slice-1-schema/05-event-participation-collections.md) | ⬜ | — | -| 1.6 | [Schema snapshot/apply tooling](./phase-1-slice-1-schema/06-snapshot-tooling.md) | ⬜ | — | -| 1.7 | [Image build & entrypoint](./phase-1-slice-1-schema/07-image-and-dockerfile.md) | ⬜ | — | +| 1.6 | [Schema snapshot/apply tooling](./phase-1-slice-1-schema/06-snapshot-tooling.md) | 🟩 | pending user commit | +| 1.7 | [Image build & entrypoint](./phase-1-slice-1-schema/07-image-and-dockerfile.md) | 🟩 | pending user commit | | 1.8 | [Gitea CI dry-run workflow](./phase-1-slice-1-schema/08-gitea-ci-dryrun.md) | ⬜ | — | | 1.9 | [Rally Albania 2026 dogfood seed](./phase-1-slice-1-schema/09-rally-albania-2026-seed.md) | ⬜ | — | diff --git a/.planning/phase-1-slice-1-schema/06-snapshot-tooling.md b/.planning/phase-1-slice-1-schema/06-snapshot-tooling.md index a4eb68f..b217231 100644 --- a/.planning/phase-1-slice-1-schema/06-snapshot-tooling.md +++ b/.planning/phase-1-slice-1-schema/06-snapshot-tooling.md @@ -56,4 +56,64 @@ Wrap Directus's native `schema snapshot` and `schema apply` commands in repo-loc ## Done -(Fill in commit SHA + one-line note when this lands.) +**Implementation complete 2026-05-01 — pending user live-test and commit.** + +Files created: + +- `scripts/schema-snapshot.sh` — host-side dev-time snapshot script. + - Verifies `docker` on PATH; verifies the `directus` compose service is in + running state (`docker compose ps --status running --services`). + - Invokes `directus schema snapshot --yes /tmp/schema-snapshot.yaml` inside + the container via `docker compose exec`. + - Copies the output file out via `docker compose cp`. + - Prints `snapshot written to snapshots/schema.yaml ( bytes)`. + - Exits 1 with a clear message if docker is missing, compose file is absent, + service is not running, snapshot command fails, or copy fails. + +- `scripts/schema-apply.sh` — image-side boot-time apply script. + - Verifies `directus` CLI is on PATH (exit 2 if not — image misconfiguration). + - Reads `SNAPSHOT_PATH` env var (default `/directus/snapshots/schema.yaml`). + - Exits 0 with a skip message if the snapshot is absent or empty/whitespace + (safe for first boot before tasks 1.4/1.5 land). + - Logs a dry-run preview (`directus schema apply --dry-run`) before applying. + - Applies via `directus schema apply --yes`; exits 1 on failure. + +- `snapshots/README.md` — lifecycle documentation; warns against hand-editing. + +**Deviations from task spec:** +- `schema:diff` npm alias was intentionally **not** added. The task brief for + this implementation pass explicitly excluded it as scope creep (dry-run is + built into the apply script). The task spec's deliverables section lists it, + but the overriding implementation brief takes precedence. If needed, add + `"schema:diff": "bash scripts/schema-apply.sh --dry-run-only"` in a follow-up + — or simply document that `docker compose exec directus directus schema apply + --dry-run /directus/snapshots/schema.yaml` is the equivalent one-liner. +- `--format=yaml` flag was NOT passed to `directus schema snapshot`. Directus + 11 snapshots to YAML by default (confirmed in source); the flag does not exist + as a standalone option in this version. The output path ends in `.yaml`, which + is sufficient to confirm format intent. + +**Acceptance criteria status:** + +Static (no Docker required — verified in sandbox): +- [x] `#!/usr/bin/env bash` shebang on both scripts. +- [x] `set -euo pipefail` on both scripts. +- [x] Both scripts marked `100755` in the git index (`git update-index --chmod=+x`). +- [x] `schema-apply.sh` skip logic: absent file → exit 0 with skip message. +- [x] `schema-apply.sh` skip logic: empty/whitespace-only file → exit 0 with skip message. +- [x] `schema-apply.sh` skip logic: real YAML content → proceeds to dry-run + apply. +- [x] `schema-snapshot.sh` stopped-stack logic: empty running-services list → exit 1 with "Directus container is not running" message. +- [x] `schema-snapshot.sh` docker-not-found logic: no docker on PATH → exit 1 with clear message. +- [x] `[schema-snapshot]` and `[schema-apply]` log prefixes on all log lines. +- [x] `SNAPSHOT_PATH` env var override supported in `schema-apply.sh` (used by CI). + +Live (verified 2026-05-01): +- [x] `schema-apply.sh` boot-time integration: container boot triggers it as entrypoint step 2/4; with no `snapshots/schema.yaml` present yet, it logs `snapshot not found at /directus/snapshots/schema.yaml — no schema to apply, skipping` and exits 0; entrypoint proceeds to step 3. +- [ ] `pnpm run schema:snapshot` against running stack writes `snapshots/schema.yaml`. **Pending tasks 1.4/1.5** — there are no collections to snapshot yet. +- [ ] Repeated `schema:apply` on an already-applied DB is a no-op (idempotent). **Pending tasks 1.4/1.5.** + +**Bug fix during live verification:** the agent's first pass invoked `directus schema apply` and `directus schema snapshot` as if `directus` were on PATH. The upstream `directus/directus:11.17.4` image does NOT expose `directus` on PATH — the CLI is invoked as `node /directus/cli.js `, matching the upstream image's CMD. Both scripts corrected: +- `schema-apply.sh`: `command -v directus` check replaced with `[[ -f /directus/cli.js ]]`; both `directus schema apply --dry-run` and `directus schema apply --yes` now use `node "${DIRECTUS_CLI}" schema apply ...`. +- `schema-snapshot.sh`: `docker compose exec directus directus schema snapshot --yes ...` now uses `docker compose exec -T directus node /directus/cli.js schema snapshot --yes ...`. The `-T` flag added to disable TTY allocation for non-interactive use. + +(Fill in commit SHA when this lands.) diff --git a/.planning/phase-1-slice-1-schema/07-image-and-dockerfile.md b/.planning/phase-1-slice-1-schema/07-image-and-dockerfile.md index 1e5e1df..dc06ff0 100644 --- a/.planning/phase-1-slice-1-schema/07-image-and-dockerfile.md +++ b/.planning/phase-1-slice-1-schema/07-image-and-dockerfile.md @@ -82,4 +82,67 @@ Build a production-ready Directus image that bakes in the snapshot, db-init migr ## Done -(Fill in commit SHA + one-line note when this lands.) +Pending commit by user. `entrypoint.sh` replaced with production boot flow 2026-05-01. + +**Deliverables produced:** + +- `entrypoint.sh` — full boot flow: db-init → schema apply → bootstrap → pm2-runtime start. Mode `100755` preserved. + +**Scope boundary honored:** +- Only `entrypoint.sh` was modified. `Dockerfile`, `compose.dev.yaml`, `package.json`, `apply-db-init.sh`, and everything under `scripts/`, `db-init/`, and `snapshots/` were untouched (parallel agent boundary for task 1.6). + +**Deviations from task 1.7 spec:** + +The task spec (`07-image-and-dockerfile.md`) shows a naive entrypoint with `exec /directus/cli.js start` as the final command. This was superseded by the implementation brief's explicit requirement (and task 1.1 Done section) to use `node /directus/cli.js bootstrap && pm2-runtime start /directus/ecosystem.config.cjs` — the upstream image's actual CMD. The final entrypoint: +1. Calls `bootstrap` as a discrete step 3 (after schema apply), then +2. Uses `exec pm2-runtime start /directus/ecosystem.config.cjs` as step 4. + +This matches the ROADMAP design rule #3 apply order and preserves pm2's crash recovery and signal handling. `exec` replaces the bash process so SIGTERM from `docker stop` reaches pm2 directly without traversal through bash. + +**Static acceptance criteria (passed):** + +- File path: `C:\Users\Administrator\projects\trm\directus\entrypoint.sh` +- Shebang: `#!/usr/bin/env bash` +- `set -euo pipefail` present (line 22) +- `log()` helper uses `printf` — no trailing newline issues +- Apply order: db-init (1/4) → schema apply (2/4) → bootstrap (3/4) → pm2-runtime (4/4) +- `exec pm2-runtime` — bash process replaced; signals reach pm2 directly +- File mode: `100755` confirmed via `git ls-files -s entrypoint.sh` before and after staging + +**Parallel agent status (task 1.6):** + +`scripts/schema-apply.sh` was NOT present when this task ran — only `scripts/apply-db-init.sh` and `scripts/schema-snapshot.sh` existed in `scripts/`. Step 2/4 of the entrypoint calls `/directus/scripts/schema-apply.sh`. With `set -euo pipefail`, a missing script causes bash to exit non-zero at that line before attempting execution (the shell resolves the command, finds it executable, then the kernel `exec` fails with ENOENT → bash reports the error and exits 127). This means the full boot sequence **cannot be live-tested until task 1.6's `schema-apply.sh` lands**. The implementation is correct; the missing dependency is a parallel-agent timing issue, not a bug. + +**Acceptance criteria — live testing deferred:** + +Live acceptance criteria (Docker boot, curl health check, restart verification) cannot be completed until `scripts/schema-apply.sh` is produced by task 1.6. Re-run the full acceptance suite after both task 1.6 and 1.7 PRs land: +- `docker compose -f compose.dev.yaml down -v` +- `docker compose -f compose.dev.yaml build` +- `docker compose -f compose.dev.yaml up -d` +- Watch for: `[entrypoint] step 1/4` → `[db-init]` output → `[entrypoint] step 2/4` → schema-apply log → `[entrypoint] step 3/4` → bootstrap log → `[entrypoint] step 4/4` → PM2 startup → server at `:8055` +- `curl http://localhost:8055/server/health` → 200 +- `docker compose -f compose.dev.yaml restart directus` → clean re-boot with "already initialized" paths + +**Live-verification result (2026-05-01) — all four steps fired in order, server up at :8055:** + +``` +[entrypoint] step 1/4: db-init → 3 applied, 0 skipped +[entrypoint] step 2/4: directus schema apply → snapshot not found, skipping (correct for Phase 1) +[entrypoint] step 3/4: directus bootstrap → system tables created, first admin role + user added +[entrypoint] step 4/4: directus start (pm2-runtime) +PM2 log: App [directus:0] online +Server started at http://0.0.0.0:8055 +``` + +**Bug fix during live verification:** the parallel `schema-apply.sh` invoked `directus` as if it were on PATH. The upstream image does NOT expose `directus` on PATH — invocation is via `node /directus/cli.js`. See task 1.6's Done section for the fix detail. Entrypoint itself was unaffected; only `schema-apply.sh` needed the change. + +**Phase 5 follow-up note (not blocking Phase 1):** + +Boot logs include `WARN: Collection "positions" doesn't have a primary key column and will be ignored` — three times (during bootstrap migrations + once at startup). Directus auto-discovers tables in the public schema and tries to register them as collections, but skips ones without a PRIMARY KEY constraint. The positions table uses `UNIQUE INDEX (device_id, ts)` instead of a PK (matching processor's pattern, see task 1.3 Done). Result: positions is **not** auto-registered as a Directus collection, so the cross-plane operator workflow (operator flips `faulty` flag via admin UI) cannot use the auto-collection path. + +This is acceptable for Phase 1 (no operator UI yet). Phase 5 (custom extensions) needs a different mechanism for the faulty-flag workflow: +- **Option A**: a custom Directus endpoint (`POST /positions/:id/flag-faulty`) that performs the UPDATE directly via the database service. Bypasses Directus's collection abstraction; thin wrapper around SQL. +- **Option B**: register positions in `directus_collections` manually with a composite primary key configured (`device_id`, `ts`). Some Directus versions support this; verify against 11.17.4. +- **Option C**: add an `id BIGSERIAL PRIMARY KEY` surrogate column to positions. Cleanest for Directus, but introduces a column processor doesn't write and slightly increases per-row storage. + +Phase 5's task file should pin one of these options before extension work begins. diff --git a/entrypoint.sh b/entrypoint.sh index 2300ac9..b9824d7 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,24 +1,38 @@ -#!/bin/sh -# TRM directus — image entrypoint (placeholder). +#!/usr/bin/env bash +# ============================================================================= +# entrypoint.sh — TRM directus image boot flow # -# Real flow (db-init runner → directus schema apply --yes → directus start) -# lands in Phase 1 task 1.7. Until then, this script replicates the upstream -# Directus image CMD so the container boots normally during tasks 1.4 and 1.5 -# (admin UI schema work). +# Apply order (non-negotiable, per ROADMAP design rule #3): +# 1. db-init runner — applies db-init/*.sql migrations against Postgres, +# guarded by the migrations_applied table. Owns DDL Directus does not +# manage (positions hypertable, faulty column). +# 2. Directus schema apply — applies snapshots/schema.yaml so the running +# schema matches what's in git. No-op if schema.yaml doesn't exist +# (Phase 1 task 1.4/1.5 hasn't produced one yet). +# 3. Directus bootstrap — idempotent first-boot setup (admin user, system +# tables). Already-bootstrapped instances treat this as a fast no-op. +# 4. Directus start under pm2-runtime — the upstream image's actual run +# pattern. pm2 provides crash recovery and signal handling inside the +# container. # -# Upstream CMD (from directus/directus:11.17.4 Dockerfile): -# node cli.js bootstrap && pm2-runtime start ecosystem.config.cjs -# -# bootstrap: idempotent — initialises the DB schema on first run, reads -# ADMIN_EMAIL / ADMIN_PASSWORD to create the initial admin user. -# pm2-runtime: starts the Directus process under PM2 so the container stays -# alive and restarts on crash without an outer supervisor. -# -# Exit codes are propagated: any non-zero exit causes the container to exit -# with that code, which compose reports as an error. +# Any failure halts boot (set -euo pipefail). Operators see a clear log line +# in container output telling them which step failed. +# ============================================================================= -set -e +set -euo pipefail +log() { + printf '[entrypoint] %s\n' "$*" +} + +log "step 1/4: db-init" +/directus/scripts/apply-db-init.sh + +log "step 2/4: directus schema apply" +/directus/scripts/schema-apply.sh + +log "step 3/4: directus bootstrap" node /directus/cli.js bootstrap +log "step 4/4: directus start (pm2-runtime)" exec pm2-runtime start /directus/ecosystem.config.cjs diff --git a/scripts/schema-apply.sh b/scripts/schema-apply.sh new file mode 100755 index 0000000..6035888 --- /dev/null +++ b/scripts/schema-apply.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +# ============================================================================= +# schema-apply.sh — TRM directus schema apply (image-side, boot-time) +# +# Applies the committed Directus schema snapshot to the running Postgres so +# the database schema matches what is in git. Called from entrypoint.sh as +# part of the three-step boot sequence: +# apply-db-init.sh → schema-apply.sh → directus start +# +# Usage +# Called automatically by entrypoint.sh (wired in Phase 1 task 1.7). +# Can also be run manually inside the container for debugging: +# bash scripts/schema-apply.sh +# +# Snapshot location +# /directus/snapshots/schema.yaml +# This is the image-baked path. The file is copied in by the Dockerfile. +# For the path to be overridden (e.g. in CI), set SNAPSHOT_PATH in the +# environment before calling this script. +# +# First-boot / no-snapshot behaviour +# If the snapshot file does not exist, or exists but contains only a +# .gitkeep marker (i.e. is empty or contains only whitespace), this script +# logs a skip message and exits 0. This is critical for Phase 1: tasks 1.4 +# and 1.5 (collection creation) have not run yet, so there is no snapshot +# to apply. The entrypoint must not fail in this state. +# +# Dry-run preview +# Before applying, the script runs `directus schema apply --dry-run` and +# prints its output. This makes container boot logs self-explanatory: +# an operator reading logs sees exactly what is about to change. +# On a clean re-deploy where the DB already matches the snapshot, the diff +# output will show "No changes to apply" (or equivalent) and the real apply +# will be a no-op. +# +# Exit codes +# 0 Applied successfully -OR- snapshot not present / empty (skip). +# 1 directus schema apply failed (real apply, not dry-run). +# 2 directus CLI not found on PATH (image misconfiguration). +# +# Environment variables (all optional) +# SNAPSHOT_PATH Override the default /directus/snapshots/schema.yaml. +# Useful in CI where the path may differ. +# DEBUG Set to any non-empty value to enable extra verbosity. +# +# Wired into entrypoint.sh in Phase 1 task 1.7. +# ============================================================================= + +set -euo pipefail + +# ----------------------------------------------------------------------------- +# Logging helpers +# ----------------------------------------------------------------------------- + +log_info() { + printf '[schema-apply] %s\n' "$*" +} + +log_error() { + printf '[schema-apply] ERROR: %s\n' "$*" >&2 +} + +# ----------------------------------------------------------------------------- +# Configuration +# ----------------------------------------------------------------------------- + +SNAPSHOT_PATH="${SNAPSHOT_PATH:-/directus/snapshots/schema.yaml}" + +# ----------------------------------------------------------------------------- +# Step 0 — Verify the directus CLI is available +# ----------------------------------------------------------------------------- +# +# Note: the upstream directus/directus image does NOT expose `directus` on +# PATH. The CLI is invoked as `node /directus/cli.js `, matching +# the upstream image's CMD (`node cli.js bootstrap && pm2-runtime ...`). +# We check that the cli.js entry script exists at the image-baked path. + +readonly DIRECTUS_CLI=/directus/cli.js + +if [[ ! -f "${DIRECTUS_CLI}" ]]; then + log_error "directus CLI not found at ${DIRECTUS_CLI}" + log_error "This script must run inside the directus container image." + exit 2 +fi + +# ----------------------------------------------------------------------------- +# Step 1 — Check for snapshot existence and non-empty content +# ----------------------------------------------------------------------------- + +if [[ ! -f "${SNAPSHOT_PATH}" ]]; then + log_info "snapshot not found at ${SNAPSHOT_PATH} — no schema to apply, skipping" + exit 0 +fi + +# A file containing only a .gitkeep placeholder (empty or whitespace only) +# is treated as absent. Use `tr` to strip whitespace and check emptiness. +snapshot_content_stripped="$(tr -d '[:space:]' < "${SNAPSHOT_PATH}")" + +if [[ -z "${snapshot_content_stripped}" ]]; then + log_info "snapshot at ${SNAPSHOT_PATH} is empty (placeholder only) — no schema to apply, skipping" + exit 0 +fi + +log_info "snapshot found at ${SNAPSHOT_PATH}" + +if [[ -n "${DEBUG:-}" ]]; then + snapshot_bytes="$(wc -c < "${SNAPSHOT_PATH}" | tr -d '[:space:]')" + log_info "snapshot size: ${snapshot_bytes} bytes" +fi + +# ----------------------------------------------------------------------------- +# Step 2 — Dry-run preview (log the diff before applying) +# ----------------------------------------------------------------------------- + +log_info "--- schema diff preview (dry-run) ---" + +dry_run_exit=0 +# Capture output and stream it; we want each line prefixed for clarity. +while IFS= read -r line; do + log_info " ${line}" +done < <(node "${DIRECTUS_CLI}" schema apply --dry-run "${SNAPSHOT_PATH}" 2>&1) || dry_run_exit=$? + +log_info "--- end diff preview ---" + +# A non-zero dry-run exit is not fatal — some Directus versions exit non-zero +# when there are pending changes to report. We always proceed to the real +# apply step; only the real apply exit code is authoritative. +if [[ "${dry_run_exit}" -ne 0 ]]; then + log_info "dry-run exited ${dry_run_exit} (non-zero dry-run exit is non-fatal; proceeding to apply)" +fi + +# ----------------------------------------------------------------------------- +# Step 3 — Apply the snapshot +# ----------------------------------------------------------------------------- + +log_info "applying schema snapshot..." + +apply_exit=0 +apply_output="" + +apply_output="$(node "${DIRECTUS_CLI}" schema apply --yes "${SNAPSHOT_PATH}" 2>&1)" || apply_exit=$? + +# Always print the apply output so it appears in container logs. +while IFS= read -r line; do + log_info " ${line}" +done <<< "${apply_output}" + +if [[ "${apply_exit}" -ne 0 ]]; then + log_error "directus schema apply failed (exit ${apply_exit})" + log_error "The container will not start. Fix the snapshot or the database state." + exit 1 +fi + +log_info "schema apply complete" diff --git a/scripts/schema-snapshot.sh b/scripts/schema-snapshot.sh new file mode 100755 index 0000000..0bf6998 --- /dev/null +++ b/scripts/schema-snapshot.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# ============================================================================= +# schema-snapshot.sh — TRM directus schema snapshot (host-side, dev-time) +# +# Captures the current Directus schema from the running dev compose stack and +# writes it to ./snapshots/schema.yaml for git commit. +# +# Usage +# Run from the repo root (where compose.dev.yaml lives): +# pnpm run schema:snapshot +# Or directly: +# bash scripts/schema-snapshot.sh +# +# Prerequisites +# - docker CLI must be on PATH. +# - The dev compose stack must be running: +# pnpm run dev (or: docker compose -f compose.dev.yaml up -d) +# - The directus service must be healthy (bootstrapped, not just started). +# +# What it does +# 1. Verifies docker is on PATH. +# 2. Verifies the compose directus service is in a running state. +# 3. Runs `directus schema snapshot --yes /tmp/schema-snapshot.yaml` inside +# the container (the container already has all required env vars for DB +# access). +# 4. Copies the generated file out to ./snapshots/schema.yaml. +# 5. Prints a one-line success log with the file size. +# +# Exit codes +# 0 Snapshot written successfully. +# 1 docker CLI not found, or directus service is not running, or snapshot +# command failed inside the container, or copy failed. +# +# Notes +# - The compose service name is `directus`; the compose file is +# `compose.dev.yaml`, resolved relative to the working directory. +# - Do NOT run this from inside the container — it is a host-side script. +# - Do NOT hand-edit snapshots/schema.yaml. Run this script after making +# schema changes via the Directus admin UI. +# +# Wired into package.json as `schema:snapshot`. +# ============================================================================= + +set -euo pipefail + +# ----------------------------------------------------------------------------- +# Logging helpers +# ----------------------------------------------------------------------------- + +log_info() { + printf '[schema-snapshot] %s\n' "$*" +} + +log_error() { + printf '[schema-snapshot] ERROR: %s\n' "$*" >&2 +} + +# ----------------------------------------------------------------------------- +# Configuration +# ----------------------------------------------------------------------------- + +COMPOSE_FILE="compose.dev.yaml" +COMPOSE_SERVICE="directus" +# Temporary path inside the container — chosen to be writable by the `node` +# user that the directus image runs as. +CONTAINER_TMP_PATH="/tmp/schema-snapshot.yaml" +# Destination on the host (relative to the repo root, where this script runs). +HOST_SNAPSHOT_PATH="./snapshots/schema.yaml" + +# ----------------------------------------------------------------------------- +# Step 1 — Verify docker CLI is available +# ----------------------------------------------------------------------------- + +if ! command -v docker > /dev/null 2>&1; then + log_error "docker CLI not found on PATH" + log_error "Install Docker Desktop or Docker Engine and ensure 'docker' is in your PATH." + exit 1 +fi + +log_info "docker CLI found: $(docker --version)" + +# ----------------------------------------------------------------------------- +# Step 2 — Verify the directus compose service is running +# ----------------------------------------------------------------------------- + +log_info "checking compose stack (${COMPOSE_FILE}) for service '${COMPOSE_SERVICE}'" + +if [[ ! -f "${COMPOSE_FILE}" ]]; then + log_error "compose file '${COMPOSE_FILE}' not found." + log_error "Run this script from the directus/ repo root (where compose.dev.yaml lives)." + exit 1 +fi + +# `docker compose ps --status running --services` lists only services that are +# in the running state. We check whether our target service appears in that list. +running_services="$(docker compose -f "${COMPOSE_FILE}" ps --status running --services 2>&1)" || { + log_error "docker compose ps failed — is Docker running?" + log_error "Output: ${running_services}" + exit 1 +} + +if ! printf '%s\n' "${running_services}" | grep -qx "${COMPOSE_SERVICE}"; then + log_error "Directus container is not running." + log_error "Start the stack first: pnpm run dev" + log_error "(Services currently running: ${running_services:-})" + exit 1 +fi + +log_info "service '${COMPOSE_SERVICE}' is running" + +# ----------------------------------------------------------------------------- +# Step 3 — Run `directus schema snapshot` inside the container +# ----------------------------------------------------------------------------- + +log_info "running 'directus schema snapshot' inside the container..." + +snapshot_output="" +snapshot_exit=0 + +snapshot_output="$( + docker compose -f "${COMPOSE_FILE}" exec -T \ + "${COMPOSE_SERVICE}" \ + node /directus/cli.js schema snapshot --yes "${CONTAINER_TMP_PATH}" 2>&1 +)" || snapshot_exit=$? + +if [[ "${snapshot_exit}" -ne 0 ]]; then + log_error "directus schema snapshot failed (exit ${snapshot_exit})" + log_error "Container output:" + # Print each line prefixed so it's clearly from the container. + while IFS= read -r line; do + log_error " > ${line}" + done <<< "${snapshot_output}" + exit 1 +fi + +# ----------------------------------------------------------------------------- +# Step 4 — Copy the snapshot out of the container +# ----------------------------------------------------------------------------- + +log_info "copying snapshot from container to ${HOST_SNAPSHOT_PATH}" + +copy_exit=0 +copy_output="$( + docker compose -f "${COMPOSE_FILE}" cp \ + "${COMPOSE_SERVICE}:${CONTAINER_TMP_PATH}" \ + "${HOST_SNAPSHOT_PATH}" 2>&1 +)" || copy_exit=$? + +if [[ "${copy_exit}" -ne 0 ]]; then + log_error "docker compose cp failed (exit ${copy_exit}): ${copy_output}" + exit 1 +fi + +# ----------------------------------------------------------------------------- +# Step 5 — Report success +# ----------------------------------------------------------------------------- + +# Compute the size of the written file for the one-line success log. +if command -v stat > /dev/null 2>&1; then + # GNU stat (Linux) and BSD stat (macOS) use different flags; try both. + snapshot_bytes="$(stat -c%s "${HOST_SNAPSHOT_PATH}" 2>/dev/null \ + || stat -f%z "${HOST_SNAPSHOT_PATH}" 2>/dev/null \ + || echo "?")" +else + snapshot_bytes="?" +fi + +log_info "snapshot written to snapshots/schema.yaml (${snapshot_bytes} bytes)" diff --git a/snapshots/README.md b/snapshots/README.md new file mode 100644 index 0000000..d669ef6 --- /dev/null +++ b/snapshots/README.md @@ -0,0 +1,57 @@ +# snapshots/ + +This directory holds the Directus schema snapshot for the TRM directus service. + +## What lives here + +- `schema.yaml` — the authoritative Directus schema: all collections, fields, + and relations. Committed to git and applied at every container boot. +- `.gitkeep` — present until the first real snapshot lands (task 1.4/1.5/1.6). + Once `schema.yaml` is committed, `.gitkeep` is no longer needed and can be + removed. + +## Do NOT hand-edit schema.yaml + +`schema.yaml` is generated programmatically. Its format is tightly coupled to +the version of Directus that produced it. Hand-editing produces subtle breakage +(key-order drift, missing internal fields, format violations) that `schema apply` +will reject or silently misinterpret. + +**The only supported workflow for schema changes is:** + +1. Edit the schema in the Directus admin UI (local dev stack). +2. Run `pnpm run schema:snapshot` from the `directus/` repo root. +3. Review the diff in `snapshots/schema.yaml`. +4. Commit and open a PR. + +## How schema.yaml is applied + +`entrypoint.sh` calls `scripts/schema-apply.sh` at every container boot. +The apply script: + +1. Skips silently if `schema.yaml` does not exist or is empty (safe for + first-boot before any collections are defined). +2. Runs a dry-run preview (`directus schema apply --dry-run`) and prints the + diff to container logs. +3. Applies the snapshot (`directus schema apply --yes`). This is idempotent — + Directus computes the diff against the live DB and applies only what has + changed. A clean re-deploy where the DB already matches the snapshot is a + no-op. + +## Snapshot/apply lifecycle + +``` +edit in admin UI + │ + ▼ +pnpm run schema:snapshot ←── writes snapshots/schema.yaml + │ + ▼ +git commit + PR + │ + ▼ +CI: directus schema apply --dry-run (fails PR if snapshot is broken) + │ + ▼ +container boot: entrypoint.sh → schema-apply.sh → directus start +```