Nov 12, 2023

Setting up Hosted macOS GitHub Actions Workflows for Electron Builds

Last week, I wrote about releasing Electron applications on macOS, going into detail on the specific steps required to go from source code to packaged application. Building your app on your development machine is one thing, but ideally, you want to publish new builds as soon as a commit is merged, irrespective of who pushed the change on your team. If you’re working in a bigger team, you also want to keep your code signing secrets, well, secret.

This week, I’m completing the release cycle with Continuous Integration (CI), so let’s get started and move the release work to GitHub Actions.


To arrive at the same starting point, please follow the previous guide to set up a Developer ID Application code signing certificate on your machine and configure an app-specific password for notarization. Once you’ve satisfied all the requirements in the previous guide, you can head back to this section.

Before we can run our build in CI, we need to make our secrets and code signing certificates available to hosted GitHub Actions runners. Following the official guide published by GitHub, we need to open Xcode, go into the signing certificates modal, and export the Developer ID Application certificate (called APPLE_SIGNING_CERTIFICATE_BASE64 from here on) we’ve been referring to in the last guide. To export the certificate, choose a secure password and remember it for later. To retrieve the base64-encoded certificate representation after exporting, run

base64 -i APPLE_SIGNING_CERTIFICATE_BASE64.p12 | pbcopy

To import the code signing certificate to GitHub Actions, you should end up with two secrets: APPLE_SIGNING_CERTIFICATE_BASE64 and APPLE_SIGNING_CERTIFICATE_PASSWORD. To create a temporary Keychain instance, configure a random KEYCHAIN_PASSWORD.

To enable app notarization for macOS builds, make sure to configure APPLE_ID, APPLE_PASSWORD, and APPLE_TEAM_ID secrets.

For publishing application builds to S3 or your preferred static storage or auto-update solution, make sure to provide the required secrets. In our case, we’ll use the Electron Forge S3 publisher, which requires the AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_REGION secrets.

Hosted Mac runners

To distribute our application to Apple Silicon and Intel Macs, we need to build the application on a machine running macOS. The actual packaging process is mostly architecture-independent: As long as your application does not use native modules, you can build Apple Silicon variants on an Intel Mac and vice-versa. The platform-specific Electron binary is simply pulled. To play it safe, we’ll write a GitHub Actions workflow that runs both the hosted Intel Mac runner, as well as the public preview of the new Apple Silicon Mac runner for arm64 builds. If you’re completely sure you don’t need it, you can just run both builds on the same x86 machine.

Before you write the Actions workflow, you need to set up your billing details and budget quota in GitHub to enable the preview Mac runners.

Putting everything together

Now that we’ve sorted out all requirements and know exactly what we need, let’s write our GitHub Actions workflow.

    name: Build desktop app
        platform: [macos-13, macos-13-xlarge]
    runs-on: ${{ matrix.platform }}

We’re using the matrix strategy (which can do some pretty powerful things) to generate two jobs running on Intel (or x86) and Apple Silicon (or arm64). Up next, we’ll set up Node.js and Python (for a node-gyp dependency).

      - name: Checkout repository
        uses: actions/checkout@v4
      - uses: actions/setup-python@v4
          python-version: '3.11'
      - uses: actions/setup-node@v3
          node-version: '20.9.0'

Once that’s ready, we can install the code signing certificate we prepared in the Actions secrets.

      - name: Install Apple codesigning certificate
        run: |

          # import certificate and provisioning profile from secrets
          echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $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 -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
          security list-keychain -d user -s $KEYCHAIN_PATH

Once everything is ready, we can install dependencies and build, package, make, and publish our application. This may vary depending on your Electron distribution, with Electron Forge we just have to run npm run publish.

      - name: Install dependencies
        run: npm ci
      - name: Build app
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_REGION: ${{ secrets.AWS_REGION }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
          NODE_ENV: production
        run: npm run publish

Finally, we unload the temporary Keychain, which is a good measure to get rid of sensitive files in the temporary GitHub Actions runner instance, just in case.

      - name: Clean up keychain
        if: ${{ always() }}
        run: |
          security delete-keychain $RUNNER_TEMP/app-signing.keychain-db

And that’s it, store the workflow in the .github/workflows directory and get building! I was surprised that it was possible to build macOS Electron apps in GitHub Actions in the first place, and I love the new Apple Silicon runners!