Milady Electrobun Release

Use when distributing Electrobun apps, configuring auto-updates, uploading artifacts, understanding update channels, or integrating the Updater API. Covers artifact naming, update.json format, bsdiff patch generation, upload targets, and the full Updater lifecycle.

install
source · Clone the upstream repo
git clone https://github.com/milady-ai/milady
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/milady-ai/milady "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/plugins/electrobun-dev/skills/electrobun-release" ~/.claude/skills/milady-ai-milady-electrobun-release && rm -rf "$T"
manifest: .claude/plugins/electrobun-dev/skills/electrobun-release/SKILL.md
source content

Electrobun Release

Publishing an Electrobun app means hosting a static set of artifacts and pointing the app's

baseUrl
at them. The Updater polls
update.json
, downloads the tarball or a binary patch, and replaces the running app.


Release Channels

ChannelBuilt withUpdatesCodesign
stable
--env=stable
YesRequired
canary
--env=canary
YesRequired
dev
electrobun dev
DisabledSkipped

Each channel is independent: a user can have

MyApp
(stable) and
MyApp-canary
(canary) installed side-by-side. App data lives in separate directories per channel:

  • macOS:
    ~/Library/Application Support/{identifier}/{channel}/
  • Windows:
    %LOCALAPPDATA%/{identifier}/{channel}/
  • Linux:
    ~/.local/share/{identifier}/{channel}/

Configuration

// electrobun.config.ts
export default defineConfig({
  release: {
    baseUrl:       "https://updates.example.com/",  // required for updates
    generatePatch: true,                             // default: true
  }
});

baseUrl
is embedded in
version.json
at build time. The Updater reads it at runtime to construct all update URLs. Without
baseUrl
, updates are silently disabled.


Artifact Naming

All remote artifacts follow

{channel}-{os}-{arch}-{filename}
.

OS strings:

macos
/
win
/
linux

Update Manifest

{channel}-{os}-{arch}-update.json

Examples:
  stable-macos-arm64-update.json
  stable-macos-x64-update.json
  stable-win-x64-update.json
  stable-linux-x64-update.json
  canary-macos-arm64-update.json
  canary-linux-arm64-update.json

Contents:

{
  "version":  "1.2.0",
  "hash":     "<sha256-of-uncompressed-tarball>",
  "platform": "macos",
  "arch":     "arm64"
}

App Tarballs

macOS:        {channel}-{os}-{arch}-{AppName}[-{channel}].app.tar.zst
Windows/Linux: {channel}-{os}-{arch}-{AppName}[-{channel}].tar.zst

Stable examples (no channel suffix in app name):
  stable-macos-arm64-MyApp.app.tar.zst
  stable-macos-x64-MyApp.app.tar.zst
  stable-win-x64-MyApp.tar.zst
  stable-linux-x64-MyApp.tar.zst
  stable-linux-arm64-MyApp.tar.zst

Canary examples (channel suffix in app name):
  canary-macos-arm64-MyApp-canary.app.tar.zst
  canary-win-x64-MyApp-canary.tar.zst

Patch Files

{channel}-{os}-{arch}-{fromHash}.patch

Example:
  stable-macos-arm64-abc123def456.patch

Patches are generated via

bsdiff
when a previous
update.json
and tarball are available from
baseUrl
at build time. If patch generation fails, the full tarball is the fallback.

Installers (for first install, not updates)

PlatformStableCanary
macOS
MyApp.dmg
MyApp-canary.dmg
Windows
MyApp-Setup.zip
(contains
MyApp-Setup.exe
)
MyApp-Setup-canary.zip
Linux
MyApp-Setup.AppImage
MyApp-Setup-canary.AppImage

Update Server URLs

The Updater constructs all URLs as:

{baseUrl}/{artifact-filename}

For a

stable
build at
https://updates.example.com/
:

URLPurpose
https://updates.example.com/stable-macos-arm64-update.json
Version check
https://updates.example.com/stable-macos-arm64-MyApp.app.tar.zst
Full download
https://updates.example.com/stable-macos-arm64-abc123.patch
Incremental patch

This flat URL scheme works with any static file host: Cloudflare R2, AWS S3, GitHub Releases, or plain nginx.


Updater API

import { Updater } from "electrobun/bun";

// 1. Check for update
const status = await Updater.checkForUpdate();
// status: "checking" | "update-available" | "no-update" | "error"

// 2. Download update (tries patch first, falls back to full tarball)
await Updater.downloadUpdate();
// status: "downloading" → "update-ready"

// 3. Apply update (replaces bundle, relaunches)
await Updater.applyUpdate();

// Read current app info
const info = await Updater.getLocalInfo();
// { version, hash, channel, baseUrl, name, identifier }

// Read update info (available version)
const update = await Updater.updateInfo();

// Track status changes
Updater.onStatusChange((status) => {
  console.log("Updater status:", status);
});

// Granular history
const history = await Updater.getStatusHistory();
await Updater.clearStatusHistory();

Status Values

StatusMeaning
checking
Fetching
update.json
update-available
Remote hash differs from local hash
no-update
Hashes match — already current
downloading
Downloading patch or tarball
update-ready
Download complete and verified
error
Any step failed

How Version Checking Works

  1. Fetches
    {baseUrl}/{channel}-{os}-{arch}-update.json
  2. Compares remote
    hash
    to local
    hash
    in
    Resources/version.json
  3. If hashes differ →
    update-available
  4. If identical →
    no-update

Hash is SHA-256 of the uncompressed tarball contents.


Download Strategy

  1. Try patch first: fetch
    {channel}-{os}-{arch}-{currentHash}.patch
  2. Apply patch via
    bspatch
    to the local cached tarball
  3. If patch missing or fails: download full
    .tar.zst
    tarball
  4. Decompress with
    zig-zstd

Local cache directory (intermediate files during update):

macOS:   ~/Library/Application Support/{id}/{channel}/self-extraction/
Windows: %LOCALAPPDATA%/{id}/{channel}/self-extraction/
Linux:   ~/.local/share/{id}/{channel}/self-extraction/

Contains: current tarball, next tarball, temp patch, compressed bundles.


Apply Update — Platform Specifics

PlatformMethod
macOSRemoves quarantine attribute, replaces
.app
bundle, relaunches
WindowsHelper script handles file locking before replacing
.exe
LinuxReplaces
.AppImage
or extracted directory, relaunches

Uploading Artifacts

Cloudflare R2 (recommended)

# Using wrangler
wrangler r2 object put mybucket/stable-macos-arm64-update.json \
  --file artifacts/stable-macos-arm64-update.json

# Using AWS CLI (R2 is S3-compatible)
aws s3 cp artifacts/ s3://mybucket/ \
  --recursive \
  --endpoint-url https://<accountid>.r2.cloudflarestorage.com

AWS S3

aws s3 cp artifacts/ s3://mybucket/releases/ --recursive

rsync to nginx / CDN origin

rsync -avz artifacts/ user@server:/var/www/updates/

GitHub Releases (simple, free)

Upload artifacts as release assets. Set

baseUrl
to the GitHub Releases download URL:

https://github.com/{owner}/{repo}/releases/download/{tag}/

Note: GitHub Releases don't support patch files efficiently (they're all separate assets). Use a CDN if patch generation is important.


Release Checklist

Before every release:

[ ] Bump version in electrobun.config.ts
[ ] Set release.baseUrl pointing to your static host
[ ] Confirm build.mac.codesign + notarize: true (macOS)
[ ] Confirm ELECTROBUN_DEVELOPER_ID is set in CI
[ ] Confirm ELECTROBUN_APPLEID / APPLEIDPASS / TEAMID are in CI secrets
[ ] Build: electrobun build --env=stable (or canary)
[ ] Verify artifacts/ contains:
    - {channel}-{os}-{arch}-update.json  (for each platform)
    - {channel}-{os}-{arch}-{AppName}.{ext}.tar.zst (for each platform)
    - {channel}-{os}-{arch}-{hash}.patch (if previous version published)
    - Installer files (DMG / ZIP / AppImage)
[ ] Upload all artifacts/ files to baseUrl host
[ ] Verify: curl https://updates.example.com/stable-macos-arm64-update.json
[ ] Tag the release in git

Common Mistakes

MistakeResultFix
baseUrl
missing or wrong
Updates silently never triggerSet
release.baseUrl
+ verify with curl
Uploaded to wrong path404 on update checkArtifacts must be at root of
baseUrl
, flat — no subdirectories
Skipped codesignmacOS users get "damaged app"Set
codesign: true
+ provide signing env vars
Skipped notarizeGatekeeper blocks on first launchSet
notarize: true
— required for distribution
Wrong OS string in filenameUpdate fails silentlyOS is
macos
/
win
/
linux
— not
darwin
/
windows
Patch generated before new tarball uploadedPatch references tarball that doesn't existUpload tarballs before running
checkForUpdate
release.baseUrl
has no trailing slash
URL construction breaksAlways end
baseUrl
with
/