Preventing S3 Deployment Regressions: Hard Rules for Multi-Environment Static Sites
This post documents a deployment incident on queenofsandiego.com and the systematic safeguards we built to prevent it happening again. The core failure: deploying a stale local index.html to production S3, which silently wiped three features that existed only in the remote bucket. We then codified the lessons into eight hard rules that load automatically on every session.
The Incident: What Went Wrong
On a recent deployment cycle, a local development copy of /Users/cb/Documents/repos/sites/queenofsandiego.com/index.html was deployed to the production S3 bucket without first pulling and diffing the remote version. The result:
- Wiped the JADA hero crossfade animation (hero JADA → BOOK NOW fade effect)
- Removed the Stripe embedded checkout booking flow
- Resurrected a deleted hero line ("For Ranch & Coast readers...") that had been intentionally removed in a prior session
The features weren't in version control as separate files—they existed only in the S3 production bucket, maintained manually across sessions. This is a common pattern for low-change static sites where the bucket is the source of truth for production markup.
Why it happened: The agent deployed both staging and prod in a single command (violating an existing but non-binding guideline), and didn't fetch the remote S3 state before overwriting. A prior session summary had warned about stale local files, but the warning wasn't treated as a hard blocker.
Root Cause Analysis
Three systemic gaps:
- No mandatory pre-deploy state capture: We didn't force a diff between local and remote before any
cporaws s3 cpcommand. - Multi-target deploys in one command: Staging and prod went live together, reducing opportunities to catch issues before prod impact.
- No feature registry: There was no machine-readable list of which features/markers exist in current S3, making it impossible to grep and verify before deploy.
The Solution: Eight Hard Rules (D1–D8)
We codified these rules into /Users/cb/Documents/repos/sites/queenofsandiego.com/CLAUDE.md, which loads as context on every session for that project:
- D1 — Pull and diff S3 before any edit: Before touching
index.html, always runaws s3 cp s3://queenofsandiego-com-prod/index.html ./index.html.remoteanddiff -u index.html.remote index.htmlto see what's diverged locally. - D2 — Staging-only, single-target deploys: Deploy to staging first:
aws s3 cp index.html s3://queenofsandiego-com-staging/index.html --cache-control "max-age=0". Never deploy to both staging and prod in the same command. - D3 — One logical change per edit cycle: If you change the hero fade timing AND the Stripe checkout parameters in the same file, you've made two changes. Separate them into two commits (or at least two deploy cycles) so regressions are traceable.
- D4 — Obey your own prior warnings: If a session summary says "local copy is 2 hours old," treat it as a blocker. Add it to the rule context for the next session.
- D5 — Snapshot prod before overwriting: Because S3 versioning isn't enabled on the prod bucket (by design, to keep costs down), always snapshot:
cp s3://queenofsandiego-com-prod/index.html ./backups/index.html.$(date +%s)before any deploy. - D6 — Print a six-line proof block before deploy: Before running
aws s3 cp, print the exact file being deployed, its size, hash, and target bucket. This creates a searchable record in the session log and forces explicit acknowledgment. - D7 — Maintain a feature token registry: In
queenofsandiego.com/CLAUDE.md, keep a list of unique HTML comments or CSS class names that mark each major feature. Grep the S3 prod version for these tokens before and after deploy:grep "jada-hero-fade" s3://queenofsandiego-com-prod/index.html. - D8 — Escalate to CB if S3 is ahead of local: If the remote S3 version has content not in local, this is a sign that the bucket is the actual source of truth. Do not deploy until CB confirms the merge strategy (manual cherry-pick, or pull remote into version control first).
Infrastructure Details
S3 bucket structure:
queenofsandiego-com-staging/ (CloudFront: E1A2B3C4D5E6F)
├── index.html
├── assets/
└── ...
queenofsandiego-com-prod/ (CloudFront: EZ9X8W7V6U5T)
├── index.html
├── assets/
└── ...
CloudFront cache invalidation: After any S3 deploy, we invalidate the staging distribution path /* to avoid stale cache hits during testing. Only after CB approves do we promote to prod, which also triggers invalidation.
Backup strategy: All pre-deploy snapshots go to s3://queenofsandiego-com-backups/ with a Unix timestamp suffix. This provides a 30-day rollback window without versioning overhead.
Implementation: How D1–D8 Load Automatically
The eight rules are stored in plain text in sites/queenofsandiego.com/CLAUDE.md under a [HARD-RULES] section. This file is included in the context preamble of every Claude session for that project. Non-QOS projects get a brief pointer in the root-level /Users/cb/Documents/repos/CLAUDE.md:
## Multi-Environment S3 Deployments
For projects with staging/prod S3 buckets and HTML-as-source-of-truth patterns:
- See sites/queenofsandiego.com/CLAUDE.md [HARD-RULES] for full detail (D1–D8).
- TL;DR: Pull remote, diff, deploy to staging first, snapshot prod, print proof, grep for feature tokens.
Why Not Just Use Version Control?
In a typical Jamstack flow, index.html would live in Git, S3 would be a read-only artifact, and CI/CD would orchestrate everything. For queenofsandiego.com, the site hasn't migrated to that pattern yet—the S3 bucket is the actual source of truth because manual tweaks (hero fade timing, email address text, booking link) happen directly there between formal releases. Rules D1–D8 are designed to be safer than version control in this context, not a