2 · Release & first production deploy (P1–P2)
Objective — cut the changelog and version, promote the verified non-production branch to the release branch (main in setup-new), stage the production .env, deploy the production target (dry-run then real), run the installer on the first deploy, and confirm the app is live — so release docs travel with the release commit and verified non-production code goes live cleanly.
Steps at a glance:
- Prepare the release — Cut the changelog and version before deploying so release docs travel with the release commit.
- Cut the first production release — The core move: put the verified non-production code live and complete the installer.
1. Prepare the release (P1)
Section titled “1. Prepare the release (P1)”Cut the changelog and version before deploying so release docs travel with the release commit.
-
Pick the version using SemVer — patch for fixes, minor for non-breaking features, major for breaking changes or a major vendor update.
Terminal window git tag --sort=-v:refname | head -5 # what's already released# Expected: the most recent release tags, newest first- ✅ The chosen version follows SemVer and doesn’t collide with an existing tag.
-
Update the internal changelog — move
[Unreleased]items into a dated version section, tag entries ([PUBLIC],[INTERNAL],[VENDOR],[SECURITY]), and reset[Unreleased].- ✅ The internal changelog has a dated, tagged version section and an empty
[Unreleased].
- ✅ The internal changelog has a dated, tagged version section and an empty
-
Update the public changelog — extract the
[PUBLIC]items and rewrite them as user benefits (“added caching” → “dashboard loads 2× faster”).The public changelog (and any in-app “what’s new” / release notes) is a customer-facing artifact — it must never name the underlying CodeCanyon vendor, script, or marketplace. Scrub every vendor/author token before committing:
Terminal window # Fails (non-empty) if any vendor/marketplace identifier leaked into the public changelog.grep -inE 'workdo|codecanyon|envato|piprahost|froiden|author-v[0-9]|<your vendor/script names>' CHANGELOG-PUBLIC.md \&& echo "STOP — scrub the vendor name from the public changelog (use your own product name + SemVer only)" \|| echo "Clean — no vendor identifiers in the public changelog"# Expected: "Clean — no vendor identifiers in the public changelog"Keep the vendor base and
author-v${VERSION}provenance in the internalZaj-CHANGELOG.mdonly (tagged[VENDOR]), never in the public one.- ✅ The public changelog reads as user-facing benefits, names only your own product + SemVer, and the vendor-token grep prints
Clean.
- ✅ The public changelog reads as user-facing benefits, names only your own product + SemVer, and the vendor-token grep prints
-
Commit the changelogs alone.
Terminal window git add Zaj-CHANGELOG.md CHANGELOG-PUBLIC.mdgit commit -m "Update changelogs for v${VERSION}"# Expected: one commit containing only the two changelog files- ✅ A single commit holds both changelogs and nothing else.
2. Cut the first production release (P2)
Section titled “2. Cut the first production release (P2)”The core move: put the verified non-production code on the release branch, deploy the production target, and complete the installer.
-
Promote the verified non-production branch → release branch.
Terminal window NON_PROD_BRANCH="staging" # example; use the verified source branch from Zaj-PROJECT.mdRELEASE_BRANCH="main" # setup-new release branch from Zaj-PROJECT.mdgit checkout "$RELEASE_BRANCH"git merge "$NON_PROD_BRANCH" --ff-onlygit push origin "$RELEASE_BRANCH"# Expected: verified non-production commits fast-forward onto the release branch when histories allow; otherwise resolve before merge- ✅ The release branch carries the verified non-production code and is pushed.
-
Stage the production
.envfirst — generate it from the Phase 4.env.tplvia the secrets manager, upload over an SSH alias (never rawuser@IP), and lock permissions before any migration preview that needs DB credentials.Terminal window SSH_PRODUCTION_ALIAS="<production-alias-from-Zaj-PROJECT>"DEPLOY_PATH="~/domains/<DOMAIN>/deploy"TMP_ENV="$(mktemp)"op inject -f -i .env.tpl -o "$TMP_ENV"test -s "$TMP_ENV" && wc -c "$TMP_ENV"ssh "$SSH_PRODUCTION_ALIAS" "mkdir -p '$DEPLOY_PATH/shared' && umask 027 && cat > '$DEPLOY_PATH/shared/.env'" < "$TMP_ENV"ssh "$SSH_PRODUCTION_ALIAS" "chmod 640 '$DEPLOY_PATH/shared/.env' && test -s '$DEPLOY_PATH/shared/.env' && wc -c '$DEPLOY_PATH/shared/.env'"shred -u "$TMP_ENV" 2>/dev/null || rm -f "$TMP_ENV"test ! -f "$TMP_ENV"# Expected: server shared/.env has a non-zero byte count, mode 640, and the local temp file is gone- ✅ The production
shared/.envis on the server at mode640, and no rendered temp file remains locally.
- ✅ The production
-
Create the PHP override (
.user.ini) — first deploy only. Long installs and migrations exceed the default PHPmax_execution_timeand return a 504. Write the override into the shared dir idempotently so it survives every release.Terminal window ssh <SSH_PRODUCTION_ALIAS> 'cat > ~/domains/<DOMAIN>/deploy/shared/.user.ini' <<'EOF'max_execution_time = 300memory_limit = 512Mmax_input_time = 300upload_max_filesize = 64Mpost_max_size = 64MEOFssh <SSH_PRODUCTION_ALIAS> "chmod 644 ~/domains/<DOMAIN>/deploy/shared/.user.ini"ssh <SSH_PRODUCTION_ALIAS> "grep -q '^max_execution_time = 300' ~/domains/<DOMAIN>/deploy/shared/.user.ini \&& echo 'OK — .user.ini present with 300s timeout' || echo 'STOP — .user.ini missing or wrong'"# Expected: "OK — .user.ini present with 300s timeout"- ✅
.user.iniexists indeploy/shared/withmax_execution_time = 300; the grep printsOK.
- ✅
-
Pre-deploy migration safety check (only after
.envis staged). Preview before you run anything destructive.Terminal window ssh <SSH_PRODUCTION_ALIAS> 'cd ~/domains/<DOMAIN>/deploy/current && php artisan migrate --pretend'# Expected: on a first deploy with the web installer, the installer creates tables — treat DROP noise as expected only when that path applies- ✅ The pretend output is reviewed in context (installer-first vs migrate-first deploy).
-
Confirm the
.envhas no placeholders — the deploy fails on an unfilled value, and every production value must be real (APP_DEBUG=false, live keys, strong passwords).Terminal window ssh <SSH_PRODUCTION_ALIAS> "grep -inE '_HERE|your_|CHANGE_ME|REPLACE|PLACEHOLDER|TODO|xxx|<[A-Z_]+>' ~/domains/<DOMAIN>/deploy/shared/.env" \&& echo "STOP — fix placeholders" || echo "Clean"# Expected: "Clean" — no placeholder tokens remain (does not false-positive on https:// URLs)- ✅ The check prints
Clean; no placeholder values remain.
- ✅ The check prints
-
Deploy — dry-run, then for real. The first deploy auto-skips migrations; the web installer creates the schema.
Terminal window dep deploy production --plandep deploy production# Expected: the plan lists steps without applying them, then the real deploy completes- ✅ The dry-run plan looks right and the real deploy completes without errors.
The next two substeps are browser actions — completing the installer wizard and verifying the live app in a browser.
-
Run the installer (first deploy only). The
/installroute is blocked at two layers —.htaccessand (if configured) a Cloudflare WAF rule. Unblock both, complete the wizard, then re-block both and verify.a. Unblock
/install(agent —.htaccess+ user — WAF):Terminal window # .htaccess layer (idempotent — only flips an un-commented rule)ssh <SSH_PRODUCTION_ALIAS> 'cd ~/domains/<DOMAIN>/deploy/current/public \&& sed -i "s/^RewriteRule \^install/# RewriteRule ^install/" .htaccess && echo "/install unblocked in .htaccess"'# Reset the install lock so the wizard can run (auto-recreated on finish).ssh <SSH_PRODUCTION_ALIAS> "rm -f ~/domains/<DOMAIN>/deploy/shared/storage/installed"👤 WAF layer (browser): in the Cloudflare dashboard, toggle the WAF rule “Block install and update routes” OFF. The
.htaccessedit alone is not enough — the WAF returns a403ahead of the origin.Verify it is reachable before opening the wizard:
Terminal window curl -sI https://<DOMAIN>/install | head -1# Expected: HTTP/2 200 (if still 403, the Cloudflare WAF rule is still ON)- ✅
curl -sI /installreturns200(both.htaccessand the WAF rule are off).
b. Complete the wizard (🔀 browser automation + human secrets): the agent drives requirements, permissions, non-secret environment fields, production URL, Install, and Finish when browser automation is available. The human enters production DB password, admin password, account-creation fields, or any other credential/secret field, then the agent resumes.
c. Re-block both layers (agent —
.htaccess+ user — WAF):Terminal window ssh <SSH_PRODUCTION_ALIAS> 'cd ~/domains/<DOMAIN>/deploy/current/public \&& sed -i "s/^# RewriteRule \^install/RewriteRule ^install/" .htaccess && echo "/install re-blocked in .htaccess"'curl -sI https://<DOMAIN>/install | head -1# Expected: HTTP/2 403👤 WAF layer: toggle the Cloudflare WAF rule “Block install and update routes” back ON.
- ✅
curl -sI /installreturns403, and both the.htaccessrule and the Cloudflare WAF rule are re-enabled.
- ✅
Installer status codes are fast to misread. If /install returns anything other than the expected open-then-closed sequence, use HTTP error triage before changing code:
/install result | Meaning to check first | Fix path |
|---|---|---|
403 while opening | .htaccess or Cloudflare WAF still blocks the route | Open both layers, then retry |
404 while opening | Laravel route, rewrite layer, or wrong public symlink | Walk Laravel → Apache → WAF → symlink/perms |
302 when expecting the wizard | App thinks install is complete or middleware redirected | Check storage/installed and installer state |
-
Verify it’s live. Load
/loginover HTTPS, have the human sign in as admin once, then confirm the post-session dashboard loads with no stack traces, no debug bar, and a valid SSL padlock.- ✅
/loginloads over HTTPS, human admin sign-in works, and the post-session dashboard is clean with a valid padlock.
- ✅
Troubleshooting
Section titled “Troubleshooting”504 timeout during deploy or installer
Section titled “504 timeout during deploy or installer”-
Cause — PHP
max_execution_timeis too short for migrations / a long first install. -
Fix — confirm
.user.iniexists indeploy/shared/withmax_execution_time = 300(step 3 above). If missing, write it and retry:Terminal window ssh <SSH_PRODUCTION_ALIAS> "test -f ~/domains/<DOMAIN>/deploy/shared/.user.ini \&& grep '^max_execution_time' ~/domains/<DOMAIN>/deploy/shared/.user.ini \|| echo 'MISSING — create .user.ini (see step 3)'"# Expected: "max_execution_time = 300" — anything else means recreate it
“Access denied” database error during the installer
Section titled ““Access denied” database error during the installer”-
Cause — the config cache holds stale database credentials.
-
Fix — clear the config cache, then retry the installer:
Terminal window ssh <SSH_PRODUCTION_ALIAS> "cd ~/domains/<DOMAIN>/deploy/current && php artisan config:clear"# Expected: "Configuration cache cleared!"
/install stays blocked while expected open
Section titled “/install stays blocked while expected open”Both .htaccess and Cloudflare WAF may block it — temporarily open both, finish the installer, then re-block both and confirm curl -sI returns 403.
/install stays open after completion
Section titled “/install stays open after completion”Installed marker missing or route block not restored — restore WAF/host block immediately and verify marker/DB install flag.
Checklist
Section titled “Checklist”Do not mark this step done until every box below is checked.
- 🤖 Release prepared (P1) — version picked, internal + public changelogs updated (public changelog vendor-token grep prints
Clean), committed alone. - 🤖 Release branch promoted — the selected non-production source branch from
Zaj-PROJECT.mdmerged into the release branch (mainin setup-new) and pushed. - 🤖
.envstaged & clean — uploaded over the SSH alias at mode640, placeholder check printsClean. - 🤖 Deployed — dry-run plan reviewed, real
dep deploy productioncompleted. - 🔀 Installer done —
/installwizard complete, route re-blocked, returns403; human-owned credentials are saved to the vault and non-secret wizard evidence is captured by the agent when browser automation is available. - 🔀 Live verified —
/loginover HTTPS, human admin sign-in done, and agent post-session dashboard verification shows a clean dashboard with a valid padlock.