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.
Prerequisites
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.
jobs:
build:
name: Build desktop app
strategy:
matrix:
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).
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- uses: actions/setup-node@v3
with:
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
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.APPLE_SIGNING_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.APPLE_SIGNING_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# 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
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
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!