--- name: vercel-deploy description: Use when deploying an existing Vercel project to production non-interactively. user-invocable: true allowed-tools: Read, Bash, Glob, Grep --- ## 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/`), 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 < "$DEPLOY_DIR/.vercel/project.json" </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 "[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 : " 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 ```