fix(update): bundle app-update.yml so electron-updater can install (forge omits it) (#2244)

* fix(update): generate app-update.yml at build time; drop runtime setFeedURL

electron-forge does not emit app-update.yml; electron-updater requires it at
process.resourcesPath to resolve the GitHub release feed. Without it every
packaged app threw ENOENT on the first download attempt, making updates
detected but never installed.

Two-part fix:
- forge.config.ts: add postPackage hook that writes app-update.yml into
  each platform's Resources dir (baked from AO_RELEASE_REPO so fork builds
  point at the fork, prod at AgentWrapper/agent-orchestrator).
- auto-updater.ts: remove setFeedURL + repo() + DEFAULT_RELEASE_REPO;
  configureFeed now only sets channel + allowDowngrade. electron-updater
  auto-loads the bundled yml on the first checkForUpdates call.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: format with prettier [skip ci]

* fix(update): generate app-update.yml before signing, not after

The postPackage hook wrote app-update.yml into Contents/Resources AFTER osxSign
sealed the bundle, so the added file was unsealed and macOS reported the app as
"damaged" (codesign: "a sealed resource is missing or invalid"). Generate it in
a prePackage hook and ship it via packagerConfig.extraResource, so it is copied
into the bundle and signed as part of the seal. The generated app-update.yml is
gitignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: format with prettier [skip ci]

* ci(release): move Intel leg to macos-15-intel and gate the feed on it

macos-13 is deprecated and has no runner capacity (multi-hour queues),
so the detached release-intel leg never completed. Switch it to the
supported macos-15-intel image in both the release and nightly workflows.

Now that the Intel leg gets a runner and builds + signs the x64 installer
reliably (verified on fork v0.10.10), re-couple it into publish-feed
(needs: [release, release-intel]) so latest-mac.yml / nightly-mac.yml
always carry the x64 entry and Intel macOS users receive auto-updates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Harshit Singh Bhandari 2026-06-28 23:36:06 +05:30 committed by GitHub
parent ff23e66055
commit 965e388792
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 45 additions and 31 deletions

View File

@ -93,15 +93,15 @@ jobs:
# macOS signing + notarization env is exported by the "macOS signing
# setup" step above via $GITHUB_ENV; the cert is in the keychain.
# Dedicated Intel (darwin-x64) nightly build leg, decoupled from the release
# matrix so the scarce macos-13 runner does not block publish-feed. Mirrors
# the release job's macOS steps exactly. publish-feed stays needs: release
# only (three fast legs); the x64 feed entry appears if this job finishes
# first, but the feed is not gated on it.
# Dedicated Intel (darwin-x64) nightly build leg. Kept separate from the
# release matrix (only a macOS host can build the x64 installer); publish-feed
# now waits on it so the x64 entry is present in nightly-mac.yml and Intel
# nightly users get auto-updates. Mirrors the release job's macOS steps exactly.
# Runner is macos-15-intel (macos-13 deprecated/no capacity; see release wf).
release-intel:
needs: guard
if: needs.guard.outputs.should_build == 'true'
runs-on: macos-13
runs-on: macos-15-intel
permissions:
contents: write
defaults:
@ -161,7 +161,7 @@ jobs:
# assets. The nightly version is only stamped in-memory on the build runners,
# so derive the tag from the guard job's computed version, not package.json.
publish-feed:
needs: [guard, release]
needs: [guard, release, release-intel]
runs-on: ubuntu-latest
permissions:
contents: write

View File

@ -92,7 +92,7 @@ jobs:
# release the publish step just created, with --clobber (so re-runs replace).
#
# Arch-coverage note: each runner only builds its host arch. macos-latest
# (Apple silicon) produces darwin-arm64; macos-13 (Intel x64) produces
# (Apple silicon) produces darwin-arm64; macos-15-intel (Intel x64) produces
# darwin-x64. Both macOS runners run this step (see the `if` below), so the
# arm64 and x64 stable aliases are now both built (spec §8).
# The Linux stable name is undecided (spec §11.3), so we build the deb/rpm
@ -155,13 +155,14 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Dedicated Intel (darwin-x64) build leg, decoupled from the release matrix so
# the scarce macos-13 runner does not block publish-feed. publish-feed needs
# only the three fast legs (release); this job uploads the x64 installer and
# stable alias independently. feed.mjs includes the x64 entry if this job
# finishes before publish-feed, but the feed is not gated on it.
# Dedicated Intel (darwin-x64) build leg. Kept a separate job (not in the
# release matrix) because only a macOS host can build the x64 installer, but
# publish-feed now waits on it (needs: [release, release-intel]) so the x64
# entry is always present in latest-mac.yml and Intel users get auto-updates.
# Runner is macos-15-intel: macos-13 is deprecated/has no capacity (multi-hour
# queues), so this leg uses the supported Intel x64 image.
release-intel:
runs-on: macos-13
runs-on: macos-15-intel
environment: release # same approval gate as the matrix release job
permissions:
contents: write
@ -216,7 +217,7 @@ jobs:
# assets and upload them to the same release. Runs once, on Linux: it only
# hashes already-built artifacts, so it needs no per-OS runner.
publish-feed:
needs: release
needs: [release, release-intel]
runs-on: ubuntu-latest
permissions:
contents: write

1
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
app-update.yml

View File

@ -2,6 +2,7 @@ import type { ForgeConfig } from "@electron-forge/shared-types";
import { VitePlugin } from "@electron-forge/plugin-vite";
import MakerNSIS from "./makers/maker-nsis";
import MakerAppImage from "./makers/maker-appimage";
import { writeFileSync } from "node:fs";
// Default GitHub release target (production). aoagents was the temporary rewrite
// home; releases land on AgentWrapper (spec §1.1).
@ -30,7 +31,7 @@ const config: ForgeConfig = {
// (.icns on macOS, .ico on Windows); Linux menu icons come from the
// deb/rpm makers below, and the runtime window icon from src/main.ts.
icon: "assets/icon",
extraResource: ["daemon", "assets/icon.png"],
extraResource: ["daemon", "assets/icon.png", "app-update.yml"],
// Notarization. Two paths:
// - CI: an App Store Connect API key. APPLE_API_KEY is a PATH to the .p8
// (the workflow decodes APPLE_API_KEY_BASE64 to a temp file), plus the
@ -53,6 +54,27 @@ const config: ForgeConfig = {
}
: undefined,
},
hooks: {
// electron-forge does not generate app-update.yml (electron-builder does);
// electron-updater reads it from the app's Resources dir at runtime to know
// which GitHub repo to pull from, else it throws ENOENT during download.
// Generate it in prePackage (BEFORE osxSign) and ship it via extraResource
// above, so it is copied into the bundle and SIGNED as part of the seal.
// Writing it after signing (a postPackage hook) adds an unsealed resource
// and macOS reports the app as "damaged". owner/repo are baked from
// AO_RELEASE_REPO at build time.
prePackage: async () => {
const { owner, name } = parseReleaseRepo(process.env.AO_RELEASE_REPO);
const yml = [
"provider: github",
`owner: ${owner}`,
`repo: ${name}`,
"updaterCacheDirName: agent-orchestrator-updater",
"",
].join("\n");
writeFileSync("app-update.yml", yml);
},
},
rebuildConfig: {},
makers: [
// Windows installer: NSIS via electron-builder (see makers/maker-nsis.ts).

View File

@ -10,22 +10,12 @@ import {
type UpdateStatus,
} from "./update-settings";
// Default release repo, mirroring backend cli.releaseRepo. Override via env for
// fork test builds (AO_RELEASE_REPO=owner/repo).
const DEFAULT_RELEASE_REPO = "AgentWrapper/agent-orchestrator";
function repo(): { owner: string; name: string } {
const [owner, name] = (process.env.AO_RELEASE_REPO || DEFAULT_RELEASE_REPO).split("/");
if (owner && name) return { owner, name };
const [defOwner, defName] = DEFAULT_RELEASE_REPO.split("/");
return { owner: defOwner, name: defName };
}
// configureFeed points electron-updater at the GitHub Releases feed for the
// chosen channel. Shared by the launch auto-check and the manual Settings flow.
// configureFeed sets the update channel on electron-updater. The repo/owner
// are loaded automatically from app-update.yml (written by forge.config.ts's
// postPackage hook into the app's Resources dir at build time). No runtime env
// or setFeedURL call is needed; electron-updater reads the bundled yml on first
// checkForUpdates.
function configureFeed(channel: UpdateChannel): void {
const { owner, name } = repo();
autoUpdater.setFeedURL({ provider: "github", owner, repo: name });
autoUpdater.channel = channel; // "latest" | "nightly"
autoUpdater.allowDowngrade = true; // permits a nightly -> stable channel switch
}