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 usingdiffor 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
stagingandprodin 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
cpto 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:
This proof block must appear in chat before theFILE: index.html LOCAL HASH: abc123def456... REMOTE HASH (before): xyz789uvw012... CLOUDFRONT DIST: E1A2B3C4D5E6F7 CACHE CLEAR TIME: ~10 minutes CHANGE: Enable Stripe embedded checkout on booking formcpcommand runs. If you can't produce it, abort. - D7: Feature-Token Registry (Grep Against S3-Current). Maintain a
FEATURES.mdfile in the repo root that lists every deployed feature as a unique token and the file(s) it lives in:
Before deploying a new version, grep the current S3 prod for each token to confirm they still exist:- 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)
If a grep returns 0 (feature token missing from S3), halt and investigate. You may be about to clobber something critical.aws s3 cp s3://queenofsandiego-prod/index.html - | grep -c "HERO_JADA_FADE" - 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:
(Path depends on what was deployed;aws cloudfront create-invalidation \ --distribution-id E1A2B3C4D5E6F7 \ --paths "/index.html" "/*"/*is safe but slower.) - Route53 Zones:
queenofsandiego.comandsailjada.comhave A records aliased to their respective CloudFront distributions. DNS propagation is nearly instant