5035bfc117
Build directus image / build-and-publish (push) Waiting to run
Third CI dry-run failure: schema-apply tried to "Create migrations_applied" and "Create positions" as Directus collections — both already exist as raw tables created by db-init pre-schema. The conflict halts schema-apply on a fresh CI DB. Why these end up in the snapshot at all: `directus schema snapshot` auto-discovers every table in the public schema, including ones owned by db-init (positions hypertable, migrations_applied guard). It registers them as ghost entries with no fields and no relations — just enough metadata to make Directus aware of the table. In local dev this never tripped because the tables existed BEFORE the snapshot ran, and any subsequent apply was a no-op against directus_collections which already had matching ghost rows. On a fresh CI DB the order is: 1. db-init pre-schema → creates the tables 2. bootstrap → installs Directus system tables (NOT the ghosts) 3. schema-apply → tries to "Create" the ghosts → conflict → fail Fixes: - snapshots/schema.yaml: stripped the migrations_applied and positions entries (24 lines each) from the collections: section. The user collections remain untouched. - scripts/schema-snapshot.sh: post-process step that filters the same ghost names from every future snapshot capture. Awk-based, applied after `docker compose cp` writes the file out. The ghost list is a bash array near the top of the new step — add to it when introducing more db-init-only tables. Snapshot is now 105 KB → ~103 KB. The user collections, fields, and relations are unchanged. positions and migrations_applied stay as raw Postgres tables managed by db-init/, never registered in directus_collections, never shown in the admin UI. That matches the schema-as-code split: Directus owns user collections; db-init owns the positions hypertable and the runner's guard table. Three CI iterations to get the boot pipeline right (port collision → ordering → ghost entries). The dry-run gate has now caught three distinct failure modes that would have damaged stage if pushed unguarded.
208 lines
7.9 KiB
Bash
Executable File
208 lines
7.9 KiB
Bash
Executable File
#!/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:-<none>})"
|
|
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 — Strip ghost-collection entries
|
|
#
|
|
# Directus's `schema snapshot` auto-discovers every table in the public schema
|
|
# and registers it in the snapshot YAML, regardless of whether the table is
|
|
# Directus-managed. This includes db-init-owned tables (positions hypertable,
|
|
# migrations_applied guard table) which we intentionally do NOT want Directus
|
|
# to manage.
|
|
#
|
|
# On a fresh CI Postgres, db-init creates these tables before schema-apply
|
|
# runs. If the snapshot includes them, schema-apply tries to "Create" them
|
|
# again as Directus collections — fails with "Invalid payload. Collection
|
|
# X already exists" because the underlying table already exists from db-init.
|
|
#
|
|
# Filter them out post-snapshot. Only the `collections:` section is affected
|
|
# (these tables have no fields/relations registered in directus_fields /
|
|
# directus_relations, so they only appear at the top of the YAML).
|
|
#
|
|
# Add new ghost names to this list when introducing more db-init-only tables.
|
|
# -----------------------------------------------------------------------------
|
|
|
|
GHOST_COLLECTIONS=( "migrations_applied" "positions" )
|
|
|
|
log_info "stripping ghost-collection entries from snapshot"
|
|
|
|
for ghost in "${GHOST_COLLECTIONS[@]}"; do
|
|
# awk pattern: skip the ` - collection: <ghost>` line and all its indented
|
|
# children (meta:, schema:, etc. — 4-space indent) until the next sibling
|
|
# ` - ` or top-level section header.
|
|
awk -v ghost="${ghost}" '
|
|
BEGIN { skip = 0 }
|
|
$0 == " - collection: " ghost { skip = 1; next }
|
|
skip && /^ - / { skip = 0 }
|
|
skip && /^[^ ]/ { skip = 0 }
|
|
!skip { print }
|
|
' "${HOST_SNAPSHOT_PATH}" > "${HOST_SNAPSHOT_PATH}.tmp" \
|
|
&& mv "${HOST_SNAPSHOT_PATH}.tmp" "${HOST_SNAPSHOT_PATH}"
|
|
done
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Step 6 — 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)"
|