mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 14:17:58 +08:00
feat: add macos build workflow and document the install path (#1911)
* feat: add macOS DMG build workflow and document the install path * fix: failing workflow * refactor: update the code based on feedback
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
name: Desktop (macOS)
|
||||
|
||||
# Builds the Agent Canvas macOS DMG (Apple Silicon).
|
||||
# pull_request: paths-filtered smoke build — the uploaded artifact is
|
||||
# what a tester downloads to verify a PR on macOS.
|
||||
# release published: rebuilds from the release tag and attaches the .dmg
|
||||
# to the GitHub Release created by release-please.
|
||||
# workflow_dispatch: manual escape hatch for any ref.
|
||||
# The app is only ad-hoc signed (no signing certs exist for any platform);
|
||||
# a downloaded DMG is quarantined by Gatekeeper, which reports the app as
|
||||
# "damaged" until the attribute is cleared:
|
||||
# xattr -d com.apple.quarantine "/Applications/Agent Canvas.app"
|
||||
# Intel DMGs are not produced — the bundled uv/node runtimes are host-arch
|
||||
# only, so Intel users build from source.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/desktop-macos.yml
|
||||
- electron/**
|
||||
- electron-builder.config.mjs
|
||||
- scripts/download-uv.mjs
|
||||
- scripts/download-node.mjs
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
concurrency:
|
||||
group: desktop-macos-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# `gh release upload` needs contents: write on release events. Fork PR runs
|
||||
# are downgraded to a read-only token by GitHub automatically.
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build-dmg:
|
||||
name: Build macOS DMG
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Node.js with npm cache
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build macOS DMG
|
||||
env:
|
||||
# Production analytics key only for release builds (same split as
|
||||
# docker.yml); PR/manual runs get the staging key. Both are public
|
||||
# client-side keys stored as repo vars — empty on fork PRs, which
|
||||
# simply disables analytics in the built app.
|
||||
VITE_POSTHOG_API_KEY: ${{ github.event_name == 'release' && vars.POSTHOG_PROD_KEY || vars.POSTHOG_STAGING_KEY }}
|
||||
# Authenticates download-uv.mjs's GitHub API version lookup so it
|
||||
# doesn't hit the unauthenticated per-IP rate limit on shared runners.
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: npm run build:desktop
|
||||
|
||||
- name: Verify DMG output
|
||||
run: |
|
||||
ls -la dist-electron
|
||||
dmg_count=$(find dist-electron -maxdepth 1 -name '*.dmg' | wc -l | tr -d ' ')
|
||||
if [ "$dmg_count" -ne 1 ]; then
|
||||
echo "::error::Expected exactly one DMG in dist-electron/, found $dmg_count"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload DMG artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: agent-canvas-macos-dmg
|
||||
path: dist-electron/*.dmg
|
||||
if-no-files-found: error
|
||||
# The DMG is large (~175 MB); keep PR artifacts long enough for
|
||||
# manual QA without hoarding storage.
|
||||
retention-days: 14
|
||||
|
||||
- name: Attach DMG to GitHub release
|
||||
if: github.event_name == 'release'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
run: gh release upload "$RELEASE_TAG" dist-electron/*.dmg --clobber
|
||||
@@ -70,6 +70,15 @@ const RUNTIME_PACKAGES = ["sirv", "httpxy"];
|
||||
|
||||
const repoRoot = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Root package.json is the single source of truth for the app version
|
||||
// (release-please bumps it). electron/package.json is a minimal manifest
|
||||
// stub pinned at 1.0.0 — `extraMetadata` below overrides its version at
|
||||
// pack time so artifact names and app.getVersion() carry the released
|
||||
// version instead.
|
||||
const rootPackageJson = JSON.parse(
|
||||
readFileSync(join(repoRoot, "package.json"), "utf8"),
|
||||
);
|
||||
|
||||
/**
|
||||
* Strip the auto-bundled node_modules from the packaged app, then restore
|
||||
* the small runtime closure of RUNTIME_PACKAGES.
|
||||
@@ -203,6 +212,10 @@ const config = {
|
||||
productName: "Agent Canvas",
|
||||
copyright: "Copyright © 2025 All Hands AI",
|
||||
|
||||
// Stamp the packaged app with the released version (see rootPackageJson
|
||||
// note above).
|
||||
extraMetadata: { version: rootPackageJson.version },
|
||||
|
||||
// Treat electron/ as the app root. electron/package.json provides the
|
||||
// Electron entry point without touching the npm-published root package.json.
|
||||
// `buildResources` points at electron/build-resources so electron-builder
|
||||
@@ -274,6 +287,12 @@ const config = {
|
||||
// Or use the dedicated script:
|
||||
// npm run build:desktop:universal
|
||||
//
|
||||
// CAUTION: the bundled uv/node extraResources are downloaded for the
|
||||
// BUILD HOST's architecture only (scripts/download-uv.mjs and
|
||||
// download-node.mjs have no arch override), so a "universal" build still
|
||||
// ships single-arch runtimes and breaks on the other architecture. Don't
|
||||
// distribute universal DMGs until the download scripts support multi-arch.
|
||||
//
|
||||
mac: {
|
||||
category: "public.app-category.developer-tools",
|
||||
target: [
|
||||
@@ -296,6 +315,10 @@ const config = {
|
||||
{ x: 410, y: 220, type: "link", path: "/Applications" },
|
||||
],
|
||||
window: { width: 540, height: 380 },
|
||||
// Default is "Agent Canvas-<version>-<arch>.dmg"; GitHub release assets
|
||||
// mangle spaces, so keep the asset name literal (matches the nsis
|
||||
// convention). ${version}/${arch}/${ext} are electron-builder macros.
|
||||
artifactName: "Agent-Canvas-${version}-${arch}.${ext}",
|
||||
},
|
||||
|
||||
// ── Windows ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -73,9 +73,15 @@ async function resolveVersion() {
|
||||
}
|
||||
|
||||
console.log("[download-uv] Fetching latest uv version from GitHub API...");
|
||||
const headers = { "User-Agent": "agent-canvas-build" };
|
||||
// Unauthenticated api.github.com calls are rate-limited per IP (60/hour) —
|
||||
// shared CI runner IPs exhaust that fast. CI passes GITHUB_TOKEN.
|
||||
if (process.env.GITHUB_TOKEN) {
|
||||
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
||||
}
|
||||
const data = await fetchJson(
|
||||
"https://api.github.com/repos/astral-sh/uv/releases/latest",
|
||||
{ "User-Agent": "agent-canvas-build" }
|
||||
headers
|
||||
);
|
||||
const version = data.tag_name?.replace(/^v/, "");
|
||||
if (!version) throw new Error("Could not parse uv version from GitHub API");
|
||||
|
||||
Reference in New Issue
Block a user