- 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
```bash
# 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 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
```bash
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
```bash
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
```bash
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`):
```bash
vercel whoami 2>&1
```
Must succeed. If not → stop: "Run `vercel login` first."
**Railway** (if any service has `platform: railway`):
```bash
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`):
```bash
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:
```bash
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:
```bash
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
```bash
cd <repo>
git status --short
```
If uncommitted changes that user approved:
```bash
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:
```bash
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:
```bash
# 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:
```bash
railway service status --all 2>&1
```
If service is not deploying within ~30s → trigger manually:
```bash
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`)
```bash
cd <repo> # or app subdirectory
fly deploy --app <service.app> --strategy rolling 2>&1
```
### Custom services (`platform: custom`)
```bash
<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
```bash
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
```bash
# 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
```bash
fly status --app <app> 2>&1
```
`healthy_when: status == "running"`
### Health checks (after all services green)
For each service with `healthcheck` path:
```bash
curl -s "<service.url><service.healthcheck>" -w "\nHTTP: %{http_code}" --max-time 10
```
### Diagnosing failures
**Railway FAILED:**
```bash
# 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.toml` `healthcheckPath`
- Missing env var → check Railway service variables
**Vercel ERROR:**
```bash
# 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 `\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`)
```bash
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`)
```bash
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[]`:
1. 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 `source`d)
2. Build the curl command:
```bash
# 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 15
```
3. 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`
4. 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 |
GitHub에서 보기