Safe Deploy Pipeline v2
TL;DR
- Use before/during deploying to staging or production
- Multi-gate: secrets, build, stage, smoke, prod, rollback plan
- Identity: verifies correct GitHub/Cloudflare/Supabase account
- Next: cm-quality-gate (post-deploy)
Overview
A deploy without gates is a deploy with hope. Hope is not a strategy.
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."
[!CAUTION] March 2026 Incident: 572 backend tests passed green while
app.jshad catastrophic syntax errors → white screen in production. This pipeline exists becausetest:gatealone was NOT enough.
The Iron Law
NO DEPLOY WITHOUT PASSING ALL GATES.
GATES ARE SEQUENTIAL. EACH MUST PASS BEFORE THE NEXT RUNS.
SYNTAX CHECK IS GATE 1. IF IT FAILS, NOTHING ELSE RUNS.
When to Use
ALWAYS when:
- Setting up a new project's deployment infrastructure
- A project has no test gate before deploy
- Project deploys directly from
git push - After a production incident caused by untested code
- Adding CI/CD to an existing project
The 8-Gate Pipeline
digraph pipeline {
rankdir=LR;
gate0 [label="Gate 0\nSecret\nHygiene", shape=box, style=filled, fillcolor="#ffc0cb"];
gate05 [label="Gate 0.5\nSecurity\nScan", shape=box, style=filled, fillcolor="#f0b3ff"];
gate1 [label="Gate 1\nSyntax", shape=box, style=filled, fillcolor="#ffcccc"];
gate2 [label="Gate 2\nTest\nSuite", shape=box, style=filled, fillcolor="#ffe0cc"];
gate3 [label="Gate 3\ni18n\nParity", shape=box, style=filled, fillcolor="#e0ccff"];
gate4 [label="Gate 4\nBuild", shape=box, style=filled, fillcolor="#ffffcc"];
gate5 [label="Gate 5\nDist\nVerify", shape=box, style=filled, fillcolor="#ccffcc"];
gate6 [label="Gate 6\nDeploy +\nSmoke", shape=box, style=filled, fillcolor="#cce5ff"];
fail [label="STOP\nFix first", shape=box, style=filled, fillcolor="#ff9999"];
gate0 -> gate05 [label="pass"];
gate0 -> fail [label="fail"];
gate05 -> gate1 [label="pass"];
gate05 -> fail [label="fail"];
gate1 -> gate2 [label="pass"];
gate1 -> fail [label="fail"];
gate2 -> gate3 [label="pass"];
gate2 -> fail [label="fail"];
gate3 -> gate4 [label="pass"];
gate3 -> fail [label="fail"];
gate4 -> gate5 [label="pass"];
gate4 -> fail [label="fail"];
gate5 -> gate6 [label="pass"];
gate5 -> fail [label="fail"];
}
Gate 0: Secret Hygiene (FASTEST FAIL — < 0.5 seconds)
[!CAUTION] March 2026 Security Incident:
SUPABASE_SERVICE_KEYwas accidentally committed towrangler.jsonc. This exposed a service-role key that bypasses Row Level Security in git history. Gate 0 prevents this from ever reaching the remote.
The Rule: Where Each Variable Lives
| Variable Type | Correct Location | WRONG Location |
|---|---|---|
| Supabase URL (public) | wrangler.jsonc vars section | ❌ Hardcoded in code |
SUPABASE_SERVICE_KEY | Cloudflare Secret (wrangler secret put) | ❌ wrangler.jsonc |
SUPABASE_ANON_KEY | Cloudflare Secret | ❌ wrangler.jsonc |
| DB connection strings | Cloudflare Secret | ❌ Anywhere in repo |
| Local dev secrets | .dev.vars (gitignored) | ❌ wrangler.jsonc |
| Build config (non-secret) | wrangler.jsonc | — |
Secret Hygiene Check (Enhanced — Repo-Wide):
Calls
cm-secret-shieldLayer 4 for deep scanning. Below is the essential check:
node -e "
const fs = require('fs');
const { execSync } = require('child_process');
// 1. Check wrangler config for secrets
const wranglerFiles = ['wrangler.jsonc', 'wrangler.toml', 'wrangler.json'];
const dangerous = ['SERVICE_KEY', 'ANON_KEY', 'DB_PASSWORD', 'SECRET_KEY', 'PRIVATE_KEY', 'API_SECRET'];
let failed = false;
for (const wf of wranglerFiles) {
if (!fs.existsSync(wf)) continue;
const src = fs.readFileSync(wf, 'utf-8');
for (const key of dangerous) {
// Check for actual values, not just variable names
const valuePattern = new RegExp(key + '\\\\s*[=:]\\\\s*[\"\'][a-zA-Z0-9/+=]{20,}', 'g');
if (valuePattern.test(src)) {
console.error('❌ DANGEROUS: ' + wf + ' contains a ' + key + ' VALUE');
console.error(' Fix: wrangler secret put ' + key + ' (then remove from ' + wf + ')');
failed = true;
}
}
}
// 2. Check .gitignore has required patterns
if (fs.existsSync('.gitignore')) {
const gi = fs.readFileSync('.gitignore', 'utf-8');
const required = ['.env', '.dev.vars'];
const missing = required.filter(r => !gi.includes(r));
if (missing.length > 0) {
console.error('❌ .gitignore missing: ' + missing.join(', '));
failed = true;
}
} else {
console.error('❌ No .gitignore found!');
failed = true;
}
// 3. Check .env files aren't tracked by git
try {
const tracked = execSync('git ls-files', { encoding: 'utf-8' });
const badFiles = ['.env', '.dev.vars', '.env.local', '.env.production'];
const trackedBad = badFiles.filter(f => tracked.split('\\n').includes(f));
if (trackedBad.length > 0) {
console.error('❌ CRITICAL: Secret files tracked by git: ' + trackedBad.join(', '));
console.error(' Fix: git rm --cached ' + trackedBad.join(' '));
failed = true;
}
} catch (e) { /* not a git repo */ }
if (failed) {
console.error('\\n🛡️ Gate 0 FAILED. Fix issues above before deploying.');
process.exit(1);
}
console.log('✅ Gate 0 passed: repo-wide secret hygiene verified');
"
Setup .dev.vars for local development:
# .dev.vars — local only, NEVER committed
SUPABASE_URL=https://YOUR_PROJECT.supabase.co
SUPABASE_SERVICE_KEY=YOUR_SERVICE_KEY
# Add to .gitignore:
echo ".dev.vars" >> .gitignore
# Commit the template:
cp .dev.vars .dev.vars.example # Remove values first
git add .dev.vars.example
If secrets were already committed:
# Remove from git history (URGENT — do before pushing)
git filter-repo --path wrangler.jsonc --invert-paths # Nuclear option
# OR just remove the value from wrangler.jsonc and add as secret:
wrangler secret put SUPABASE_SERVICE_KEY
# Then rotate the key immediately in Supabase dashboard
Gate 0.5: Security Scan (Snyk + Aikido — Parallel)
[!IMPORTANT] CodyMaster internal: This gate is MANDATORY. Both Snyk and Aikido must pass. User projects: This gate is SUGGESTED by default. Becomes MANDATORY if CVEs or risk flags were detected during development.
Run both scanners in parallel:
# Snyk — dependency vulnerabilities
snyk test
# Aikido — SAST + dependencies + secrets + IaC
aikido-api-client scan-release <repo> $(git rev-parse HEAD) \
--minimum-severity-level="HIGH"
For CodyMaster (maximum strictness):
aikido-api-client scan-release <repo> $(git rev-parse HEAD) \
--minimum-severity-level="HIGH" \
--fail-on-sast-scan \
--fail-on-secrets-scan
Gate decision:
- Both pass → proceed to Gate 1
- Either fails → STOP. Fix before continuing. Invoke
cm-security-gatefor remediation.
See
cm-security-gatefor full setup, flag reference, and remediation workflow.
Gate 1: Syntax Validation (FAST FAIL)
[!IMPORTANT] This gate runs in < 1 second and catches the EXACT class of errors that caused the March 2026 incident. Run it BEFORE the test suite (which takes 10-30s).
| Stack | Command | What it checks |
|---|---|---|
| Vanilla JS | node -c path/to/app.js | JavaScript parse errors |
| TypeScript | npx tsc --noEmit | Type errors + syntax |
| Python | python -m py_compile app.py | Python syntax |
| Go | go vet ./... | Go static analysis |
For frontend monoliths without TypeScript:
# Ultra-fast syntax check — fails in < 1s if broken
node -c public/static/app.js
Why separate from Gate 2?
node -ctakes < 1 second. Test suite takes 10-30 seconds.- If syntax