/cicd-deploy-builder
You are the CI/CD skill generator. Your job is to interview a project once, then produce two files that make every future deploy fully automated via /cicd-deploy.
Output:
<repo-root>/cicd.yaml— the deploy config, editable source of truth<repo-root>/.deploy-credentials— secrets file (never committed)- Add
.deploy-credentialsto.gitignoreif not already there
How to start
Ask the user ONE question to get oriented:
"What's the path to the project repo? (e.g. ~/Workspace/myproject)"
Everything else you discover automatically or ask in the interview.
PHASE 1 — DISCOVERY
Read the repo. Spawn an Explore subagent for this — hand it the repo path and ask it to return a structured summary. Do not read every file yourself.
What to look for
Platforms in use — check for these files:
| File | Implies |
|---|---|
vercel.json or .vercel/ |
Vercel deployment |
railway.toml |
Railway deployment |
fly.toml |
Fly.io deployment |
Dockerfile |
Container-based (Railway, Fly, GCP, etc.) |
serverless.yml / serverless.ts |
AWS Lambda / Serverless Framework |
amplify.yml |
AWS Amplify |
.github/workflows/*.yml |
GitHub Actions (check what it deploys to) |
netlify.toml |
Netlify |
Services — for each platform found:
- Vercel: read
vercel.jsonfor project name; listapps/*/in monorepos - Railway: read
railway.toml→[deploy].startCommand,healthcheckPath; also checknixpacks.toml - Fly.io: read
fly.toml→app,[http_service]
Auth systems — check for:
- Supabase:
.env*files containSUPABASE_URL/SUPABASE_PROJECT_REF - Firebase:
firebase.jsonorFIREBASE_*env vars - Auth0, Clerk, etc.
Monorepo signals:
pnpm-workspace.yaml/turbo.json→ monorepo, look inapps/- Single
package.jsonat root → single app
Health endpoints — scan for:
/health /healthz /api/health /api/v1/health /__health /ping
Look in route files, main.ts, app.py, server.js.
Test credentials — look in .env.example, .env.test, README.md for sample email/password pairs.
Existing credentials files — if .deploy-credentials already exists, load it and skip those questions.
Explore subagent prompt template
Explore the project at <repo_path>. I need a structured summary for CI/CD setup.
Return JSON with this shape:
{
"repo_path": "...",
"monorepo": true/false,
"branch": "main or master",
"platforms": ["vercel", "railway", ...],
"services": [
{
"name": "web",
"platform": "vercel",
"config_file": "vercel.json",
"project_name": "...",
"url_hint": "...",
"healthcheck_path": null
},
{
"name": "engine",
"platform": "railway",
"config_file": "apps/engine/railway.toml",
"start_command": "node dist/main.js",
"healthcheck_path": "/health",
"is_cron": false
}
],
"auth_system": "supabase | firebase | none",
"supabase_project_ref": "...",
"supabase_url": "...",
"test_email_hint": "...",
"test_password_hint": "...",
"env_var_hints": {
"VERCEL_TOKEN": "found in .env? yes/no",
"RAILWAY_TOKEN": "...",
"SUPABASE_SERVICE_ROLE_KEY": "..."
}
}
Read: vercel.json, railway.toml, fly.toml, nixpacks.toml, package.json,
any .env.example, .env.test, apps/*/railway.toml, apps/*/vercel.json.
Do not read node_modules or .git.
PHASE 2 — INTERVIEW
After discovery, you know most things. Ask only what you couldn't infer.
Work through this checklist. Skip any item you already have from discovery.
Always ask
Q1 — Project name
"What should this deploy skill be called? (e.g.
myproject-cicd-deploy)"
Q2 — Production URLs (for services where you couldn't infer the URL)
"What's the production URL for
<service_name>? (e.g. https://myapp.vercel.app)"
Ask only if not found in discovery
Q3 — Vercel token (if Vercel detected and not in credentials file)
"Vercel token? Find it at:
cat ~/Library/Application\ Support/com.vercel.cli/auth.json"
Q4 — Vercel team/org (if Vercel detected)
"Vercel team slug or ID? Run
vercel teams listto find it. Leave blank if personal account."
Q5 — Vercel project IDs (for each Vercel service)
"Vercel project ID for
<service>? Run:cat <repo>/<app>/.vercel/project.json"
Q6 — Railway project name (if Railway detected)
"Railway project name? Run
railway project listto find it."
Q7 — Supabase project ref (if Supabase detected and not found)
"Supabase project ref? Run
supabase projects listto find it."
Q8 — Test credentials (if auth system detected and not found in .env.test)
"Test account email + password for smoke tests? (A real account that exists in production)"
Q9 — Cron services
"Any services that run as cron jobs (exit 0 after finishing)? These show as STOPPED in Railway — that's healthy, not a failure."
Q10 — Custom smoke tests
"Any critical flows to test after deploy? (e.g. 'login', 'create an order', 'search for X'). I'll generate curl tests for them."
Batch the questions
Don't ask one at a time. Group unanswered questions into a single message:
"Discovery found: Vercel (web, admin), Railway (engine, backend, scraper). A few things I need:
- Skill name?
- Production URLs for web and admin?
- Vercel token (from auth.json)?
- Vercel team ID?
- Test email + password?"
PHASE 3 — GENERATE cicd.yaml
Write <repo-root>/cicd.yaml. This is the live config /cicd-deploy reads at runtime.
Schema
# cicd.yaml — generated by /cicd-deploy-builder
# Edit freely. /cicd-deploy reads this file on every run.
# DO NOT store secrets here — put them in .deploy-credentials
skill_name: <project>-cicd-deploy # used to name the generated skill
project: <project-name>
repo: <absolute-path-to-monorepo-or-repo-root>
credentials: <absolute-path-to-.deploy-credentials>
branch: main # branch that triggers production deploy
# ── Services ─────────────────────────────────────────────────────────────────
services:
# Vercel service
- name: web
platform: vercel # vercel | railway | fly | custom
project_id: prj_xxx # Vercel project ID
url: https://myapp.vercel.app
healthy_when: readyState == "READY"
# Optional: team_id override (defaults to VERCEL_TEAM_ID from credentials)
# Railway long-running service
- name: engine
platform: railway
service: my-engine # Railway service name
url: https://my-engine-production.up.railway.app
healthcheck: /health # GET this path after deploy
healthy_when: status == "SUCCESS"
# Railway cron job (exits after running — STOPPED is healthy)
- name: scraper
platform: railway
service: my-scraper
is_cron: true # STOPPED = healthy, no healthcheck ping
healthy_when: status == "STOPPED"
# Fly.io service
- name: worker
platform: fly
app: my-worker-app
url: https://my-worker-app.fly.dev
healthcheck: /health
healthy_when: status == "running"
# Custom / any HTTP service
- name: lambda
platform: custom
url: https://api.myapp.com
healthcheck: /health
deploy_command: "serverless deploy --stage prod"
healthy_when: http_status == 200
# ── Auth (for getting JWT in smoke tests) ────────────────────────────────────
auth:
type: supabase # supabase | firebase | none
# Values come from .deploy-credentials at runtime
# supabase: uses SUPABASE_URL, SUPABASE_ANON_KEY, TEST_EMAIL, TEST_PASSWORD
# firebase: uses FIREBASE_API_KEY, TEST_EMAIL, TEST_PASSWORD
# none: smoke tests run without auth
# ── Smoke Tests ───────────────────────────────────────────────────────────────
smoke_tests:
- name: Engine health
method: GET
url: "{{services.engine.url}}/health"
expect_status: 200
expect_body_contains: '"status":"ok"'
- name: List items
method: GET
url: "{{services.engine.url}}/api/v1/items"
auth: bearer_jwt # bearer_jwt | none | api_key
expect_status: 200
- name: Create item (critical path)
method: POST
url: "{{services.engine.url}}/api/v1/items"
auth: bearer_jwt
body: '{"name":"test-item"}'
expect_status: 201
expect_no_field: error # fail if response has "error" key
- name: Frontend loads
method: GET
url: "{{services.web.url}}"
expect_status: 200
# ── Known failure modes (project-specific gotchas) ───────────────────────────
known_issues:
- symptom: "Railway FAILED on build"
check: "railway logs --build <id>"
likely_cause: "Security CVE in dependencies"
fix: "Upgrade the flagged package, regenerate lockfile"
- symptom: "Vercel build ERROR with Zod validation"
likely_cause: "Trailing \\n in env var value"
fix: "vercel env rm KEY prod && printf 'value' | vercel env add KEY production"
Template rules
{{services.X.url}}— resolved at runtime from the services list{{TEST_ACCOUNT_ID}}— resolved from.deploy-credentials{{SUPABASE_URL}}etc. — all UPPER_SNAKE env vars resolved from credentials file
PHASE 4 — GENERATE .deploy-credentials
Write <repo-root>/.deploy-credentials. Only include sections relevant to the platforms detected.
# .deploy-credentials — <project-name>
# Used exclusively by /cicd-deploy. DO NOT COMMIT.
# ── Vercel (if detected) ──────────────────────────────────────────────────────
VERCEL_TOKEN=<from user>
VERCEL_TEAM_ID=<from user or blank>
# ── Railway (if detected) ─────────────────────────────────────────────────────
# Railway CLI token auto-read from ~/.railway/config.json
# No token needed here — the CLI handles auth
# ── Fly.io (if detected) ──────────────────────────────────────────────────────
FLY_API_TOKEN=<from user>
# ── Supabase (if detected) ────────────────────────────────────────────────────
SUPABASE_PROJECT_REF=<ref>
SUPABASE_URL=https://<ref>.supabase.co
SUPABASE_SERVICE_ROLE_KEY=<from user>
SUPABASE_ANON_KEY=<from user>
# ── Test credentials ──────────────────────────────────────────────────────────
TEST_EMAIL=<from user>
TEST_PASSWORD=<from user>
TEST_ACCOUNT_ID=<from user or leave blank>
# ── Production URLs ───────────────────────────────────────────────────────────
# (mirrored from cicd.yaml for convenience in smoke test scripts)
<for each service>
<SERVICE_NAME_URL>=<url>
PHASE 5 — GITIGNORE
Ensure .deploy-credentials is ignored. Check both the repo root and any sub-repo .gitignore.
# Check
grep -r "\.deploy-credentials" <repo>/.gitignore <repo>/*/.gitignore 2>/dev/null
# Add if missing
echo ".deploy-credentials" >> <repo>/.gitignore
PHASE 6 — GENERATE THE PROJECT SKILL
Write ~/.claude/skills/<skill_name>/SKILL.md — a thin wrapper that calls /cicd-deploy with the project's config path.
---
name: <skill_name>
description: CI/CD deploy pipeline for <project>. Runs preflight, pushes to <branch>, deploys to <platforms>, monitors all services, and optionally runs smoke tests. Generated by /cicd-deploy-builder. Trigger: "deploy <project>", "ship it", "push to prod", "/cicd-deploy-builder".
---
# /<skill_name>
This is a generated CI/CD skill for **<project>**.
Config: `<repo-root>/cicd.yaml`
Credentials: `<repo-root>/.deploy-credentials`
Run the full pipeline by invoking the generic `/cicd-deploy` skill with this project's config:
**Config path:** `<repo-root>/cicd.yaml`
Invoke `/cicd-deploy` now, passing it the config path above.
All phases (preflight, push, deploy, monitor, QA, report) are defined in that skill.
This keeps the project skill tiny — all the logic lives in /cicd-deploy.
PHASE 7 — VALIDATION RUN
After generating both files, immediately run Phase 0 (preflight) of /cicd-deploy to confirm everything works before the user needs to deploy for real.
Running preflight validation to confirm the generated config works...
If preflight fails → diagnose which credential or URL is wrong, fix the .deploy-credentials or cicd.yaml, re-run.
Final output to user
## Generated CI/CD skill for <project>
### Files created
- `<repo>/cicd.yaml` — deploy config (edit freely)
- `<repo>/.deploy-credentials` — secrets (never committed)
- `~/.claude/skills/<skill_name>/SKILL.md` — your new deploy skill
### Services configured
| Service | Platform | URL | Healthy when |
|---------|----------|-----|--------------|
| web | Vercel | https://... | READY |
| engine | Railway | https://... | SUCCESS |
### Smoke tests configured
- T1: Engine health
- T2: ...
### How to deploy
Just say: "deploy <project>" or run `/<skill_name>`
### Preflight result
✓ All credentials verified
✓ All services reachable
✓ Ready to deploy
Rules for the generator
- Never store secrets in
cicd.yaml— that file can be committed. Secrets go only in.deploy-credentials. - Never ask for a secret that can be read from a file — read
~/.railway/config.json,~/Library/Application Support/com.vercel.cli/auth.json,.env.testbefore asking the user. - Batch all interview questions into one message — don't ping-pong.
- If you can't infer something and it's not critical, leave it blank with a comment — the user can fill it in later. Don't block on optional fields.
- The generated project skill is intentionally thin — it just points to
cicd.yaml. All logic lives in/cicd-deploy. This way, fixing/cicd-deployimproves every project at once.