Skip to content
prod 352bb92
Browse

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:

  1. Prepare the release — Cut the changelog and version before deploying so release docs travel with the release commit.
  2. Cut the first production release — The core move: put the verified non-production code live and complete the installer.

Cut the changelog and version before deploying so release docs travel with the release commit.

  1. 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.
  2. 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].
  3. 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 internal Zaj-CHANGELOG.md only (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.
  4. Commit the changelogs alone.

    Terminal window
    git add Zaj-CHANGELOG.md CHANGELOG-PUBLIC.md
    git 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.

The core move: put the verified non-production code on the release branch, deploy the production target, and complete the installer.

  1. Promote the verified non-production branch → release branch.

    Terminal window
    NON_PROD_BRANCH="staging" # example; use the verified source branch from Zaj-PROJECT.md
    RELEASE_BRANCH="main" # setup-new release branch from Zaj-PROJECT.md
    git checkout "$RELEASE_BRANCH"
    git merge "$NON_PROD_BRANCH" --ff-only
    git 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.
  2. Stage the production .env first — generate it from the Phase 4 .env.tpl via the secrets manager, upload over an SSH alias (never raw user@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/.env is on the server at mode 640, and no rendered temp file remains locally.
  3. Create the PHP override (.user.ini) — first deploy only. Long installs and migrations exceed the default PHP max_execution_time and 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 = 300
    memory_limit = 512M
    max_input_time = 300
    upload_max_filesize = 64M
    post_max_size = 64M
    EOF
    ssh <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.ini exists in deploy/shared/ with max_execution_time = 300; the grep prints OK.
  4. Pre-deploy migration safety check (only after .env is 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).
  5. Confirm the .env has 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.
  6. Deploy — dry-run, then for real. The first deploy auto-skips migrations; the web installer creates the schema.

    Terminal window
    dep deploy production --plan
    dep 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.

  1. Run the installer (first deploy only). The /install route is blocked at two layers — .htaccess and (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 .htaccess edit alone is not enough — the WAF returns a 403 ahead 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 /install returns 200 (both .htaccess and 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 /install returns 403, and both the .htaccess rule 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 resultMeaning to check firstFix path
403 while opening.htaccess or Cloudflare WAF still blocks the routeOpen both layers, then retry
404 while openingLaravel route, rewrite layer, or wrong public symlinkWalk Laravel → Apache → WAF → symlink/perms
302 when expecting the wizardApp thinks install is complete or middleware redirectedCheck storage/installed and installer state
  1. Verify it’s live. Load /login over 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.

    • /login loads over HTTPS, human admin sign-in works, and the post-session dashboard is clean with a valid padlock.
  • Cause — PHP max_execution_time is too short for migrations / a long first install.

  • Fix — confirm .user.ini exists in deploy/shared/ with max_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.

Installed marker missing or route block not restored — restore WAF/host block immediately and verify marker/DB install flag.

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.md merged into the release branch (main in setup-new) and pushed.
  • 🤖 .env staged & clean — uploaded over the SSH alias at mode 640, placeholder check prints Clean.
  • 🤖 Deployed — dry-run plan reviewed, real dep deploy production completed.
  • 🔀 Installer done/install wizard complete, route re-blocked, returns 403; 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/login over HTTPS, human admin sign-in done, and agent post-session dashboard verification shows a clean dashboard with a valid padlock.