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.
git clone https://github.com/milady-ai/milady
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"
.claude/plugins/electrobun-dev/skills/electrobun-release/SKILL.mdElectrobun 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
| Channel | Built with | Updates | Codesign |
|---|---|---|---|
| | Yes | Required |
| | Yes | Required |
| | Disabled | Skipped |
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)
| Platform | Stable | Canary |
|---|---|---|
| macOS | | |
| Windows | (contains ) | |
| Linux | | |
Update Server URLs
The Updater constructs all URLs as:
{baseUrl}/{artifact-filename}
For a
stable build at https://updates.example.com/:
| URL | Purpose |
|---|---|
| Version check |
| Full download |
| 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
| Status | Meaning |
|---|---|
| Fetching |
| Remote hash differs from local hash |
| Hashes match — already current |
| Downloading patch or tarball |
| Download complete and verified |
| Any step failed |
How Version Checking Works
- Fetches
{baseUrl}/{channel}-{os}-{arch}-update.json - Compares remote
to localhash
inhashResources/version.json - If hashes differ →
update-available - If identical →
no-update
Hash is SHA-256 of the uncompressed tarball contents.
Download Strategy
- Try patch first: fetch
{channel}-{os}-{arch}-{currentHash}.patch - Apply patch via
to the local cached tarballbspatch - If patch missing or fails: download full
tarball.tar.zst - 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
| Platform | Method |
|---|---|
| macOS | Removes quarantine attribute, replaces bundle, relaunches |
| Windows | Helper script handles file locking before replacing |
| Linux | Replaces 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
| Mistake | Result | Fix |
|---|---|---|
missing or wrong | Updates silently never trigger | Set + verify with curl |
| Uploaded to wrong path | 404 on update check | Artifacts must be at root of , flat — no subdirectories |
| Skipped codesign | macOS users get "damaged app" | Set + provide signing env vars |
| Skipped notarize | Gatekeeper blocks on first launch | Set — required for distribution |
| Wrong OS string in filename | Update fails silently | OS is / / — not / |
| Patch generated before new tarball uploaded | Patch references tarball that doesn't exist | Upload tarballs before running |
has no trailing slash | URL construction breaks | Always end with |