While working on Ghostmail, an Electron-based Maildir frontend I’ve built, I had to get the app ready for distribution on other people’s machines by code signing and notarising it.

I’m using Electron Forge to build the app, and I expected everything to work pretty much out of the box. However, I ran into an important, niche issue that should be highlighted.

I added the dotenv package to my development dependencies, initialised it at the top, and filled out my .env file with the relevant values, taking care to create an app-specific password for my account.


const { FusesPlugin } = require("@electron-forge/plugin-fuses");
const { FuseV1Options, FuseVersion } = require("@electron/fuses");
if (process.env.NODE_ENV !== "production") {
  require("dotenv").config();
}

module.exports = {
  packagerConfig: {
    asar: true,
    icon: "assets/ghost",
    osxSign: {},
    osxNotarize: {
      tool: "notarytool",
      appleId: process.env.APPLE_ID,
      appleIdPassword: process.env.APPLE_PASSWORD,
      teamId: process.env.APPLE_TEAM_ID,
    },
  },
  rebuildConfig: {},
  makers: [
    {
      name: "@electron-forge/maker-zip",
      platforms: ["darwin"],
      config: (arch) => ({
        // Note that we must provide this S3 URL here
        // in order to support smooth version transitions
        // especially when using a CDN to front your updates
        macUpdateManifestBaseUrl: `${process.env.S3_PUBLIC_HOSTNAME}/Ghostmail/darwin/${arch}`,
      }),
    },
  ],
  publishers: [
    {
      name: "@electron-forge/publisher-s3",
      config: {
        bucket: process.env.S3_BUCKET,
        region: "auto",
        endpoint: process.env.S3_ENDPOINT,
        accessKeyId: process.env.S3_ACCESS_KEY,
        secretAccessKey: process.env.S3_SECRET_KEY,
        public: true,
      },
    },
  ],
  plugins: [
    {
      name: "@electron-forge/plugin-auto-unpack-natives",
      config: {},
    },
    // Fuses are used to enable/disable various Electron functionality
    // at package time, before code signing the application
    new FusesPlugin({
      version: FuseVersion.V1,
      [FuseV1Options.RunAsNode]: false,
      [FuseV1Options.EnableCookieEncryption]: true,
      [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
      [FuseV1Options.EnableNodeCliInspectArguments]: false,
      [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
      [FuseV1Options.OnlyLoadAppFromAsar]: true,
    }),
  ],
};

My Apple ID has existed for well over a decade. The first thing I had to do was leave an expired team I was no longer involved with. Immediately, problems. I logged in and out and kept seeing the old team, and after 3-4 attempts over 30+ minutes, the old team was gone and I was able to join again with my own membership.

From there, running my npm run package script to generate Ghostmail and get it signed and notarised, I kept getting stuck.

Every time, no matter how long I left it, I’d get stuck on the following debug output:

electron-notarize:notarytool zip succeeded, attempting to upload to Apple +7s

I eventually extrapolated the command that Forge was running, dropped the JSON output and wait flags from it, and ran it directly:

xcrun notarytool submit /var/folders/kg/g2tj3w9n6tj3fvn9kqydcj3m0000gn/T/electron-notarize-FCWaM3/Ghostmail.zip --apple-id [email protected] --password pass-goes-here --team-id ABC123

This one uploaded the zip and gave me a successful notarisation response within 30 seconds! 🤯

I decided to re-run the original package command and it still continued to fail.

At this point, I had a sneaking suspicion that the problem was that whilst my actual Apple ID was very old, my membership in the Developer Programme was brand new. When I re-ran the command again the next day, it succeeded without issue.

Key takeaway

The important takeaway is that you seemingly cannot sign up for the Developer Programme and start notarising in the first ~8 hours - you must be patient.