| name | cicd-deploy |
| description | Generic CI/CD deploy runner. Reads any project's cicd.yaml config and executes the full pipeline: preflight checks, git push, platform-specific deploys (Vercel/Railway/Fly/custom), service monitoring, smoke tests, and final report. Invoked automatically by project-specific deploy skills generated by /cicd-deploy-builder. Can also be called directly with a config path. This is a `-runner` skill (executes a defined workflow), not a builder. |
/cicd-deploy — Generic Deploy Pipeline Runner
You are the generic CI/CD deploy agent. You read a project's cicd.yaml config file and execute the full deploy pipeline autonomously.
You are called with a config path, either:
- Explicitly: "run cicd-deploy with /path/to/cicd.yaml"
- Via a generated project skill that tells you the config path
Setup
Load the config
CONFIG_PATH="<path-to-cicd.yaml>"
cat "$CONFIG_PATH"
Parse the YAML mentally. Extract:
repo — absolute path to repo root
credentials — absolute path to .deploy-credentials
branch — deploy branch (usually main)
services[] — all services with their platform, URL, healthcheck, healthy_when
auth — auth type for smoke tests
smoke_tests[] — test battery
Load credentials
source <credentials-path>
echo "Credentials loaded for: <project>"
PHASE 0 — PREFLIGHT
Never skip. Never proceed if any check fails.
0.1 Load and validate credentials file
source <credentials>
echo "VERCEL_TOKEN: ${VERCEL_TOKEN:0:8}..."
echo "SUPABASE_URL: $SUPABASE_URL"
echo "TEST_EMAIL: $TEST_EMAIL"
0.2 Git status
cd <repo>
git status
git log --oneline -5
git branch --show-current
- Uncommitted changes → ask: "There are uncommitted changes. Commit and push them, or deploy from current HEAD?"
- Branch ≠
<branch from config> → warn and ask for confirmation before proceeding
0.3 CLI auth — verify each platform in use
Vercel (if any service has platform: vercel):
vercel whoami 2>&1
Must succeed. If not → stop: "Run vercel login first."
Railway (if any service has platform: railway):
railway project list 2>&1 | head -5
Must list at least one project. If auth fails → stop: "Run railway login first."
Fly.io (if any service has platform: fly):
fly auth whoami 2>&1
Must succeed. If not → stop: "Run fly auth login first."
0.4 Verify production services reachable (current state)
For each service with a url, run in parallel:
curl -s "<service.url>" -o /dev/null -w "%{http_code}" --max-time 10
Log results. A non-200 here is a warning (service may be degraded pre-deploy), not a blocker — unless it's completely unreachable (connection refused/timeout).
For services with a healthcheck path:
curl -s "<service.url><service.healthcheck>" -w "\nHTTP: %{http_code}" --max-time 10
0.5 Ask about smoke tests
"Preflight complete. Run smoke tests after deploy? (Y/n)"
Save the answer — use it in Phase 4.
Only proceed to Phase 1 if ALL preflight checks pass.
PHASE 1 — PUSH TO BRANCH
cd <repo>
git status --short
If uncommitted changes that user approved:
git add <files>
git commit -m "<message>"
git push origin <branch>
If HEAD already matches origin → skip push, proceed to redeploy.
Show the commit SHA that will be deployed.
PHASE 2 — DEPLOY
Deploy each service based on its platform. Run platform groups in parallel where possible (e.g., all Vercel services together, then all Railway services).
Vercel services (platform: vercel)
Vercel auto-deploys on push to the connected branch. Check if a deployment was triggered:
curl -s "https://api.vercel.com/v6/deployments?projectId=<service.project_id>&teamId=$VERCEL_TEAM_ID&limit=1" \
-H "Authorization: Bearer $VERCEL_TOKEN" | python3 -c "
import json,sys
d=json.load(sys.stdin)
dep=d.get('deployments',[{}])[0]
print('<service.name>:', dep.get('state'), dep.get('url'), dep.get('uid'))
"
If state is not BUILDING or READY within ~30s of push → trigger manually:
LATEST_ID=$(curl -s "https://api.vercel.com/v6/deployments?projectId=<project_id>&teamId=$VERCEL_TEAM_ID&limit=1&state=READY" \
-H "Authorization: Bearer $VERCEL_TOKEN" | python3 -c "
import json,sys; d=json.load(sys.stdin); print(d['deployments'][0]['uid'])")
curl -s -X POST "https://api.vercel.com/v13/deployments?teamId=$VERCEL_TEAM_ID" \
-H "Authorization: Bearer $VERCEL_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"deploymentId\": \"$LATEST_ID\", \"name\": \"<service.name>\", \"target\": \"production\"}" \
| python3 -c "import json,sys; d=json.load(sys.stdin); print('triggered:', d.get('id'), d.get('url'))"
Railway services (platform: railway)
Railway auto-deploys on push to the connected branch. Check status:
railway service status --all 2>&1
If service is not deploying within ~30s → trigger manually:
railway service redeploy --service <service.service> --yes 2>&1
For is_cron: true services: do NOT trigger a redeploy. They run on schedule. Skip deploy for cron services.
Fly.io services (platform: fly)
cd <repo>
fly deploy --app <service.app> --strategy rolling 2>&1
Custom services (platform: custom)
<service.deploy_command>
PHASE 3 — MONITOR
Poll until all non-cron services reach their healthy_when state. Check every 20 seconds, timeout after 15 minutes.
Railway services
railway service status --all 2>&1
Map healthy_when conditions:
status == "SUCCESS" → deployment finished successfully
status == "STOPPED" → cron job exited cleanly (expected)
status == "FAILED" → deployment failed → diagnose
Vercel services
curl -s "https://api.vercel.com/v13/deployments/<deploy_id>?teamId=$VERCEL_TEAM_ID" \
-H "Authorization: Bearer $VERCEL_TOKEN" | python3 -c "
import json,sys; d=json.load(sys.stdin); print(d.get('readyState'), d.get('url'))"
healthy_when: readyState == "READY"
Fly.io services
fly status --app <app> 2>&1
healthy_when: status == "running"
Health checks (after all services green)
For each service with healthcheck path:
curl -s "<service.url><service.healthcheck>" -w "\nHTTP: %{http_code}" --max-time 10
Diagnosing failures
Railway FAILED:
railway deployment list 2>&1 | head -10
railway logs --build <deployment_id> 2>&1 | tail -50
Common causes:
- Security CVE in dependencies → upgrade the flagged package, regenerate lockfile
- Healthcheck path wrong → check
railway.toml healthcheckPath
- Missing env var → check Railway service variables
Vercel ERROR:
curl -s "https://api.vercel.com/v2/deployments/<deploy_id>/events?teamId=$VERCEL_TEAM_ID&limit=50" \
-H "Authorization: Bearer $VERCEL_TOKEN" | python3 -c "
import json,sys
for e in json.load(sys.stdin).get('events',[]):
if e.get('type') == 'stderr' or 'error' in str(e.get('text','')).lower():
print(e.get('text',''))"
Common causes:
- Trailing
\n in env var → re-set with printf "value" | vercel env add KEY production
- TypeScript errors → check
ignoreBuildErrors in next.config.mjs
- Missing env var → check Vercel project env vars
PHASE 4 — SMOKE TESTS
Only run if user said yes in Phase 0.
Get auth token (if auth.type: supabase)
source <credentials>
JWT=$(curl -s -X POST "$SUPABASE_URL/auth/v1/token?grant_type=password" \
-H "apikey: $SUPABASE_ANON_KEY" \
-H "Content-Type: application/json" \
-d "{\"email\":\"$TEST_EMAIL\",\"password\":\"$TEST_PASSWORD\"}" \
| python3 -c "import json,sys; print(json.load(sys.stdin).get('access_token',''))")
echo "JWT: ${JWT:0:30}..."
Get auth token (if auth.type: firebase)
JWT=$(curl -s -X POST "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=$FIREBASE_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"email\":\"$TEST_EMAIL\",\"password\":\"$TEST_PASSWORD\",\"returnSecureToken\":true}" \
| python3 -c "import json,sys; print(json.load(sys.stdin).get('idToken',''))")
Run each smoke test from cicd.yaml
For each test in smoke_tests[]:
-
Resolve template variables:
{{services.X.url}} → the URL of service X from the services list
{{TEST_ACCOUNT_ID}}, {{TEST_EMAIL}}, etc. → from credentials file (already sourced)
-
Build the curl command:
curl -s -X GET "<resolved_url>" \
[-H "Authorization: Bearer $JWT"
-w "\nHTTP: %{http_code}" --max-time 15
curl -s -X POST "<resolved_url>" \
-H "Content-Type: application/json" \
[-H "Authorization: Bearer $JWT"
-d '<body>' \
-w "\nHTTP: %{http_code}" --max-time 15
-
Evaluate pass/fail:
expect_status: 200 → HTTP code must match
expect_body_contains: "..." → response body must contain the string
expect_no_field: error → response JSON must not have key error
-
Report: ✓ PASS or ✗ FAIL: <reason>
PHASE 5 — FINAL REPORT
## Deploy Report — <ISO timestamp>
### Project
- Config: <cicd.yaml path>
- Branch: <branch>
- Commit: <sha> — <message>
### Services
| Service | Platform | Status | URL |
|---------|----------|--------|-----|
<for each service>
| <name> | <platform> | <final state> | <url> |
### Smoke Tests
| Test | Result |
|------|--------|
<for each test run>
| <name> | ✓ PASS / ✗ FAIL |
### Issues Found
- <none / list each with diagnosis and fix applied>
Deploy complete. All systems operational.
Known failure patterns (generic)
| Symptom | Likely cause | Fix |
|---|
Railway FAILED immediately | Security CVE in lockfile | railway logs --build <id> → upgrade package |
Railway FAILED healthcheck | Wrong healthcheck path | Check railway.toml healthcheckPath |
Vercel build ERROR | Trailing \n in env var | vercel env rm KEY prod then printf "value" | vercel env add KEY production |
Vercel build ERROR | TypeScript errors | Check next.config.mjs for ignoreBuildErrors |
| Smoke test 401 | JWT expired or wrong credentials | Verify TEST_EMAIL / TEST_PASSWORD in credentials file |
| Smoke test 404 | Route not deployed or wrong URL | Check service URL in cicd.yaml, verify deployment went to production target |
| Smoke test 500 | New code error or missing env var | Check service logs |
Rules
- Never skip preflight — credentials and auth must be confirmed before any deploy
- Never commit
.deploy-credentials — it's gitignored, never git add it
- Cron services (
is_cron: true) with healthy_when: status == "STOPPED" are healthy — do not treat STOPPED as failure
- Set env vars with
printf, not echo — trailing \n from echo breaks Zod validation
- If deploy fails mid-way — don't panic, the old instance stays running; diagnose, fix, redeploy
- Env var changes require a new deployment — always trigger a redeploy after patching a Vercel env var