/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
# The config path is provided by the caller
CONFIG_PATH="<path-to-cicd.yaml>"
cat "$CONFIG_PATH"
Parse the YAML mentally. Extract:
repo— absolute path to repo rootcredentials— absolute path to.deploy-credentialsbranch— deploy branch (usuallymain)services[]— all services with their platform, URL, healthcheck, healthy_whenauth— auth type for smoke testssmoke_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>
# Spot-check that key vars are set
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:
# Get latest ready deployment ID first
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'])")
# Redeploy
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> # or app subdirectory
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 successfullystatus == "STOPPED"→ cron job exited cleanly (expected)status == "FAILED"→ deployment failed → diagnose
Vercel services
# Poll each Vercel deployment by ID
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:
# Get the failed deployment ID
railway deployment list 2>&1 | head -10
# Check build logs for root cause
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.tomlhealthcheckPath - Missing env var → check Railway service variables
Vercel ERROR:
# Get build logs via API
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
\nin env var → re-set withprintf "value" | vercel env add KEY production - TypeScript errors → check
ignoreBuildErrorsinnext.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 (alreadysourced)
Build the curl command:
# GET test curl -s -X GET "<resolved_url>" \ [-H "Authorization: Bearer $JWT" # if auth: bearer_jwt] \ -w "\nHTTP: %{http_code}" --max-time 15 # POST test curl -s -X POST "<resolved_url>" \ -H "Content-Type: application/json" \ [-H "Authorization: Bearer $JWT" # if auth: bearer_jwt] \ -d '<body>' \ -w "\nHTTP: %{http_code}" --max-time 15Evaluate pass/fail:
expect_status: 200→ HTTP code must matchexpect_body_contains: "..."→ response body must contain the stringexpect_no_field: error→ response JSON must not have keyerror
Report:
✓ PASSor✗ 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, nevergit addit - Cron services (
is_cron: true) withhealthy_when: status == "STOPPED"are healthy — do not treat STOPPED as failure - Set env vars with
printf, notecho— trailing\nfromechobreaks 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