feat(devops): macOS 构建接入 Developer ID 签名 + 公证

- compile-macos.sh:flutter build 后 codesign(hardened runtime + timestamp
  + Release.entitlements)→ notarytool 公证(复用 App Store Connect API key)
  → stapler staple → 重新打包,产出已签名+公证+stapled 的 .app
- 强制策略:缺 MACOS_DEVELOPER_ID_CERT_P12_BASE64 / APPSTORE_API_KEY_P8_BASE64
  时在 flutter build 前 fail-fast,绝不产出未签名包
- deploy-client.yml:build-macos 步骤注入证书与 API key secrets
- 新增 docs/macos-signing.md:Developer ID Application 证书创建与 secret 配置

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-17 12:56:08 +08:00
parent 40d9aba657
commit 48e282dd97
3 changed files with 152 additions and 2 deletions
+75 -2
View File
@@ -10,6 +10,20 @@ VER="${TAG#client-v}"
echo "==> compile-macos: version=${VER}"
# macOS 通过官网下载 + 应用内自更新分发,未签名/未公证的包会被 Gatekeeper 拦截,
# 因此【强制】签名+公证:缺任一关键 secret 立即报错、流水线红,绝不产出未签名包。
# 在跑昂贵的 flutter build 之前先做这一检查,快速失败。详见 docs/macos-signing.md。
#
# 需要的 CI secrets
# MACOS_DEVELOPER_ID_CERT_P12_BASE64 Developer ID Application 证书 (.p12) 的 base64
# MACOS_DEVELOPER_ID_CERT_PASSWORD .p12 导出密码
# APPSTORE_API_KEY_ID / APPSTORE_API_ISSUER_ID / APPSTORE_API_KEY_P8_BASE64 复用 iOS 的 App Store Connect API key(公证用)
if [ -z "${MACOS_DEVELOPER_ID_CERT_P12_BASE64:-}" ] || [ -z "${APPSTORE_API_KEY_P8_BASE64:-}" ]; then
echo "ERROR: macOS 签名/公证所需 secret 未配置,拒绝产出未签名包。" >&2
echo " 需要 MACOS_DEVELOPER_ID_CERT_P12_BASE64 + APPSTORE_API_KEY_P8_BASE64,详见 docs/macos-signing.md" >&2
exit 1
fi
# Sync Flutter pubspec version (BSD sed on macOS)
sed -i '' "s/^version:.*/version: ${VER}+1/" client/pubspec.yaml
@@ -21,11 +35,70 @@ flutter build macos --release \
"--dart-define=APP_VERSION=v${VER}"
cd ..
# Package .app bundle into zip
APP_SRC="client/build/macos/Build/Products/Release/jiu_client.app"
# --- 代码签名 + 公证(Developer ID + Notarization---
# secret 已在脚本开头校验过(缺失会 fail-fast),此处必有证书与 API key。
echo "==> compile-macos: signing + notarizing"
WORK="$(mktemp -d)"
KEYCHAIN="$WORK/jiu-mac.keychain-db"
cleanup_mac() {
security delete-keychain "$KEYCHAIN" 2>/dev/null || true
rm -rf "$WORK"
}
trap cleanup_mac EXIT
# 1. 临时 keychain 导入 Developer ID Application 证书
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 "${MACOS_DEVELOPER_ID_CERT_P12_BASE64}" | base64 --decode > "$WORK/devid.p12"
security import "$WORK/devid.p12" -k "$KEYCHAIN" \
-P "${MACOS_DEVELOPER_ID_CERT_PASSWORD:-}" -T /usr/bin/codesign -T /usr/bin/security
# 允许 codesign 非交互访问私钥
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PWD" "$KEYCHAIN" >/dev/null
# 把临时 keychain 加入搜索列表(保留默认 login keychain
security list-keychains -d user -s "$KEYCHAIN" login.keychain-db
# 解出签名身份 "Developer ID Application: NAME (TEAMID)"
IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN" \
| grep 'Developer ID Application' | head -1 | sed -E 's/.*"(.*)"/\1/')"
if [ -z "$IDENTITY" ]; then
echo "ERROR: Developer ID Application identity not found in keychain" >&2
exit 1
fi
echo "==> compile-macos: signing identity '${IDENTITY}'"
# 2. inside-out 签名:先签嵌套 framework/dylib,再签 app(均启用 hardened runtime + 安全时间戳)
if [ -d "${APP_SRC}/Contents/Frameworks" ]; then
find "${APP_SRC}/Contents/Frameworks" \( -name '*.framework' -o -name '*.dylib' \) -print0 \
| xargs -0 -I{} codesign --force --options runtime --timestamp --sign "$IDENTITY" {}
fi
codesign --force --options runtime --timestamp \
--entitlements client/macos/Runner/Release.entitlements \
--sign "$IDENTITY" "${APP_SRC}"
codesign --verify --deep --strict --verbose=2 "${APP_SRC}"
# 3. 提交公证(复用 App Store Connect API key),阻塞等待结果
ditto -c -k --keepParent "${APP_SRC}" "$WORK/notarize.zip"
echo "${APPSTORE_API_KEY_P8_BASE64}" | base64 --decode > "$WORK/AuthKey.p8"
xcrun notarytool submit "$WORK/notarize.zip" \
--key "$WORK/AuthKey.p8" \
--key-id "${APPSTORE_API_KEY_ID}" \
--issuer "${APPSTORE_API_ISSUER_ID}" \
--wait
# 4. staple 公证票据进 .app(离线也能通过 Gatekeeper),并核验
xcrun stapler staple "${APP_SRC}"
xcrun stapler validate "${APP_SRC}"
spctl -a -vvv -t install "${APP_SRC}" || true # 期望 source=Notarized Developer ID
echo "==> compile-macos: signed + notarized + stapled"
# Package .app bundle into zip(此时 APP_SRC 已签名+stapled
# ditto preserves symlinks, Unix permissions, and extended attributes (code-signing).
# Python's zipfile follows symlinks and drops permissions — do NOT use it for .app bundles.
mkdir -p dist
APP_SRC="client/build/macos/Build/Products/Release/jiu_client.app"
ditto -c -k --keepParent "${APP_SRC}" dist/jiu-macos-x64.zip
echo "Packaged dist/jiu-macos-x64.zip ($(du -sh dist/jiu-macos-x64.zip | cut -f1))"