最近把我们的 Electron 桌面应用配置上了 Mac 端的 TestFlight,用 GitHub Actions 实现了自动化构建和上传。踩了不少坑,记录一下整个流程。
我对 Electron 和 MAS 上架流程也不熟悉,这篇文章是一点点摸索出来的。目前已经能够成功上传到 TestFlight,但如有错漏请务必帮忙指出! 部分记录是通过和 AI 对话的方式存留下来的,可能有错漏。
⚠️ 时间成本警告:整个 workflow 会消耗大量 GitHub Actions 免费额度!苹果的代码签名和公证(notarization)过程需要硬等 3~5 分钟,这段时间 runner 只能空转等待苹果服务器响应,非常不划算。如果你的项目频繁构建,建议考虑自建 runner 或优化触发策略。
这是苹果官方的文档 Upload builds,介绍了如何使用 Apple 官方工具及 API 将 App 构建版本上传至 App Store Connect 的指南。
先放最终的效果,为什么会有这么麻烦的东西啊(小声抱怨):
配置完成后:
- 手动触发 workflow
- 勾选 “Upload to App Store Connect” 则上传到 TestFlight,否则仅打包
- 在 App Store Connect 的 TestFlight 标签页查看构建
- 分发给测试人员

整个流程从 DMG 直接下载迁移到 TestFlight 大概花了半天时间,主要是在各种证书和 Bundle ID 配置上踩坑。


根据文档里 Apple 的推荐上传方式如下:
- Xcode:苹果官方集成开发环境(IDE),支持从开发、测试到提交的全流程管理
- Transporter:提供图形界面的 macOS 应用,适合简单快速地上传并查看交付日志和历史
- xcrun altool:通过 Xcode 自带的 xcrun 来调用 altool,这是命令行工具,可用于验证应用二进制文件并将其上传到 App Store Connect
- App Store Connect API:基于 REST 的 API,支持通过 JSON Web Tokens (JWT) 进行身份验证,实现自动化上传流程
因为我们要使用 action,所以这里我们使用 xcrun altool 进行上传
背景
我们的应用是用 Electron + React + TypeScript 构建的,之前通过 GitHub Releases 分发 DMG 安装包。现在想通过 TestFlight 进行 beta 测试,最终上架 Mac App Store。以下步骤假设你的 app 名称为 AppCat。
前置条件:在 App Store Connect 创建应用
在开始配置证书之前,需要在 App Store Connect 创建应用:
- 打开 App Store Connect - 我的 App
- 点击 ”+” → 新建 App
- 平台勾选 macOS
- 名称填写应用名
- Bundle ID 选择或创建一个(必须和
electron-builder.yml中的appId完全一致!) - SKU 随便填,如
appcat-macos
⚠️ 如果跳过这一步,后面上传时会报错 “No suitable application records were found”。
我们的应用之前上架过 iOS,所以这里就不赘述了,就是常规的上架流程,现在是要上架 Mac 端的 App Store。
两种分发方式的区别
首先要理解,Mac 应用有两种分发方式,它们需要不同的证书:
| 分发方式 | 证书类型 | 用途 |
|---|---|---|
| 直接下载 (DMG) | Developer ID Application | 网站/GitHub 下载安装 |
| Mac App Store | 3rd Party Mac Developer Application | App Store / TestFlight |
另外,MAS 版本强制要求沙盒,这意味着一些功能(如 desktopCapturer 屏幕截图)可能受限。
第一步:创建证书
1.1 创建 CSR 文件
打开本地的钥匙串访问:
顶部菜单栏 → 钥匙串访问 → 证书助理 → 从证书颁发机构请求证书...

然后填写:
- 用户电子邮件地址:你的邮箱
- 常用名称:随便填,如 “AppCat MAS”
- 选择”存储到磁盘”
- CA 邮件地址:留空
保存 .certSigningRequest 文件。

1.2 创建 Mac App Distribution 证书
- 打开 Apple Developer - Certificates
- 点击 ”+” 按钮
- 选择 Mac App Distribution
- 上传刚才的 CSR 文件
- 下载证书,双击安装到钥匙串(选择”登录”钥匙串)
1.3 创建 Mac Installer Distribution 证书
重复上面的步骤,但选择 Mac Installer Distribution。这个证书用于签名 .pkg 安装包。
第二步:创建 Provisioning Profile
- 打开 Apple Developer - Profiles
- 点击 ”+”
- 选择 Mac App Store Connect
- 选择你的 App ID(必须和 App Store Connect 中的 Bundle ID 一致!)
- 选择刚创建的 Mac App Distribution 证书
- 命名并下载
⚠️ 重要:Profile 中的 Bundle ID 必须和 App Store Connect 中的完全一致,否则上传会报错 “No suitable application records were found”。
这一步得到 appcat_profile_mas.provisionprofile 文件(文件名可自定义,但要和后续配置保持一致)。
第三步:创建 App Store Connect API Key
用于 CI/CD 自动上传构建。
- 打开 App Store Connect - 用户和访问 - 密钥
- 点击 ”+” 创建新密钥
- 名称随便填,权限选 App 管理 或 管理员
- 点击 “生成”
找到 Issuer ID
Issuer ID 显示在密钥页面的顶部,是一个 UUID 格式的字符串,例如:
50ce4b17-dd5e-4550-877b-7a7bb0d608d7
所有属于同一团队的 API Key 共享同一个 Issuer ID,这个值不是敏感信息。
获取 Key ID
创建密钥后,Key ID 会显示在密钥列表中(密钥 ID 列),例如 BU64538829。
下载 .p8 私钥文件
点击 “下载 API 密钥” 下载 .p8 文件:
- 文件名格式为
AuthKey_XXXXX.p8,其中XXXXX是 Key ID - 这个文件是 API 认证的私钥,泄露会导致安全问题
⚠️ 极其重要:
.p8私钥文件整个团队只能下载一次!下载后立即保存到安全位置(如密码管理器)。如果丢失,只能撤销当前密钥并重新创建,届时需要更新所有使用该密钥的 CI/CD 配置。
这一步得到三个值:
AuthKey_XXXXX.p8文件 → 后面转为ASC_API_KEY- Key ID → 对应后续我们在环境变量设置的
ASC_API_KEY_ID - Issuer ID → 对应
ASC_ISSUER_ID
第四步:导出证书为 .p12
CI/CD 需要 .p12 格式的证书。
- 打开钥匙串访问
- 左侧选择”登录”,上方选择”我的证书”
- 找到 “3rd Party Mac Developer Application: xxx”
- 右键 → 导出
- 格式选
.p12 - 设置密码
- 同样导出 “3rd Party Mac Developer Installer: xxx”
这一步得到了 mas_app.p12 和 mas_installer.p12 文件。
第五步:electron-builder 配置
在 electron-builder.yml 中添加 MAS 配置:
appId: app.bundlename.AppCat # 必须和 App Store Connect 一致!这里替换为你的 app bundleId
# 现有的 Developer ID 配置保持不变
mac:
entitlementsInherit: build/entitlements.mac.plist
hardenedRuntime: true
notarize: true
# 新增 Mac App Store 配置
mas:
hardenedRuntime: false # MAS 用沙盒代替
entitlements: build/entitlements.mas.plist
entitlementsInherit: build/entitlements.mas.inherit.plist
provisioningProfile: build/appcat_profile_mas.provisionprofile
notarize: false # MAS 不需要 notarize
category: public.app-category.productivity # 这里选择你的应用分类
# PKG 配置
pkg:
isRelocatable: false
overwriteAction: upgrade
artifactName: ${name}-${version}-mas.${ext}
第六步:创建 MAS Entitlements
MAS 必须启用沙盒。创建 build/entitlements.mas.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>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
</dict>
</plist>
创建 build/entitlements.mas.inherit.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>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.inherit</key>
<true/>
</dict>
</plist>
第七步:配置 GitHub Secrets
在 GitHub 仓库的 Settings → Environments 中创建 staging 环境,添加以下 Secrets:
# 转换文件为 base64
base64 -i mas_app.p12 | pbcopy # → MAS_APP_CERT
base64 -i mas_installer.p12 | pbcopy # → MAS_INSTALLER_CERT
base64 -i appcat_profile_mas.provisionprofile | pbcopy # → MAS_PROVISIONING_PROFILE
base64 -i AuthKey_XXXXX.p8 | pbcopy # → ASC_API_KEY
| Secret | 描述 |
|---|---|
MAS_APP_CERT | Mac App Distribution 证书 (base64) |
MAS_INSTALLER_CERT | Mac Installer Distribution 证书 (base64) |
MAC_CERTS_PASSWORD | 证书密码 |
MAS_PROVISIONING_PROFILE | Provisioning Profile (base64) |
ASC_API_KEY | App Store Connect API 私钥 (base64) |
ASC_API_KEY_ID | API Key ID |
ASC_ISSUER_ID | Issuer ID |

第八步:GitHub Actions Workflow
创建 .github/workflows/release-mas.yml:
这里其实我创建了自己的 self-host action runner 试了试,就没去掉,可以忽略,默认行为是用 GitHub 的 runner。
name: Build and Release MAS
on:
workflow_dispatch:
inputs:
upload_to_app_store:
description: "Upload to App Store Connect"
type: boolean
default: false
use_self_hosted_runner:
description: "Use self-hosted runner"
type: boolean
default: false
jobs:
build-mas:
runs-on: ${{ inputs.use_self_hosted_runner && fromJSON('["self-hosted", "macOS", "ARM64"]') || 'macos-14' }}
environment: staging
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Import Mac App Store Certificates
uses: apple-actions/import-codesign-certs@v3
with:
p12-file-base64: ${{ secrets.MAS_APP_CERT }}
p12-password: ${{ secrets.MAC_CERTS_PASSWORD }}
keychain-password: ${{ secrets.MAC_CERTS_PASSWORD }}
- name: Import Mac Installer Certificate
uses: apple-actions/import-codesign-certs@v3
with:
p12-file-base64: ${{ secrets.MAS_INSTALLER_CERT }}
p12-password: ${{ secrets.MAC_CERTS_PASSWORD }}
create-keychain: false
keychain-password: ${{ secrets.MAC_CERTS_PASSWORD }}
- name: Download Provisioning Profile
run: |
echo "${{ secrets.MAS_PROVISIONING_PROFILE }}" | base64 -d > build/appcat_profile_mas.provisionprofile
- name: Build MAS App
run: MAS_BUILD=true pnpm exec electron-vite build && pnpm exec electron-builder --mac mas --publish never
env:
# 你的 env 环境变量等
VITE_API_BASE_URL: ${{ vars.VITE_API_BASE_URL }}
- name: Upload to App Store Connect
if: ${{ inputs.upload_to_app_store }}
run: |
# Setup API Key file
mkdir -p ~/.private_keys
echo "${{ secrets.ASC_API_KEY }}" | base64 -d > ~/.private_keys/AuthKey_${{ secrets.ASC_API_KEY_ID }}.p8
# Find and upload the pkg file
PKG_FILE=$(find dist -name "*.pkg" -type f | head -1)
echo "Uploading: $PKG_FILE"
xcrun altool --upload-app \
--type macos \
--file "$PKG_FILE" \
--apiKey ${{ secrets.ASC_API_KEY_ID }} \
--apiIssuer ${{ secrets.ASC_ISSUER_ID }}
- name: Upload Build Artifacts
uses: actions/upload-artifact@v4
with:
name: mas-build
path: |
dist/*.pkg
retention-days: 14
踩坑记录
证书导入密码问题
第二次导入证书时必须指定 keychain-password,而且要和第一次创建 keychain 时的密码一致:
# 第一次导入,创建 keychain
- uses: apple-actions/import-codesign-certs@v3
with:
keychain-password: ${{ secrets.MAC_CERTS_PASSWORD }} # 必须指定!
# 第二次导入,复用 keychain
- uses: apple-actions/import-codesign-certs@v3
with:
create-keychain: false
keychain-password: ${{ secrets.MAC_CERTS_PASSWORD }} # 必须一致!
No suitable application records 错误
这个错误可能有多种原因:
原因 1:Bundle ID 不匹配
必须确保这三个地方的 Bundle ID 完全一致:
electron-builder.yml中的appId- App Store Connect 中的 Bundle ID
- Provisioning Profile 绑定的 App ID
查看 Profile 绑定的 Bundle ID:
security cms -D -i appcat_profile_mas.provisionprofile | grep -A1 "application-identifier"
原因 2:API Key 权限不够
创建 API Key 时必须选择 App 管理 (App Manager) 或 管理员 (Admin) 权限。
原因 3:App Store Connect 没有 macOS 应用
确保在 App Store Connect 中已创建应用,且平台包含 macOS。
GH_TOKEN 报错
MAS 构建不需要发布到 GitHub,添加 --publish never:
electron-builder --mac mas --publish never
缺少高分辨率图标
App Store 要求 1024x1024 的图标(512pt @2x)。确保 .icns 文件包含这个尺寸。
API Key 必须写入文件
xcrun altool 需要从文件读取 .p8 私钥,不能直接传入 base64 字符串。在 CI 中需要先解码到指定目录:
- name: Upload to App Store Connect
run: |
# altool 会在这个目录查找私钥文件
mkdir -p ~/.private_keys
echo "${{ secrets.ASC_API_KEY }}" | base64 -d > ~/.private_keys/AuthKey_${{ secrets.ASC_API_KEY_ID }}.p8
xcrun altool --upload-app \
--apiKey ${{ secrets.ASC_API_KEY_ID }} \
--apiIssuer ${{ secrets.ASC_ISSUER_ID }} \
...
PKG 文件路径问题
xcrun altool --file dist/*.pkg 可能会报错 “file cannot be found”,原因:
- 无匹配文件时:bash 默认会把
dist/*.pkg当作字面字符串传递 - 多文件匹配时:所有匹配的文件都会作为参数传递,但
--file只接受一个
用 find 命令显式获取文件路径更可靠:
PKG_FILE=$(find dist -name "*.pkg" -type f | head -1)
xcrun altool --upload-app --file "$PKG_FILE" ...
希望这篇指南能帮你少走弯路。
再次提醒:这个 workflow 会消耗大量 GitHub Actions 免费额度,苹果的公证过程硬等 3~5 分钟,runner 只能空转。如果你的团队频繁构建,强烈建议自建 runner 或者优化触发策略(比如只在打 tag 时触发)。
喜欢的话,留下你的评论吧~