fix(ci): iOS 改用本机 login keychain 证书+本地 profile+本地 .p8 签名

CI iOS 报 'No valid code signing certificates':原 IOS_DIST_CERT_P12_BASE64 secret
里的证书过期。改用本机 login keychain 的有效 Apple Distribution 证书(到2027-07)
+ 自动发现的 App Store profile + 本机 .p8,不再依赖 base64 secret。
team/key-id 带默认,issuer 走 gitea secret APPSTORE_API_ISSUER_ID(已设)。
本地实测:过了证书+profile 检查、进入 archive(签名修复生效),后续 iOS 平台组件另议。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P9G7E3wmAYL9KeYCVZVsqu
This commit is contained in:
wangjia
2026-07-20 09:26:49 +08:00
parent cac84b4388
commit dccf3610b5
+58 -76
View File
@@ -1,16 +1,17 @@
#!/usr/bin/env bash
# compile-ios.sh <tag> — build signed iOS IPA and upload to TestFlight (App Store Connect)
#
# 需要的 CI secrets(缺任意一个则优雅跳过,不阻塞流水线):
# IOS_DIST_CERT_P12_BASE64 Apple Distribution 证书 (.p12) 的 base64
# IOS_DIST_CERT_PASSWORD .p12 导出密码
# IOS_PROVISIONING_PROFILE_BASE64 App Store 类型 provisioning profile (.mobileprovision) 的 base64
# IOS_TEAM_ID Apple Developer Team ID10 位)
# APPSTORE_API_KEY_ID App Store Connect API Key ID
# APPSTORE_API_ISSUER_ID App Store Connect API Issuer ID
# APPSTORE_API_KEY_P8_BASE64 API Key (.p8) 的 base64
# 用**本机 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 改用本机材料)。
#
# 详见 docs/ios-signing.md。
# 需要 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
@@ -19,7 +20,7 @@ set -euo pipefail
TAG="$1"
VER="${TAG#client-v}"
# CFBundleVersionbuild 号必须单调递增否则 TestFlight 拒绝重复上传。
# CFBundleVersion(build 号)必须单调递增,否则 TestFlight 拒绝重复上传。
MAJOR="$(echo "$VER" | cut -d. -f1)"
MINOR="$(echo "$VER" | cut -d. -f2)"
PATCH="$(echo "$VER" | cut -d. -f3)"
@@ -27,81 +28,62 @@ BUILD=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
echo "==> compile-ios: version=${VER} build=${BUILD}"
# --- 凭证缺失则跳过(账号/证书尚未配置时不阻塞发版)---
if [ -z "${IOS_DIST_CERT_P12_BASE64:-}" ] || \
[ -z "${IOS_PROVISIONING_PROFILE_BASE64:-}" ] || \
[ -z "${APPSTORE_API_KEY_P8_BASE64:-}" ]; then
echo "==> compile-ios: SKIP — iOS signing secrets not configured (see docs/ios-signing.md)"
exit 0
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)"
cleanup() {
# 恢复默认搜索列表(构建期临时独占,见下)——失败路径也必须恢复
security list-keychains -d user -s login.keychain-db 2>/dev/null || true
security delete-keychain "$KEYCHAIN" 2>/dev/null || true
rm -rf "$WORK"
rm -f "$HOME/Library/MobileDevice/Provisioning Profiles/jiu-ci.mobileprovision" 2>/dev/null || true
}
trap cleanup EXIT
# shellcheck disable=SC2064
trap "rm -rf '$WORK'" EXIT
# --- 1. 临时 keychain 导入发行证书 ---
KEYCHAIN="$WORK/jiu-ci.keychain-db"
KEYCHAIN_PWD="ci-temp-$$"
security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN"
echo "${IOS_DIST_CERT_P12_BASE64}" | base64 --decode > "$WORK/dist.p12"
security import "$WORK/dist.p12" -k "$KEYCHAIN" \
-P "${IOS_DIST_CERT_PASSWORD:-}" -T /usr/bin/codesign -T /usr/bin/security
# 补齐证书链:p12 只含叶子证书,独占搜索列表后 login 里的 Apple WWDR 中间证书
# 不可见会报 errSecInternalComponent(unable to build chain)——把本机已有的
# WWDR 中间证书(纯公钥)一并导入临时 keychain。
security find-certificate -a -c "Apple Worldwide Developer Relations" -p \
~/Library/Keychains/login.keychain-db /Library/Keychains/System.keychain \
> "$WORK/wwdr.pem" 2>/dev/null || true
if [ -s "$WORK/wwdr.pem" ]; then
security import "$WORK/wwdr.pem" -k "$KEYCHAIN" -T /usr/bin/codesign 2>/dev/null || true
else
echo "WARN: 本机未找到 Apple WWDR 中间证书,签名可能报 unable to build chain" >&2
# --- 自动发现已装的 App Store 分发 profile(AppID=TEAM.BUNDLE、无 ProvisionedDevices)---
PROFILE_NAME=""
PDIR="$HOME/Library/MobileDevice/Provisioning Profiles"
if [ -d "$PDIR" ]; then
for p in "$PDIR"/*.mobileprovision; do
[ -e "$p" ] || continue
plist="$(security cms -D -i "$p" 2>/dev/null)" || continue
appid="$(printf '%s' "$plist" | /usr/libexec/PlistBuddy -c 'Print :Entitlements:application-identifier' /dev/stdin 2>/dev/null || true)"
# App Store profile:AppID 匹配且无 ProvisionedDevices(区别于开发/AdHoc profile)
if [ "$appid" = "${TEAM_ID}.${BUNDLE_ID}" ] \
&& ! printf '%s' "$plist" | /usr/libexec/PlistBuddy -c 'Print :ProvisionedDevices' /dev/stdin >/dev/null 2>&1; then
PROFILE_NAME="$(printf '%s' "$plist" | /usr/libexec/PlistBuddy -c 'Print :Name' /dev/stdin 2>/dev/null || true)"
[ -n "$PROFILE_NAME" ] && break
fi
done
fi
# 允许 codesign 非交互访问私钥
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PWD" "$KEYCHAIN" >/dev/null
# 构建期独占搜索列表(fastlane 同款):只见临时 keychain,隔离本机其他项目的
# 同名「Apple Distribution」证书(2026-07-07 事故:login 里另一项目的新证书被
# xcodebuild 优先选中,与 Jiu App Store profile 不配对 → archive 失败)。
# trap cleanup 恢复 login,失败路径也不遗留。
security list-keychains -d user -s "$KEYCHAIN"
[ -n "$PROFILE_NAME" ] || PROFILE_NAME="Jiu App Store" # 已知已装 profile 名兜底
echo "==> compile-ios: 使用 provisioning profile '${PROFILE_NAME}'"
# --- 2. 安装 provisioning profile ---
PROFILES_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
mkdir -p "$PROFILES_DIR"
echo "${IOS_PROVISIONING_PROFILE_BASE64}" | base64 --decode > "$WORK/profile.mobileprovision"
# 解出 profile 的 Name 与 UUIDExportOptions 需要)
security cms -D -i "$WORK/profile.mobileprovision" > "$WORK/profile.plist"
PROFILE_NAME="$(/usr/libexec/PlistBuddy -c 'Print :Name' "$WORK/profile.plist")"
PROFILE_UUID="$(/usr/libexec/PlistBuddy -c 'Print :UUID' "$WORK/profile.plist")"
cp "$WORK/profile.mobileprovision" "$PROFILES_DIR/${PROFILE_UUID}.mobileprovision"
echo "==> compile-ios: profile '${PROFILE_NAME}' (${PROFILE_UUID})"
# --- 3. App Store Connect API Key(供上传使用)---
API_KEYS_DIR="$HOME/.appstoreconnect/private_keys"
mkdir -p "$API_KEYS_DIR"
echo "${APPSTORE_API_KEY_P8_BASE64}" | base64 --decode > "$API_KEYS_DIR/AuthKey_${APPSTORE_API_KEY_ID}.p8"
# --- 4. 同步版本号 ---
# --- 同步版本号 ---
sed -i '' "s/^version:.*/version: ${VER}+${BUILD}/" client/pubspec.yaml
# --- 5. 生成 ExportOptions.plistmanual 签名app-store 分发---
BUNDLE_ID="com.yanmei.jiu"
# --- ExportOptions.plist(manual 签名,app-store 分发)---
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>${IOS_TEAM_ID}</string>
<key>teamID</key><string>${TEAM_ID}</string>
<key>signingStyle</key><string>manual</string>
<key>uploadBitcode</key><false/>
<key>uploadSymbols</key><true/>
@@ -113,7 +95,7 @@ cat > "$WORK/ExportOptions.plist" <<EOF
</plist>
EOF
# --- 6. 构建签名 IPA ---
# --- 构建签名 IPA(用 login keychain 的分发证书 + 上面的 profile)---
cd client
flutter build ipa --release \
--build-name="${VER}" \
@@ -124,17 +106,17 @@ flutter build ipa --release \
"--dart-define=APP_VERSION=v${VER}"
cd ..
IPA="$(ls client/build/ios/ipa/*.ipa | head -1)"
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}"
# --- 7. 上传 TestFlightApp Store Connect---
# --- 上传 TestFlight(App Store Connect;本机 .p8 由 key-id 自动定位)---
echo "==> compile-ios: uploading to TestFlight"
xcrun altool --upload-app -f "$IPA" -t ios \
--apiKey "${APPSTORE_API_KEY_ID}" \
--apiIssuer "${APPSTORE_API_ISSUER_ID}"
--apiKey "${KEY_ID}" \
--apiIssuer "${ISSUER}"
echo "==> compile-ios: done — uploaded build ${BUILD} to TestFlight"