新增部署技能,含 iOS TestFlight 完整部署流程: - SSH 远程构建 + 无签名 Archive + Export 签名上传 - ASC API 补全合规/测试说明/版本关联 - 10 个坑的经验教训总结 - 一键部署脚本模板 + 检查清单 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
10 KiB
10 KiB
name, description
| name | description |
|---|---|
| dev-deploy | 应用部署技能。支持 iOS TestFlight、Docker 容器等多平台部署。当用户提到部署、发布、TestFlight、上架、build、archive 相关任务时自动激活。 |
应用部署 Skill (dev-deploy)
概述
管理应用从构建到发布的完整部署流程,支持多平台:
- 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_VERSIONin 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(自动)
Push 到 develop 分支自动触发 staging 部署。
Production
./scripts/build-and-push.sh prod --detect --deploy --wait --verify
详见项目 scripts/build-and-push.sh。
与需求工作流集成
部署完成后更新需求状态:
# 推进到 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) |