Files
ai-proj-helper/skills-dev/dev-deploy-plugin/skills/SKILL.md
T
johnQ 237631147a chore(skill): 清理全部 23 条历史校验警告
- 11 个超 20KB 的 SKILL.md 拆分,大章节移入 references/details.md
- 7 处非标准字段(arguments/user_invocable)移入 metadata
- 修 req-dev 4 条相对链接;validate 跳过行内代码避免 ![](...) 误报
- validate-skills.py: 23 warns -> 0 warns,0 errors
2026-10-31 22:21:07 +08:00

17 KiB
Raw Blame History

name, description
name description
dev-deploy 应用部署技能。支持 iOS TestFlight、Docker 容器等多平台部署。当用户提到部署、发布、TestFlight、上架、build、archive 相关任务时自动激活。

概述

管理应用从构建到发布的完整部署流程,支持多平台:

  • iOS: TestFlight 内测 / App Store 发布
  • Docker: Staging / Production 容器部署

集成 ai-proj 任务系统进行部署记录和需求阶段推进。


命令参考

命令 说明
/deploy ios iOS TestFlight 部署
/deploy docker [staging|prod] Docker 容器部署
/deploy status 查看部署状态

iOS TestFlight 部署

前置条件

项目 要求
构建机器 macOS + Xcode(通过 SSH 访问)
签名证书 Apple Distribution 证书已安装在 Keychain
Provisioning Profile App Store Distribution profile 已安装
API Key App Store Connect API Key (.p8)
sshpass 本机安装用于非交互 SSH(brew install hudochenkov/sshpass/sshpass)
xcodegen 构建机器安装用于从 project.yml 生成 xcodeproj

完整部署流程

1. git push        → 代码推送到远程仓库
2. SSH 连接构建机   → git pull 拉取最新代码
3. xcodebuild archive → 无签名构建 Archive
4. xcodebuild -exportArchive → Distribution 签名 + 上传 TestFlight
5. ASC API 补全     → 合规信息 + 测试说明 + build 关联版本
6. 验证             → 确认 TestFlight 状态为 IN_BETA_TESTING

Step 1: SSH 连接构建机

# 使用 sshpass 进行非交互 SSH
sshpass -p '<password>' ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no <user>@<host> '<command>'

关键经验:

  • SSH 远程 codesign 需要先解锁 Keychain,否则报 errSecInternalComponent
  • 还需要 set-key-partition-list 授权 codesign 访问密钥
# 必须在每次 SSH 会话开头执行
security unlock-keychain -p "<password>" ~/Library/Keychains/login.keychain-db
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "<password>" ~/Library/Keychains/login.keychain-db

Step 2: 拉取代码

cd <repo-path> && git pull origin develop

Step 3: Archive(无签名)

关键经验:Archive 阶段不要签名。原因:

  • xcodebuild CLI 签名参数会泄漏到 SPM 依赖的 targets,导致 "does not support provisioning profiles" 错误
  • 正确做法是 archive 时禁用签名,在 export 阶段单独签名
xcodebuild archive \
  -project XiaoquCRM.xcodeproj \
  -scheme XiaoquCRM \
  -destination 'generic/platform=iOS' \
  -configuration Release \
  -archivePath ~/Desktop/XiaoquCRM.xcarchive \
  -skipMacroValidation \
  CODE_SIGNING_ALLOWED=NO \
  CODE_SIGNING_REQUIRED=NO

常见错误及解决:

错误 原因 解决
Macro "X" must be enabled Swift Macros 安全限制 加 -skipMacroValidation
cannot find type 'AdminFeature' xcodeproj 未包含新文件 运行 xcodegen generate 重新生成
SPM 依赖报签名错误 签名参数泄漏到依赖 Archive 用 CODE_SIGNING_ALLOWED=NO

Step 4: Export + 上传 TestFlight

# ExportOptions.plist(提前创建在构建机上)
cat > /tmp/ExportOptions.plist << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>method</key>
    <string>app-store-connect</string>
    <key>destination</key>
    <string>upload</string>
    <key>teamID</key>
    <string>{TEAM_ID}</string>
    <key>signingStyle</key>
    <string>manual</string>
    <key>signingCertificate</key>
    <string>Apple Distribution</string>
    <key>provisioningProfiles</key>
    <dict>
        <key>{BUNDLE_ID}</key>
        <string>{PROFILE_NAME}</string>
    </dict>
    <key>manageAppVersionAndBuildNumber</key>
    <true/>
</dict>
</plist>
EOF

# Export + Upload
xcodebuild -exportArchive \
  -archivePath ~/Desktop/XiaoquCRM.xcarchive \
  -exportOptionsPlist /tmp/ExportOptions.plist \
  -exportPath ~/Desktop/XiaoquCRM-export \
  -authenticationKeyPath {API_KEY_PATH} \
  -authenticationKeyID {KEY_ID} \
  -authenticationKeyIssuerID {ISSUER_ID}

关键经验:

问题 教训
errSecInternalComponent SSH 远程签名前必须 unlock-keychain + set-key-partition-list
No signing certificate "iOS Distribution" 机器上没装 Distribution 证书,需在 Xcode > Accounts 登录 Apple ID 下载
Redundant Binary Upload build number 重复,需要在 project.yml 递增 CURRENT_PROJECT_VERSION
Missing required icon file 需要 Assets.xcassets/AppIcon.appiconset 含 1024x1024 PNG
UIInterfaceOrientation iPad 错误 必须声明 iPad 四方向支持,或设置 UIRequiresFullScreen=true
Cloud signing permission error API Key 权限不够或 Issuer ID 错误;改用手动签名 + 本地 profile

Step 5: ASC API 补全 TestFlight 信息

上传成功后,需要通过 App Store Connect API 补全三项信息,否则测试者收不到通知或无法安装:

5.1 生成 JWT Token

import jwt, time
key = open("AuthKey_XXXXXX.p8").read()
token = jwt.encode(
    {"iss": "{ISSUER_ID}", "iat": int(time.time()),
     "exp": int(time.time()) + 1200, "aud": "appstoreconnect-v1"},
    key, algorithm="ES256",
    headers={"kid": "{KEY_ID}"}  # ← 必须包含 kid!
)

关键经验:JWT 必须包含 headers={"kid": KEY_ID},否则 401 认证失败。还需要安装 cryptography 库支持 ES256。

5.2 设置出口合规

PATCH /v1/builds/{build_id}
{"data": {"type": "builds", "id": "{build_id}",
  "attributes": {"usesNonExemptEncryption": false}}}

不设置此项,build 会卡在 "Missing Compliance" 状态,内部测试者无法安装。

5.3 填写测试说明 (whatsNew)

# 先获取 localization ID
GET /v1/builds/{build_id}/betaBuildLocalizations

# 更新 whatsNew
PATCH /v1/betaBuildLocalizations/{loc_id}
{"data": {"type": "betaBuildLocalizations", "id": "{loc_id}",
  "attributes": {"whatsNew": "更新内容..."}}}

5.4 关联 Build 到 App Store 版本

关键经验:App Store Connect 页面的 App Icon 来自关联的 build。如果没有把 build 关联到 App Store 版本,图标显示为空。

# 关联 build 到版本
PATCH /v1/appStoreVersions/{version_id}/relationships/build
{"data": {"type": "builds", "id": "{build_id}"}}

Step 6: 验证部署状态

# 检查 build 状态
GET /v1/builds/{build_id}?include=buildBetaDetail

# 期望结果:
# processingState: VALID
# internalBuildState: IN_BETA_TESTING
# usesNonExemptEncryption: false

一键部署脚本模板

将以上步骤整合为单次 SSH 调用:

sshpass -p '<password>' ssh -o PreferredAuthentications=password \
  -o PubkeyAuthentication=no <user>@<host> '
# 0. Keychain
security unlock-keychain -p "<password>" ~/Library/Keychains/login.keychain-db
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "<password>" ~/Library/Keychains/login.keychain-db 2>/dev/null

# 1. Pull
cd <repo> && git pull origin develop

# 2. Archive
cd ios && rm -rf ~/Desktop/App.xcarchive ~/Desktop/App-export
xcodebuild archive -project App.xcodeproj -scheme App \
  -destination "generic/platform=iOS" -configuration Release \
  -archivePath ~/Desktop/App.xcarchive \
  -skipMacroValidation CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO \
  2>&1 | tail -1

# 3. Export + Upload
xcodebuild -exportArchive \
  -archivePath ~/Desktop/App.xcarchive \
  -exportOptionsPlist /tmp/ExportOptions.plist \
  -exportPath ~/Desktop/App-export \
  -authenticationKeyPath <key_path> \
  -authenticationKeyID <key_id> \
  -authenticationKeyIssuerID <issuer_id> \
  2>&1 | grep -E "Upload|EXPORT|error:" | tail -5
'

iOS 部署检查清单

部署前逐项确认:

  • build number 已递增(CURRENT_PROJECT_VERSION in project.yml)
  • xcodegen generate 已运行(新文件已包含在 xcodeproj 中)
  • 代码已 push 到远程仓库
  • 构建机可 SSH 访问
  • Assets.xcassets 包含 1024x1024 App Icon
  • Info.plist 包含 iPad 四方向支持
  • Distribution 证书已安装在构建机 Keychain

部署后逐项确认:

  • Archive 成功
  • Export + Upload 成功
  • 合规信息已设置(usesNonExemptEncryption)
  • 测试说明已填写(whatsNew)
  • Build 已关联到 App Store 版本
  • TestFlight 状态为 IN_BETA_TESTING
  • 测试者收到更新通知

Docker Staging/Production 部署

架构概览

develop push → Build Image → Push ACR → SSH Deploy (staging) → Health Check
main push    → Build Image → Push ACR → 人工审批 → SSH Deploy (prod) → Health Check
组件 说明
服务器 39.104.87.246(阿里云 ECS)
Registry Aliyun ACR: crpi-q4nnuivosic0zc98.cn-beijing.personal.cr.aliyuncs.com
镜像 xiaoqu-gateway, xiaoqu-web
SSH Key ~/.ssh/xiaoqu.pem
部署方式 Docker Compose

完整部署流程

1. 本地构建镜像   → docker build -t <image>:<tag>
2. 推送到 ACR     → docker push <registry>/<image>:<tag>
3. SSH 到服务器   → docker compose pull + up -d
4. 健康检查       → curl /health
5. 通知           → 飞书 Webhook 发送部署结果

Staging 部署(develop 分支自动触发)

Push 到 develop 分支自动触发 staging 部署。流程:

# 1. 构建镜像(tag 用 commit SHA 前 8 位)
TAG=$(git rev-parse --short=8 HEAD)
REGISTRY=crpi-q4nnuivosic0zc98.cn-beijing.personal.cr.aliyuncs.com

docker build -t $REGISTRY/xiaoqu-gateway:$TAG -f gateway/Dockerfile .
docker build -t $REGISTRY/xiaoqu-web:$TAG -f web/Dockerfile .

# 2. 推送到 ACR
docker push $REGISTRY/xiaoqu-gateway:$TAG
docker push $REGISTRY/xiaoqu-web:$TAG

# 3. SSH 部署
ssh -i ~/.ssh/xiaoqu.pem root@39.104.87.246 "
  cd /opt/xiaoqu/staging
  export IMAGE_TAG=$TAG
  docker compose pull
  docker compose up -d
"

# 4. 健康检查
sleep 10
curl -sf http://39.104.87.246:8080/health || echo 'Health check failed!'

Production 部署(手动审批)

Production 部署需要人工确认,不会自动触发:

# 使用 build-and-push 脚本
./scripts/build-and-push.sh prod --detect --deploy --wait --verify

# 或手动执行:
TAG=v1.2.3  # 使用语义化版本号
REGISTRY=crpi-q4nnuivosic0zc98.cn-beijing.personal.cr.aliyuncs.com

# 构建 + 推送
docker build -t $REGISTRY/xiaoqu-gateway:$TAG -f gateway/Dockerfile .
docker build -t $REGISTRY/xiaoqu-web:$TAG -f web/Dockerfile .
docker push $REGISTRY/xiaoqu-gateway:$TAG
docker push $REGISTRY/xiaoqu-web:$TAG

# 部署(生产环境目录)
ssh -i ~/.ssh/xiaoqu.pem root@39.104.87.246 "
  cd /opt/xiaoqu/production
  export IMAGE_TAG=$TAG
  docker compose pull
  docker compose up -d
"

# 验证
curl -sf http://39.104.87.246/health && echo 'Production deploy OK'

build-and-push 脚本模板

#!/bin/bash
# scripts/build-and-push.sh
set -euo pipefail

ENV=${1:-staging}
REGISTRY=crpi-q4nnuivosic0zc98.cn-beijing.personal.cr.aliyuncs.com
SERVER=39.104.87.246
SSH_KEY=~/.ssh/xiaoqu.pem
IMAGES=(xiaoqu-gateway xiaoqu-web)

# 确定 tag
if [ "$ENV" = "prod" ]; then
  TAG=${2:-$(git describe --tags --abbrev=0)}
else
  TAG=$(git rev-parse --short=8 HEAD)
fi

echo "=== Deploying to $ENV with tag $TAG ==="

# 构建
for img in "${IMAGES[@]}"; do
  echo "Building $img..."
  docker build -t $REGISTRY/$img:$TAG -f ${img#xiaoqu-}/Dockerfile .
done

# 推送
for img in "${IMAGES[@]}"; do
  echo "Pushing $img..."
  docker push $REGISTRY/$img:$TAG
done

# 部署
DEPLOY_DIR=/opt/xiaoqu/$ENV
ssh -i $SSH_KEY root@$SERVER "
  cd $DEPLOY_DIR
  export IMAGE_TAG=$TAG
  docker compose pull
  docker compose up -d --remove-orphans
"

# 健康检查(重试 3 次)
echo "Waiting for health check..."
for i in 1 2 3; do
  sleep 5
  if curl -sf http://$SERVER/health > /dev/null 2>&1; then
    echo "✓ Health check passed"
    exit 0
  fi
  echo "Attempt $i failed, retrying..."
done

echo "✗ Health check failed after 3 attempts"
exit 1

Docker Compose 示例

# docker-compose.yml
version: "3.8"
services:
  gateway:
    image: crpi-q4nnuivosic0zc98.cn-beijing.personal.cr.aliyuncs.com/xiaoqu-gateway:${IMAGE_TAG:-latest}
    ports:
      - "8080:8080"
    environment:
      - DATABASE_URL=postgres://...
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    restart: unless-stopped

  web:
    image: crpi-q4nnuivosic0zc98.cn-beijing.personal.cr.aliyuncs.com/xiaoqu-web:${IMAGE_TAG:-latest}
    ports:
      - "3000:3000"
    depends_on:
      gateway:
        condition: service_healthy
    restart: unless-stopped

部署前健康检查

部署前进行预检,避免部署失败浪费时间。

iOS 预检

preflight_ios() {
  local errors=0

  # 检查 Distribution 证书
  if ! security find-identity -v -p codesigning | grep -q "Apple Distribution"; then
    echo "ERROR: Apple Distribution 证书未安装"
    ((errors++))
  fi

  # 检查 Provisioning Profile 有效期
  local profile_dir="$HOME/Library/MobileDevice/Provisioning Profiles"
  if [ -d "$profile_dir" ]; then
    for profile in "$profile_dir"/*.mobileprovision; do
      local expiry
      expiry=$(security cms -D -i "$profile" 2>/dev/null | plutil -extract ExpirationDate raw - 2>/dev/null)
      if [ -n "$expiry" ]; then
        local expiry_epoch
        expiry_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$expiry" "+%s" 2>/dev/null)
        local now_epoch
        now_epoch=$(date "+%s")
        if [ "$expiry_epoch" -lt "$now_epoch" ]; then
          echo "WARNING: Profile 已过期: $(basename "$profile")"
          ((errors++))
        fi
      fi
    done
  else
    echo "ERROR: Provisioning Profiles 目录不存在"
    ((errors++))
  fi

  # 检查 API Key
  if [ ! -f "${API_KEY_PATH:-/dev/null}" ]; then
    echo "ERROR: ASC API Key (.p8) 文件不存在: $API_KEY_PATH"
    ((errors++))
  fi

  # 检查 Xcode
  if ! xcode-select -p > /dev/null 2>&1; then
    echo "ERROR: Xcode Command Line Tools 未安装"
    ((errors++))
  fi

  if [ $errors -gt 0 ]; then
    echo "iOS 预检失败: $errors 个问题"
    return 1
  fi
  echo "iOS 预检通过"
  return 0
}

Docker 预检

preflight_docker() {
  local errors=0

  # 检查 Docker daemon
  if ! docker info > /dev/null 2>&1; then
    echo "ERROR: Docker daemon 未运行"
    ((errors++))
  fi

  # 检查 ACR registry 可达
  local registry=crpi-q4nnuivosic0zc98.cn-beijing.personal.cr.aliyuncs.com
  if ! docker login $registry --username dummy --password dummy 2>&1 | grep -qv "connection refused"; then
    # login 会失败但不应该是 connection refused
    echo "WARNING: ACR registry 可能不可达(将在 push 时验证)"
  fi

  # 检查 SSH 连通性
  if ! ssh -i ~/.ssh/xiaoqu.pem -o ConnectTimeout=5 -o BatchMode=yes root@39.104.87.246 "echo ok" > /dev/null 2>&1; then
    echo "ERROR: 无法 SSH 连接到部署服务器 39.104.87.246"
    ((errors++))
  fi

  # 检查服务器磁盘空间
  local disk_usage
  disk_usage=$(ssh -i ~/.ssh/xiaoqu.pem root@39.104.87.246 "df -h / | tail -1 | awk '{print \$5}' | tr -d '%'" 2>/dev/null)
  if [ -n "$disk_usage" ] && [ "$disk_usage" -gt 85 ]; then
    echo "WARNING: 服务器磁盘使用率 ${disk_usage}%(建议清理 docker system prune)"
  fi

  # 检查本地磁盘空间
  local local_disk
  local_disk=$(df -h . | tail -1 | awk '{print $5}' | tr -d '%')
  if [ "$local_disk" -gt 90 ]; then
    echo "ERROR: 本地磁盘使用率 ${local_disk}%,空间不足"
    ((errors++))
  fi

  if [ $errors -gt 0 ]; then
    echo "Docker 预检失败: $errors 个问题"
    return 1
  fi
  echo "Docker 预检通过"
  return 0
}

与需求工作流集成

部署完成后更新需求状态:

# 推进到 released
ai-proj req advance --id <req_id> --to released

# 创建部署任务并关联
ai-proj task create --title "【部署】TestFlight 发布: {需求标题}"
ai-proj req link --id <req_id> --task-ids <task_id>

# 附加部署文档
ai-proj task append-doc --id <task_id> --content "部署记录..."

经验教训汇总

iOS TestFlight 部署的 10 个坑

# 坑 解决方案
1 SSH 远程 codesign 失败 unlock-keychain + set-key-partition-list
2 SPM 依赖报签名错误 Archive 阶段 CODE_SIGNING_ALLOWED=NO,Export 阶段签名
3 Swift Macros 被拒 -skipMacroValidation
4 xcodeproj 缺文件 新增源文件后必须 xcodegen generate
5 无 Distribution 证书 Xcode > Accounts 登录 Apple ID 自动下载
6 build number 冲突 每次部署前递增 CURRENT_PROJECT_VERSION
7 缺 App Icon Assets.xcassets + AppIcon.appiconset + 1024x1024 PNG
8 iPad 方向验证失败 声明四方向或 UIRequiresFullScreen=true
9 ASC API 401 JWT 必须包含 kid header + 正确的 Issuer ID
10 App Store 图标为空 需将 build 关联到 App Store 版本(PATCH relationships/build)
11 SSH 长连接断开 xcodebuild 3-4 分钟无输出,Tailscale 断连。用 nohup 后台执行
12 xcodegen 后 cwd 错乱 cd ios && xcodegen && cd .. 失败时不回退。用 subshell (cd ios && xcodegen)

详细内容

以下章节已移到 references/details.md:

  • 回滚策略
  • 部署监控