用 GitHub Actions 自动化 Electron 上架 MAS(Mac App Store)

发表于 2026-01-23 23:02 2749 字 14 min read

cos avatar

cos

FE / ACG / 手工 / 深色模式强迫症 / INFP / 兴趣广泛养两只猫的老宅女 / remote

本文详细记录了将 Electron 桌面应用通过 TestFlight 上架的完整流程,包括配置 Mac App Store 分发所需的证书、签名文件、权限清单、GitHub Secrets 和 CI/CD 工作流。重点解决了 Bundle ID 一致性、证书导入密码、API Key 权限、文件路径匹配等常见坑点,并强调了流程对 GitHub Actions 配额的高消耗,建议团队优化触发策略或自建 Runner。

最近把我们的 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 的指南。

先放最终的效果,为什么会有这么麻烦的东西啊(小声抱怨):

配置完成后:

  1. 手动触发 workflow
  2. 勾选 “Upload to App Store Connect” 则上传到 TestFlight,否则仅打包
  3. 在 App Store Connect 的 TestFlight 标签页查看构建
  4. 分发给测试人员

整个流程从 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 创建应用:

  1. 打开 App Store Connect - 我的 App
  2. 点击 ”+” → 新建 App
  3. 平台勾选 macOS
  4. 名称填写应用名
  5. Bundle ID 选择或创建一个(必须和 electron-builder.yml 中的 appId 完全一致!
  6. SKU 随便填,如 appcat-macos

⚠️ 如果跳过这一步,后面上传时会报错 “No suitable application records were found”。

我们的应用之前上架过 iOS,所以这里就不赘述了,就是常规的上架流程,现在是要上架 Mac 端的 App Store。

两种分发方式的区别

首先要理解,Mac 应用有两种分发方式,它们需要不同的证书

分发方式证书类型用途
直接下载 (DMG)Developer ID Application网站/GitHub 下载安装
Mac App Store3rd Party Mac Developer ApplicationApp Store / TestFlight

另外,MAS 版本强制要求沙盒,这意味着一些功能(如 desktopCapturer 屏幕截图)可能受限。

第一步:创建证书

1.1 创建 CSR 文件

打开本地的钥匙串访问

顶部菜单栏 → 钥匙串访问 → 证书助理 → 从证书颁发机构请求证书...

然后填写:

  • 用户电子邮件地址:你的邮箱
  • 常用名称:随便填,如 “AppCat MAS”
  • 选择”存储到磁盘”
  • CA 邮件地址:留空

保存 .certSigningRequest 文件。

1.2 创建 Mac App Distribution 证书

  1. 打开 Apple Developer - Certificates
  2. 点击 ”+” 按钮
  3. 选择 Mac App Distribution
  4. 上传刚才的 CSR 文件
  5. 下载证书,双击安装到钥匙串(选择”登录”钥匙串)

1.3 创建 Mac Installer Distribution 证书

重复上面的步骤,但选择 Mac Installer Distribution。这个证书用于签名 .pkg 安装包。

第二步:创建 Provisioning Profile

  1. 打开 Apple Developer - Profiles
  2. 点击 ”+”
  3. 选择 Mac App Store Connect
  4. 选择你的 App ID(必须和 App Store Connect 中的 Bundle ID 一致!
  5. 选择刚创建的 Mac App Distribution 证书
  6. 命名并下载

⚠️ 重要:Profile 中的 Bundle ID 必须和 App Store Connect 中的完全一致,否则上传会报错 “No suitable application records were found”。

这一步得到 appcat_profile_mas.provisionprofile 文件(文件名可自定义,但要和后续配置保持一致)。

第三步:创建 App Store Connect API Key

用于 CI/CD 自动上传构建。

  1. 打开 App Store Connect - 用户和访问 - 密钥
  2. 点击 ”+” 创建新密钥
  3. 名称随便填,权限选 App 管理管理员
  4. 点击 “生成”

找到 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 格式的证书。

  1. 打开钥匙串访问
  2. 左侧选择”登录”,上方选择”我的证书”
  3. 找到 “3rd Party Mac Developer Application: xxx”
    • 右键 → 导出
    • 格式选 .p12
    • 设置密码
  4. 同样导出 “3rd Party Mac Developer Installer: xxx”

这一步得到了 mas_app.p12mas_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_CERTMac App Distribution 证书 (base64)
MAS_INSTALLER_CERTMac Installer Distribution 证书 (base64)
MAC_CERTS_PASSWORD证书密码
MAS_PROVISIONING_PROFILEProvisioning Profile (base64)
ASC_API_KEYApp Store Connect API 私钥 (base64)
ASC_API_KEY_IDAPI Key ID
ASC_ISSUER_IDIssuer 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”,原因:

  1. 无匹配文件时:bash 默认会把 dist/*.pkg 当作字面字符串传递
  2. 多文件匹配时:所有匹配的文件都会作为参数传递,但 --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 时触发)。

喜欢的话,留下你的评论吧~