Every target bundles main/preload/renderer to build/ (via electron-vite build, not electron-builder's default) and packages an installer into dist/.
npm run build:win # NSIS installer -> dist/
npm run build:mac # DMG -> dist/ (macOS host only)
npm run build:linux # AppImage + .deb -> dist/The Windows and Linux installers can also be built in the container without a
local Node install — make package-win / make package-linux run them in an
amd64 service (electron-builder's bundled makensis is x86_64-only and shells
out to wine). A DMG needs macOS's hdiutil, so make package-mac runs on the
host. The DMG window size, icon positions and backdrop live in
electron-builder.yml (backdrop rendered from resources/dmg-background.svg).
electron-builder.yml sets asar: true and flips the Electron fuses that harden
the packaged binary at pack time:
runAsNode,enableNodeCliInspectArguments,enableNodeOptionsEnvironmentVariableoff — stopsELECTRON_RUN_AS_NODE=1 "Diff Bro.exe" script.jsturning the installed, trusted binary into a generic Node interpreter (a LOLBin).onlyLoadAppFromAsaron — the runtime refuses to load app code from anywhere but the ASAR.enableCookieEncryptionon.resetAdHocDarwinSignatureon — mandatory on Apple silicon. Flipping the fuse wire rewrites the Electron binary and invalidates the linker-signed ad-hoc signature it ships with, andmac.identity: nullmeans electron-builder signs nothing afterwards. Without it the DMG contains an app whose signature is structurally broken (codesign --verifyreports "code has no resources but signature indicates they must be present"), which macOS reports as "damaged" and which clearing quarantine does not fix. With it,@electron/fusesre-signs the whole bundle ad-hoc right after the flip.
enableEmbeddedAsarIntegrityValidation is intentionally off until real
signing lands: it binds the ASAR hash to the code signature, and on an
unsigned / ad-hoc-signed macOS build that makes a quarantined app fail to launch
as "Diff Bro is damaged" even after the user clears quarantine. The fuses are
flipped before the ad-hoc re-signature, so the flip order is fine — the
integrity fuse specifically needs a Developer ID signature to be safe.
Turn it back on together with signing (DEVELOPMENT_PLAN.md Phase 3).
Code signing (Windows Authenticode + macOS notarization) is the remaining
prerequisite — see below and DEVELOPMENT_PLAN.md Phase 3.
build:win extracts electron-builder's winCodeSign cache, which contains
macOS .dylib files stored as symlinks. Creating a symlink on Windows needs
SeCreateSymbolicLinkPrivilege, which normal accounts lack unless Developer
Mode is on — otherwise the build fails with "Cannot create symbolic link: A
required privilege is not held by the client."
Enable it once via Settings → Privacy & security → For developers → Developer Mode, then re-run. If a build already failed partway, clear the partial cache:
Remove-Item -Recurse -Force "$env:LOCALAPPDATA\electron-builder\Cache\winCodeSign"Pushing a tag matching v*.*.* runs
.github/workflows/release.yml: it audits
dependencies, lints + tests, syncs package.json's version to the tag (so the
app reports the right version), builds Windows and macOS installers in parallel,
and attaches them to a GitHub Release.
git tag v0.1.0 && git push origin v0.1.0Installers are version-stamped (diff-bro-Setup-v0.1.8.exe,
diff-bro-v0.1.8.dmg — see artifactName in electron-builder.yml), so a
downloaded file stays identifiable once it's away from the release page.
${version} resolves from package.json, which the Sync version to the
release tag step rewrites from the tag before packaging.
Because the filename changes every release, GitHub's
…/releases/latest/download/<name> shortcut no longer works — it requires a
fixed name. The README links to the releases page instead. Three places must
change together when the name does: electron-builder.yml, the asset() /
ASSET_TEMPLATE pair in scripts/gen-homebrew-cask.mjs, and the
gh release download --pattern in the homebrew job of release.yml.
Builds are unsigned (no code-signing cert / Apple Developer account yet — see
DEVELOPMENT_PLAN.md Phase 3): Windows shows a SmartScreen warning, and macOS
Gatekeeper marks the app "damaged"/"can't be opened" because the download is
quarantined and unsigned. Clear the quarantine after copying to Applications:
xattr -dr com.apple.quarantine "/Applications/Diff Bro.app"(or right-click → Open on first launch). There is deliberately no auto-update — installers are the only distribution path.
Diff Bro installs as a Homebrew cask from a personal tap — a separate
repo named homebrew-<name> (e.g. mindaugaskasp/homebrew-tap, tapped as
mindaugaskasp/tap). The cask (packaging/homebrew/diff-bro.rb)
points at the tagged release DMG and pins its sha256.
Automated on release. The homebrew job in release.yml runs after the
GitHub Release is published: it downloads the released diff-bro-v<version>.dmg,
regenerates the cask (version + sha256) via
scripts/gen-homebrew-cask.mjs, and pushes
it to the tap — so brew upgrade --cask diff-bro always gets the latest. It
skips cleanly unless two things are configured:
- a repo secret
HOMEBREW_TAP_TOKEN— a PAT with write access to the tap repo; - optionally a repo variable
HOMEBREW_TAP_REPO(defaults tomindaugaskasp/homebrew-tap).
One-time setup: create the homebrew-tap repo, add the token, then let the
next release populate Casks/diff-bro.rb (or seed it once with make brew-cask
- a manual copy). Regenerate locally anytime with
make brew-cask(VERSION=x.y.zand/orDMG=dist/diff-bro-v0.1.8.dmgto override the source).
Because the build isn't notarized yet, the cask's caveats tell users to clear
the quarantine flag with xattr after installing. Homebrew 6 removed
brew install --cask --no-quarantine — the flag now errors out as an invalid
option, and the HOMEBREW_CASK_OPTS route is dead code (cask_opts_quarantine?
has no callers), so every cask install is quarantined. That makes the ad-hoc
signature from resetAdHocDarwinSignature load-bearing: without it, clearing
quarantine does not rescue the app.
The cask declares depends_on arch: :arm64 and depends_on macos: :monterey
(a bare symbol is Homebrew's ">=" form) to match the arm64-only DMG and the
app's LSMinimumSystemVersion, so brew install fails fast on an unsupported
Mac. Without the macos stanza brew audit crashes outright. Validate a
change with brew style and brew audit --cask --online against a local tap.
Submitting to the official homebrew-cask isn't an option until the app is signed + notarized (Phase 3).
release.yml captures the npm ci log and runs
scripts/check-install-warnings.mjs,
which fails the build on any new deprecation / engine warning that isn't in its
reviewed allowlist (the current allowlisted ones are build-time-only transitive
deps of electron-builder's native-rebuild toolchain, which this app never runs).
A dedicated audit job runs npm audit --audit-level=moderate on a cheap
runner before any packaging, and build needs: it — so a tag push with a
known moderate/high/critical vulnerability in the dependency tree fails the
release fast, before spending Windows/macOS build minutes. Tighten the threshold
to --audit-level=low to block on any advisory at all.