$npx -y skills add tody-agent/codymaster --skill cm-safe-deployUse when setting up deployment infrastructure for any project - establishes multi-gate deploy pipeline with test gates, build verification, frontend safety checks, and rollback strategy before code reaches production
| 1 | # Safe Deploy Pipeline v2 |
| 2 | |
| 3 | ## TL;DR |
| 4 | - **Use before/during** deploying to staging or production |
| 5 | - **Multi-gate**: secrets, build, stage, smoke, prod, rollback plan |
| 6 | - **Identity**: verifies correct GitHub/Cloudflare/Supabase account |
| 7 | - **Next**: cm-quality-gate (post-deploy) |
| 8 | |
| 9 | ## Overview |
| 10 | |
| 11 | A deploy without gates is a deploy with hope. Hope is not a strategy. |
| 12 | |
| 13 | **Core principle:** Every project needs a multi-gate deploy pipeline. Code passes through syntax → tests → i18n → build → verify → deploy, with hard stops at each gate. No gate skipping. No "it'll be fine." |
| 14 | |
| 15 | > [!CAUTION] |
| 16 | > **March 2026 Incident:** 572 backend tests passed green while `app.js` had catastrophic syntax errors → white screen in production. This pipeline exists because `test:gate` alone was NOT enough. |
| 17 | |
| 18 | ## The Iron Law |
| 19 | |
| 20 | ``` |
| 21 | NO DEPLOY WITHOUT PASSING ALL GATES. |
| 22 | GATES ARE SEQUENTIAL. EACH MUST PASS BEFORE THE NEXT RUNS. |
| 23 | SYNTAX CHECK IS GATE 1. IF IT FAILS, NOTHING ELSE RUNS. |
| 24 | ``` |
| 25 | |
| 26 | ## When to Use |
| 27 | |
| 28 | **ALWAYS** when: |
| 29 | - Setting up a new project's deployment infrastructure |
| 30 | - A project has no test gate before deploy |
| 31 | - Project deploys directly from `git push` |
| 32 | - After a production incident caused by untested code |
| 33 | - Adding CI/CD to an existing project |
| 34 | |
| 35 | ## The 8-Gate Pipeline |
| 36 | |
| 37 | ```dot |
| 38 | digraph pipeline { |
| 39 | rankdir=LR; |
| 40 | gate0 [label="Gate 0\nSecret\nHygiene", shape=box, style=filled, fillcolor="#ffc0cb"]; |
| 41 | gate05 [label="Gate 0.5\nSecurity\nScan", shape=box, style=filled, fillcolor="#f0b3ff"]; |
| 42 | gate1 [label="Gate 1\nSyntax", shape=box, style=filled, fillcolor="#ffcccc"]; |
| 43 | gate2 [label="Gate 2\nTest\nSuite", shape=box, style=filled, fillcolor="#ffe0cc"]; |
| 44 | gate3 [label="Gate 3\ni18n\nParity", shape=box, style=filled, fillcolor="#e0ccff"]; |
| 45 | gate4 [label="Gate 4\nBuild", shape=box, style=filled, fillcolor="#ffffcc"]; |
| 46 | gate5 [label="Gate 5\nDist\nVerify", shape=box, style=filled, fillcolor="#ccffcc"]; |
| 47 | gate6 [label="Gate 6\nDeploy +\nSmoke", shape=box, style=filled, fillcolor="#cce5ff"]; |
| 48 | fail [label="STOP\nFix first", shape=box, style=filled, fillcolor="#ff9999"]; |
| 49 | |
| 50 | gate0 -> gate05 [label="pass"]; |
| 51 | gate0 -> fail [label="fail"]; |
| 52 | gate05 -> gate1 [label="pass"]; |
| 53 | gate05 -> fail [label="fail"]; |
| 54 | gate1 -> gate2 [label="pass"]; |
| 55 | gate1 -> fail [label="fail"]; |
| 56 | gate2 -> gate3 [label="pass"]; |
| 57 | gate2 -> fail [label="fail"]; |
| 58 | gate3 -> gate4 [label="pass"]; |
| 59 | gate3 -> fail [label="fail"]; |
| 60 | gate4 -> gate5 [label="pass"]; |
| 61 | gate4 -> fail [label="fail"]; |
| 62 | gate5 -> gate6 [label="pass"]; |
| 63 | gate5 -> fail [label="fail"]; |
| 64 | } |
| 65 | ``` |
| 66 | |
| 67 | --- |
| 68 | |
| 69 | ### Gate 0: Secret Hygiene (FASTEST FAIL — < 0.5 seconds) |
| 70 | |
| 71 | > [!CAUTION] |
| 72 | > **March 2026 Security Incident:** `SUPABASE_SERVICE_KEY` was accidentally committed to `wrangler.jsonc`. This exposed a service-role key that bypasses Row Level Security in git history. Gate 0 prevents this from ever reaching the remote. |
| 73 | |
| 74 | **The Rule: Where Each Variable Lives** |
| 75 | |
| 76 | | Variable Type | Correct Location | WRONG Location | |
| 77 | |--------------|-----------------|----------------| |
| 78 | | Supabase URL (public) | `wrangler.jsonc` vars section | ❌ Hardcoded in code | |
| 79 | | `SUPABASE_SERVICE_KEY` | Cloudflare Secret (`wrangler secret put`) | ❌ `wrangler.jsonc` | |
| 80 | | `SUPABASE_ANON_KEY` | Cloudflare Secret | ❌ `wrangler.jsonc` | |
| 81 | | DB connection strings | Cloudflare Secret | ❌ Anywhere in repo | |
| 82 | | Local dev secrets | `.dev.vars` (gitignored) | ❌ `wrangler.jsonc` | |
| 83 | | Build config (non-secret) | `wrangler.jsonc` | — | |
| 84 | |
| 85 | **Secret Hygiene Check (Enhanced — Repo-Wide):** |
| 86 | |
| 87 | > Calls `cm-secret-shield` Layer 4 for deep scanning. Below is the essential check: |
| 88 | |
| 89 | ```bash |
| 90 | node -e " |
| 91 | const fs = require('fs'); |
| 92 | const { execSync } = require('child_process'); |
| 93 | |
| 94 | // 1. Check wrangler config for secrets |
| 95 | const wranglerFiles = ['wrangler.jsonc', 'wrangler.toml', 'wrangler.json']; |
| 96 | const dangerous = ['SERVICE_KEY', 'ANON_KEY', 'DB_PASSWORD', 'SECRET_KEY', 'PRIVATE_KEY', 'API_SECRET']; |
| 97 | let failed = false; |
| 98 | |
| 99 | for (const wf of wranglerFiles) { |
| 100 | if (!fs.existsSync(wf)) continue; |
| 101 | const src = fs.readFileSync(wf, 'utf-8'); |
| 102 | for (const key of dangerous) { |
| 103 | // Check for actual values, not just variable names |
| 104 | const valuePattern = new RegExp(key + '\\\\s*[=:]\\\\s*[\"\'][a-zA-Z0-9/+=]{20,}', 'g'); |
| 105 | if (valuePattern.test(src)) { |
| 106 | console.error('❌ DANGEROUS: ' + wf + ' contains a ' + key + ' VALUE'); |
| 107 | console.error(' Fix: wrangler secret put ' + key + ' (then remove from ' + wf + ')'); |
| 108 | failed = true; |
| 109 | } |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | // 2. Check .gitignore has required patterns |
| 114 | if (fs.existsSync('.gitignore')) { |
| 115 | const gi = fs.readFileSync('.gitignore', 'utf-8'); |
| 116 | const required = ['.env', '.dev.vars']; |
| 117 | const missing = required.filter(r => !gi.include |