diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 213de4a62c..9e01205542 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,9 @@ -# Desktop release builds — macOS (.dmg, arm64 + Intel) and Windows (.msi + NSIS .exe). +# Desktop release builds — macOS (.dmg, arm64 + Intel), Windows (.msi + NSIS .exe), and +# Linux x64 (.AppImage + .deb). # -# 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. +# CI calls the SAME scripts developers run locally (packaging/build_dmg.sh, +# build_windows.ps1, build_linux.sh); 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 @@ -50,6 +51,8 @@ jobs: slug: macos-arm64 - os: windows-latest slug: windows + - os: ubuntu-latest # x64 + slug: linux-x64 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -66,6 +69,16 @@ jobs: with: workspaces: surfaces/gui/src-tauri + - name: Install Linux bundler dependencies + if: runner.os == 'Linux' + # Tauri's AppImage/.deb bundlers shell out to these; webkit2gtk is the embedded + # webview, the rest is the standard Tauri Linux prerequisite set. + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential libssl-dev libgtk-3-dev libwebkit2gtk-4.1-dev \ + libappindicator3-dev librsvg2-dev patchelf libfuse2 fakeroot dpkg desktop-file-utils + - uses: actions/setup-python@v5 with: python-version: "3.12" @@ -120,6 +133,13 @@ jobs: TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: ./packaging/build_windows.ps1 + - name: Build AppImage + .deb (Linux) + if: runner.os == 'Linux' + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + run: bash packaging/build_linux.sh + - name: Stage artifacts (versioned + stable names) run: | mkdir -p out @@ -133,6 +153,15 @@ jobs: # 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 + elif [ "$RUNNER_OS" = "Linux" ]; then + cp "$BUNDLE"/appimage/*.AppImage out/ + cp "$BUNDLE"/appimage/*.AppImage out/OpenWorker-${{ matrix.slug }}.AppImage + cp "$BUNDLE"/deb/*.deb out/ + cp "$BUNDLE"/deb/*.deb out/OpenWorker-${{ matrix.slug }}.deb + # Updater signature for the AppImage (present only when the updater key secret + # is configured). The .sig signs CONTENT, so the stable rename is safe. + SIG=$(ls "$BUNDLE"/appimage/*.AppImage.sig 2>/dev/null | head -1 || true) + [ -n "$SIG" ] && cp "$SIG" out/OpenWorker-${{ matrix.slug }}.AppImage.sig else cp "$BUNDLE"/dmg/*.dmg out/ cp "$BUNDLE"/dmg/*.dmg out/OpenWorker-${{ matrix.slug }}.dmg diff --git a/README.md b/README.md index d96cf1399f..4311edc4ee 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ desktop app uses an in-memory launch token instead and never writes it to disk. To run the full desktop app instead of the browser UI, replace step 3 with `npm run tauri dev` (from `surfaces/gui/`) - the Tauri shell launches the window and supervises the server itself. -Tests: `.venv/bin/pytest` (server), `npm test` and `npm run e2e` in `surfaces/gui` (GUI unit + hermetic end-to-end). Desktop bundles are built with `packaging/build_dmg.sh` / `packaging/build_windows.ps1`. +Tests: `.venv/bin/pytest` (server), `npm test` and `npm run e2e` in `surfaces/gui` (GUI unit + hermetic end-to-end). Desktop bundles are built with `packaging/build_dmg.sh` / `packaging/build_windows.ps1` / `packaging/build_linux.sh` (AppImage + .deb). ## Repository layout @@ -98,7 +98,7 @@ Tests: `.venv/bin/pytest` (server), `npm test` and `npm run e2e` in `surfaces/gu | `coworker/` | Python backend - agent engine, model providers, connectors, MCP client, memory, automations | | `surfaces/gui/` | Desktop app - React UI + Tauri shell that supervises the server | | `stt/` | Speech-to-text sidecar (Rust) for voice input | -| `packaging/` | Installer builds (macOS DMG, Windows), auto-update manifest, dev bootstrap | +| `packaging/` | Installer builds (macOS DMG, Windows, Linux AppImage/.deb), auto-update manifest, dev bootstrap | | `docs/` | Design specs and decision logs | | `tests/` | Backend test suite | diff --git a/packaging/build_linux.sh b/packaging/build_linux.sh new file mode 100755 index 0000000000..aa13a93658 --- /dev/null +++ b/packaging/build_linux.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Build the Linux desktop app + AppImage and .deb package. +# +# The Linux counterpart to build_dmg.sh / build_windows.ps1: +# 1. PyInstaller-bundle the server into a standalone onedir folder (no venv at runtime). +# 2. Stage it at binaries/sidecar/ for Tauri's `resources` slot. +# 3. `tauri build --bundles appimage,deb` -> OpenWorker .AppImage + .deb (resources copied in). +# +# Prerequisites (mirrors build_windows.ps1's header): +# - Rust (rustup) + Node/npm, and the GUI deps installed (npm ci in surfaces/gui). +# - A Python venv at .venv (repo root) with this package installed editable, plus the +# build-only deps: +# python3 -m venv .venv +# .venv/bin/pip install -e . pyinstaller tzdata typer +# `typer` is needed only at BUILD time: PyInstaller walks the `mcp` package and +# `mcp.cli` calls sys.exit() at import if typer is absent, which aborts the freeze. +# (aisuite installs like any other dependency - git-pinned in pyproject.toml.) +# - Linux system deps for the Tauri bundlers (Debian/Ubuntu package names): +# build-essential libssl-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev +# librsvg2-dev patchelf libfuse2 fakeroot dpkg desktop-file-utils +# +# The result is UNSIGNED - Tauri's updater signing (minisign) still applies if +# TAURI_SIGNING_PRIVATE_KEY is set, same as the other platform scripts; there is no OS-level +# code signing on Linux for AppImage/.deb. +# +# Experimental (use-at-your-own-risk) connectors are EXCLUDED from this build by default - +# the spec strips coworker.connectors.experimental. Self-builders can opt in with: +# COWORKER_EXPERIMENTAL=1 ./build_linux.sh +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +PLATFORM="$(cd "$HERE/.." && pwd)" +GUI="$PLATFORM/surfaces/gui" +APP="OpenWorker" +VPY="$PLATFORM/.venv/bin/python" +TRIPLE="$(rustc -vV | sed -n 's/host: //p')" # e.g. x86_64-unknown-linux-gnu + +echo "==> [1/3] PyInstaller: bundling openworker-server ($TRIPLE)" +"$PLATFORM/.venv/bin/pyinstaller" --noconfirm --clean \ + --distpath "$HERE/dist" --workpath "$HERE/build" "$HERE/openworker-server.spec" + +echo "==> [2/3] staging sidecar resources" +# Onedir bundle (exe + _internal/) ships via Tauri `resources`, landing at sidecar/ next to +# the app binary - onefile's per-launch self-extraction cost seconds of boot splash. +mkdir -p "$GUI/src-tauri/binaries" +rm -rf "$GUI/src-tauri/binaries/sidecar" "$GUI/src-tauri/binaries/openworker-server-$TRIPLE" +cp -r "$HERE/dist/openworker-server" "$GUI/src-tauri/binaries/sidecar" +chmod +x "$GUI/src-tauri/binaries/sidecar/openworker-server" + +echo "==> [3/3] tauri build (--bundles appimage,deb)" +# Auto-update artifact (.AppImage + minisign .sig): produced only when the updater signing +# key is available (CI secret TAURI_SIGNING_PRIVATE_KEY). Keyless builds skip the overlay so +# dev/fork builds keep working; keyless RELEASES strand Linux installs without auto-update. +UPDATER_OVERLAY=() +if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then + UPDATER_OVERLAY=(--config '{"bundle":{"createUpdaterArtifacts":true}}') +else + echo " WARNING: no updater signing key - building WITHOUT auto-update artifacts (not releasable)." +fi +# ${arr[@]+…} guard: plain "${arr[@]}" on an EMPTY array is an "unbound variable" under +# set -u on bash < 4.4 (see build_dmg.sh) - keep the same guard for consistency. +( cd "$GUI" && npm run tauri build -- --bundles appimage,deb ${UPDATER_OVERLAY[@]+"${UPDATER_OVERLAY[@]}"} ) + +BUNDLE="$GUI/src-tauri/target/release/bundle" +echo "" +echo "Done." +echo "AppImage: $BUNDLE/appimage/" +echo ".deb: $BUNDLE/deb/" diff --git a/packaging/make_update_manifest.py b/packaging/make_update_manifest.py index 513f82235b..78e5a23d9c 100644 --- a/packaging/make_update_manifest.py +++ b/packaging/make_update_manifest.py @@ -11,6 +11,7 @@ OpenWorker-macos-arm64.app.tar.gz(.sig) -> platforms["darwin-aarch64"] OpenWorker-windows-setup.exe(.sig) -> platforms["windows-x86_64"] + OpenWorker-linux-x64.AppImage(.sig) -> platforms["linux-x86_64"] URLs point at the TAG-pinned GitHub download path (releases/download//), never at `latest/` — a manifest must reference exactly the artifacts it shipped with, @@ -35,6 +36,7 @@ ARTIFACTS = { "OpenWorker-macos-arm64.app.tar.gz": "darwin-aarch64", "OpenWorker-windows-setup.exe": "windows-x86_64", + "OpenWorker-linux-x64.AppImage": "linux-x86_64", }