3 · Harden, verify & sync (P3–P4)
Objective — lock down the installer-friendly 777 permissions the deployer leaves behind, confirm production schema matches staging, then fast-forward every branch back to a shared base and tag the release — so you have a clean, drift-free, rollback-anchored production state.
Background
Section titled “Background”Two short passes catch the most common day-one production failures: the deployer may leave installer-friendly 777 permissions behind, and schema drift between environments causes runtime errors and data corruption — production must match staging.
1. Lock down permissions (P3)
Section titled “1. Lock down permissions (P3)”Tighten the installer-friendly permissions once the installer is done.
-
Reset storage permissions and re-lock the
.env.Terminal window ssh <SSH_PRODUCTION_ALIAS> "cd ~/domains/<DOMAIN>/deploy/shared/storage \&& find . -type d -exec chmod 775 {} \; && find . -type f -exec chmod 664 {} \;"ssh <SSH_PRODUCTION_ALIAS> "chmod 640 ~/domains/<DOMAIN>/deploy/shared/.env"# Expected: storage dirs become 775, files 664, and .env is 640- ✅ Storage directories are
775, files664, and the.envis640.
- ✅ Storage directories are
2. Verify schema parity (P3)
Section titled “2. Verify schema parity (P3)”Production must match staging — a logical schema diff is cleanest.
-
Check pending migrations — expect
Nothing to migrate(or only those explicitly left pending).Terminal window ssh <SSH_PRODUCTION_ALIAS> "cd ~/domains/<DOMAIN>/deploy/current && php artisan migrate --pretend"# Expected: "Nothing to migrate" (or only intentionally-pending migrations)- ✅ No unexpected pending migrations.
-
Compare staging vs production schema. A logical schema diff is cleanest; a filtered
migrate:status/dump diff works without extra tooling. If real differences appear, stop and investigate before continuing.- ✅ The staging↔production schema diff is clean (or every difference is explained and resolved).
-
Sanity-check vendor migration file counts — and triage a mismatch, don’t panic. Compare the count of vendor migration files on each host; a mismatch is often expected, not a fault.
Terminal window STAGING_COUNT=$(ssh <SSH_STAGING_ALIAS> "cd ~/domains/<DOMAIN>/deploy/current \&& find vendor -name '*.php' -path '*/database/migrations/*' 2>/dev/null | wc -l | tr -d ' '")PROD_COUNT=$(ssh <SSH_PRODUCTION_ALIAS> "cd ~/domains/<DOMAIN>/deploy/current \&& find vendor -name '*.php' -path '*/database/migrations/*' 2>/dev/null | wc -l | tr -d ' '")echo "Staging: $STAGING_COUNT | Production: $PROD_COUNT"[ "$STAGING_COUNT" = "$PROD_COUNT" ] && echo "Match" || echo "Mismatch — triage below"# Expected: "Match", OR a mismatch you can explain via the tableSituation Cause Action Production has more files Stale composer cache on the server dep clear_composer_cache production, then redeployStaging has more, diff is only dev packages ( debugbar,telescope,dusk)Dev deps not deployed under --no-devExpected and correct — no action Staging has more, diff shows non-dev packages Deploy didn’t finish cleanly Redeploy: dep deploy production- ✅ Counts match, or the difference is explained by the table (dev-only packages absent under
--no-devis expected).
- ✅ Counts match, or the difference is explained by the table (dev-only packages absent under
-
Export the production schema baseline into your project DB folder for future diffs.
- ✅ A production schema baseline is saved for future comparison.
3. Capture server-side changes — ServerSync (P4.5)
Section titled “3. Capture server-side changes — ServerSync (P4.5)”The web installer and first-run writes create files on the production server that aren’t in git yet (the installed marker, generated config, vendor-published assets). Capture them back — but production capture is far more dangerous than staging, so review every change by hand.
-
Audit
clear_paths↔GIT_ONLY_PATHSsymmetry first — and STOP on any gap.clear_pathsindeploy.phpdrifts over time (AI configs, docs, templates). If the production ServerSync workflow’sGIT_ONLY_PATHSfell behind, the capture will propose deleting git-only files — and on production those deletions go live. Run the audit and do not trigger the capture until it passes.Terminal window CLEAR_PATHS=$(awk '/add\(.clear_paths./ { capture=1; next }capture && /\]\);/ { capture=0 }capture { print }' deploy.php | grep -oE "'[^']+'" | tr -d "'" | sort -u)parse_git_only_paths() {awk '/^[[:space:]]*GIT_ONLY_PATHS:/ { capture=1; next }capture && /^[[:space:]]*$/ { capture=0 }capture && /^[[:space:]]*#/ { next }capture && /^[[:space:]]*[A-Z_]+:/ { capture=0 }capture { print }' "$1" | tr ' ' '\n' | grep -v '^[[:space:]]*$' | grep -v '^>-$' | sort -u}PROTECTED=$(parse_git_only_paths .github/workflows/server-sync-production.yml)MISSING=$(comm -23 <(echo "$CLEAR_PATHS") <(echo "$PROTECTED"))if [ -n "$MISSING" ]; thenecho "STOP — these clear_paths are NOT protected by GIT_ONLY_PATHS in server-sync-production.yml:"echo "$MISSING" | sed 's/^/ /'echo "Add them to GIT_ONLY_PATHS, commit, merge to the default branch, then re-run."elseecho "OK — every clear_path is protected; safe to trigger production ServerSync"fi# Expected: "OK — every clear_path is protected ..." — fix and re-run until it does- ✅ The audit prints
OK; noclear_pathsentry is unprotected (or the gap was added toGIT_ONLY_PATHS, merged, and the audit re-run until clean).
- ✅ The audit prints
-
Run the capture against production, exactly as in Phase 5 · ServerSync capture but pointed at the production alias — it opens a PR rather than committing directly.
- ✅ A ServerSync PR is open with the production-side file changes for review (or there were no server-side changes to capture).
-
Review the diff file-by-file. Approve additions/edits that belong in git; reject anything that would delete live state or re-pull a
clear_pathsfile. Re-run theclear_paths↔GIT_ONLY_PATHSsymmetry audit before merging.- ✅ Every
D(deleted) file is individually justified, the symmetry audit passes, and only intended changes merge.
- ✅ Every
4. Sync branches & tag the release (P4)
Section titled “4. Sync branches & tag the release (P4)”Bring every branch back to a shared base, then stamp an immutable marker.
-
Confirm no branch has diverged, then fast-forward. Check each branch is an ancestor of
productionfirst —--ff-onlywill fail loudly on divergence, but the precheck tells you which branch and lets you stop before touching any.Terminal window git fetch originfor B in main staging develop; dogit merge-base --is-ancestor "$B" production \&& echo "$B OK — fast-forwardable" \|| echo "$B DIVERGED — investigate before syncing"done# Expected: all three print "OK — fast-forwardable"; STOP on any "DIVERGED"Only once all three report
OK, fast-forward each and push:Terminal window git checkout main && git merge production --ff-only && git push origin maingit checkout staging && git merge production --ff-only && git push origin staginggit checkout develop && git merge production --ff-only && git push origin develop# Expected: each branch fast-forwards to production and pushes cleanly- ✅ The precheck shows all three branches
OK, andmain,staging,developall point atproduction.
- ✅ The precheck shows all three branches
-
Tag the release on
productionand push the tag — this is your rollback and audit anchor.Terminal window git checkout productiongit tag -a v${VERSION} -m "Release v${VERSION}"git push origin v${VERSION}# Expected: the annotated tag is created and pushed to the remote- ✅
v${VERSION}is tagged onproductionand pushed.
- ✅
-
Finalize. Re-enable migrations in the deploy config for future deploys, update project status/docs to “live”, and return to
developwith a clean tree.- ✅ Migrations re-enabled, docs marked “live”, and
developis checked out with a clean tree.
- ✅ Migrations re-enabled, docs marked “live”, and
5. Record & snapshot the release (P7)
Section titled “5. Record & snapshot the release (P7)”Four closing tasks make the release reproducible and auditable: redeploy if code changed during hardening, record the release in the changelog, snapshot the schema as a versioned baseline, and capture the complete vendor-customization manifest.
-
Redeploy only if code changed since the P2 production deploy.
Terminal window git log production..develop --oneline -- "*.php" "*.js" "*.css" "*.blade.php" "deploy.php" "composer.json" "composer.lock"# Empty → nothing changed; the branch sync in step 4 is enough.# Commits → deploy through the chain: develop → staging (deploy staging) → production (deploy production) → main.- ✅ Output is empty (no redeploy needed), or the changed code was deployed through staging → production and
mainre-synced.
- ✅ Output is empty (no redeploy needed), or the changed code was deployed through staging → production and
-
Record the release. On
develop, move[Unreleased]items into a new dated[vX.Y.Z]section ofCHANGELOG.md(categories: Added, Changed, Fixed, Security, Configured), and setProjectCard.mdto Production: Live · Current Release: vX.Y.Z.- ✅
CHANGELOG.mdhas a dated[vX.Y.Z]section andProjectCard.mdreads “Live”.
- ✅
-
Export a versioned schema baseline so future vendor-update diffs compare against a known-good production schema.
Never pass production credentials on the CLI (
-u 'mysql://user:pass@…'lands in shell history andps). Use the same gitignored env pattern as Phase 5 · Atlas Cloud:Terminal window ssh -L 3308:127.0.0.1:3306 <production-alias> -N &# .env.atlas (gitignored — see Phase 3 optional Atlas): ATLAS_PRODUCTION_URL="mysql://USER:PASS@127.0.0.1:3308/DBNAME"# Load without printing: direnv allow OR set -a && source .env.atlas && set +a[ -n "$ATLAS_PRODUCTION_URL" ] || { echo "ABORT: set ATLAS_PRODUCTION_URL in .env.atlas (never on the command line)" >&2; exit 1; }mkdir -p Admin-Local/1-Project/3-ProjectDB/2-Snapshots/v${VERSION}-releaseatlas schema inspect -u "$ATLAS_PRODUCTION_URL" --format '{{ sql . }}' \> Admin-Local/1-Project/3-ProjectDB/2-Snapshots/v${VERSION}-release/production.sql# Non-Atlas alternative: mysqldump --no-data via the same SSH tunnel + ~/.my.cnf (Phase 4)- ✅ A
2-Snapshots/v${VERSION}-release/production.sqlbaseline is saved (and copied into1-Current/).
- ✅ A
-
Capture the vendor-customization manifest — the complete list of every app file you changed from the pristine vendor. On a first full setup this is the capstone record: it’s your vendor-update playbook later (these are the files a new vendor release can conflict with) and the audit trail of your whole customization surface.
vendor/is gitignored, so this is the app-file diff against the frozenauthor-v${VERSION}baseline created in Phase 2.Terminal window mkdir -p Admin-Local/3-Versions/1-v${VERSION}/2-Modificationsgit diff --name-status "author-v${VERSION}..HEAD" \> Admin-Local/3-Versions/1-v${VERSION}/2-Modifications/vendor-drift-v${VERSION}.txt# Expected: a list of A/M/D paths — the full set of changes from the vendor originalecho "Modified vendor files — each MUST be recorded in _CUSTOMIZATIONS.md:"git diff --name-status "author-v${VERSION}..HEAD" \| awk '$1 ~ /^M/ {print $2}' \| grep -vE '^(packages/ZajModules/|resources/vendor-customizations/|Admin-Local/)' \|| echo " (none — every edit is inside a customization dir)"- ✅ A
2-Modifications/vendor-drift-v${VERSION}.txtmanifest is saved, and every modified vendor file it lists is recorded in_CUSTOMIZATIONS.md(or wrapped inZAJ:BEGIN/ENDmarkers).
- ✅ A
Checklist
Section titled “Checklist”Do not mark this step done until every box below is checked.
- 🤖 Permissions locked — storage
775/664,.env640on production. - 🤖 No pending migrations —
migrate --pretendreports nothing unexpected. - 🤖 Schema parity verified — staging↔production diff clean, production baseline exported.
- 🔀 ServerSync reviewed (P4.5) — production ServerSync PR reviewed file-by-file (no blind deletes), symmetry audit passed — or confirmed there were no server-side changes to capture.
- 🤖 Branches synced —
main,staging,developall fast-forwarded toproduction. - 🤖 Release tagged —
v${VERSION}annotated onproductionand pushed. - 🤖 Redeploy check (P7) —
git log production..developis empty, or the changed code was redeployed through staging → production. - 🤖 Release recorded (P7) —
CHANGELOG.mdhas a dated[vX.Y.Z]section andProjectCard.mdreads “Live”. - 🤖 Schema baseline snapshotted (P7) —
2-Snapshots/v${VERSION}-release/production.sqlexported. - 🤖 Vendor-drift manifest captured (P7) —
vendor-drift-v${VERSION}.txtsaved; every modified vendor file is recorded in_CUSTOMIZATIONS.md.