Answer
Code goes to staging with ./deploy.sh the moment it is tested, and to production only with tools/promote.sh, which ships staging's exact image after a backup and a clean gate and puts production back by itself if anything fails; this page is the operator's checklist for both.
Today
| Staging deploy | Production promote | Demo deploy | |
|---|---|---|---|
| Command | ./deploy.sh |
tools/promote.sh |
tools/deploy-demo.sh brew/deploy.sh:7-53 brew/tools/promote.sh:16-119 brew/tools/deploy-demo.sh:9-26 |
| Builds an image | yes, --source . |
no, reuses staging's image by digest | yes brew/deploy.sh:7-53 brew/tools/promote.sh:16-119 brew/tools/deploy-demo.sh:9-26 |
| Refuses a dirty tree | yes | no tree check; refuses a staging sha not on origin/main | yes brew/deploy.sh:7-53 brew/tools/promote.sh:16-119 brew/tools/deploy-demo.sh:9-26 |
| Runs the test suite | yes, one retry after 20 s | no, it relies on staging | no brew/deploy.sh:7-53 brew/tools/promote.sh:16-119 brew/tools/deploy-demo.sh:9-26 |
| Backup first | no | on-demand Cloud SQL backup, waited for | no brew/deploy.sh:7-53 brew/tools/promote.sh:16-119 brew/tools/deploy-demo.sh:9-26 |
| Gate | none | db/prod-clean.mjs on the candidate and the worker |
none brew/deploy.sh:7-53 brew/tools/promote.sh:16-119 brew/tools/deploy-demo.sh:9-26 |
| Rollback | by hand, newest 3 revisions kept | by itself on any failure after a change | by hand, newest 3 kept brew/deploy.sh:7-53 brew/tools/promote.sh:16-119 brew/tools/deploy-demo.sh:9-26 |
| Smoke checks | curls 4 staging pages, never fails the deploy | 6 pages through the load balancer, 5 tries each | none brew/deploy.sh:7-53 brew/tools/promote.sh:16-119 brew/tools/deploy-demo.sh:9-26 |
Sources: brew/deploy.sh:7-16 brew/deploy.sh:39-53 brew/tools/promote.sh:16-22 brew/tools/promote.sh:54-119 brew/tools/deploy-demo.sh:9-26
Rules in force
- Deploy after every tested change without asking; Param tests on the live staging URL. Still ask before anything that spends money or touches real ads or stores. memory always-deploy-to-test
- A deploy never waits for background work. The old instance stops starting Plays and saves; the new one resumes. Never add a wait back. brew/CLAUDE.md memory brew-pro-switch-and-flush
- Nothing dummy or from our testing reaches production; the clean gate refuses a promote on any finding. memory prod-clean-only brew/db/prod-clean.mjs:1-6
- One promote at a time, through the lock
gs://brew-prod-backups/locks/promote.lock; a lock older than 2 hours is taken over. brew/tools/promote.sh:25-36 - Never edit
promote.shwhile it runs; bash reads it as it goes and that once caused a rollback. brew/.wt-prod/docs/production.md:33 memory brew-prod-backend - After a staging deploy that ran on the bucket, run the
brew-importjob. brew/deploy.sh:30-32 memory brew-storage - Model choice is set by environment at deploy time: staging defaults to Gemini, Pro for Plays and Flash for the rest. brew/deploy.sh:23-29 brew/deploy.sh:41
Tomorrow
| Item | Version | Tag |
|---|---|---|
Staging on every tested change, production only by promote.sh |
v1 | Stays |
| Staging smoke checks fail the deploy when a page is not 200 | v1 | Changes |
| A short release note per promote, written into the handbook changelog | v1 | New |
| A second person able to promote | v2 | New, after onboarding |
How we do it
Staging
- Commit only your lines, push, then deploy from a clean worktree of origin/main. memory brew-parallel-sessions-deploy
cd /Users/param/Documents/ChatGPT/brew
git pull --rebase && git push origin HEAD:main
git worktree add --detach "$TMPDIR/brew-deploy" origin/main
ln -s /Users/param/Documents/ChatGPT/brew/node_modules "$TMPDIR/brew-deploy/node_modules"
cd "$TMPDIR/brew-deploy" && BRANDS_DIR=/Users/param/Documents/ChatGPT/brew/.brands ./deploy.sh
- Confirm the live site serves your commit: grep the served
app.jsfor a new identifier and check the revision'sgit-shalabel descends from your commit. memory brew-qa-sweep-2026-10-19 - After a change on the pack or card path, time the seed, not just the status code:
curl -s -o /dev/null -w '%{time_total}\n' https://brew.joinsquareup.com/b/cozysnugglet-shop/seed.js
- Roll back by hand if needed:
gcloud run services update-traffic shopos-feed --to-revisions=<rev>=100 --region=asia-south1
Production
- Staging must run the commit you want, built from origin/main, and have been tested there.
- From a clean checkout of origin/main:
tools/promote.sh
- Read the eight steps as they print. Any
STOP:line means production traffic did not move or was put back.ROLLBACK FAILEDmeans put the named revision back by hand withupdate-traffic. brew/tools/promote.sh:49-52 - If the lock is held and no promote is running:
gcloud storage rm gs://brew-prod-backups/locks/promote.lock. brew/tools/promote.sh:32 - After the promote, open feed.joinsquareup.com and one partner brand signed in.
Full step detail lives in Release and promote.
Numbers
| Metric | Definition | Where measured |
|---|---|---|
| Promotes per week | Runs of promote.sh that reach 8/8 |
promote output; Proposed: log each in the changelog |
| Promote rollbacks | Runs that print "rolling back" | same, Proposed |
| Staging deploys per day | New shopos-feed revisions |
gcloud run revisions list, Proposed |
| Time from staging to production | Hours between a commit's staging deploy and its promote | Proposed |
Open
- How often should production be promoted? No rhythm is written down; Next carries one pending promote of the 26 Oct jam fixes. Proposal: promote at most once a day, after Param has walked Arata and one other partner on staging.
- Should staging smoke checks fail the deploy? Today they only print codes. Proposal: yes, fail on any non-200 from
/or/api/config.