1 · Ship — code → test → deploy
Objective — get a finished local change live the same safe way every time: pick small-vs-big, work on develop, pass the database safety gate, then deploy through the selected non-production target(s) from Zaj-PROJECT.md before production and tag the release.
Background
Section titled “Background”Shipping is dangerous in exactly two ways: a migration can drop data, and a bad merge can take the site down. This workflow removes both risks by making the path identical every time — a small change and a big feature flow through the same gate, so you never improvise on the day something breaks.
Read Zaj-PROJECT.md before you start. Use its environment matrix for the real non-production branch, deploy target, URL, SSH alias, PHP binary, DB engine/collation, and production target. If this ship changes composer.json, composer.lock, Node build tooling, DB driver/collation, deploy paths, or server runtime facts, rerun the parity checks and update Zaj-PROJECT.md plus .tool-versions in the same change.
For the command-only card (Patterns A–D, quick smoke checks), use the Code → test → ship runbook.
The default branch model is develop → selected non-production branch (often staging) → production, then mirror production into main as a backup. Zaj-PROJECT.md can name several non-production rows (staging-primary, qa, uat, client-demo, etc.); deploy through the row selected for this release.
NON_PROD_BRANCH="staging" # example; source branch from Zaj-PROJECT.mdDEPLOY_TARGET="staging" # example; Deployer target from Zaj-PROJECT.mdNON_PROD_URL="https://nonprod.example.com"# Expected: values match the environment row selected for this shipflowchart LR D["develop<br/>(work here)"] --> S["non-production<br/>(selected test server)"] S --> P["production<br/>(live)"] P --> M["main<br/>(backup)"]0. Sync the local DB before DB-dependent UI work
Section titled “0. Sync the local DB before DB-dependent UI work”Use this pre-work only when the change depends on database-backed UI state — landing pages, settings, plans, seeded content, or any admin-panel value that local cannot reproduce from code alone.
1. Decide small or big
Section titled “1. Decide small or big”Before touching code, size the change. A one-line fix and a multi-file feature take different paths into git — getting this right keeps your history clean and your develop branch deployable.
-
Classify the change against the size table.
Size Definition Action Small 1–3 commits, simple change Work directly on developBig 4+ commits, multi-file feature Create a temporary branch - ✅ You know whether you’re working directly on
developor on aNewFeature/...branch.
- ✅ You know whether you’re working directly on
2. Confirm the starting state
Section titled “2. Confirm the starting state”Stale or out-of-sync branches are the most common cause of a confusing deploy. Verify you’re on develop, up to date, and that staging/production carry nothing develop doesn’t.
-
Pull develop and confirm the branches are synced.
Terminal window git checkout develop && git pull origin developgit log --oneline "develop..$NON_PROD_BRANCH" # Expected: empty (no commits)git log --oneline "$NON_PROD_BRANCH..production" # Expected: empty (no commits)# Expected: on develop, up to date, both log commands print nothing- ✅ You’re on
develop, up to date, and both range logs are empty.
- ✅ You’re on
If a server had admin-panel changes that aren’t in git yet, capture them with Server sync before you ship — otherwise this deploy can overwrite them.
3. Do the work and commit
Section titled “3. Do the work and commit”Make the change, test it locally, and commit with the right type prefix so the history reads as a log of what kind of change shipped when.
-
For a big change, branch first. Skip this for a small change on
develop.Terminal window git checkout -b NewFeature/descriptive-name# Expected: switched to a new branch named NewFeature/descriptive-name- ✅ Big changes live on their own branch; small changes stay on
develop.
- ✅ Big changes live on their own branch; small changes stay on
-
Make the change, test locally, then commit with a typed message. Test in the browser at your local site (e.g.
PROJECT.test) before committing.Terminal window git add [files]git commit -m "🔨 🟪 T3 Add-Feature: [description]"# Bug fix instead: 🔧 🟦 T2 Fix-Bug: [description]# Config change: 🔧 🟦 T2 Setup-Config: [description]git push origin [branch]# Expected: the commit lands and pushes to its branch- ✅ The change works locally and is committed with a type prefix that matches it.
4. Clear the database safety gate
Section titled “4. Clear the database safety gate”This is the non-negotiable gate before any deploy that might include migrations. A migration that drops a table or column can destroy customer data — so you inspect every pending migration and let Atlas lint flag the dangerous ones before they ever reach a server.
-
List pending migrations, then dry-run them to read the SQL.
Terminal window php artisan migrate:status # which migrations are pendingphp artisan migrate --pretend # the SQL each would run — no DB change# Expected: a clear list; if nothing is pending, the gate is already clear- ✅ You can see exactly what SQL would run — or confirm nothing is pending.
-
Run Atlas lint and STOP on a destructive finding. Atlas flags dangerous operations by code; the two that block a deploy are
DS102(DROP TABLE) andDS103(DROP COLUMN).Terminal window atlas migrate lint --env local --latest 1# Expected: no DS102 / DS103. STOP if either appears — back up the DB and get explicit approval first.- ✅ Lint is clean, or a destructive finding was handled (database backed up, explicit approval obtained, tested on staging first).
-
Back up the database before any approved destructive op — rollback net first. A
DROP,MODIFY COLUMN, or anymigrate:fresh(which drops every table before re-running) must be preceded by a verified dump you can restore from.Terminal window # Herd Pro ships MariaDB → the dump tool is mariadb-dump (mysqldump was renamed; alias it if you prefer).mariadb-dump -u [DB_USER] -p [DB_NAME] > "backup-$(date +%Y%m%d-%H%M%S).sql" # or: mysqldump ...ls -lh backup-*.sql # Expected: a non-empty .sql dump exists before the migration runs- ✅ A verified, non-empty backup exists before the destructive migration runs — or there is no destructive op to guard.
5. Deploy develop → non-production → production
Section titled “5. Deploy develop → non-production → production”With the gate clear, run the standard deploy sequence. Each environment gets a deploy, then a human test, before the next one — the selected non-production target catches what local missed; production is verified the moment it’s live.
-
For a big change, merge the feature branch back into develop first. Skip for small changes already on
develop.Terminal window git checkout develop && git merge NewFeature/descriptive-name# Expected: the feature branch is merged into develop- ✅ All the work is on
developbefore promotion begins.
- ✅ All the work is on
-
Push develop, then promote to the selected non-production branch and deploy.
Terminal window git push origin developgit checkout "$NON_PROD_BRANCH" && git merge develop && git push origin "$NON_PROD_BRANCH"dep deploy "$DEPLOY_TARGET"# Expected: zero-downtime release completes on the selected non-production server- ✅ Non-production deployed cleanly — then 👤 test on
$NON_PROD_URLand confirm the change works.
- ✅ Non-production deployed cleanly — then 👤 test on
-
Promote the tested non-production branch to production and deploy. 👤 USER step — production is human-gated. Only after the selected non-production target tested clean. Who runs this — 👤 you (production is human-gated). The agent prepares and verifies everything up to this point; a human runs the actual production push + deploy. Agents are correctly fenced from production deploys (real-user / real-money actions), the same way branch-protection and live-payment actions gate them.
Terminal window git checkout production && git merge "$NON_PROD_BRANCH" && git push origin productiondep deploy production# Expected: zero-downtime release completes on the production server- ✅ Production deployed (by 👤 you) — then 👤 test critical paths on production (login, checkout, the changed feature).
6. Tag and finalize
Section titled “6. Tag and finalize”A live deploy isn’t done until it’s labelled and backed up. Tag the version, mirror production into main, and return to develop so the next change starts from a clean, deployable base.
-
Tag the release, update main, and return to develop. Version scheme: first update after a major is
vX.X.X-a; subsequent ones-b,-c; a significant change gets a newvX.X.Y.Terminal window git tag -a v[VERSION] -m "[Description]"git push origin v[VERSION]git checkout main && git merge production && git push origin maingit checkout develop# Expected: tag pushed, main mirrors production, you're back on develop- ✅ The version is tagged,
mainmatchesproduction, and you’re ondevelopagain. UpdateZaj-CHANGELOG.mdif the change was significant; delete the temp branch only with user confirmation.
- ✅ The version is tagged,
Checklist
Section titled “Checklist”Do not mark this step done until every box below is checked.
- 🤖 Branches synced — on
develop, up to date;develop..stagingandstaging..productionboth empty. - 🔀 Local DB Sync handled when needed — DB-dependent UI work used a staging → Herd sync first, or this gate was explicitly marked N/A.
- 🤖 Committed with type — the change is committed with the right prefix (
T3 Add-Feature,T2 Fix-Bug, etc.). - 🔀 DB gate cleared —
migrate:statusreviewed, Atlas lint clean (or destructive finding handled: verified DB dump taken first, explicit approval, tested on staging). No DS102/DS103 shipped unguarded; nomigrate:fresh/reset/wipeagainst staging or production. - 👤 Non-production tested — the change verified on the selected non-production URL before promoting.
- 👤 Production deployed + tested — 👤 you ran the production push +
dep deploy production(production is human-gated), then verified critical paths on production after deploy. - 🤖 Tagged + main updated — version tag pushed,
mainmirrorsproduction, back ondevelop.