Flutter macOS 应用构建:GitHub Actions + 自动管理签名 (Automatically Manage Signing)
Github 官方文档 “Installing an Apple certificate on macOS runners for Xcode development”
阐述了如何在 macOS 环境中装载 Apple 证书以便为代码签名。不过该文档基于手动管理配置文件(Provision Proile)
,
而对于启用自动管理签名(Automatically manage signing)
的项目,由于使用云签名(Cloud signing)
,
因此无需创建该配置文件。
而在 Xcode 13 及更高版本中,xcodebuild
可以使用 App Store Connect API 在 Apple Developer 上进行身份验证。
因此我们可以使用该验证方式在 CI 中对代码进行自动签名。
需要准备
- 一个 Apple 开发者账号。
- 一个 App Store Connect API 对应的团队密钥(导出为
P8
文件)。- Issuer ID
- API Key
- API Certificate (
Auth_xxx.p8
)
- 一个 Developer Id Application Certificate(导出为
P12
文件)。- Certificate (
***.p12
) - Certificate Password
- Certificate (
App Store Connect API 团队密钥
访问 App Store Connect / 用户和访问 / App Store Connect API,
选择 团队密钥
,然后添加一个新的密钥,完成后下载 p8
证书。
此时我们拥有以下三个需要的信息:
- Issuer ID: 可以从“团队密钥”下找到。
- API Key: 可以从“团队密钥 / 有效”下找到(中文名:密钥 ID)。
- API Certificate:上面下载的文件,由于只能下载一次,请妥善保存。
Developer Id Application Certificate
一个快捷的生成方式:直接在 Xcode 中进行生成。
- 导航到
Xcode --> Settings... --> Accounts --> Manage Certificates...
。 - 点击左下角
+
,点击Develop ID Application
,等待创建完成。 - 找到刚刚创建的 Certificate,右键单击,选取
Export Certificate
。 - 导出时需要密码,随机生成一个即可,记得记录这个密码,最后会生成一个
p12
文件。
此时我们拥有以下两个需要的信息:
- Certificate: 刚刚导出的
p12
文件。 - Certificate Password: 导出时输入的密码。
Github Action Secret Keys
将上面的信息存储到 Repository secrets
中,其中两个文件使用 base64
进行编码。
base64 -i ${your-api-auth}.p8 | pbcopy
base64 -i ${your-certificate}.p12 | pbcopy
同时我们需要一个 APPLE_KEYCHAIN_PASSWORD
,后续导入证书时需要使用,随机生成一个字符串填入即可。
Github Action 流程
导入证书
使用以下步骤进行证书导入,这里和 Github 官方文档的区别在于:我们不需要导入配置文件(Provision Profile)
。
jobs/<build-app>/steps:
- name: Import Certificate
env:
KEYCHAIN_PASSWORD: $
run: |
# create variables
CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# import certificate from secrets
echo "$" | base64 --decode > $CERTIFICATE_PATH
# create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# import certificate to keychain
security import $CERTIFICATE_PATH \
-k $KEYCHAIN_PATH \
-P "$" \
-A -t cert -f pkcs12
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
导入 API Key
由于使用配置文件,我们需要使用 “App Store Connect API”,因此需要导入 p8
文件:
jobs/<build-app>/steps:
- name: Extract App Store Connect API Key
env:
APPLE_API_KEY_ID: $
APPLE_API_AUTHKEY_P8_BASE64: $
run: |
mkdir ./private_keys
echo -n "$APPLE_API_AUTHKEY_P8_BASE64" | base64 --decode --output ./private_keys/AuthKey_$APPLE_API_KEY_ID.p8
手动构建
由于我们需要手动签名,因此不能直接使用 flutter build macos --release
进行构建(会报“找不到配置文件”的错误)。
此时我们需要手动运行构建命名,如下:
由于后续签名需要,这里直接导出归档文件。
jobs/<build-app>/steps:
- name: Build APP
env:
APPLE_TEAM_ID: $
run: |
flutter build macos --release --config-only
xcodebuild CODE_SIGNING_ALLOWED=NO \
-workspace macos/Runner.xcworkspace \
-scheme Runner \
-configuration Release \
-archivePath build/macos/Runner.xcarchive \
archive
# ls -al build/macos/Runner.xcarchive/Products/Applications
签名应用
签名导出时需要使用 xcodebuild -exportArchive
命名,此时需要手动创建一个 ExportOptions.plist
文件。
该文件的具体格式可 GUI 创建方法参考官方文档,这里只给出一个示例:
注意 plist 本身并不支持诸如
<string>${APPLE_TEAM_ID}</string>
之类的格式,这里是作为一个 template, 由envsubst
进行变量替换生成真正的 plist 文件。
<?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>manageAppVersionAndBuildNumber</key>
<false/>
<key>method</key>
<string>developer-id</string>
<key>teamID</key>
<string>${APPLE_TEAM_ID}</string>
</dict>
</plist>
这里注意需要使用 developer-id
,该方法依赖 Developer Id Application Certificate
,而我们在上面已经进行导入,
参考“导入证书”。
我们假定将该文件存放在 ./installers/macos_exporter/GithubExportOptions.plist
,构建以下步骤:
jobs/<build-app>/steps:
- name: Signed APP
env:
APPLE_API_ISSUER_ID: $
APPLE_API_KEY_ID: $
APPLE_TEAM_ID: $
run: |
envsubst \
< ./installers/macos_exporter/GithubExportOptions.plist \
> ./installers/macos_exporter/GithubExportOptions.resolved.plist
cat ./installers/macos_exporter/GithubExportOptions.resolved.plist
plutil -lint ./installers/macos_exporter/GithubExportOptions.resolved.plist
xcodebuild -exportArchive -archivePath ./build/macos/Runner.xcarchive \
-exportPath ./build/macos/Build/Products/Release \
-exportOptionsPlist ./installers/macos_exporter/GithubExportOptions.resolved.plist \
-allowProvisioningUpdates \
-authenticationKeyIssuerID $APPLE_API_ISSUER_ID \
-authenticationKeyID $APPLE_API_KEY_ID \
-authenticationKeyPath `pwd`/private_keys/AuthKey_$APPLE_API_KEY_ID.p8
最终我们将签名的应用导出到 ./build/macos/Build/Products/Release
。
整体流程
这里直接粘贴自己项目中的流程,仅供参考:
jobs:
# other actions
build-macos-dmg:
name: "Build macos DMG"
runs-on: macos-latest
steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ^16
- uses: actions/checkout@v4
- uses: ./.github/actions/setup_flutter
- name: Import Certificate
env:
KEYCHAIN_PASSWORD: $
run: |
# refs: https://docs.github.com/en/actions/use-cases-and-examples/deploying/installing-an-apple-certificate-on-macos-runners-for-xcode-development
# create variables
CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# import certificate from secrets
echo "$" | base64 --decode > $CERTIFICATE_PATH
# create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# import certificate to keychain
security import $CERTIFICATE_PATH \
-k $KEYCHAIN_PATH \
-P "$" \
-A -t cert -f pkcs12
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Extract App Store Connect API Key
env:
APPLE_API_KEY_ID: $
APPLE_API_AUTHKEY_P8_BASE64: $
run: |
mkdir ./private_keys
echo -n "$APPLE_API_AUTHKEY_P8_BASE64" | base64 --decode --output ./private_keys/AuthKey_$APPLE_API_KEY_ID.p8
- name: Build APP
env:
APPLE_TEAM_ID: $
run: |
flutter build macos --release --config-only
xcodebuild CODE_SIGNING_ALLOWED=NO \
-workspace macos/Runner.xcworkspace \
-scheme Runner \
-configuration Release \
-archivePath build/macos/Runner.xcarchive \
archive
ls -al build/macos/Runner.xcarchive/Products/Applications
- name: Signed APP
env:
APPLE_API_ISSUER_ID: $
APPLE_API_KEY_ID: $
APPLE_TEAM_ID: $
run: |
envsubst \
< ./installers/macos_exporter/GithubExportOptions.plist \
> ./installers/macos_exporter/GithubExportOptions.resolved.plist
cat ./installers/macos_exporter/GithubExportOptions.resolved.plist
plutil -lint ./installers/macos_exporter/GithubExportOptions.resolved.plist
xcodebuild -exportArchive -archivePath ./build/macos/Runner.xcarchive \
-exportPath ./build/macos/Build/Products/Release \
-exportOptionsPlist ./installers/macos_exporter/GithubExportOptions.resolved.plist \
-allowProvisioningUpdates \
-authenticationKeyIssuerID $APPLE_API_ISSUER_ID \
-authenticationKeyID $APPLE_API_KEY_ID \
-authenticationKeyPath `pwd`/private_keys/AuthKey_$APPLE_API_KEY_ID.p8
- uses: actions/setup-node@v4
with:
node-version: 20
token: $
- run: npm install -g appdmg
- name: Build DMP
run: appdmg ./installers/dmg_creator/config.json ./build/macos/Build/Products/Release/mhabit.dmg
- name: Released - MacOS
uses: ncipollo/release-action@v1
with:
allowUpdates: true
omitBodyDuringUpdate: true
omitDraftDuringUpdate: true
omitPrereleaseDuringUpdate: true
artifacts: >
build/macos/Build/Products/Release/mhabit.dmg
token: $
问题:应用签名时出现 Segmentation fault: 11
参考该 ISSUE:FB13797668: xcodebuild crashes systematically when exporting an archive (segfault)
该问题已在 Xcode 16 Beta 4
进行修复。如果出现该问题,请指定执行时的 Xcode 版本(而不是使用容器自带的版本):
jobs/...:
# https://github.com/maxim-lobanov/setup-xcode
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ^16 #