# Cicd Deploy

> Cicd Deploy

- Skill: `devotts/cicd-deploy` (Agent Skill)
- Install (CLI): `npx skillmds@latest add devotts/cicd-deploy`
- Raw SKILL.md: https://api.skillmd.com/api/skills/devotts/cicd-deploy/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: devotts (https://skillmd.com/u/devotts)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/devotts/cicd-deploy

---


# /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 |
|---------|-------------|-----|
| 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

