Skip to content
prod e051e98
Browse

5 · Verify migrations & schema

Objective — prove the install is genuinely complete: confirm every migration ran (modules included), troubleshoot pending ones the right way, capture a normalized schema baseline for cross-environment diffs, then run the 11-point gate that shows the app actually works.

The installer says it finished — now prove it. Pending migrations are the #1 cause of deploy failures, so catch them locally where a fix is cheap, then snapshot the schema as your drift baseline.

Start with the cheap top-level check.

  1. Scan for any pending migration.

    Terminal window
    php artisan migrate:status --no-ansi | grep "Pending" && echo "PENDING FOUND" || echo "All migrations ran"
    # Expected: "All migrations ran"
    • ✅ No Pending rows surface.

2. Verify the migration count — modules included

Section titled “2. Verify the migration count — modules included”

Apps using nwidart/laravel-modules often hide migrations inside individual module directories. Miss those and a “files = ran” match hides real drift.

  1. Count ran migrations against total files, modules included.

    Terminal window
    RAN=$(php artisan migrate:status --no-ansi 2>/dev/null | grep -c 'Ran')
    MAIN_FILES=$(ls -1 database/migrations/*.php 2>/dev/null | wc -l | xargs)
    if [ -d Modules ]; then
    MODULE_FILES=$(find Modules -type f -name '*.php' -path '*/database/migrations/*' 2>/dev/null | wc -l | xargs)
    TOTAL=$((MAIN_FILES + MODULE_FILES))
    else
    TOTAL=$MAIN_FILES
    fi
    echo "Ran: $RAN | Total files: $TOTAL"
    [ "$RAN" -eq "$TOTAL" ] && echo "✅ Exact match" || echo "⚠️ Investigate $((TOTAL - RAN)) migration(s)"
    # Expected: "✅ Exact match" (or a small gap to investigate in §3)
    • ✅ Ran count matches the total migration files (or the gap is understood).
  2. Read the result against this table.

    Ran vs TotalStatusAction
    Equal✅ HealthyProceed
    Ran < Total by 1–3⚠️ Check pendingInvestigate (§3) — may be cosmetic
    Ran ≪ Total (big gap)🔴 ProblemStop; fix before deploying
    Ran > Totalℹ️ Vendor update removed filesNot a blocker; note which records reference missing files
    • ✅ The gap (if any) is classified.

Laravel tracks migrations by filename in the migrations table, not by inspecting the schema. There are four reasons one stays pending.

  1. Diagnose why a migration is pending.

    ReasonHow to verifyAction
    An error occurredphp artisan migrate --verbose shows a red errorFix the migration error
    up() is empty/commentedOpen the fileSafe to skip (mark as ran)
    Conditional logic skipped itif (!Schema::hasTable(...)) guardConfirm the table/column already exists
    Class not founduse references a missing classConfirm the class exists
    • ✅ The cause of each pending migration is identified.
  2. Run the genuinely-pending ones and re-check.

    Terminal window
    php artisan migrate --verbose # reveals hidden errors
    php artisan migrate # run the genuinely-pending ones
    php artisan migrate:status --no-ansi | grep "Pending" || echo "All clear"
    # Expected: "All clear"
    • ✅ No real pending migrations remain.

The same filename-tracking quirk causes a different problem once you deploy:

A schema baseline turns future drift into a readable diff. Create the structure and export schema-only.

  1. Export a schema-only dump (Atlas if installed, otherwise mariadb-dump/mysqldump --no-data).

    Terminal window
    mkdir -p Admin-Local/1-Project/3-ProjectDB/{1-Current,2-Snapshots,3-VendorOriginal}
    DB_NAME=$(grep DB_DATABASE .env | cut -d= -f2)
    # Atlas if installed (cleaner output); otherwise mariadb-dump/mysqldump --no-data
    if command -v atlas >/dev/null 2>&1; then
    atlas schema inspect -u "mysql://root:@127.0.0.1:3306/${DB_NAME}" --format '{{ sql . }}' \
    > Admin-Local/1-Project/3-ProjectDB/1-Current/local.sql
    else
    DUMP=$(command -v mariadb-dump || command -v mysqldump)
    "$DUMP" -h 127.0.0.1 -u root --no-data "${DB_NAME}" \
    > Admin-Local/1-Project/3-ProjectDB/1-Current/local.sql
    fi
    # Expected: local.sql written with the schema (no data)
    • ✅ A schema-only local.sql exists under 1-Current/.
  2. Normalize the dump so only real drift surfaces — strip environment-specific noise idempotently.

    Terminal window
    BASELINE="Admin-Local/1-Project/3-ProjectDB/1-Current/local.sql"
    sed \
    -e '/^-- MariaDB dump/d' \
    -e '/^-- MySQL dump/d' \
    -e '/^-- Host:/d' \
    -e '/^-- Server version/d' \
    -e '/^-- Dump completed/d' \
    -e '/^\/\*M!999999/d' \
    -e '/^\/\*M!100616/d' \
    -e 's/ AUTO_INCREMENT=[0-9]*//' \
    -e 's|/\*!40101 SET character_set_client = utf8 \*/|/*!40101 SET character_set_client = utf8mb4 */|' \
    "$BASELINE" > "${BASELINE}.tmp" && mv "${BASELINE}.tmp" "$BASELINE"
    # Expected: dump-date/host/version/AUTO_INCREMENT noise removed; real structure untouched
    • ✅ The baseline carries no environment noise — only structural definitions.
  3. Snapshot the vendor original (first setup only) and verify the baseline.

    Terminal window
    # First setup of this vendor version only:
    # cp …/1-Current/local.sql …/3-VendorOriginal/v[VERSION].sql
    BASELINE="Admin-Local/1-Project/3-ProjectDB/1-Current/local.sql"
    [ -s "$BASELINE" ] && echo "$(wc -l < "$BASELINE") lines"
    grep -c "^CREATE TABLE" "$BASELINE" # should match the DB table count
    grep -c "^-- Host:\|^-- Server version\|AUTO_INCREMENT=" "$BASELINE" # expect 0 noise lines
    # Expected: a non-empty line count, CREATE TABLE count = DB tables, 0 noise lines
    • ✅ Baseline is non-empty, CREATE TABLE count matches the DB, and 0 noise lines remain.

One class of “drift” is a false alarm worth pre-empting:

This one-liner proves the app is genuinely functional before any commit or deploy — then a human confirms the rendered UI.

  1. Run the 11-point gate.

    Terminal window
    DB_NAME=$(grep DB_DATABASE .env | cut -d= -f2)
    echo "1. Vendor: $([ -f vendor/autoload.php ] && echo OK || echo MISSING)"
    echo "2. Node: $([ -d node_modules ] && echo OK || echo MISSING)"
    echo "3. Build: $([ -f public/build/manifest.json ] && echo OK || echo MISSING)"
    echo "4. Storage link: $([ -L public/storage ] && echo Linked || echo Broken)"
    echo "5. Installer: $([ -f storage/installed ] && echo Present || echo MISSING)"
    echo "6. Database: $(php artisan db:show >/dev/null 2>&1 && echo Connected || echo Failed)"
    echo "7. Tables: $(mysql -h 127.0.0.1 -u root -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='${DB_NAME}';" 2>/dev/null)"
    echo "8. Migrations: $(php artisan migrate:status --no-ansi 2>/dev/null | grep -q Pending && echo 'PENDING — see §3' || echo 'Up to date')"
    echo "9. App Key: $(grep -q '^APP_KEY=base64' .env && echo Set || echo MISSING)"
    echo "10. HTTP /: $(curl -sI -o /dev/null -w '%{http_code}' https://[PROJECT_NAME].test/)"
    echo "11. HTTP /login: $(curl -sI -o /dev/null -w '%{http_code}' https://[PROJECT_NAME].test/login)"
    # Expected: 1–6 OK/Linked/Present/Connected, 7 ≥ 50, 8 "Up to date", 9 "Set", 10 200/302, 11 200
    • ✅ Every gate line passes (or the failing line maps to a known fix below).
  2. Confirm the rendered app in a browser (👤) — log in and check the dashboard, nav, images, and CSS, with no console errors.

    • ✅ The dashboard renders fully after login.
  3. Scan the log for anything missed.

    Terminal window
    tail -50 storage/logs/laravel.log | grep -i "error\|exception\|fatal" || echo "No errors"
    # Expected: "No errors"
    • ✅ The log tail is clean.

If a gate check fails, the most common fixes: rerun page 1 (1–3), page 3 (4), page 4 (5,7), check .env + Herd DB service (6), php artisan key:generate (9), and the page-3 root-500 note (10).

Do not mark this step done until every box below is checked.

  • 🤖 No pending migrationsmigrate:status clean, modules counted.
  • 🤖 Vendor duplicates resolved — via the migrations table, not file edits.
  • 🤖 Baseline written — normalized local.sql; 0 noise lines; CREATE TABLE count matches DB.
  • 🤖 11-point gate green — all eleven checks pass.
  • 👤 Browser verified — login + dashboard confirmed; laravel.log clean.