{
 "id": "8.2",
 "title": "Deploy and promote",
 "department": "Ops and infrastructure",
 "department_slug": "ops-and-infrastructure",
 "direction": null,
 "branded": false,
 "status": "draft",
 "owner": "Param",
 "last_verified": "Mon Oct 26",
 "holds": "Step by step, checks, rollback",
 "urls": {
  "html": "https://handbook.joinsquareup.com/ops-and-infrastructure/deploy-and-promote",
  "markdown": "https://handbook.joinsquareup.com/ops-and-infrastructure/deploy-and-promote.md",
  "json": "https://handbook.joinsquareup.com/ops-and-infrastructure/deploy-and-promote.json"
 },
 "other_directions": [],
 "tldr": {
  "headline": "Staging takes every tested change; production only by promote",
  "points": [
   {
    "icon": "rocket",
    "text": "./deploy.sh puts a tested change on staging right away, without asking."
   },
   {
    "icon": "lock",
    "text": "tools/promote.sh ships staging's exact image after a backup and a clean gate."
   },
   {
    "icon": "check",
    "text": "If anything fails after traffic changes, promote puts production back by itself."
   },
   {
    "icon": "clock",
    "text": "Only one promote at a time; a lock older than 2 hours is taken over."
   },
   {
    "icon": "warn",
    "text": "Never edit promote.sh while it runs; that once caused a rollback."
   }
  ],
  "stat": {
   "value": "8",
   "label": "Steps printed during a promote"
  },
  "decide": "How often should production be promoted, and should staging smoke checks fail a deploy?",
  "mood": "waiting"
 },
 "sources": [
  "brew origin/main a559de6: deploy.sh:5-53, tools/promote.sh:1-119, tools/prod-env.sh:1-13, tools/deploy-demo.sh:7-26, db/prod-clean.mjs:1-6, CLAUDE.md 'Release rhythm'",
  "brew/.wt-prod/docs/production.md:23-33",
  "memory always-deploy-to-test, prod-clean-only, brew-parallel-sessions-deploy, brew-qa-sweep-2026-10-19, brew-prod-backend, brew-pro-switch-and-flush, brew-storage",
  "handbook content/engineering/release-and-promote.md, content/engineering/parallel-sessions.md"
 ],
 "open_questions": [
  "**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`."
 ],
 "parts": {
  "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 |\n|---|---|---|---|\n| Command | `./deploy.sh` | `tools/promote.sh` | `tools/deploy-demo.sh` [src: brew/deploy.sh:7-53] [src: brew/tools/promote.sh:16-119] [src: brew/tools/deploy-demo.sh:9-26] |\n| Builds an image | yes, `--source .` | no, reuses staging's image by digest | yes [src: brew/deploy.sh:7-53] [src: brew/tools/promote.sh:16-119] [src: brew/tools/deploy-demo.sh:9-26] |\n| Refuses a dirty tree | yes | no tree check; refuses a staging sha not on origin/main | yes [src: brew/deploy.sh:7-53] [src: brew/tools/promote.sh:16-119] [src: brew/tools/deploy-demo.sh:9-26] |\n| Runs the test suite | yes, one retry after 20 s | no, it relies on staging | no [src: brew/deploy.sh:7-53] [src: brew/tools/promote.sh:16-119] [src: brew/tools/deploy-demo.sh:9-26] |\n| Backup first | no | on-demand Cloud SQL backup, waited for | no [src: brew/deploy.sh:7-53] [src: brew/tools/promote.sh:16-119] [src: brew/tools/deploy-demo.sh:9-26] |\n| Gate | none | `db/prod-clean.mjs` on the candidate and the worker | none [src: brew/deploy.sh:7-53] [src: brew/tools/promote.sh:16-119] [src: brew/tools/deploy-demo.sh:9-26] |\n| Rollback | by hand, newest 3 revisions kept | by itself on any failure after a change | by hand, newest 3 kept [src: brew/deploy.sh:7-53] [src: brew/tools/promote.sh:16-119] [src: brew/tools/deploy-demo.sh:9-26] |\n| Smoke checks | curls 4 staging pages, never fails the deploy | 6 pages through the load balancer, 5 tries each | none [src: brew/deploy.sh:7-53] [src: brew/tools/promote.sh:16-119] [src: brew/tools/deploy-demo.sh:9-26] |\n\nSources: [src: brew/deploy.sh:7-16] [src: brew/deploy.sh:39-53] [src: brew/tools/promote.sh:16-22] [src: brew/tools/promote.sh:54-119] [src: brew/tools/deploy-demo.sh:9-26]\n\n### Rules in force\n\n- 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. [src: memory always-deploy-to-test]\n- A deploy never waits for background work. The old instance stops starting Plays and saves; the new one resumes. Never add a wait back. [src: brew/CLAUDE.md] [src: memory brew-pro-switch-and-flush]\n- Nothing dummy or from our testing reaches production; the clean gate refuses a promote on any finding. [src: memory prod-clean-only] [src: brew/db/prod-clean.mjs:1-6]\n- One promote at a time, through the lock `gs://brew-prod-backups/locks/promote.lock`; a lock older than 2 hours is taken over. [src: brew/tools/promote.sh:25-36]\n- Never edit `promote.sh` while it runs; bash reads it as it goes and that once caused a rollback. [src: brew/.wt-prod/docs/production.md:33] [src: memory brew-prod-backend]\n- After a staging deploy that ran on the bucket, run the `brew-import` job. [src: brew/deploy.sh:30-32] [src: memory brew-storage]\n- Model choice is set by environment at deploy time: staging defaults to Gemini, Pro for Plays and Flash for the rest. [src: brew/deploy.sh:23-29] [src: brew/deploy.sh:41]",
  "Tomorrow": "| Item | Version | Tag |\n|---|---|---|\n| Staging on every tested change, production only by `promote.sh` | v1 | Stays |\n| Staging smoke checks fail the deploy when a page is not 200 | v1 | Changes |\n| A short release note per promote, written into the handbook changelog | v1 | New |\n| A second person able to promote | v2 | New, after onboarding |",
  "How we do it": "### Staging\n\n1. Commit only your lines, push, then deploy from a clean worktree of origin/main. [src: memory brew-parallel-sessions-deploy]\n\n```bash\ncd /Users/param/Documents/ChatGPT/brew\ngit pull --rebase && git push origin HEAD:main\ngit worktree add --detach \"$TMPDIR/brew-deploy\" origin/main\nln -s /Users/param/Documents/ChatGPT/brew/node_modules \"$TMPDIR/brew-deploy/node_modules\"\ncd \"$TMPDIR/brew-deploy\" && BRANDS_DIR=/Users/param/Documents/ChatGPT/brew/.brands ./deploy.sh\n```\n\n2. Confirm the live site serves your commit: grep the served `app.js` for a new identifier and check the revision's `git-sha` label descends from your commit. [src: memory brew-qa-sweep-2026-10-19]\n3. After a change on the pack or card path, time the seed, not just the status code:\n\n```bash\ncurl -s -o /dev/null -w '%{time_total}\\n' https://brew.joinsquareup.com/b/cozysnugglet-shop/seed.js\n```\n\n4. Roll back by hand if needed:\n\n```bash\ngcloud run services update-traffic shopos-feed --to-revisions=<rev>=100 --region=asia-south1\n```\n\n### Production\n\n1. Staging must run the commit you want, built from origin/main, and have been tested there.\n2. From a clean checkout of origin/main:\n\n```bash\ntools/promote.sh\n```\n\n3. Read the eight steps as they print. Any `STOP:` line means production traffic did not move or was put back. `ROLLBACK FAILED` means put the named revision back by hand with `update-traffic`. [src: brew/tools/promote.sh:49-52]\n4. If the lock is held and no promote is running: `gcloud storage rm gs://brew-prod-backups/locks/promote.lock`. [src: brew/tools/promote.sh:32]\n5. After the promote, open feed.joinsquareup.com and one partner brand signed in.\n\nFull step detail lives in [Release and promote](/engineering/release-and-promote).",
  "Numbers": "| Metric | Definition | Where measured |\n|---|---|---|\n| Promotes per week | Runs of `promote.sh` that reach 8/8 | promote output; Proposed: log each in the changelog |\n| Promote rollbacks | Runs that print \"rolling back\" | same, Proposed |\n| Staging deploys per day | New `shopos-feed` revisions | `gcloud run revisions list`, Proposed |\n| Time from staging to production | Hours between a commit's staging deploy and its promote | Proposed |",
  "Open": "1. **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.\n2. **Should staging smoke checks fail the deploy?** Today they only print codes. Proposal: yes, fail on any non-200 from `/` or `/api/config`."
 }
}