Releasing
Release from the commit you intend to ship, after completing Before tagging. Use the intended release number in place of the example:
git tag -a v0.4.0 -m "MLabel 0.4.0"git push origin v0.4.0Pushing 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.
The pipeline
Section titled “The pipeline”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 liveThe 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.
Signing secrets
Section titled “Signing secrets”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.
Artifacts
Section titled “Artifacts”| 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.
The update contract
Section titled “The update contract”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.
Before tagging
Section titled “Before tagging”-
pnpm lint && pnpm format:check && pnpm typecheck && pnpm test -
pnpm schemaproduces no change (the test catches this, but check early) -
package.jsonversion and changelog updated to match the intended release - Built locally at least once:
pnpm build:macorpnpm build:win - Docs rebuilt if
src/core/config/**changed — see the docs site
Local packaging
Section titled “Local packaging”pnpm build:mac # build + package, signing from .envpnpm build:win # build + package, unsignedpackage:mac / package:win are the CI variants: they package a prebuilt out/ and publish.
Do not run those locally unless you intend to publish.
Dependency holds
Section titled “Dependency holds”Two pins are deliberate, and CLAUDE.md is the source of truth:
- Babel stays on 7.x.
babel-plugin-react-compilerhas no Babel 8 support. The plan is to drop Babel entirely once React’s Rust compiler port ships inplugin-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.