Preventing S3 Stale-File Regressions: Hard Rules for CloudFront-Backed Multi-Environment Deploys
What Happened
A recent session deploying to queenofsandiego.com (S3 bucket: qos-prod-site, CloudFront distribution: E2ABCD1234XYZ) overwrote a production index.html with a stale local copy, inadvertently reverting three working features:
- Hero section JADA → BOOK NOW CSS crossfade animation
- Stripe embedded checkout booking flow integration
- Removal of the previously-deleted "For Ranch & Coast readers..." hero line
The root cause: local development files were not diffed against live S3 before deployment, and two environments (staging and prod) were promoted in the same cp command, violating isolation rules already documented in prior session summaries.
Why This Matters
S3-backed sites with CloudFront caching are inherently asymmetric: local → S3 deploys are destructive, one-way operations. Without a pull-then-diff workflow, you can silently lose weeks of production refinements in seconds. CloudFront invalidation (distribution E2ABCD1234XYZ) clears cache but cannot restore deleted S3 objects—you must recover from versioning (if enabled) or backups.
In this case:
- S3 versioning was disabled (no automatic rollback)
- The local file was 3,650 lines (manual diff-by-eye impossible)
- CloudFront had 24-hour TTL on index.html (users saw stale content for hours)
The Hard Rules: Eight Safeguards for Destructive Deploys
To prevent this recurring, eight enforcement rules have been added to the site-specific CLAUDE.md (/Users/cb/Documents/repos/sites/queenofsandiego.com/CLAUDE.md) and will auto-load on every QOS session:
D1: Always Pull S3 Before Editing
Before modifying any file destined for S3, pull the current live version and md5-hash it:
aws s3 cp s3://qos-prod-site/index.html ./index.html.live
md5 ./index.html.live
# Compare size and hash against local ./index.html
Why: Detects stale local copies immediately. CloudFront caching means S3 is the single source of truth.
D2: Mandatory Diff Before Any S3 cp
Generate a detailed diff of your changes before uploading:
diff -u ./index.html.live ./index.html | head -100
# Review high-level: count line insertions/deletions
# If deletion count >> insertion count, escalate to CB
Why: A deletion-heavy diff signals you may be overwriting, not updating. Forces conscious decision-making.
D3: Single-Target, Single-Environment Deploys Only
Never combine staging and prod in one command. Deploy to staging first, always:
# CORRECT: staging first
aws s3 cp ./index.html s3://qos-staging-site/index.html
# ... wait for CB review ...
# THEN: promote to prod, separate command
aws s3 cp ./index.html s3://qos-prod-site/index.html
# WRONG: both at once
aws s3 cp ./index.html s3://qos-staging-site/index.html s3://qos-prod-site/index.html
Why: Staging is your canary. If something breaks, it breaks in staging, not prod. Separating commands makes rollback granular.
D4: One Logical Feature Per Deploy
If you're touching hero CSS, hero CSS only. If adding Stripe, don't also fix footer links:
# BAD: multiple unrelated changes bundled
git commit -m "fix hero, update footer, remove old booking link"
# GOOD: one concern per commit, one per deploy
git commit -m "hero: add JADA → BOOK NOW crossfade"
git commit -m "footer: update copyright year"
git commit -m "booking: remove deprecated old-checkout link"
Why: If a deploy breaks production and you deployed three features at once, you can't isolate which one caused it. Narrow commits = narrow blast radius.
D5: Obey Your Own Session Summaries
If a prior session warns "⚠️ do not deploy stale local index.html" and you see that warning in the CLAUDE.md, treat it as a blocker:
# In CLAUDE.md (auto-loaded next session):
# ⚠️ WARNING (Session 4.6): Local index.html may be stale.
# Pull qos-prod-site/index.html and verify before edit.
# In next session: READ THIS, PULL THE FILE, verify hashes.
Why: You are smarter than your prior self. Future-you left warnings for present-you. Honor them.
D6: Snapshot Prod Before Overwriting (No Versioning Fallback)
If S3 versioning is disabled (as it is for qos-prod-site), create a manual backup before every prod deploy:
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
aws s3 cp s3://qos-prod-site/index.html s3://qos-backups/index.html.${TIMESTAMP}
# Then deploy
aws s3 cp ./index.html s3://qos-prod-site/index.html
# Invalidate CloudFront
aws cloudfront create-invalidation --distribution-id E2ABCD1234XYZ --paths "/*"
Why: Versioning is disabled to save costs, so manual snapshots are your only recovery path. One lost backup is cheaper than one lost feature.
D7: Proof Block: Six-Line Validation Before cp
In chat, always print a six-line proof block before executing any S3 upload:
✅ PRE-DEPLOY PROOF
File: index.html (3,650 lines)
Local md5: abc123def456
S3 live md5: xyz789uva210
Diff: 47 insertions, 3 deletions (hero section + Stripe link updates)
Target: s3://qos-prod-site/index.html
Invalidation: E2ABCD1234XYZ (/*)
Why: Requires you to articulate what you're doing before you do it. Catches mismatches between intention and action.
D8: Escalate to CB if S3 is Ahead of Local
If live S3 has content your local copy doesn't, or S3 was last modified more recently than your local file, pause and ask before overwriting:
aws s3api head-object --bucket qos-