Automating Electron App Submission to MAS (Mac App Store) with GitHub Actions

发表于 2026-01-23 23:02 2213 字 12 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。

This article has been machine-translated from Chinese. The translation may contain inaccuracies or awkward phrasing. If in doubt, please refer to the original Chinese version.

I recently configured our Electron desktop app for TestFlight on the Mac side, implementing automated builds and uploads with GitHub Actions. I ran into quite a few pitfalls along the way, so here’s a record of the entire process.

I’m not very familiar with Electron or the MAS submission process either — this article was figured out step by step through trial and error. It’s currently working and successfully uploading to TestFlight, but please point out any errors or omissions! Some parts of this record were saved through conversations with AI, so there may be inaccuracies.

Warning: Time Cost: The entire workflow will consume a significant amount of GitHub Actions free credits! Apple’s code signing and notarization process requires waiting 3-5 minutes, during which the runner can only idle waiting for Apple’s server response — very inefficient. If your project builds frequently, consider setting up a self-hosted runner or optimizing your trigger strategy.

Here is Apple’s official documentation Upload builds, which introduces how to use Apple’s official tools and API to upload app builds to App Store Connect.

Let me show the final result first — why does something this complicated have to exist (quiet complaining):

After configuration is complete:

  1. Manually trigger the workflow
  2. Check “Upload to App Store Connect” to upload to TestFlight; otherwise, it only packages the build
  3. View the build in the TestFlight tab of App Store Connect
  4. Distribute to testers

The entire migration from direct DMG download to TestFlight took about half a day, mostly spent troubleshooting various certificate and Bundle ID configurations.

According to the documentation, Apple’s recommended upload methods are:

  • Xcode: Apple’s official integrated development environment (IDE), supporting the full workflow from development, testing, to submission
  • Transporter: A macOS application with a graphical interface, suitable for quick and easy uploads with delivery logs and history viewing
  • xcrun altool: A command-line tool invoked through Xcode’s built-in xcrun, which can validate app binaries and upload them to App Store Connect
  • App Store Connect API: A REST-based API that supports authentication via JSON Web Tokens (JWT), enabling automated upload workflows

Since we’re using Actions, we’ll use xcrun altool for uploading.

Background

Our application is built with Electron + React + TypeScript, and was previously distributed via GitHub Releases as DMG installers. We now want to use TestFlight for beta testing and eventually publish to the Mac App Store. The following steps assume your app name is AppCat.

Prerequisite: Create the App in App Store Connect

Before configuring certificates, you need to create the app in App Store Connect:

  1. Open App Store Connect - My Apps
  2. Click ”+” -> New App
  3. Check macOS for the platform
  4. Enter the app name
  5. Select or create a Bundle ID (must match the appId in electron-builder.yml exactly!)
  6. Fill in the SKU with anything, such as appcat-macos

Warning: If you skip this step, you’ll get a “No suitable application records were found” error when uploading.

Our app was previously published on iOS, so I won’t go into detail here — it’s just the standard submission process. Now we need to publish to the Mac App Store.

Differences Between Two Distribution Methods

First, you need to understand that Mac apps have two distribution methods, each requiring different certificates:

Distribution MethodCertificate TypePurpose
Direct Download (DMG)Developer ID ApplicationWebsite/GitHub download
Mac App Store3rd Party Mac Developer ApplicationApp Store / TestFlight

Additionally, MAS versions require sandboxing, which means some features (such as desktopCapturer screen capture) may be restricted.

Step 1: Create Certificates

1.1 Create a CSR File

Open Keychain Access on your local machine:

Menu Bar -> Keychain Access -> Certificate Assistant -> Request a Certificate From a Certificate Authority...

Then fill in:

  • User Email Address: Your email
  • Common Name: Anything, such as “AppCat MAS”
  • Select “Saved to disk”
  • CA Email Address: Leave blank

Save the .certSigningRequest file.

1.2 Create a Mac App Distribution Certificate

  1. Open Apple Developer - Certificates
  2. Click the ”+” button
  3. Select Mac App Distribution
  4. Upload the CSR file you just created
  5. Download the certificate and double-click to install it in Keychain (select the “login” keychain)

1.3 Create a Mac Installer Distribution Certificate

Repeat the steps above, but select Mac Installer Distribution. This certificate is used to sign .pkg installer packages.

Step 2: Create a Provisioning Profile

  1. Open Apple Developer - Profiles
  2. Click ”+”
  3. Select Mac App Store Connect
  4. Select your App ID (must match the Bundle ID in App Store Connect!)
  5. Select the Mac App Distribution certificate you just created
  6. Name it and download

Important: The Bundle ID in the Profile must exactly match the one in App Store Connect, otherwise uploading will fail with “No suitable application records were found”.

This step produces the appcat_profile_mas.provisionprofile file (the filename can be customized, but it must be consistent with subsequent configurations).

Step 3: Create an App Store Connect API Key

This is used for CI/CD automated build uploads.

  1. Open App Store Connect - Users and Access - Keys
  2. Click ”+” to create a new key
  3. Name it anything; for permissions, select App Manager or Admin
  4. Click “Generate”

Finding the Issuer ID

The Issuer ID is displayed at the top of the keys page, as a UUID-format string, for example:

50ce4b17-dd5e-4550-877b-7a7bb0d608d7

All API Keys belonging to the same team share the same Issuer ID. This value is not sensitive information.

Getting the Key ID

After creating the key, the Key ID will be displayed in the key list (Key ID column), for example BU64538829.

Downloading the .p8 Private Key File

Click “Download API Key” to download the .p8 file:

  • The filename format is AuthKey_XXXXX.p8, where XXXXX is the Key ID
  • This file is the API authentication private key; leaking it would cause security issues

Extremely Important: The .p8 private key file can only be downloaded once per team! Save it to a secure location (such as a password manager) immediately after downloading. If lost, you can only revoke the current key and create a new one, which means updating all CI/CD configurations that use that key.

This step produces three values:

  • AuthKey_XXXXX.p8 file -> later converted to ASC_API_KEY
  • Key ID -> corresponds to the ASC_API_KEY_ID environment variable we’ll set later
  • Issuer ID -> corresponds to ASC_ISSUER_ID

Step 4: Export Certificates as .p12

CI/CD requires certificates in .p12 format.

  1. Open Keychain Access
  2. On the left, select “login”; at the top, select “My Certificates”
  3. Find “3rd Party Mac Developer Application: xxx”
    • Right-click -> Export
    • Choose .p12 format
    • Set a password
  4. Similarly export “3rd Party Mac Developer Installer: xxx”

This step produces the mas_app.p12 and mas_installer.p12 files.

Step 5: electron-builder Configuration

Add MAS configuration in electron-builder.yml:

appId: app.bundlename.AppCat # Must match App Store Connect! Replace with your app bundleId

# Keep existing Developer ID configuration unchanged
mac:
  entitlementsInherit: build/entitlements.mac.plist
  hardenedRuntime: true
  notarize: true

# New Mac App Store configuration
mas:
  hardenedRuntime: false # MAS uses sandbox instead
  entitlements: build/entitlements.mas.plist
  entitlementsInherit: build/entitlements.mas.inherit.plist
  provisioningProfile: build/appcat_profile_mas.provisionprofile
  notarize: false # MAS doesn't need notarize
  category: public.app-category.productivity # Choose your app category here

# PKG configuration
pkg:
  isRelocatable: false
  overwriteAction: upgrade
  artifactName: ${name}-${version}-mas.${ext}

Step 6: Create MAS Entitlements

MAS requires sandboxing. Create 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>

Create build/entitlements.mas.inherit.plist (child process inheritance):

<?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>

Step 7: Configure GitHub Secrets

Create a staging environment in the GitHub repository under Settings -> Environments, and add the following Secrets:

# Convert files to 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
SecretDescription
MAS_APP_CERTMac App Distribution cert (base64)
MAS_INSTALLER_CERTMac Installer Distribution cert (base64)
MAC_CERTS_PASSWORDCertificate password
MAS_PROVISIONING_PROFILEProvisioning Profile (base64)
ASC_API_KEYApp Store Connect API private key (base64)
ASC_API_KEY_IDAPI Key ID
ASC_ISSUER_IDIssuer ID

Step 8: GitHub Actions Workflow

Create .github/workflows/release-mas.yml:

I actually created my own self-hosted action runner to try it out, so I left it in — you can ignore it. The default behavior uses GitHub’s 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:
          # Your env variables etc.
          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

Troubleshooting Notes

Certificate Import Password Issue

When importing the second certificate, you must specify keychain-password, and it must match the password used when creating the keychain the first time:

# First import, creates keychain
- uses: apple-actions/import-codesign-certs@v3
  with:
    keychain-password: ${{ secrets.MAC_CERTS_PASSWORD }} # Must be specified!

# Second import, reuses keychain
- uses: apple-actions/import-codesign-certs@v3
  with:
    create-keychain: false
    keychain-password: ${{ secrets.MAC_CERTS_PASSWORD }} # Must match!

“No suitable application records” Error

This error can have multiple causes:

Cause 1: Bundle ID Mismatch

The Bundle ID must be exactly the same in all three places:

  • appId in electron-builder.yml
  • Bundle ID in App Store Connect
  • App ID bound to the Provisioning Profile

To check the Bundle ID bound to the Profile:

security cms -D -i appcat_profile_mas.provisionprofile | grep -A1 "application-identifier"

Cause 2: Insufficient API Key Permissions

When creating the API Key, you must select App Manager or Admin permissions.

Cause 3: No macOS App in App Store Connect

Ensure you’ve created an app in App Store Connect and the platform includes macOS.

GH_TOKEN Error

MAS builds don’t need to publish to GitHub. Add --publish never:

electron-builder --mac mas --publish never

Missing High-Resolution Icon

The App Store requires a 1024x1024 icon (512pt @2x). Ensure your .icns file includes this size.

API Key Must Be Written to a File

xcrun altool needs to read the .p8 private key from a file; it cannot accept a base64 string directly. In CI, you need to decode it to the specified directory first:

- name: Upload to App Store Connect
  run: |
    # altool looks for the private key file in this directory
    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 File Path Issue

xcrun altool --file dist/*.pkg may fail with “file cannot be found” because:

  1. When there are no matching files: Bash will pass dist/*.pkg as a literal string by default
  2. When multiple files match: All matching files are passed as arguments, but --file only accepts one

Using the find command to explicitly get the file path is more reliable:

PKG_FILE=$(find dist -name "*.pkg" -type f | head -1)
xcrun altool --upload-app --file "$PKG_FILE" ...

I hope this guide helps you avoid some detours.

One more reminder: This workflow will consume a significant amount of GitHub Actions free credits. Apple’s notarization process requires waiting 3-5 minutes with the runner idling. If your team builds frequently, I strongly recommend setting up a self-hosted runner or optimizing your trigger strategy (for example, only triggering on tag pushes).

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

© 2020 - 2026 cos @cosine
Powered by theme astro-koharu · Inspired by Shoka