关于 vercel-deploy
这项分阶段技能从本地目录将现有 Vercel 项目部署到生产环境。它要求明确提供项目参数并逐步验证,遇到失败即停止,不会创建项目或依赖隐式状态。
主要功能
- 01要求明确提供目录和项目参数
- 02通过 Vercel API 检查项目关联
- 03使用 CLI 以非交互方式部署
- 04轮询部署直至就绪或报错
- 05验证生产域名的响应
适用场景
- 01从本地检出目录部署现有项目
- 02根据配置的域名验证生产部署
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ fellowship-dev/ dogfooded-skills --skill vercel-deploy安装后在 Claude Code 中输入/vercel-deploy确认已加载。
详细介绍
用途
从本地目录将现有的 Vercel 项目部署到生产环境。设计目标是绝不挂起、绝不创建新项目,也绝不假设存在隐式状态。
必需的密钥
必须以环境变量的形式提供(来自 crew 密钥存储):
VERCEL_TOKEN— 具有部署权限的访问令牌(这是唯一真正的密钥)
必需的输入
所有输入都以 KEY=value 键值对的形式传入技能调用或任务说明中。请从参数中解析这些值——绝不要从环境变量中读取。
DEPLOY_DIR— 可部署目录的路径(例如,./ui、仓库根目录)PROD_DOMAIN— 预期的生产域名(例如,pylot.fellowship.dev)VERCEL_ORG_ID— 组织/团队 ID(以team_开头)VERCEL_PROJECT_ID— 项目 ID(以prj_开头)
这些值来自仓库操作手册(GET /admin/playbooks/<repo>),而非密钥。操作人员读取操作手册,并在调用此技能时传入这些值。只能通过此技能的 CLI 流程进行部署——绝不要依赖 Vercel 的 Git 自动部署集成;它会绕过上述来自操作手册的输入,以及“禁止的操作”中的检查。
可选的输入
DEPLOY_BRANCH— 要部署的 Git 分支。未指定时默认为main。部署前始终会检出并拉取该分支。DEPLOY_AUTHOR— 已验证的 Vercel 团队成员的 Git 名称(例如,maxfindel)。如果与DEPLOY_EMAIL一起提供,阶段 00 会在部署前修正提交作者。仅当 Vercel 强制要求提交作者属于团队时才需要。DEPLOY_EMAIL— 与作者匹配的 Git 邮箱。设置DEPLOY_AUTHOR时必须提供。
禁止的操作
- 绝不要创建新的 Vercel 项目。 如果项目不存在,请停止并报告失败。
- 绝不要假设项目会自动关联。 始终验证
.vercel/project.json是否存在,或直接写入该文件。 - 绝不要将 Git 仓库连接到 Vercel 项目。 所有部署都通过 CLI 推送,而非由 Git 触发。
- 绝不要在未带
--token和--yes的情况下运行vercel。 缺少这些参数时,CLI 会永久挂起。 - 绝不要运行
vercel link。 它会进行交互式提示。请直接写入.vercel/project.json。 - 绝不要运行
vercel project create或等效命令。 创建项目有单独的流程。
阶段概览
| 阶段 | 名称 | 用途 |
|---|---|---|
| 00 | author-fix | 如果设置了 DEPLOY_AUTHOR,则修正提交作者(可选,否则跳过) |
| 01 | preflight | 验证密钥和工具,并计算部署上下文 |
| 02 | link | 确保 .vercel/project.json 存在;通过 API 验证项目 |
| 03 | deploy | 以非交互方式运行 vercel deploy --prod |
| 04 | poll | 轮询 Vercel API,直到部署状态变为 READY 或 ERROR |
| 05 | verify | 确认生产域名可访问,并输出结果 |
阶段 00 — 修正作者(有条件执行)
如果未提供 DEPLOY_AUTHOR,请跳过此阶段。 并非所有 Vercel 项目都会强制要求提交作者属于团队。
当 Vercel 强制执行此要求时(TEAM_ACCESS_REQUIRED / seatBlock),机器人账号(例如,fry-lobster)会阻止 CLI 部署。解决方法是创建一个由团队成员作为作者的空提交。
# 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
阶段 01 — 预检
验证所有密钥和输入均已提供、工具均已安装,并检出部署分支。
# 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"
阶段 02 — 关联
直接写入 .vercel/project.json(绝不要使用 vercel link),然后通过 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)"
阶段 03 — 部署
以非交互方式运行 vercel deploy --prod。必须使用 --token 和 --yes——缺少这些参数时,CLI 会挂起。
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"
阶段 04 — 轮询
轮询 Vercel API,直到部署状态变为 READY 或 ERROR。120 秒后超时。
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
}
阶段 05 — 验证
确认生产域名可访问(HTTP 200、301 或 302),然后输出结果。
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
验证命令执行后,将匹配的已解析标记作为助手最后一整行输出(不能通过 Bash 输出,也不能放在代码围栏中):
- 已验证:[pylot:$PYLOT_OUTCOME_NONCE] outcome="vercel-deploy succeeded" project=$PROJECT_NAME domain=$PROD_DOMAIN deploy_url=$DEPLOY_URL status=success
- 失败:[pylot:$PYLOT_OUTCOME_NONCE] outcome="vercel-deploy failed at stage 05: $PROD_DOMAIN returned HTTP $HTTP_STATUS" status=failed
执行模型
- 按顺序执行各阶段。只有当前阶段的检查点通过后,才能继续。
- 不得跳过阶段。 每个检查点都必须经过验证。
- 快速失败。 如果任何检查点失败,立即停止并报告
status=failed。 - 各阶段之间通过
/tmp/vercel-deploy-ctx.env传递状态。
关键规则
- 仅按顺序执行。第 N 阶段的检查点通过后,才能开始第 N+1 阶段。
- 不得跳过阶段。必须运行每个阶段,并验证每个检查点。
- 快速失败。如果任何检查点失败,立即停止并报告。
- 每次调用
vercel或npx vercel时,都必须包含--token="$VERCEL_TOKEN" --yes。
错误处理
如果任何阶段失败:
- 输出失败的阶段及确切错误
- 如果有 Vercel API 响应正文,则将其包含在内
- 输出:
[pylot:$PYLOT_OUTCOME_NONCE] outcome="vercel-deploy failed at stage <NN>: <reason>" status=failed - 不得自动重试
结果
成功时,输出:
[pylot:$PYLOT_OUTCOME_NONCE] outcome="vercel-deploy succeeded" project=$PROJECT_NAME domain=$PROD_DOMAIN deploy_url=$DEPLOY_URL status=success