Building for macOS
Step-by-step instructions for building and publishing the Fabric Agents macOS app (DMG + ZIP) from a Mac machine.
macOS builds must be done on a Mac — electron-builder requires macOS to produce .dmg and .zip artifacts, and code signing requires the Apple toolchain. This guide walks through the full process: build, checksum, upload to Cloudflare R2, and deploy.
Recommended release path
For full cross-platform releases, use the GitHub Actions workflow:
.github/workflows/release-electron-cloudflare.yml
It builds:
- Windows on
windows-latest(native EXE resources/icon) - Linux on
ubuntu-latest - macOS on
macos-latest
Then it assembles electron/latest, regenerates latest.json, uploads to R2, and deploys Cloudflare.
Use this macOS guide as the manual fallback when you need to build/publish from a local Mac.
Prerequisites
Required
- macOS — any version supported by Xcode 15+
- Xcode Command Line Tools
xcode-select --install - Bun — the monorepo package manager
curl -fsSL https://bun.sh/install | bash - Wrangler — Cloudflare CLI for R2 uploads and worker deploys
Then authenticate:npm install -g wranglerwrangler login
Optional (for signed + notarized builds)
Unsigned builds work for testing. For public distribution you need:
- Apple Developer Program membership
- A Developer ID Application certificate in your Keychain
- An app-specific password for your Apple ID (for notarization)
Set these in .env at the repo root (or export them in your shell):
APPLE_SIGNING_IDENTITY="Developer ID Application: Your Name (TEAMID)"
APPLE_ID="you@example.com"
APPLE_TEAM_ID="XXXXXXXXXX"
APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx"Clone and Install
git clone <repo-url>
cd fabric-agents
bun installBuild
The build script handles everything: downloading the Bun runtime, copying the SDK and interceptors, compiling the app, and packaging with electron-builder.
arm64 (Apple Silicon)
bash apps/electron/scripts/build-dmg.sh arm64x64 (Intel)
bash apps/electron/scripts/build-dmg.sh x64Both architectures produce two artifacts each — a DMG (installer) and a ZIP (portable, used by the install script):
| Arch | DMG | ZIP |
|---|---|---|
| arm64 | apps/electron/release/Fabric-Agent-arm64.dmg | apps/electron/release/Fabric-Agent-arm64.zip |
| x64 | apps/electron/release/Fabric-Agent-x64.dmg | apps/electron/release/Fabric-Agent-x64.zip |
Typical build time is 5–10 minutes per architecture. The script prints progress at each step.
Build script options
bash apps/electron/scripts/build-dmg.sh [arm64|x64] [--upload] [--latest] [--script]| Flag | Description |
|---|---|
arm64 | x64 | Target architecture (default: arm64) |
--upload | Upload artifacts to S3 after building (requires S3 env vars) |
--latest | Also update the electron/latest path on S3 (requires --upload) |
--script | Also upload install-app.sh (requires --upload) |
Compute Checksums
The install script verifies each download against a SHA-512 checksum stored in latest.json. Compute checksums for both ZIPs:
cd apps/electron/release
python3 - <<'EOF'
import hashlib, base64
files = {
"darwin-arm64": "Fabric-Agent-arm64.zip",
"darwin-x64": "Fabric-Agent-x64.zip",
}
for key, filename in files.items():
with open(filename, "rb") as f:
digest = hashlib.sha512(f.read()).digest()
print(f"{key}: {base64.b64encode(digest).decode()}")
EOFNote the two base64 strings — you will need them in the next step.
Update latest.json
Open apps/fabric-agents-worker/deploy/electron/latest/latest.json and update the darwin-arm64 and darwin-x64 entries with the URLs and checksums you just computed. Also update published_at to the current UTC time.
{
"version": "0.5.1",
"published_at": "2026-03-01T20:00:00Z",
"assets": {
"darwin-arm64": {
"url": "https://agents.fabric.pro/electron/latest/Fabric-Agent-arm64.zip",
"sha512": "<arm64 base64 checksum>"
},
"darwin-x64": {
"url": "https://agents.fabric.pro/electron/latest/Fabric-Agent-x64.zip",
"sha512": "<x64 base64 checksum>"
},
"linux-x64": {
"url": "https://agents.fabric.pro/electron/latest/Fabric-Agent-x64.AppImage",
"sha512": "..."
},
"windows-x64": {
"url": "https://agents.fabric.pro/electron/latest/Fabric-Agent-win32-x64.zip",
"sha512": "..."
}
}
}Keep the existing linux-x64 and windows-x64 entries unchanged.
Upload to Cloudflare R2
First, export your Cloudflare account ID and an API token with R2 write permission:
export CLOUDFLARE_ACCOUNT_ID="<your-cloudflare-account-id>"
export CLOUDFLARE_API_TOKEN="<your-r2-scoped-api-token>"Ask a repo maintainer for the Fabric Agents account ID and a scoped token if you don't have one. Never paste these into public channels or commit them to the repo.
Upload the two ZIPs and the updated manifest:
RELEASE_DIR="apps/electron/release"
MANIFEST="apps/fabric-agents-worker/deploy/electron/latest/latest.json"
# Upload arm64 ZIP
npx wrangler r2 object put \
fabric-agents-releases/electron/latest/Fabric-Agent-arm64.zip \
--file "$RELEASE_DIR/Fabric-Agent-arm64.zip" \
--content-type application/zip \
--remote
# Upload x64 ZIP
npx wrangler r2 object put \
fabric-agents-releases/electron/latest/Fabric-Agent-x64.zip \
--file "$RELEASE_DIR/Fabric-Agent-x64.zip" \
--content-type application/zip \
--remote
# Upload updated manifest
npx wrangler r2 object put \
fabric-agents-releases/electron/latest/latest.json \
--file "$MANIFEST" \
--content-type application/json \
--remoteYou can also upload the DMGs if you want them available for direct download:
npx wrangler r2 object put \
fabric-agents-releases/electron/latest/Fabric-Agent-arm64.dmg \
--file "$RELEASE_DIR/Fabric-Agent-arm64.dmg" \
--content-type application/octet-stream \
--remote
npx wrangler r2 object put \
fabric-agents-releases/electron/latest/Fabric-Agent-x64.dmg \
--file "$RELEASE_DIR/Fabric-Agent-x64.dmg" \
--content-type application/octet-stream \
--remoteDeploy the Worker
The worker serves the static latest.json copy from its deploy/ assets directory. Deploying it makes the new manifest live at https://agents.fabric.pro/electron/latest/latest.json.
cd apps/fabric-agents-worker
npx wrangler deploy(This reuses the CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN you exported above.)
Verify
Confirm the live manifest reflects the new checksums:
curl -s https://agents.fabric.pro/electron/latest/latest.json | python3 -m json.toolThen test the install script on macOS:
bash <(curl -fsSL https://agents.fabric.pro/install-app.sh)Cross-Platform Publish (Mac + Linux/Windows)
For a full release, publish all three platform artifacts into electron/latest:
- macOS (built on Mac):
Fabric-Agent-arm64.zipFabric-Agent-x64.zip
- Linux (can be built on Linux):
Fabric-Agent-x86_64.AppImage
- Windows (can be produced on Linux as portable ZIP):
Fabric-Agent-win32-x64.zip
Linux host commands (Linux + Windows artifacts)
# From repo root
bun run electron:dist:linuxFor Windows from Linux, nsis requires Wine. If Wine is unavailable, build portable output and zip win-unpacked:
# Build app artifacts (main/preload/renderer/resources)
bun run electron:build
# Try ZIP-only Windows packaging
npx electron-builder --config electron-builder.yml --project apps/electron --win zip --x64
# If Wine is missing, package win-unpacked manually:
cd apps/electron/release
rm -f Fabric-Agent-win32-x64.zip
zip -r -q Fabric-Agent-win32-x64.zip win-unpackedUpdate and publish latest.json
latest.json is the source of truth for install scripts (install-app.sh / install-app.ps1).
Recommended flow:
# 1) Download current manifest (keeps existing mac entries)
npx wrangler r2 object get fabric-agents-releases/electron/latest/latest.json \
--remote --file /tmp/latest.current.json --config cloudflare/agents-worker/wrangler.toml
# 2) Build/update linux + windows yml metadata in apps/electron/release/
# (latest-linux.yml from linux build, latest.yml from windows zip metadata)
# 3) Write merged /tmp/latest.json (update linux-x64 + windows-x64 assets/checksums/sizes)
# and keep darwin-* entries unless new mac builds were produced.Upload:
npx wrangler r2 object put fabric-agents-releases/electron/latest/Fabric-Agent-x86_64.AppImage \
--remote --file apps/electron/release/Fabric-Agent-x86_64.AppImage \
--config cloudflare/agents-worker/wrangler.toml
npx wrangler r2 object put fabric-agents-releases/electron/latest/Fabric-Agent-win32-x64.zip \
--remote --file apps/electron/release/Fabric-Agent-win32-x64.zip \
--config cloudflare/agents-worker/wrangler.toml
npx wrangler r2 object put fabric-agents-releases/electron/latest/latest-linux.yml \
--remote --file apps/electron/release/latest-linux.yml \
--config cloudflare/agents-worker/wrangler.toml
npx wrangler r2 object put fabric-agents-releases/electron/latest/latest.yml \
--remote --file apps/electron/release/latest.yml \
--config cloudflare/agents-worker/wrangler.toml
npx wrangler r2 object put fabric-agents-releases/electron/latest/latest.json \
--remote --file /tmp/latest.json \
--config cloudflare/agents-worker/wrangler.tomlSmoke checks
- Linux installer path:
curl -fsSL https://agents.fabric.pro/install-app.sh | bash - Windows installer path:
irm https://agents.fabric.pro/install-app.ps1 | iex
Both installers must resolve assets via https://agents.fabric.pro/electron/latest/latest.json and verify SHA512 before install.
Troubleshooting
CSC_LINK / signing errors
If you don't have a Developer ID certificate, set CSC_IDENTITY_AUTO_DISCOVERY=false to skip signing:
CSC_IDENTITY_AUTO_DISCOVERY=false bash apps/electron/scripts/build-dmg.sh arm64Unsigned builds open fine on your own machine but will be blocked by Gatekeeper on other Macs unless the user right-clicks → Open.
Notarization fails
Notarization requires:
APPLE_ID— your Apple ID emailAPPLE_TEAM_ID— your 10-character team ID (visible in developer.apple.com)APPLE_APP_SPECIFIC_PASSWORD— an app-specific password created at appleid.apple.com
If any of these are missing, notarization is skipped and you get a signed but non-notarized DMG.
"bun: command not found" in the build script
Install Bun first:
curl -fsSL https://bun.sh/install | bash
source ~/.zshrc # or restart your terminalChecksum mismatch after upload
Always compute checksums after the ZIP is fully written to disk, and upload the exact file you checksummed. Re-running the build script produces a different ZIP (timestamps differ) so checksums must be recomputed if you rebuild.
Wrangler "More than one account available"
Set the account ID explicitly (ask a repo maintainer if you don't already have it — it is not stored in the repo):
export CLOUDFLARE_ACCOUNT_ID="<your-cloudflare-account-id>"