# Vercel Deploy

> Use when deploying an existing Vercel project to production non-interactively.

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

---


## Purpose

Deploy an existing Vercel project to production from a local directory. Designed to never hang, never create new projects, and never assume implicit state.

## Required Secrets

Must be available as environment variables (from crew secret store):

- `VERCEL_TOKEN` — Access token with deploy permissions (this is the only real secret)

## Required Inputs

All inputs are passed as `KEY=value` pairs in the skill invocation or task description. Parse them from the args — **never read these from environment variables**.

- `DEPLOY_DIR` — Path to the deployable directory (e.g., `./ui`, repo root)
- `PROD_DOMAIN` — Expected production domain (e.g., `pylot.fellowship.dev`)
- `VERCEL_ORG_ID` — Organization/team ID (starts with `team_`)
- `VERCEL_PROJECT_ID` — Project ID (starts with `prj_`)

These values come from the **repo playbook** (`GET /admin/playbooks/<repo>`), not from secrets. The operator reads the playbook and passes them when invoking this skill. Deploy only through this skill's CLI flow — never rely on Vercel's Git auto-deploy integration; it bypasses the playbook-sourced inputs above and the checks in Forbidden Actions.

## Optional Inputs

- `DEPLOY_BRANCH` — Git branch to deploy from. **Defaults to `main`** if not specified. Always checks out and pulls before deploying.
- `DEPLOY_AUTHOR` — Git name of a verified Vercel team member (e.g., `maxfindel`). If provided along with `DEPLOY_EMAIL`, Stage 00 will fix the commit author before deploying. Only needed when Vercel enforces team membership on the commit author.
- `DEPLOY_EMAIL` — Git email matching the author. Required if `DEPLOY_AUTHOR` is set.

## Forbidden Actions

1. **NEVER create a new Vercel project.** If it doesn't exist, STOP and report failure.
2. **NEVER assume project auto-links.** Always verify or write `.vercel/project.json` explicitly.
3. **NEVER connect a git repo to the Vercel project.** All deploys are CLI pushes, not git-triggered.
4. **NEVER run `vercel` without `--token` and `--yes`.** The CLI hangs forever without them.
5. **NEVER run `vercel link`.** It prompts interactively. Write `.vercel/project.json` directly.
6. **NEVER run `vercel project create` or equivalent.** Separate procedure exists for that.

## Stage Overview

| Stage | Name | Purpose |
|-------|------|---------|
| 00 | author-fix | Fix commit author if DEPLOY_AUTHOR is set (optional, skipped otherwise) |
| 01 | preflight | Verify secrets, tools, compute deploy context |
| 02 | link | Ensure `.vercel/project.json` exists; verify project via API |
| 03 | deploy | Run `vercel deploy --prod` non-interactively |
| 04 | poll | Poll Vercel API until deployment reaches READY or ERROR |
| 05 | verify | Confirm production domain is reachable, emit outcome |

## Stage 00 — Author Fix (conditional)

**Skip this stage if `DEPLOY_AUTHOR` is not provided.** Not all Vercel projects enforce team membership on commit authors.

When Vercel does enforce it (`TEAM_ACCESS_REQUIRED` / `seatBlock`), bot accounts (e.g. `fry-lobster`) will block CLI deploys. The fix is an empty commit with a team member as author.

```bash
# Only run if DEPLOY_AUTHOR is set in the skill params
if [ -n "$DEPLOY_AUTHOR" ] && [ -n "$DEPLOY_EMAIL" ]; then
  HEAD_AUTHOR=$(git log -1 --format='%an')
  if [ "$HEAD_AUTHOR" != "$DEPLOY_AUTHOR" ]; then
    echo "[vercel-deploy] HEAD author '$HEAD_AUTHOR' is not a Vercel team member."
    echo "[vercel-deploy] Creating empty commit with author '$DEPLOY_AUTHOR' to unblock deploy."
    git commit --allow-empty \
      --author="$DEPLOY_AUTHOR <$DEPLOY_EMAIL>" \
      -m "chore: vercel deploy author fix (empty commit)"
    git push origin HEAD
  fi
else
  echo "[vercel-deploy] Stage 00 skipped — no DEPLOY_AUTHOR configured"
fi
```

## Stage 01 — Preflight

Verify all secrets and inputs exist, tools are installed, and checkout the deploy branch.

```bash
# Verify required secret (from crew secret store — the only real secret)
[ -z "$VERCEL_TOKEN" ] && { echo "[vercel-deploy] MISSING secret: VERCEL_TOKEN"; exit 1; }

# Verify required inputs (parsed from skill args / task description — from the repo playbook)
[ -z "$DEPLOY_DIR" ]        && { echo "[vercel-deploy] MISSING input: DEPLOY_DIR — check the repo playbook"; exit 1; }
[ -z "$PROD_DOMAIN" ]       && { echo "[vercel-deploy] MISSING input: PROD_DOMAIN — check the repo playbook"; exit 1; }
[ -z "$VERCEL_ORG_ID" ]     && { echo "[vercel-deploy] MISSING input: VERCEL_ORG_ID — check the repo playbook"; exit 1; }
[ -z "$VERCEL_PROJECT_ID" ] && { echo "[vercel-deploy] MISSING input: VERCEL_PROJECT_ID — check the repo playbook"; exit 1; }

# Optional: warn if author-fix params are incomplete (both or neither)
if [ -n "$DEPLOY_AUTHOR" ] && [ -z "$DEPLOY_EMAIL" ]; then
  echo "[vercel-deploy] WARNING: DEPLOY_AUTHOR set but DEPLOY_EMAIL missing — Stage 00 will be skipped"
fi

# Verify vercel CLI is available
command -v vercel >/dev/null 2>&1 || command -v npx >/dev/null 2>&1 || {
  echo "[vercel-deploy] vercel CLI not available — install with: npm i -g vercel"
  exit 1
}

# Verify deploy directory exists
[ -d "$DEPLOY_DIR" ] || { echo "[vercel-deploy] DEPLOY_DIR not found: $DEPLOY_DIR"; exit 1; }

# Checkout and pull deploy branch
DEPLOY_BRANCH="${DEPLOY_BRANCH:-main}"
git checkout "$DEPLOY_BRANCH" && git pull origin "$DEPLOY_BRANCH" || {
  echo "[vercel-deploy] Failed to checkout/pull $DEPLOY_BRANCH"
  exit 1
}

# Save state for downstream stages
cat > /tmp/vercel-deploy-ctx.env <<EOF
DEPLOY_BRANCH=$DEPLOY_BRANCH
DEPLOY_DIR=$DEPLOY_DIR
PROD_DOMAIN=$PROD_DOMAIN
EOF

echo "[vercel-deploy] Stage 01 complete — preflight passed, branch: $DEPLOY_BRANCH"
```

## Stage 02 — Link

Write `.vercel/project.json` directly (never use `vercel link`), then verify the project exists via API.

```bash
source /tmp/vercel-deploy-ctx.env

# Write project link file directly
mkdir -p "$DEPLOY_DIR/.vercel"
cat > "$DEPLOY_DIR/.vercel/project.json" <<EOF
{
  "orgId": "$VERCEL_ORG_ID",
  "projectId": "$VERCEL_PROJECT_ID"
}
EOF

# Verify project exists via Vercel API
PROJECT_JSON=$(curl -sf \
  -H "Authorization: Bearer $VERCEL_TOKEN" \
  "https://api.vercel.com/v9/projects/$VERCEL_PROJECT_ID?teamId=$VERCEL_ORG_ID")

[ -z "$PROJECT_JSON" ] && {
  echo "[vercel-deploy] Stage 02 failed: project $VERCEL_PROJECT_ID not found — STOP, do not create"
  exit 1
}

PROJECT_NAME=$(echo "$PROJECT_JSON" | python3 -c \
  "import sys,json; d=json.load(sys.stdin); print(d.get('name','unknown'))" 2>/dev/null || echo "unknown")

echo "PROJECT_NAME=$PROJECT_NAME" >> /tmp/vercel-deploy-ctx.env
echo "[vercel-deploy] Stage 02 complete — linked to project '$PROJECT_NAME' ($VERCEL_PROJECT_ID)"
```

## Stage 03 — Deploy

Run `vercel deploy --prod` non-interactively. Must use `--token` and `--yes` — without them the CLI hangs.

```bash
source /tmp/vercel-deploy-ctx.env

cd "$DEPLOY_DIR"
DEPLOY_OUTPUT=$(npx vercel deploy --prod --token="$VERCEL_TOKEN" --yes 2>&1)
DEPLOY_EXIT=$?

echo "$DEPLOY_OUTPUT"

if [ $DEPLOY_EXIT -ne 0 ]; then
  echo "[vercel-deploy] Stage 03 failed: vercel deploy exited $DEPLOY_EXIT"
  exit 1
fi

# Extract deployment URL (last https:// line in output)
DEPLOY_URL=$(echo "$DEPLOY_OUTPUT" | grep -E '^https://' | tail -1)
[ -z "$DEPLOY_URL" ] && {
  echo "[vercel-deploy] Stage 03 failed: no deployment URL found in vercel output"
  exit 1
}

echo "DEPLOY_URL=$DEPLOY_URL" >> /tmp/vercel-deploy-ctx.env
echo "[vercel-deploy] Stage 03 complete — deployment URL: $DEPLOY_URL"
```

## Stage 04 — Poll

Poll the Vercel API until the deployment reaches `READY` or `ERROR`. Timeout after 120 seconds.

```bash
source /tmp/vercel-deploy-ctx.env

MAX_WAIT=120
ELAPSED=0
DEPLOY_HOST=$(echo "$DEPLOY_URL" | sed 's|https://||')

while [ $ELAPSED -lt $MAX_WAIT ]; do
  DEPLOY_STATE=$(curl -sf \
    -H "Authorization: Bearer $VERCEL_TOKEN" \
    "https://api.vercel.com/v13/deployments?url=${DEPLOY_HOST}&teamId=$VERCEL_ORG_ID&limit=1" \
    | python3 -c \
      "import sys,json; d=json.load(sys.stdin); deps=d.get('deployments',[]); print(deps[0].get('state','UNKNOWN') if deps else 'NOT_FOUND')" \
      2>/dev/null || echo "API_ERROR")

  echo "[vercel-deploy] Deploy state: $DEPLOY_STATE (${ELAPSED}s elapsed)"

  case "$DEPLOY_STATE" in
    READY)
      echo "[vercel-deploy] Stage 04 complete — deployment READY"
      break
      ;;
    ERROR|CANCELED)
      echo "[vercel-deploy] Stage 04 failed: deployment reached terminal state $DEPLOY_STATE"
      exit 1
      ;;
    *)
      sleep 10
      ELAPSED=$((ELAPSED + 10))
      ;;
  esac
done

[ $ELAPSED -ge $MAX_WAIT ] && {
  echo "[vercel-deploy] Stage 04 failed: timed out after ${MAX_WAIT}s — last state: $DEPLOY_STATE"
  exit 1
}
```

## Stage 05 — Verify

Confirm the production domain is reachable (HTTP 200, 301, or 302), then emit outcome.

```bash
source /tmp/vercel-deploy-ctx.env

VERIFY_URL="https://$PROD_DOMAIN"
HTTP_STATUS=$(curl -sf -o /dev/null -w "%{http_code}" --max-time 15 "$VERIFY_URL" 2>/dev/null || echo "000")

echo "[vercel-deploy] $VERIFY_URL → HTTP $HTTP_STATUS"

case "$HTTP_STATUS" in
  200|301|302)
    echo "[vercel-deploy] Stage 05 complete — production domain verified"
    echo "[pylot] outcome=\"vercel-deploy succeeded\" project=$PROJECT_NAME domain=$PROD_DOMAIN deploy_url=$DEPLOY_URL status=done"
    ;;
  *)
    echo "[vercel-deploy] Stage 05 failed: $VERIFY_URL returned HTTP $HTTP_STATUS"
    echo "[pylot] outcome=\"vercel-deploy failed at stage 05: $PROD_DOMAIN returned HTTP $HTTP_STATUS\" status=failed"
    exit 1
    ;;
esac
```

## Execution Model

- Follow stages **in order**. Each stage's checkpoint must pass before proceeding.
- **Do not skip stages.** Every checkpoint is verified.
- **Fail fast.** If any checkpoint fails, stop and report with `status=failed`.
- State is passed between stages via `/tmp/vercel-deploy-ctx.env`.

## Critical Rules

1. Sequential execution only. Stage N's checkpoint must pass before starting stage N+1.
2. No skipping. Every stage runs, every checkpoint is verified.
3. Fail fast. If any checkpoint fails, stop and report.
4. Every `vercel` or `npx vercel` invocation MUST include `--token="$VERCEL_TOKEN" --yes`.

## Error Handling

If any stage fails:
1. Print which stage failed and the exact error
2. Include the Vercel API response body if available
3. Emit: `[pylot] outcome="vercel-deploy failed at stage <NN>: <reason>" status=failed`
4. Do NOT retry automatically

## Outcome

On success, emit:
```
[pylot] outcome="vercel-deploy succeeded" project=$PROJECT_NAME domain=$PROD_DOMAIN deploy_url=$DEPLOY_URL status=done
```

