From ef8bd91d7727b7a9e13193dc29a65b5bf3b21cdb Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Sat, 2 May 2026 10:51:39 +0200 Subject: [PATCH] Reorder boot: bootstrap before schema-apply (and harden schema-apply) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second CI dry-run failure exposed two more issues: 1. Schema-apply runs against a fresh Postgres → fails with "Directus isn't installed on this database. Please run 'directus bootstrap' first." Bootstrap is what creates Directus's system tables; schema apply requires those tables to exist. Local dev never tripped this because bootstrap had been done in earlier sessions. 2. `node cli.js schema apply` printed an ERROR but exited 0 in the not-installed case. schema-apply.sh trusted the exit code, reported "schema apply complete," and the chain continued — until the post-schema migration tried to ALTER TABLE on user tables that never got created. Fixes: - entrypoint.sh: reorder steps from pre-schema → schema-apply → post-schema → bootstrap → start to pre-schema → bootstrap → schema-apply → post-schema → start Bootstrap is idempotent ("Database already initialized, skipping install" on warm DB) so adding it earlier costs nothing on warm boots and unblocks fresh boots. - .gitea/workflows/build.yml: dry-run chain updated to mirror the new entrypoint order. Bootstrap is now part of the pre-boot validation, not skipped for speed. CI dry-run now genuinely covers the same path the production entrypoint takes (minus the final pm2-runtime step, which doesn't add validation value). - scripts/schema-apply.sh: defense in depth. After the apply call succeeds (exit 0), grep the output for ' ERROR: ' and fail loudly if found. Catches the silent-failure pattern Directus's CLI exhibits when bootstrap hasn't run. Error message names the likely cause (schema-apply before bootstrap) for fast operator triage. This is the second Phase 1 architectural correction exposed by the CI dry-run gate. The gate is paying for itself in the very first PR it runs against. --- .gitea/workflows/build.yml | 20 +++++++++++++------- entrypoint.sh | 28 ++++++++++++++++------------ scripts/schema-apply.sh | 12 ++++++++++++ 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 4914005..33f9473 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -68,12 +68,18 @@ jobs: # ------------------------------------------------------------------------- # Dry-run boot — the gate that protects the registry from broken images. # - # Runs the pre-boot script chain (apply-db-init.sh → schema-apply.sh → - # apply-db-init.sh against db-init-post) against the throwaway Postgres - # service above. Mirrors the entrypoint's first three steps. - # Intentionally does NOT run `directus bootstrap` or `directus start` — - # that would require waiting for the HTTP server to come up, which adds - # minutes and tests nothing new. + # Runs the entrypoint's first FOUR steps against the throwaway Postgres: + # pre-schema db-init → bootstrap → schema-apply → post-schema db-init + # + # Bootstrap is required: schema-apply fails on a fresh DB with + # "Directus isn't installed on this database" if bootstrap hasn't created + # Directus's system tables first. The `directus schema apply` CLI prints + # an ERROR but exits 0 in that case, so an earlier "skip bootstrap for + # speed" version of this dry-run silently masked snapshot apply failures. + # + # Step 5 (`pm2-runtime start`) is intentionally skipped — that would + # require waiting for the HTTP server to come up, which adds minutes and + # tests nothing new beyond what the prior steps already validated. # # --network host: the service container is mapped on 127.0.0.1:5432; the # docker run container sees it as localhost:5432 only when host networking @@ -110,7 +116,7 @@ jobs: -e ADMIN_PASSWORD=ci-password-not-secret \ -e PUBLIC_URL=http://localhost:8055 \ trm-directus:ci \ - -c '/directus/scripts/apply-db-init.sh && /directus/scripts/schema-apply.sh && DB_INIT_DIR=/directus/db-init-post /directus/scripts/apply-db-init.sh && echo "dry-run ok"' + -c '/directus/scripts/apply-db-init.sh && node /directus/cli.js bootstrap && /directus/scripts/schema-apply.sh && DB_INIT_DIR=/directus/db-init-post /directus/scripts/apply-db-init.sh && echo "dry-run ok"' # ------------------------------------------------------------------------- # Registry login — runs only if the dry-run succeeded (default: workflow diff --git a/entrypoint.sh b/entrypoint.sh index f216dbe..b8dfee2 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -6,16 +6,20 @@ # 1. db-init runner (PRE-schema) — applies db-init/*.sql migrations against # Postgres. These are migrations for tables Directus does NOT manage # (positions hypertable, faulty column, future PostGIS extension). -# 2. Directus schema apply — applies snapshots/schema.yaml so the running -# schema matches what's in git. This creates the Directus-managed -# tables (organizations, events, entries, etc.). No-op if schema.yaml -# doesn't exist or is empty. -# 3. db-init runner (POST-schema) — applies db-init-post/*.sql migrations. +# 2. Directus bootstrap — installs Directus's system tables on the database +# (directus_users, directus_collections, etc.) and creates the first +# admin user from ADMIN_EMAIL / ADMIN_PASSWORD if no users exist yet. +# Idempotent — already-bootstrapped databases treat this as a fast no-op +# ("Database already initialized, skipping install"). +# 3. Directus schema apply — applies snapshots/schema.yaml so the running +# schema matches what's in git. This creates the user collections +# (organizations, events, entries, etc.). REQUIRES bootstrap to have run +# first; otherwise fails with "Directus isn't installed on this database." +# No-op if schema.yaml doesn't exist or is empty. +# 4. db-init runner (POST-schema) — applies db-init-post/*.sql migrations. # These are constraints/indexes on Directus-managed tables that the # snapshot YAML format cannot capture (composite UNIQUE constraints). # Must run AFTER schema-apply because the tables don't exist before then. -# 4. Directus bootstrap — idempotent first-boot setup (admin user, system -# tables). Already-bootstrapped instances treat this as a fast no-op. # 5. Directus start under pm2-runtime — the upstream image's actual run # pattern. pm2 provides crash recovery and signal handling inside the # container. @@ -33,14 +37,14 @@ log() { log "step 1/5: db-init (pre-schema)" /directus/scripts/apply-db-init.sh -log "step 2/5: directus schema apply" +log "step 2/5: directus bootstrap" +node /directus/cli.js bootstrap + +log "step 3/5: directus schema apply" /directus/scripts/schema-apply.sh -log "step 3/5: db-init (post-schema)" +log "step 4/5: db-init (post-schema)" DB_INIT_DIR=/directus/db-init-post /directus/scripts/apply-db-init.sh -log "step 4/5: directus bootstrap" -node /directus/cli.js bootstrap - log "step 5/5: directus start (pm2-runtime)" exec pm2-runtime start /directus/ecosystem.config.cjs diff --git a/scripts/schema-apply.sh b/scripts/schema-apply.sh index 6035888..c4fa90d 100755 --- a/scripts/schema-apply.sh +++ b/scripts/schema-apply.sh @@ -151,4 +151,16 @@ if [[ "${apply_exit}" -ne 0 ]]; then exit 1 fi +# Defense in depth: directus CLI's `schema apply` has been observed to log +# ERROR-level messages (e.g. "Directus isn't installed on this database. Please +# run \"directus bootstrap\" first.") while still exiting 0. Treat any line +# containing ' ERROR: ' (with the leading space and trailing colon — Directus's +# pino-formatted error pattern) as a fatal signal even if the CLI exited cleanly. +if grep -qE ' ERROR: ' <<< "${apply_output}"; then + log_error "directus schema apply logged ERROR-level output (CLI exited 0 but failed silently)" + log_error "Common cause: schema apply ran before directus bootstrap on a fresh DB." + log_error "Operator action: ensure entrypoint runs 'directus bootstrap' BEFORE schema-apply." + exit 1 +fi + log_info "schema apply complete"