Preventing S3 Deployment Regressions: Hard Rules for Multi-Environment Static Sites
Last session, a stale local index.html was deployed to production S3, silently wiping three working features: a hero crossfade animation, Stripe embedded checkout, and reversing a prior deletion. The regression went undetected because the deployment process lacked guardrails. This post documents the technical failure, the hard rules we implemented to prevent it, and how they generalize to any multi-environment S3 workflow.
What Happened: The Failure Pattern
- Root cause: Local
/Users/cb/Documents/repos/sites/queenofsandiego.com/index.htmlwas stale (missing three weeks of S3 edits). A Sonnet session copied it to both S3 staging and prod in a single command, overwriting the current prod version. - Detection lag: The stale file was never diffed against S3 before upload. No proof was printed to chat. No feature registry was consulted.
- Scope creep: Two environment targets (staging + prod) were deployed in one logical operation, violating the staging-first principle already documented but not enforced.
- Lost data:
- Hero JADA → BOOK NOW CSS fade (crossfade on page load, hours of animation work)
- Stripe embedded checkout integration (booking flow dependency)
- Prior deletion of "For Ranch & Coast readers..." hero line was undone
Why This Happened: The Infrastructure Gap
The sailjada.com site is 3,650 lines of vanilla HTML/CSS/JavaScript served from S3 (CloudFront distribution d1234xxxxx.cloudfront.net), with edits made both locally (via editor) and directly to S3 (via console or deployment scripts). Without versioning, a local file can silently become the source of truth and overwrite newer remote state.
The deployment process had no steps that:
- Pulled current S3 state before editing
- Diffed local against remote
- Printed proof (hash, line count, critical feature tokens) before upload
- Enforced single-target, single-logical-change deploys
- Escalated when S3 was ahead of local
The Solution: Eight Hard Rules (D1–D8)
We embedded these rules into /Users/cb/Documents/repos/sites/queenofsandiego.com/CLAUDE.md, which auto-loads at the start of every QOS development session:
D1: Always Pull S3 State Before Editing
aws s3 cp s3://queenofsandiego-com/index.html ./index.html.s3-current --region us-west-2
diff -u index.html.s3-current index.html | head -50
Why: Prevents deploying stale local copies. Exposes drift immediately.
D2: Staging First, Single Target, One Logical Change Per Deploy
Never deploy staging and prod in the same command. Never combine unrelated edits (e.g., pricing + hero copy) in one deploy. Tag each deploy with a brief reason.
Why: Isolates regressions to one environment. Makes rollback trivial. Audit trail ties changes to decisions.
D3: Print a Six-Line Proof Block Before Every cp or sync
# Before: cp index.html s3://queenofsandiego-com/index.html
# Print this to chat:
echo "=== DEPLOY PROOF ==="
echo "File: index.html | Size: $(wc -c < index.html) bytes"
echo "MD5: $(md5sum index.html | cut -d' ' -f1)"
echo "Line count: $(wc -l < index.html)"
grep -c "BOOK NOW\|Stripe\|hero.*fade" index.html || echo "TOKENS: NONE FOUND"
echo "Target: s3://queenofsandiego-com/staging/index.html"
echo "=== END PROOF ==="
Why: Creates an immutable record in chat. Prevents finger-slip deploys. Makes it obvious if tokens are missing.
D4: Maintain a Feature Token Registry
Before deploying, grep the local file for critical tokens. Store these in a comment block at the top of CLAUDE.md:
# FEATURE TOKENS (grep before deploy):
# - "crossfade-hero" (CSS animation class, hero JADA→BOOK NOW)
# - "Stripe.redirectToCheckout" (booking flow via embedded checkout)
# - "const KEELY50 =" (promo code Keely Hoyt referral logic)
# - "bookings@sailjada.com" (Gmail Send-As alias for GAS)
# - "orphan-sweep" (nightly GAS trigger to close abandoned bookings)
Why: Quick visual check. Catches deletions before they ship. Grep is faster than manual review of 3,650 lines.
D5: Snapshot Prod Before Overwriting
aws s3 cp s3://queenofsandiego-com/index.html s3://queenofsandiego-com/.backup/index.html.$(date +%s) --region us-west-2
Why: S3 has no built-in versioning on this bucket. Backups live in .backup/ for 7 days. Manual emergency restore takes 30 seconds.
D6: Obey Prior Session Warnings
If a prior session documented a known risk (e.g., "local index.html is stale, pull S3 before edit"), treat it as a blocker. Escalate to CB before proceeding.
Why: Sessions are continuations. Ignoring prior context is how we regressed three features.
D7: One File Per Logical Change
If editing index.html for pricing, don't also edit booking-form.js in the same deploy. If both need to ship, make two staging deployments, review both, then promote both to prod.
Why: Isolates which edit caused a regression. Simplifies code review and rollback.
D8: Escalate to CB if S3 Is Ahead of Local
If the diff shows S3 has newer code (modification time or content) than local, stop. Ask CB which version should be canonical. Never overwrite remote with stale local.
Why: Prevents silent data loss when multiple people edit (CB via console, agent via script).
Infrastructure Setup
- S3 bucket:
queenofsandiego-com(us-west-2) - CloudFront distribution:
d1234xxxxx.cloudfront.net(origin: S3, index.html caching TTL 300s) - Backup location:
s3://quee