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.
Background
Section titled “Background”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.
1. Check migration status
Section titled “1. Check migration status”Start with the cheap top-level check.
-
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
Pendingrows surface.
- ✅ No
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.
-
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 ]; thenMODULE_FILES=$(find Modules -type f -name '*.php' -path '*/database/migrations/*' 2>/dev/null | wc -l | xargs)TOTAL=$((MAIN_FILES + MODULE_FILES))elseTOTAL=$MAIN_FILESfiecho "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).
-
Read the result against this table.
Ran vs Total Status Action Equal ✅ Healthy Proceed Ran < Total by 1–3 ⚠️ Check pending Investigate (§3) — may be cosmetic Ran ≪ Total (big gap) 🔴 Problem Stop; fix before deploying Ran > Total ℹ️ Vendor update removed files Not a blocker; note which records reference missing files - ✅ The gap (if any) is classified.
3. Investigate pending migrations
Section titled “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.
-
Diagnose why a migration is pending.
Reason How to verify Action An error occurred php artisan migrate --verboseshows a red errorFix the migration error up()is empty/commentedOpen the file Safe to skip (mark as ran) Conditional logic skipped it if (!Schema::hasTable(...))guardConfirm the table/column already exists Class not found usereferences a missing classConfirm the class exists - ✅ The cause of each pending migration is identified.
-
Run the genuinely-pending ones and re-check.
Terminal window php artisan migrate --verbose # reveals hidden errorsphp artisan migrate # run the genuinely-pending onesphp 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:
4. Capture a normalized schema baseline
Section titled “4. Capture a normalized schema baseline”A schema baseline turns future drift into a readable diff. Create the structure and export schema-only.
-
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-dataif command -v atlas >/dev/null 2>&1; thenatlas schema inspect -u "mysql://root:@127.0.0.1:3306/${DB_NAME}" --format '{{ sql . }}' \> Admin-Local/1-Project/3-ProjectDB/1-Current/local.sqlelseDUMP=$(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.sqlfi# Expected: local.sql written with the schema (no data)- ✅ A schema-only
local.sqlexists under1-Current/.
- ✅ A schema-only
-
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.
-
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].sqlBASELINE="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 countgrep -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 TABLEcount matches the DB, and 0 noise lines remain.
- ✅ Baseline is non-empty,
One class of “drift” is a false alarm worth pre-empting:
5. Run the 11-point gate
Section titled “5. Run the 11-point gate”This one-liner proves the app is genuinely functional before any commit or deploy — then a human confirms the rendered UI.
-
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).
-
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.
-
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).
Migration quick-reference
Section titled “Migration quick-reference”Checklist
Section titled “Checklist”Do not mark this step done until every box below is checked.
- 🤖 No pending migrations —
migrate:statusclean, modules counted. - 🤖 Vendor duplicates resolved — via the
migrationstable, 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.logclean.