Releasing
git tag v1.0.0 && git push origin v1.0.0Pushing a v* tag is the entire release process.
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 liveThree properties are worth understanding:
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 |
Windows ships unsigned. Adding win.signtoolOptions or Azure Trusted Signing later
needs no other change.
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.
Updates require app.isPackaged. They no-op in dev, so the only way to test the path is 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 bumped, if you keep it in step with tags - 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.