Skip to content

Releasing

Release from the commit you intend to ship, after completing Before tagging. Use the intended release number in place of the example:

Terminal window
git tag -a v0.4.0 -m "MLabel 0.4.0"
git push origin v0.4.0

Pushing a v* tag starts the release process. If packaging fails, repair the workflow on main, then run Actions → Release → Run workflow with the existing tag. The retry checks out that tag in every job, so its application code stays unchanged. Retries refuse to replace an already published release.

tag pushed
│
├── verify lint · format · typecheck · test (parallel matrix)
├── build electron-vite build → upload out/ (once, platform-agnostic)
└── draft create a draft release if missing
│
▼
package (macOS + Windows in parallel)
│ download out/, package, sign, upload installers + latest*.yml
▼
publish flip the draft live

The pipeline keeps the application commit and uploaded artifacts consistent:

The bundle is built once. Both packaging jobs download the same out/ artifact rather than rebuilding. The bundle is platform-agnostic; only packaging is not.

The draft is created up front. Two runners uploading into a release neither has created would race. Pre-creating a draft means both upload into something that already exists, and publish un-drafts it once everything has landed.

The version comes from the tag, passed as --config.extraMetadata.version. package.json is not the source of truth at release time, so a tag and a stale version field cannot disagree.

Under Settings → Secrets and variables → Actions:

Secret Purpose
APPLE_API_KEY_B64 base64 of the App Store Connect .p8 key
APPLE_API_KEY_ID Key ID
APPLE_API_ISSUER Issuer UUID
APPLE_TEAM_ID Apple Developer Team ID
CSC_LINK base64 of the Developer ID Application .p12
CSC_KEY_PASSWORD Password for that .p12

The workflow imports the macOS certificate into a temporary keychain and passes its path as CSC_KEYCHAIN. It uses the keychain password for unlocking and partition access, and CSC_KEY_PASSWORD only for importing the certificate. This avoids a signing bug in the pinned electron-builder version. Signing is required for macOS; the workflow removes the temporary signing material after packaging.

The certificate secrets are only available to the macOS import step.

Windows ships unsigned. Changing Windows signing requires validating the packaging workflow and the resulting installer.

Platform Assets
macOS .dmg (the human download) and .zip (required by Squirrel.Mac)
Windows NSIS installer and a portable .exe
Both latest-mac.yml / latest.yml — the update metadata

Both macOS assets ship on purpose. latest-mac.yml points at the zip, so dropping it breaks updates for every existing install; the dmg exists because installing from it avoids the Gatekeeper translocation that would otherwise block those updates.

The portable and dmg filenames are pinned in electron-builder.yml so the app can deep-link the exact asset when it cannot self-update.

electron-updater must stay external — listed in nodeExternals in electron.vite.config.ts and in package.json dependencies. Bundling it breaks updates silently: the packaged app looks fine and simply never updates again.

Development builds can check for updates and offer a download link. Installing an update in place requires a packaged build.

  • pnpm lint && pnpm format:check && pnpm typecheck && pnpm test
  • pnpm schema produces no change (the test catches this, but check early)
  • package.json version and changelog updated to match the intended release
  • Built locally at least once: pnpm build:mac or pnpm build:win
  • Docs rebuilt if src/core/config/** changed — see the docs site
Terminal window
pnpm build:mac # build + package, signing from .env
pnpm build:win # build + package, unsigned

package:mac / package:win are the CI variants: they package a prebuilt out/ and publish. Do not run those locally unless you intend to publish.

Two pins are deliberate, and CLAUDE.md is the source of truth:

  • Babel stays on 7.x. babel-plugin-react-compiler has no Babel 8 support. The plan is to drop Babel entirely once React’s Rust compiler port ships in plugin-react — not to migrate to Babel 8 in between.
  • Electron 43 is held. Revisit at 43.0.1+; 42.x receives patches until roughly October 2026.