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.