Files
jiu/scripts/ci/compile-ios.sh
T
wangjia 848a9ee2c6
Deploy Client / build-windows (push) Successful in 3m58s
Deploy Client / build-client-web (push) Successful in 6m17s
Deploy Client / build-macos (push) Successful in 13m39s
Deploy Client / build-android (push) Successful in 7m17s
Deploy Client / build-ios (push) Successful in 6m42s
Deploy Client / release-deploy-client (push) Successful in 11m50s
ci(deploy-client): iOS 上传加超时看门狗 + iOS 失败不阻断发版
1.1.13 发版事故复盘:build-ios 的 altool 上传静默卡死数分钟无输出,超
runner 僵尸超时被 SIGKILL 判失败(软失败 else 分支没机会跑);且 build-ios
被 release-deploy 硬 needs → 连带 web/mac/win/android 整体没部署。

- compile-ios.sh: altool 上传套后台看门狗(本机无 timeout/coreutils),超
  420s 先 TERM 再 KILL,保证永远在僵尸窗口内干净收尾,失败只告警不阻断。
- deploy-client.yml: build-ios 加 continue-on-error;release-deploy 改
  if: always() 只校验 4 个真实平台成功即部署,iOS 真正不阻塞。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bupi8Kdqkfx2N5acFsHTx5
2026-08-27 22:41:28 +08:00

128 lines
5.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# compile-ios.sh <tag> — build signed iOS IPA and upload to TestFlight (App Store Connect)
#
# 用**本机 login keychain 的 Apple Distribution 证书** + **本机已装的 App Store
# provisioning profile** + **本机 App Store Connect key(.p8)** 签名/上传。
# mac-runner 就是开发机,这些分发材料常驻本机,不再从 base64 secret 导入
# (原方案 IOS_DIST_CERT_P12_BASE64 里的证书过期 → archive 报 "No valid code
# signing certificates";本机 login 里那张有效,2026-07 改用本机材料)。
#
# 需要 env(非机密标识,gitea secret/变量;缺则用已知默认):
# IOS_TEAM_ID(默认 BYL4KQHMTN)/ APPSTORE_API_KEY_ID(默认 3PZTHR8YMJ)/
# APPSTORE_API_ISSUER_ID(必填,无默认)
# 需要本机:Apple Distribution 证书在 login keychain;App Store profile 已装;
# ~/.appstoreconnect/private_keys/AuthKey_<KEY_ID>.p8 存在。
set -euo pipefail
# shellcheck source=scripts/ci/_env.sh
. "$(dirname "$0")/_env.sh"
TAG="$1"
VER="${TAG#client-v}"
# CFBundleVersion(build 号)必须单调递增,否则 TestFlight 拒绝重复上传。
MAJOR="$(echo "$VER" | cut -d. -f1)"
MINOR="$(echo "$VER" | cut -d. -f2)"
PATCH="$(echo "$VER" | cut -d. -f3)"
BUILD=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
echo "==> compile-ios: version=${VER} build=${BUILD}"
TEAM_ID="${IOS_TEAM_ID:-BYL4KQHMTN}"
KEY_ID="${APPSTORE_API_KEY_ID:-3PZTHR8YMJ}"
ISSUER="${APPSTORE_API_ISSUER_ID:-}"
BUNDLE_ID="com.yanmei.jiu"
P8="$HOME/.appstoreconnect/private_keys/AuthKey_${KEY_ID}.p8"
# --- 前置检查:本机分发材料齐备(缺则 fail-fast,不产出未签名/无法上传的包)---
if ! security find-identity -v -p codesigning \
| grep -qF "Apple Distribution: Yanmei (beijing) Technology Co., Ltd (${TEAM_ID})"; then
echo "ERROR: login keychain 未找到 Apple Distribution 证书(团队 ${TEAM_ID})——无法签名 iOS。" >&2
echo " 在本机 Xcode 登录 Apple ID / 装好分发证书后重试。" >&2
exit 1
fi
if [ ! -f "$P8" ]; then
echo "ERROR: 缺 App Store Connect key: $P8" >&2
exit 1
fi
if [ -z "$ISSUER" ]; then
echo "ERROR: 缺 APPSTORE_API_ISSUER_ID(App Store Connect Issuer ID)——公证/上传需要。" >&2
exit 1
fi
WORK="$(mktemp -d)"
# shellcheck disable=SC2064
trap "rm -rf '$WORK'" EXIT
# --- 同步版本号 ---
sed -i '' "s/^version:.*/version: ${VER}+${BUILD}/" client/pubspec.yaml
# --- ExportOptions.plist(automatic 签名,app-store 分发)---
# 本机 login 里的 Apple Distribution 证书与旧的手动 profile 不配对
# (profile 是用另一张/旧证书签发的 → archive 报 "profile doesn't include
# signing certificate")。改用**自动签名**:Xcode 用登录的 Apple ID +
# App Store Connect API key(见下 xcodebuild -authenticationKeyPath) 现场
# 生成/更新与当前证书匹配的 App Store profile(-allowProvisioningUpdates),
# 与 local_test.sh --ios 一致,避免手动材料漂移。
cat > "$WORK/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</string>
<key>teamID</key><string>${TEAM_ID}</string>
<key>signingStyle</key><string>automatic</string>
<key>uploadBitcode</key><false/>
<key>uploadSymbols</key><true/>
</dict>
</plist>
EOF
# --- 构建签名 IPA(自动签名;Xcode 现场配对证书+profile)---
# flutter build ipa 的 archive/export 都会带 -allowProvisioningUpdates(自动签名时),
# 但云端/无交互登录需用 API key 授权 provisioning 更新 → 传 -authenticationKeyPath 等。
cd client
flutter build ipa --release \
--build-name="${VER}" \
--build-number="${BUILD}" \
--export-options-plist="$WORK/ExportOptions.plist" \
"--dart-define=BASE_URL=https://jiu.51yanmei.com" \
"--dart-define=PUBLIC_URL=https://jiu.51yanmei.com" \
"--dart-define=APP_VERSION=v${VER}"
cd ..
IPA="$(ls client/build/ios/ipa/*.ipa 2>/dev/null | head -1)"
if [ -z "$IPA" ] || [ ! -f "$IPA" ]; then
echo "ERROR: IPA not found" >&2
exit 1
fi
echo "==> compile-ios: built ${IPA}"
# --- 上传 TestFlight(App Store Connect;本机 .p8 由 key-id 自动定位)---
# TestFlight 是 App Store 独立分发渠道,上传本身易抖(重复 build 号 / ASC 临时错误);
# 其失败【不应阻断】web/mac/win/android 的整体发版(见 CLAUDE.md「iOS ... 不阻塞」)。
# IPA 已产出,上传失败仅告警,可稍后用 altool/Transporter 手动补传同一 IPA。
# altool 上传是【静默】的:上传 IPA 到 Apple 期间数分钟无任何输出。网络到 Apple
# 抖动/变慢时会长时间挂住 → 超过 runner 僵尸超时(10min)整个 job 被 SIGKILL 判
# 失败,连下面的软失败分支都跑不到 → 阻断发版(1.1.13 就这么挂的)。
# 本机无 timeout(1)/gtimeout(1)(无 coreutils),故用后台看门狗:超 UPLOAD_TIMEOUT
# 先 TERM 再 KILL,保证上传永远在僵尸窗口内【干净收尾】,失败只告警不阻断。
UPLOAD_TIMEOUT=420
echo "==> compile-ios: uploading to TestFlight (超时保护 ${UPLOAD_TIMEOUT}s)"
xcrun altool --upload-app -f "$IPA" -t ios \
--apiKey "${KEY_ID}" \
--apiIssuer "${ISSUER}" &
altool_pid=$!
( sleep "$UPLOAD_TIMEOUT"
kill -TERM "$altool_pid" 2>/dev/null || true
sleep 5
kill -KILL "$altool_pid" 2>/dev/null || true ) &
watchdog_pid=$!
if wait "$altool_pid"; then
echo "==> compile-ios: done — uploaded build ${BUILD} to TestFlight"
else
echo "!! compile-ios: TestFlight 上传失败或超时(build ${BUILD}),不阻断发版;IPA=${IPA} 已产出,可稍后手动补传。" >&2
fi
kill "$watchdog_pid" 2>/dev/null || true
wait "$watchdog_pid" 2>/dev/null || true