๐ Full Skill Source โ This is the complete, unedited SKILL.md file. Nothing is hidden or summarized.
Safe Deploy Pipeline v2 โ
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.js had catastrophic syntax errors โ white screen in production. This pipeline exists because test:gate alone 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 7-Gate Pipeline โ
digraph pipeline {
rankdir=LR;
gate0 [label="Gate 0\nSecret\nHygiene", shape=box, style=filled, fillcolor="#ffc0cb"];
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 -> gate1 [label="pass"];
gate0 -> 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_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.
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:
node -e "
const src = require('fs').readFileSync('wrangler.jsonc', 'utf-8');
const dangerous = ['SERVICE_KEY', 'ANON_KEY', 'DB_PASSWORD', 'SECRET_KEY', 'PRIVATE_KEY'];
const found = dangerous.filter(k => src.includes(k));
if (found.length > 0) {
console.error('โ DANGEROUS: Found potential secrets in wrangler.jsonc:', found);
console.error(' Fix: wrangler secret put KEY_NAME (then remove from wrangler.jsonc)');
process.exit(1);
}
console.log('โ
Gate 0 passed: no sensitive keys in wrangler.jsonc');
"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.exampleIf 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 dashboardGate 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.jsWhy separate from Gate 2?
node -ctakes < 1 second. Test suite takes 10-30 seconds.- If syntax is broken, 100% of tests will fail anyway โ but with confusing error messages.
- A fast syntax check gives you the EXACT line number of the error instantly.
REQUIRED SUB-SKILL: Use cm-quality-gate for parser-based validation inside the test suite (Layer 1).
Gate 2: Test Suite โ
The test suite MUST include:
| Test Category | What it validates | Priority |
|---|---|---|
| Frontend safety | JS syntax, function integrity, corruption patterns | CRITICAL |
| Backend API | Routes return correct data | Required |
| Business logic | Calculations, rules, validation | Required |
| i18n sync | Translation key parity, orphaned keys | Required for multi-lang |
| Integration | End-to-end workflows | Recommended |
Setup the test:gate script:
{
"scripts": {
"test:gate": "vitest run --reporter=verbose"
}
}Gate decision:
IF 0 failures โ proceed to Gate 3
IF any failures โ STOP. Fix before continuing.REQUIRED SUB-SKILL: Use cm-quality-gate for enforcement discipline.
Gate 3: i18n Parity Check (for multi-language projects) โ
NOTE
Skip this gate if the project does not have i18n. For projects with i18n, this gate catches what test suites can miss: key drift between languages that causes blank strings in production.
# All language files must have identical key counts
node -e "
const fs = require('fs');
const path = require('path');
const I18N_DIR = 'public/static/i18n';
const langs = ['vi','en','th','ph'];
const results = {};
let allMatch = true;
for (const lang of langs) {
const filePath = path.join(I18N_DIR, lang + '.json');
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
const flatKeys = JSON.stringify(data).split('\":').length - 1;
results[lang] = flatKeys;
console.log(lang + ': ' + flatKeys + ' keys');
}
const counts = Object.values(results);
if (new Set(counts).size !== 1) {
console.error('โ KEY PARITY FAILURE! Counts differ across languages.');
console.error(JSON.stringify(results));
process.exit(1);
} else {
console.log('โ
Key parity: all languages have ' + counts[0] + ' keys');
}
// Check for null/empty values
let nullCount = 0;
for (const lang of langs) {
const data = JSON.parse(fs.readFileSync(path.join(I18N_DIR, lang + '.json'), 'utf-8'));
const check = (obj, prefix) => {
for (const [k, v] of Object.entries(obj)) {
if (k === '_meta') continue;
if (typeof v === 'object' && v !== null) { check(v, prefix + '.' + k); continue; }
if (v === null || v === undefined || v === '') {
console.error(' โ ' + lang + '.' + prefix + '.' + k + ' is null/empty');
nullCount++;
}
}
};
check(data, lang);
}
if (nullCount > 0) {
console.error('โ Found ' + nullCount + ' null/empty translation values!');
process.exit(1);
}
console.log('โ
No null/empty values');
"What this catches:
- Keys added to
vi.jsonbut forgotten inen.jsonโ blank strings for English users - Null values from bad translation scripts โ
t()returns key name instead of translation - Key count drift between languages โ inconsistent UX
Gate 4: Build Verification โ
Production build must succeed without errors.
npm run buildWhat this catches that tests don't:
- Import resolution failures
- Tree-shaking errors
- Missing environment variables
- Asset compilation failures
- Bundle size explosions
Optional: Bundle size guard:
{
"scripts": {
"build:verify": "npm run build && node -e \"const s=require('fs').statSync('dist/_worker.js').size; if(s>2e6) {console.error('Bundle too large: '+s); process.exit(1)}\""
}
}Gate 5: Dist Asset Verification (NEW) โ
IMPORTANT
The build can "succeed" but produce an incomplete dist/ directory. This gate catches missing critical assets.
# Verify critical files exist in dist/
node -e "
const fs = require('fs');
const required = [
'dist/_worker.js',
'dist/static/app.js',
'dist/static/style.css',
'dist/static/i18n/vi.json',
'dist/static/i18n/en.json',
'dist/static/i18n/th.json',
'dist/static/i18n/ph.json',
];
const missing = required.filter(f => !fs.existsSync(f));
if (missing.length > 0) {
console.error('โ Missing files in dist/:');
missing.forEach(f => console.error(' ' + f));
process.exit(1);
}
console.log('โ
All ' + required.length + ' critical files present in dist/');
"Adapt required array to your project. At minimum, verify:
- Worker/server entry point exists
- Frontend JS/CSS files exist
- Translation files are copied
- Critical images/assets are present
Gate 6: Deploy + Post-Deploy Smoke Test โ
Only after Gates 1-5 pass.
Deploy command varies by platform:
| Platform | Command |
|---|---|
| Cloudflare Pages | npx wrangler pages deploy dist/ |
| Vercel | npx vercel --prod |
| Netlify | npx netlify deploy --prod |
Post-deploy verification:
# Smoke test the deployed URL โ must return 200
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://your-app.pages.dev)
if [ "$STATUS" != "200" ]; then
echo "โ POST-DEPLOY SMOKE TEST FAILED! Status: $STATUS"
echo "โ Consider immediate rollback."
exit 1
fi
echo "โ
Smoke test passed (HTTP $STATUS)"Composing the Deploy Script โ
package.json (Recommended) โ
{
"scripts": {
"predeploy:syntax": "node -c public/static/app.js",
"predeploy:i18n": "node scripts/check-i18n-parity.js",
"predeploy:dist": "node scripts/verify-dist.js",
"deploy": "npm run predeploy:syntax && npm run test:gate && npm run predeploy:i18n && npm run build && npm run predeploy:dist && YOUR_DEPLOY_COMMAND"
}
}Key insight: Chain gates with &&. If any gate fails, the chain stops immediately.
Rollback Protocol โ
When a deployment causes issues:
| Severity | Action | Command |
|---|---|---|
| White screen (syntax) | Revert last commit, redeploy | git revert HEAD && npm run deploy |
| Broken translations | Revert JSON files, redeploy | git checkout HEAD~1 -- public/static/i18n/*.json && npm run deploy |
| API error | Revert server code, redeploy | git revert HEAD && npm run deploy |
| Partial breakage | Cherry-pick fix, deploy | Fix โ test โ deploy |
Cloudflare Pages specific:
# Rollback to previous deployment
wrangler pages deployments list --project-name prms
wrangler pages deployment rollback <deployment-id> --project-name prmsSetting Up for a New Project โ
Step 1: Create test infrastructure โ
npm install -D vitest acornStep 2: Create package.json scripts โ
{
"scripts": {
"test:gate": "vitest run --reporter=verbose",
"build": "YOUR_BUILD_COMMAND",
"deploy": "node -c public/static/app.js && npm run test:gate && npm run build && YOUR_DEPLOY_COMMAND"
}
}Step 3: Add frontend safety tests โ
REQUIRED SUB-SKILL: Follow cm-quality-gate to create test file with all layers.
Step 4: Create deploy workflow โ
Create .agents/workflows/deploy.md.
Red Flags โ STOP โ
- โ Deploying without running test:gate
- โ Skipping syntax check ("tests will catch it")
- โ Skipping build step ("tests passed so it'll build")
- โ Running tests and deploy in parallel
- โ "Tests passed last time" (run them NOW)
- โ "Only changed one file" (test everything)
- โ No frontend safety tests for JS projects
- โ No dist/ verification after build
- โ No post-deploy smoke test
- โ No i18n parity check for multi-language apps
Rationalization Table โ
| Excuse | Reality |
|---|---|
| "Tests passed earlier" | Code changed since then. Run fresh. |
| "Build always works" | Until it doesn't. 30 seconds to verify. |
| "It's a one-line change" | One line broke 600 lines of app.js. Test it. |
| "CI will catch it" | CI runs AFTER push. Catch BEFORE push. |
| "Just a hotfix" | Hotfixes need MORE testing, not less. |
| "Syntax check is redundant" | node -c takes 0.5s and prevented the March 2026 disaster. |
| "i18n parity is overkill" | Missing keys โ blank strings in production. |
| "dist/ is always complete" | Build tools can silently skip assets. Check. |
Integration with Other Skills โ
| Skill | When |
|---|---|
cm-quality-gate | Setting up Gate 2 frontend tests and Test Gate |
cm-safe-i18n | Adding i18n-specific gates |
cm-terminal | Monitoring gate commands |
The Bottom Line โ
6 gates. Sequential. Each must pass. No exceptions.
Syntax โ Tests โ i18n โ Build โ Dist Verify โ Deploy + Smoke.
This is non-negotiable.