# Desktop release builds — macOS (.dmg, arm64 + Intel) and Windows (.msi + NSIS .exe). # # CI calls the SAME scripts developers run locally (packaging/build_dmg.sh and # build_windows.ps1); this file only provisions the toolchain (Node, Rust, a Python venv at # .venv with PyInstaller) and publishes the results. # # Triggers: # - tag push `v*` → builds all targets and attaches them to a DRAFT GitHub Release # (review, then publish by hand). # - manual run → builds and uploads workflow artifacts only (no release). # # Each installer is uploaded twice: once with its versioned name (archive) and once with a # stable name (OpenWorker-macos-arm64.dmg, …) so the website can link to # github.com//releases/latest/download/ # and never need updating. # # macOS signing + notarization: Tauri's bundler handles both during `tauri build` when the # APPLE_* env vars are present (import cert → sign app + sidecar with hardened runtime → # notarize via notarytool → staple). Driven by repo secrets: # APPLE_CERTIFICATE base64 .p12 (Developer ID Application cert + key) # APPLE_CERTIFICATE_PASSWORD the .p12 export password # APPLE_SIGNING_IDENTITY e.g. "Developer ID Application: Name (TEAMID)" # APPLE_API_KEY_CONTENT base64 App Store Connect API .p8 (notarytool) # APPLE_API_KEY the API key id # APPLE_API_ISSUER the API issuer id # When the secrets are absent (forks, scratch runs) the build degrades to unsigned — # installable via `xattr -cr`. Windows remains unsigned (Authenticode is a later step). name: Release on: push: tags: ["v*", "app-v*"] workflow_dispatch: defaults: run: shell: bash jobs: build: strategy: fail-fast: false matrix: include: - os: macos-latest # Apple Silicon slug: macos-arm64 # Intel macOS: macos-13 retired in Dec 2025; macos-15-intel replaced it and is # the LAST x86_64 image Actions will offer (available until Aug 2027). Builds # natively — the sidecar is a PyInstaller freeze, which cannot cross-compile. - os: macos-15-intel slug: macos-x64 - os: windows-latest slug: windows runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: npm cache-dependency-path: surfaces/gui/package-lock.json - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 with: workspaces: surfaces/gui/src-tauri - uses: actions/setup-python@v5 with: python-version: "3.12" - name: Set up the sidecar venv (.venv) # The build scripts expect a venv at .venv with the package + PyInstaller. # typer/tzdata are build-time-only (PyInstaller walks mcp.cli, which needs typer; # tzdata ships zoneinfo for Windows). aisuite installs like any other dependency # (git-pinned in pyproject.toml). run: | python -m venv .venv if [ "$RUNNER_OS" = "Windows" ]; then VPY=.venv/Scripts/python; else VPY=.venv/bin/python; fi "$VPY" -m pip install --upgrade pip "$VPY" -m pip install -e ".[bedrock]" pyinstaller typer tzdata "$VPY" -c "import aisuite, coworker" # fail fast if either import breaks - name: npm ci working-directory: surfaces/gui run: npm ci - name: Build .dmg (macOS) if: runner.os == 'macOS' env: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT }} APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} # Auto-update artifact signing (minisign, separate from Apple signing). Absent → # the build script skips updater artifacts with a warning (fork/scratch runs). TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: | # Unset empty APPLE_* vars so runs without secrets stay cleanly unsigned # (Tauri treats a present-but-empty var as a config error). for v in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY APPLE_API_KEY APPLE_API_ISSUER; do [ -n "$(eval echo "\${$v:-}")" ] || unset "$v" done if [ -n "${APPLE_API_KEY_CONTENT:-}" ]; then echo "$APPLE_API_KEY_CONTENT" | base64 -d > "$RUNNER_TEMP/AuthKey.p8" export APPLE_API_KEY_PATH="$RUNNER_TEMP/AuthKey.p8" fi unset APPLE_API_KEY_CONTENT bash packaging/build_dmg.sh - name: Build .msi + NSIS .exe (Windows) if: runner.os == 'Windows' shell: pwsh env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: ./packaging/build_windows.ps1 - name: Stage artifacts (versioned + stable names) run: | mkdir -p out BUNDLE=surfaces/gui/src-tauri/target/release/bundle if [ "$RUNNER_OS" = "Windows" ]; then cp "$BUNDLE"/nsis/*.exe out/ cp "$BUNDLE"/nsis/*.exe out/OpenWorker-windows-setup.exe cp "$BUNDLE"/msi/*.msi out/ cp "$BUNDLE"/msi/*.msi out/OpenWorker-windows.msi # Updater signature for the NSIS installer (present only when the updater key # secret is configured). The .sig signs CONTENT, so the stable rename is safe. SIG=$(ls "$BUNDLE"/nsis/*.exe.sig 2>/dev/null | head -1 || true) [ -n "$SIG" ] && cp "$SIG" out/OpenWorker-windows-setup.exe.sig else cp "$BUNDLE"/dmg/*.dmg out/ cp "$BUNDLE"/dmg/*.dmg out/OpenWorker-${{ matrix.slug }}.dmg # macOS updater artifact: the signed .app tarball the installed app swaps in. if [ -f "$BUNDLE"/macos/OpenWorker.app.tar.gz ]; then cp "$BUNDLE"/macos/OpenWorker.app.tar.gz out/OpenWorker-${{ matrix.slug }}.app.tar.gz cp "$BUNDLE"/macos/OpenWorker.app.tar.gz.sig out/OpenWorker-${{ matrix.slug }}.app.tar.gz.sig fi fi ls -la out - uses: actions/upload-artifact@v4 with: name: ${{ matrix.slug }} path: out/* if-no-files-found: error release: if: startsWith(github.ref, 'refs/tags/v') needs: build runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: path: dist merge-multiple: true - name: Compose the auto-update manifest (latest.json) # Shipped apps poll releases/latest/download/latest.json (via the branded # download.openworker.com redirect) — publishing this release IS pushing the # update. The tag must match tauri.conf.json's version or installed apps would # see a permanent phantom update; fail loudly on drift. Runs only when signed # updater artifacts exist (i.e. the TAURI_SIGNING_PRIVATE_KEY secret is set). run: | TAG="${GITHUB_REF_NAME}" CONF_VERSION=$(python3 -c "import json; print(json.load(open('surfaces/gui/src-tauri/tauri.conf.json'))['version'])") if [ "${TAG#v}" != "$CONF_VERSION" ]; then echo "::error::tag $TAG != tauri.conf.json version $CONF_VERSION — bump the config before tagging" exit 1 fi if ls dist/*.sig >/dev/null 2>&1; then python3 packaging/make_update_manifest.py \ --version "${TAG#v}" --tag "$TAG" --repo "$GITHUB_REPOSITORY" \ --dist dist --out dist/latest.json \ --notes "OpenWorker ${TAG#v}" else echo "::warning::no updater signatures in dist/ — release ships WITHOUT auto-update manifest" fi - uses: softprops/action-gh-release@v2 with: draft: true files: dist/* generate_release_notes: true