name: Desktop release # Builds and publishes the Electron desktop app via electron-forge. # Generates a GitHub Release with installers + update manifests, then uploads # stable, version-free asset aliases so `ao start`'s constant # releases/latest/download/ URL resolves (spec §8, §11.4). # # Each target OS builds on its own runner so the bundled `ao` daemon is compiled # natively for that platform. build-daemon.mjs keys the binary off the build # host's platform, so cross-OS packaging (e.g. building the Windows installer on # macOS) would ship a non-Windows binary named `ao` and the app could not launch # the daemon (issues #235/#256). The per-OS matrix keeps host == target, and the # Linux daemon is built on the Linux runner (AgentWrapper/agent-orchestrator#2191). # # ⚠️ Until macOS code signing + notarization secrets are configured (see # frontend/docs/desktop-release.md), published builds are UNSIGNED and will # NOT auto-update on macOS. The workflow still produces installable artifacts. # # ── Cutting a FORK test release (dev loop) ──────────────────────────────────── # Test releases go to harshitsinghbhandari/agent-orchestrator (the fork), never # to AgentWrapper. To cut one, on the FORK: # - push a `desktop-v*` tag (e.g. `git push fork desktop-v0.0.0-test1`), or # - run this workflow via `workflow_dispatch` from the fork's Actions tab. # AO_RELEASE_REPO MUST be set to the fork for a test run. It is derived # automatically below from `github.repository`, so a fork run publishes to the # fork and a run on AgentWrapper publishes to AgentWrapper, with no edit. The # matching test `ao` binary is built with # `-ldflags -X ...cli.releaseRepo=harshitsinghbhandari/agent-orchestrator` so it # fetches from the same fork (spec §6.3). on: push: tags: - "desktop-v*" workflow_dispatch: jobs: release: strategy: fail-fast: false matrix: os: [macos-latest, windows-latest, ubuntu-latest] runs-on: ${{ matrix.os }} # Gates the signed release behind the `release` environment's required # reviewers: any write-access user can push a desktop-v* tag, but only # designated approvers can let the build that consumes the signing secrets # proceed. Configure reviewers in repo Settings > Environments > release. environment: release permissions: contents: write defaults: run: working-directory: frontend steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: npm cache-dependency-path: frontend/package-lock.json # The daemon is compiled by build-daemon.mjs during prepackage/premake, so # the Go toolchain must be present and pinned on every runner. - uses: actions/setup-go@v5 with: go-version-file: backend/go.mod cache-dependency-path: backend/go.sum - run: npm ci - name: macOS signing setup if: startsWith(matrix.os, 'macos') uses: ./.github/actions/macos-signing-setup with: csc-link: ${{ secrets.CSC_LINK }} csc-key-password: ${{ secrets.CSC_KEY_PASSWORD }} apple-api-key-base64: ${{ secrets.APPLE_API_KEY_BASE64 }} apple-api-key-id: ${{ secrets.APPLE_API_KEY_ID }} apple-api-issuer: ${{ secrets.APPLE_API_ISSUER }} apple-signing-identity: ${{ secrets.APPLE_SIGNING_IDENTITY }} - name: Publish shell: bash run: | # Retry transient macOS sign/notary flakes (Apple notary -1009, # osx-sign keychain races) so the build self-heals instead of failing. for i in 1 2 3; do if npm run publish; then exit 0; fi echo "::warning::publish attempt $i/3 failed (likely a transient macOS sign/notary flake)" [ "$i" -lt 3 ] && sleep 30 done echo "::error::publish failed after 3 attempts" >&2 exit 1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} AO_RELEASE_REPO: ${{ github.repository }} # macOS signing + notarization env (APPLE_SIGNING_IDENTITY, APPLE_API_KEY, # APPLE_API_KEY_ID, APPLE_API_ISSUER) is exported by the "macOS signing # setup" step above via $GITHUB_ENV; the Developer ID cert is in the # keychain. No-op on non-macOS runners. # Upload stable, version-free asset aliases for this runner's platform so # `ao start`'s constant releases/latest/download/ URL resolves. The # forge makers emit versioned, productName-derived filenames; we copy them # to the space-free names `ao start` fetches and upload to the v # 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-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 # but intentionally upload no Linux alias yet. - name: Upload stable asset aliases (macOS) if: startsWith(matrix.os, 'macos') shell: bash run: | # electron-forge publisher-github creates the release as v (PublisherGithub.js: `${tagPrefix ?? 'v'}${version}`), NOT the # triggering git tag. Target that release, not GITHUB_REF_NAME, or the # upload hits a non-existent release. tag="v$(node -p "require('./package.json').version")" arch="$(uname -m)" case "$arch" in arm64) alias_name="agent-orchestrator-darwin-arm64.zip" ;; x86_64) alias_name="agent-orchestrator-darwin-x64.zip" ;; *) echo "unsupported macOS arch: $arch" >&2; exit 1 ;; esac # maker-zip output: out/make/zip/darwin//-.zip src="$(ls out/make/zip/darwin/*/*.zip | head -n1)" if [ -z "$src" ]; then echo "no macOS zip found under out/make/zip/darwin" >&2; exit 1; fi cp "$src" "$alias_name" gh release upload "$tag" "$alias_name" --clobber env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload stable asset aliases (Windows) if: matrix.os == 'windows-latest' shell: bash run: | # Forge publishes to v (see the macOS step), not the git tag. tag="v$(node -p "require('./package.json').version")" alias_name="agent-orchestrator-win32-x64.exe" # NSIS (electron-builder) output: out/make/ Setup .exe src="$(ls "out/make/"*Setup*.exe | head -n1)" if [ -z "$src" ]; then echo "no NSIS installer found under out/make" >&2; exit 1; fi cp "$src" "$alias_name" gh release upload "$tag" "$alias_name" --clobber env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Linux: deb/rpm AND an AppImage build on this runner (issue #2191). The # AppImage is the fetch-and-run artifact for `ao start` (spec §11.3); we # copy it to the stable, space-free name the bootstrapper fetches and # upload to the v release. The versioned deb/rpm are still # published by the forge publisher above for users who want a system package. - name: Upload stable asset aliases (Linux) if: matrix.os == 'ubuntu-latest' shell: bash run: | # Forge publishes to v (see the macOS step), not the git tag. tag="v$(node -p "require('./package.json').version")" alias_name="agent-orchestrator-linux-x64.AppImage" # AppImage (electron-builder) output: out/make/-.AppImage src="$(ls out/make/*.AppImage | head -n1)" if [ -z "$src" ]; then echo "no AppImage found under out/make" >&2; exit 1; fi cp "$src" "$alias_name" gh release upload "$tag" "$alias_name" --clobber env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 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-15-intel environment: release # same approval gate as the matrix release job permissions: contents: write defaults: run: working-directory: frontend steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: npm cache-dependency-path: frontend/package-lock.json - uses: actions/setup-go@v5 with: go-version-file: backend/go.mod cache-dependency-path: backend/go.sum - run: npm ci - name: macOS signing setup uses: ./.github/actions/macos-signing-setup with: csc-link: ${{ secrets.CSC_LINK }} csc-key-password: ${{ secrets.CSC_KEY_PASSWORD }} apple-api-key-base64: ${{ secrets.APPLE_API_KEY_BASE64 }} apple-api-key-id: ${{ secrets.APPLE_API_KEY_ID }} apple-api-issuer: ${{ secrets.APPLE_API_ISSUER }} apple-signing-identity: ${{ secrets.APPLE_SIGNING_IDENTITY }} - name: Publish shell: bash run: | # Retry transient macOS sign/notary flakes (Apple notary -1009, # osx-sign keychain races) so the build self-heals instead of failing. for i in 1 2 3; do if npm run publish; then exit 0; fi echo "::warning::publish attempt $i/3 failed (likely a transient macOS sign/notary flake)" [ "$i" -lt 3 ] && sleep 30 done echo "::error::publish failed after 3 attempts" >&2 exit 1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} AO_RELEASE_REPO: ${{ github.repository }} - name: Upload stable asset aliases (macOS) shell: bash run: | tag="v$(node -p "require('./package.json').version")" arch="$(uname -m)" case "$arch" in arm64) alias_name="agent-orchestrator-darwin-arm64.zip" ;; x86_64) alias_name="agent-orchestrator-darwin-x64.zip" ;; *) echo "unsupported macOS arch: $arch" >&2; exit 1 ;; esac src="$(ls out/make/zip/darwin/*/*.zip | head -n1)" if [ -z "$src" ]; then echo "no macOS zip found under out/make/zip/darwin" >&2; exit 1; fi cp "$src" "$alias_name" gh release upload "$tag" "$alias_name" --clobber env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # After every platform has built and uploaded its installers, generate the # electron-updater feed (latest*.yml + .blockmap sidecars) from the versioned # 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, release-intel] runs-on: ubuntu-latest permissions: contents: write defaults: run: working-directory: frontend steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: npm cache-dependency-path: frontend/package-lock.json # feed.mjs imports app-builder-lib's blockmap generator, so deps are needed. - run: npm ci - name: Generate and upload the latest feed shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # Forge publishes to v (not the git tag). tag="v$(node -p "require('./package.json').version")" mkdir -p dist gh release download "$tag" --dir dist --clobber node scripts/feed.mjs dist "${tag#v}" latest shopt -s nullglob assets=(dist/latest*.yml dist/*.blockmap) if [ ${#assets[@]} -eq 0 ]; then echo "no feed assets generated" >&2; exit 1; fi gh release upload "$tag" "${assets[@]}" --clobber