Skip to content
prod 352bb92
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.

Steps at a glance:

  1. Check migration status — Start with the cheap top-level check.
  2. Verify the migration count — modules included — Apps using nwidart/laravel-modules often hide migrations inside individual module directories.
  3. Investigate pending migrations — Laravel tracks migrations by filename in the migrations table, not by inspecting the schema. There are four reasons one stays pending.
  4. Capture a normalized schema baseline — A schema baseline turns future drift into a readable diff. Create the structure and export schema-only.
  5. Run the 11-point gate — This one-liner proves the app is genuinely functional before any commit or deploy — then browser automation confirms the rendered UI when available.

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/6-Schema/{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/6-Schema/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/6-Schema/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/6-Schema/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/6-Schema/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 browser automation confirms the rendered UI when available.

  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 — the human logs in once, then the agent checks the dashboard, nav, images, and CSS with no console errors. Use an isolated browser instance so the user’s tab is not disturbed.

    browser_navigate https://[PROJECT_NAME].test/login
    browser_snapshot
    # Expected: login form renders
    # 👤 Human signs in here; agent does not type credentials or submit the auth form.
    browser_wait_for dashboard text or dashboard URL
    browser_snapshot
    browser_console_messages
    # Expected: dashboard renders; navigation, CSS, and images are visible; no console errors
    • ✅ The dashboard renders fully after login. If the browser hits HTTP 500, tail storage/logs/laravel.log before guessing.
  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 — human sign-in + agent dashboard check confirmed via Playwright/Browser MCP when available; laravel.log clean.