新增部署技能,含 iOS TestFlight 完整部署流程: - SSH 远程构建 + 无签名 Archive + Export 签名上传 - ASC API 补全合规/测试说明/版本关联 - 10 个坑的经验教训总结 - 一键部署脚本模板 + 检查清单 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
326 lines
10 KiB
Markdown
326 lines
10 KiB
Markdown
---
|
||
name: dev-deploy
|
||
description: 应用部署技能。支持 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 连接构建机
|
||
|
||
```bash
|
||
# 使用 sshpass 进行非交互 SSH
|
||
sshpass -p '<password>' ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no <user>@<host> '<command>'
|
||
```
|
||
|
||
**关键经验**:
|
||
- SSH 远程 codesign 需要先**解锁 Keychain**,否则报 `errSecInternalComponent`
|
||
- 还需要 `set-key-partition-list` 授权 codesign 访问密钥
|
||
|
||
```bash
|
||
# 必须在每次 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: 拉取代码
|
||
|
||
```bash
|
||
cd <repo-path> && git pull origin develop
|
||
```
|
||
|
||
### Step 3: Archive(无签名)
|
||
|
||
**关键经验**:Archive 阶段**不要签名**。原因:
|
||
- xcodebuild CLI 签名参数会泄漏到 SPM 依赖的 targets,导致 "does not support provisioning profiles" 错误
|
||
- 正确做法是 archive 时禁用签名,在 export 阶段单独签名
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
# 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
|
||
|
||
```python
|
||
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: 验证部署状态
|
||
|
||
```python
|
||
# 检查 build 状态
|
||
GET /v1/builds/{build_id}?include=buildBetaDetail
|
||
|
||
# 期望结果:
|
||
# processingState: VALID
|
||
# internalBuildState: IN_BETA_TESTING
|
||
# usesNonExemptEncryption: false
|
||
```
|
||
|
||
### 一键部署脚本模板
|
||
|
||
将以上步骤整合为单次 SSH 调用:
|
||
|
||
```bash
|
||
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(自动)
|
||
|
||
Push 到 `develop` 分支自动触发 staging 部署。
|
||
|
||
### Production
|
||
|
||
```bash
|
||
./scripts/build-and-push.sh prod --detect --deploy --wait --verify
|
||
```
|
||
|
||
详见项目 `scripts/build-and-push.sh`。
|
||
|
||
---
|
||
|
||
## 与需求工作流集成
|
||
|
||
部署完成后更新需求状态:
|
||
|
||
```bash
|
||
# 推进到 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) |
|