Automated Electron build with release to Mac App Store, Microsoft Store, Snapcraft

I just released LosslessCut to stores on three different platforms Windows, Mac and Linux: Mac App Store, Microsoft Store, Snapcraft. I want to share the process and the hurdles I went through in this article.

I think the key to any well-maintained project is to have an automated release pipeline, or else it will be a hassle every time one wants to do bug fixes and improvements. I have now taken some time to set up automated build and signing for Mac OS (notarization) as well as release to the Mac App Store, Microsoft Store and Snapcraft. I have used many CI systems but I find that the new Github Actions are very fast and feature rich, and with its reusable action ecosystem it is quite easy to use. And all the setup and configuration is of course available open source for anyone to see here. See the actions in action(heh) here, as a free reference for anyone else needing to set up an automated build and release process of their Electron app.

Building and releasing for big stores like Microsoft and Mac App Store can be a true hassle and a nightmare that can take weeks to set up, however thanks to awesome projects like electron-builder and action-electron-builder, it is not really that bad and only took me a few days.

Here are the most valuable resources I used to set up everything:

Biggest issues

The things that I had the most trouble with were:

Getting all the correct metadata in place

This is mostly handled by electron-builder and can be seen in package.json, but some things did not give an error until a human looked at it during the review process.

Hardened runtime

In package.json, hardenedRuntime needs to be set to true for the mac platform, because notarized apps need to be hardened.

However for Mac App Store signed apps mas, it needs to be set to false.

Microsoft Store

Microsoft Store review

Reviewers complained about:

Mac downloadable binary

Notarization

Notarization happens only for downloadable binaries after they are built (not for Mac App Store submitted binaries). Therefore the mac target (but not mas).

Notarization is uploading the .app to Apples servers so they can verify it. Uploads the mac binary (has to be signed with Developer ID Application identity) https://developer.apple.com/forums/thread/128772

Mac App Store

Signing for mas and mas-dev uses identities Apple Distribution and Apple Development respectively. (this is new for Xcode 11, used to be Mac App Distribution and Mac Development). Finally pkg (which is uploaded to Mac App Store) is signed with 3rd Party Mac Developer Application identity.
https://github.com/electron-userland/electron-builder/issues/2513

The entitlements files

These files need to contain the correct entitlements in order to be able to open files and directories on the filesystem, as well as inherit these rights to the ffmpeg process. Without any entitlements, the app is not allowed to read/write files.

One thing I struggled a bit with is that Mac Store Apps need to have opened a file using the system open dialog before the app can read or write. In order to write to a directory, like LosslessCut does, I need to first present the user with an Open dialog to select which directory to output to. This seems to be a requirement in order for apps not to just write to any folder on the system.

App Icon

Icon needs to be .icns format and have 512 and 1024 sizes in it

Touch Bar API

Reviewers complained about what I believe is Electron using the Touch Bar API, but my app does not use it:

“If your app does not integrate Touch Bar functionality, please indicate this information in the Review Notes section for each version of your app in iTunes Connect when submitting for review”

So need to make sure to write this in Review notes for every release.

ffmpeg private API usage

Getting ffmpeg through App Store review is a bit tricky. The ffmpeg static build that I originally used has a lot of stuff built into them. One thing that App Store review complained about is a call to _SecIdentityCreate which I traced back to this file. But it could be disabled by the --disable-securetransport option. So I needed to build a custom ffmpeg without this flag.

nm ffmpeg | grep SecIdentityCreate
nm ffprobe | grep SecIdentityCreate

See also:

Other than that, with Electron 8, there was no other private API usages complained about.

When building ffmpeg on Mac OS, even with all options indicating it should include everything as static, it will still link .dylib files dynamically.

otool -L ffmpeg | grep /usr/local

I solved this by only building my own ffmpeg using GitHub Actions and stripping away all external dependencies like codecs (because we are only doing muxing operations which only requires ffmpeg core functionality.)

See also:

Building ffmpeg to support older Mac OS versions

On the first try I got an error report on older Mac OS X (10.13):

Command was killed with SIGABRT (Aborted): ffprobe -of json -show_format -i vid.mp4
dyld: lazy symbol binding failed: Symbol not found: ____chkstk_darwin
Referenced from: ffprobe (which was built for Mac OS X 10.15)
Expected in: /usr/lib/libSystem.B.dylib

This can be verified by running nm ffprobe | grep ____chkstk_darwin

In order to fix this I had to add -mmacosx-version-min=10.10 to --extra-clfags and --extra-ldflags

Then rebuild and verify that the ____chkstk_darwin symbol reference is gone.

Certificate creation/renewal

This also needs to be re-done when certificates expire.

Go to App Store Connect - Certificates and (re)create the following certs:

  • Developer ID Application
  • Mac Installer Distribution
  • Apple Distribution
  • Apple Development

Then dowload them and double click to load into Keychain access.

Important: Now need to regenerate the provisioning profile after creating new certificates (select the new Apple Distribution certificate), then add the new .provisionprofile file into the project (overwrite existing). Similarly for for development provisioning profile.

Then from Keychain access (use My Certificates tab to get certficates with keys), select and export the following certificates to p12, and set MAC_CERTS and MAC_CERTS_PASSWORD in github secrets for the project (randomly generate the password):

  • Apple Distribution (mas signing for App Store)
  • 3rd Party Mac Developer Installer (for wrapped .pkg for App Store)
  • Developer ID Application (needed for notarization)

See https://github.com/samuelmeuli/action-electron-builder#code-signing

Mas build can no longer be run locally on a dev Mac. For running mas app locally, we need to create a separate provisioning profile for development, with the Developer Mac’s UUID and use mas-dev with that profile. See this issue.

Troubleshooting

Testing signing locally

Make sure you have these in your keychain: Apple Development, Apple Distribution.

With specific certificates only:

export CSC_KEY_PASSWORD='...'
export CSC_LINK='...'
npx electron-builder --mac
  • CSC_KEY_PASSWORD is the password set when exporting the p12
  • CSC_LINK is base64 of certificates like action-electron-builder accepts

Check signature

Check certificate of built app’s signature:

pkgutil --check-signature dist/mas/LosslessCut.app

Check certificate of app’s Provision Profile:

security cms -D -i dist/mas/LosslessCut.app/Contents/embedded.provisionprofile

These two certs should match. also for mas-dev or the app will not start (permission denied)

Test APK upload Locally

API_KEY and API_ISSUER can be found in App Store Connect (Users/Keys).

xcrun altool --upload-app -f dist/mas/LosslessCut-mac.pkg --apiKey API_KEY --apiIssuer API_ISSUER

Make sure you have the key in ~/.appstoreconnect/private_keys/AuthKey_API_KEY.p8

See also