mcpField
V
vercel-deploy

vercel-deploy

github.com/fellowship-devCloud & DevOps3170 installsUpdated 2w ago
Repository

About vercel-deploy

A staged skill for deploying an existing Vercel project to production from a local directory. It requires explicit project inputs, verifies each stage, and stops on failure rather than creating projects or relying on implicit state.

Key features

  1. 01Require explicit directory and project inputs
  2. 02Check project linkage through the Vercel API
  3. 03Deploy non-interactively with the CLI
  4. 04Poll until deployment is ready or errors
  5. 05Verify the production domain response

Use cases

  1. 01Deploy an existing project from a local checkout
  2. 02Verify a production deploy against its configured domain

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/fellowship-dev/dogfooded-skills --skill vercel-deploy

After installing, type/vercel-deployin Claude Code to confirm it loaded.

Details

From the project README

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

StageNamePurpose
00author-fixFix commit author if DEPLOY_AUTHOR is set (optional, skipped otherwise)
01preflightVerify secrets, tools, compute deploy context
02linkEnsure .vercel/project.json exists; verify project via API
03deployRun vercel deploy --prod non-interactively
04pollPoll Vercel API until deployment reaches READY or ERROR
05verifyConfirm 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.

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

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

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.

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.

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.

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 "[vercel-deploy] Stage 05 failed: $VERIFY_URL returned HTTP $HTTP_STATUS"
    exit 1
    ;;
esac

After the verification command, emit the matching resolved marker as your final full assistant line (not from Bash and not inside a fence):

  • Verified: [pylot:$PYLOT_OUTCOME_NONCE] outcome="vercel-deploy succeeded" project=$PROJECT_NAME domain=$PROD_DOMAIN deploy_url=$DEPLOY_URL status=success
  • Failed: [pylot:$PYLOT_OUTCOME_NONCE] outcome="vercel-deploy failed at stage 05: $PROD_DOMAIN returned HTTP $HTTP_STATUS" status=failed

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:$PYLOT_OUTCOME_NONCE] outcome="vercel-deploy failed at stage <NN>: <reason>" status=failed
  4. Do NOT retry automatically

Outcome

On success, emit:

[pylot:$PYLOT_OUTCOME_NONCE] outcome="vercel-deploy succeeded" project=$PROJECT_NAME domain=$PROD_DOMAIN deploy_url=$DEPLOY_URL status=success

Comments

Markdown supported