```html

Preventing S3 Deployment Regressions: Hard Rules for Multi-Environment CI/CD Safety

Over a recent development session, a stale local index.html was deployed to production S3, wiping three previously-working features on queenofsandiego.com: the JADA→BOOK NOW hero crossfade, the Stripe embedded checkout booking flow, and correctly removing (but then resurrecting) a deleted "Ranch & Coast" hero line. The root cause wasn't a technical failure—it was process failure: the deployment lacked validation gates between local state and remote state, and ignored prior session warnings about stale local files.

This post documents the hard rules we've encoded to prevent this class of regression, why each rule exists, and how to operationalize them for multi-site, multi-environment deployments.

The Incident: What Happened

The deployment command deployed both staging and prod in a single operation, violating a prior rule that staging must be promoted separately. The local index.html had not been pulled or diffed against S3 before editing, so the session was unaware that production had newer code. When the stale local file was pushed, it overwrote the newer remote state with no snapshot, no dry-run output, and no proof-of-change validation.

The result: three feature regressions that required manual recovery and a full re-deploy cycle.

The Solution: Eight Hard Rules (D1–D8)

We've codified these rules into /Users/cb/Documents/repos/sites/queenofsandiego.com/CLAUDE.md, which auto-loads on every session for that site. The rules are:

  • D1: Pull and Diff Before Edit. Before touching any HTML, CSS, or JS file that deploys to S3, run aws s3 cp s3://<bucket>/<key> ./ and compare the remote version against your local working copy using diff or visual inspection. Print the diff in chat before proceeding. This reveals whether local is stale, newer, or divergent.
  • D2: Single-Target, Staging-First Deploys. Never deploy to staging and prod in the same command or loop. Always deploy to staging first, get explicit approval from the site owner (CB), then deploy to prod as a separate, clearly-logged operation. This prevents accidental production overwrites and gives a manual review gate.
  • D3: One Logical Change Per Deployment. Each deploy should map to one user-visible feature or bug fix. If you're touching the hero fade CSS, the Stripe checkout flow, and email templates in the same session, make three separate deployments to staging, three separate reviews, three separate prod promotes. This isolates blast radius and makes rollback surgical.
  • D4: Obey Prior Session-Summary Warnings. At the end of every session, a summary is written to memory files (/Users/cb/.claude/projects/*/memory/) that flags known risks: stale files, pending approvals, blockers, and infrastructure debt. Before any deploy in a follow-up session, re-read the prior summary. If it says "local index.html is 2 commits behind S3 prod," that is a blocker until resolved.
  • D5: Snapshot Prod Before Overwriting (No S3 Versioning Fallback). Before any cp to S3, download the current prod version and store it locally with a timestamp: aws s3 cp s3://<bucket>/<key> ./backups/<key>-$(date +%s).bak. S3 versioning may not be enabled; a local snapshot is your only safety net. Store these in a ./backups/ directory at the repo root and commit them if the deployment succeeds.
  • D6: Proof Block Before Every cp Command. Before deploying, print a six-line proof block in chat showing: (1) the file being deployed, (2) local file hash (MD5 or SHA256), (3) remote file hash before deploy, (4) the CloudFront distribution ID that will be invalidated, (5) the expected time to cache clear (typically 5–15 minutes for sailjada CloudFront), and (6) a human-readable summary of what feature this deploy enables or fixes. Example:
    FILE: index.html
    LOCAL HASH: abc123def456...
    REMOTE HASH (before): xyz789uvw012...
    CLOUDFRONT DIST: E1A2B3C4D5E6F7
    CACHE CLEAR TIME: ~10 minutes
    CHANGE: Enable Stripe embedded checkout on booking form
    
    This proof block must appear in chat before the cp command runs. If you can't produce it, abort.
  • D7: Feature-Token Registry (Grep Against S3-Current). Maintain a FEATURES.md file in the repo root that lists every deployed feature as a unique token and the file(s) it lives in:
    - HERO_JADA_FADE: index.html, line 245–289 (CSS keyframes + fade trigger)
    - STRIPE_EMBEDDED: index.html, line 1450–1550 (Stripe.js load + form bind)
    - RANCH_COAST_REMOVED: index.html (line 1850 deleted, commit abc123)
    
    Before deploying a new version, grep the current S3 prod for each token to confirm they still exist:
    aws s3 cp s3://queenofsandiego-prod/index.html - | grep -c "HERO_JADA_FADE"
    
    If a grep returns 0 (feature token missing from S3), halt and investigate. You may be about to clobber something critical.
  • D8: Escalate to CB If S3 is Ahead of Local. If the remote version has commits or changes that local doesn't have, this is a blocker. Do not deploy. Instead, escalate to CB with a clear message: "S3 prod is 2 commits ahead of local. Local needs to sync or be explicitly reverted before deploying feature X." This forces a human decision about which state is canonical.

Infrastructure: CloudFront and S3 Setup

The queenofsandiego.com and sailjada.com deployments use:

  • S3 Buckets:
    • queenofsandiego-prod (production HTML, CSS, JS, assets)
    • queenofsandiego-staging (staging environment, separate bucket)
    • sailjada-prod (sailjada production)
    • sailjada-staging (sailjada staging)
  • CloudFront Distributions: Each S3 bucket has a CloudFront distribution with a 5-minute default TTL. After deploying to S3, the distribution ID must be invalidated to purge edge caches:
    aws cloudfront create-invalidation \
      --distribution-id E1A2B3C4D5E6F7 \
      --paths "/index.html" "/*"
    
    (Path depends on what was deployed; /* is safe but slower.)
  • Route53 Zones: queenofsandiego.com and sailjada.com have A records aliased to their respective CloudFront distributions. DNS propagation is nearly instant