openclaw/.github/workflows/linux-app.yml
Dallin Romney 614f559915
fix(release): expose Linux builder admission and retain failed bundles (#170297)
* fix(release): expose Linux builder admission and retain failed bundles

* fix(release): preserve unconfirmed Linux dispatch status

* fix(ci): make Linux build event admission explicit

* fix(ci): separate Linux mirror dispatch from release builds

* fix(ci): budget cold Linux source package builds
2026-10-14 18:14:52 -07:00

448 lines
18 KiB
YAML

name: Linux App
on:
pull_request:
paths:
- "apps/linux/**"
- "scripts/lib/openclaw-bun.json"
- "scripts/stage-openclaw-bun.sh"
- "test/scripts/stage-openclaw-bun.test.ts"
- ".github/workflows/linux-app.yml"
- ".github/workflows/linux-app-release.yml"
- "!apps/linux/README.md"
- "!apps/linux/omarchy/README.md"
workflow_dispatch:
concurrency:
group: linux-app-${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
permissions:
contents: read
jobs:
build:
name: Build Linux companion
# Match the release build base for native tests and manually built bundles.
runs-on: ubuntu-22.04
# Leave room for cold source packaging and the native desktop proof afterward.
timeout-minutes: 105
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
- name: Install Tauri system dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
at-spi2-core \
build-essential \
curl \
dbus-x11 \
ffmpeg \
file \
gir1.2-atspi-2.0 \
gnome-keyring \
gstreamer1.0-libav \
gstreamer1.0-plugins-bad \
gstreamer1.0-plugins-good \
gstreamer1.0-tools \
imagemagick \
libatk-adaptor \
libayatana-appindicator3-dev \
libegl1 \
libgles2 \
librsvg2-dev \
libssl-dev \
libwebkit2gtk-4.1-dev \
libxdo-dev \
mesa-utils \
openbox \
patchelf \
unzip \
pciutils \
procps \
psmisc \
python3-gi \
wget \
wmctrl \
x11-utils \
xauth \
xdg-utils \
xdotool \
xvfb
- name: Install Rust
run: rustup toolchain install stable --profile minimal --component rustfmt
- name: Cache Cargo
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/registry
~/.cargo/git
apps/linux/src-tauri/target
key: linux-app-${{ runner.os }}-${{ hashFiles('apps/linux/src-tauri/Cargo.lock') }}
restore-keys: |
linux-app-${{ runner.os }}-
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
cache-mode: restore
install-bun: "false"
- name: Test native dashboard authentication and runtime boundaries
run: |
node --test \
apps/linux/scripts/test-native-control-auth.mjs \
apps/linux/scripts/test-runtime-boundary.mjs \
apps/linux/scripts/test-stage-runtime.mjs \
apps/linux/scripts/test-stage-package.mjs
- name: Test packaged runtime ABI scanner
run: |
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover \
-s apps/linux/tests -p 'test_packaged_runtime_smoke.py'
- name: Test desktop sharing proof report ordering
run: |
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover \
-s apps/linux/tests -p 'test_desktop_sharing_reports.py'
- name: Test native first-run accessibility role matching
run: |
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover \
-s apps/linux/tests -p 'test_first_run.py'
- name: Check Rust formatting
working-directory: apps/linux/src-tauri
run: cargo +stable fmt --check
- name: Test Omarchy desktop handoff
run: |
PYTHONDONTWRITEBYTECODE=1 /usr/bin/python3 -m unittest discover \
-s apps/linux/omarchy -p 'test_*.py'
- name: Run Rust tests
working-directory: apps/linux/src-tauri
run: cargo +stable test --locked --all-targets
- name: Build native inline browser candidate
working-directory: apps/linux/src-tauri
env:
# Cold Linux declarations also exceed the package builder's 45-minute default.
OPENCLAW_DOCKER_PACKAGE_BUILD_TIMEOUT_MS: "3600000"
run: |
node ../scripts/stage-runtime.mjs
# The native fixture supplies its CLI at the same build-time boundary.
python3 ../tests/stage_onboarding_runtime.py target/desktop-runtime
cargo +stable build --locked
- name: Test native inline browser
id: inline-browser
shell: bash
run: |
mkdir -p "${RUNNER_TEMP}/linux-inline-browser"
xvfb-run -a -s '-screen 0 1280x1024x24' dbus-run-session -- \
/usr/bin/python3 apps/linux/tests/first_run.py \
apps/linux/src-tauri/target/debug/openclaw-desktop --inline-browser \
--artifacts-dir "${RUNNER_TEMP}/linux-inline-browser" \
2>&1 | tee "${RUNNER_TEMP}/linux-inline-browser/run.log"
- name: Upload native inline browser proof
if: always() && steps.inline-browser.outcome != 'skipped'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: linux-inline-browser
retention-days: 14
if-no-files-found: warn
path: ${{ runner.temp }}/linux-inline-browser
- name: Test native Gateway switching
id: gateway-switch
shell: bash
run: |
mkdir -p "${RUNNER_TEMP}/linux-gateway-switch"
xvfb-run -a -s '-screen 0 1440x1080x24' dbus-run-session -- \
/usr/bin/python3 apps/linux/tests/first_run.py \
apps/linux/src-tauri/target/debug/openclaw-desktop --gateway-switch \
--artifacts-dir "${RUNNER_TEMP}/linux-gateway-switch" \
2>&1 | tee "${RUNNER_TEMP}/linux-gateway-switch/run.log"
mkdir -p "${RUNNER_TEMP}/linux-gateway-switch/onboarding"
node scripts/run-node-package-bin.mts vite build --config apps/linux/tests/fixtures/vite.onboarding.config.ts
xvfb-run -a -s '-screen 0 1440x1080x24' dbus-run-session -- \
/usr/bin/python3 apps/linux/tests/first_run.py \
apps/linux/src-tauri/target/debug/openclaw-desktop --gateway-onboarding \
--artifacts-dir "${RUNNER_TEMP}/linux-gateway-switch/onboarding" \
2>&1 | tee "${RUNNER_TEMP}/linux-gateway-switch/onboarding/run.log"
- name: Upload native Gateway switching proof
if: always() && steps.gateway-switch.outcome != 'skipped'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: linux-gateway-switch
retention-days: 14
if-no-files-found: warn
path: ${{ runner.temp }}/linux-gateway-switch
- name: Test native desktop sharing
id: desktop-sharing
shell: bash
run: |
mkdir -p "${RUNNER_TEMP}/linux-desktop-sharing"
xvfb-run -a -s '-screen 0 1440x1080x24' dbus-run-session -- \
/usr/bin/python3 apps/linux/tests/first_run.py \
apps/linux/src-tauri/target/debug/openclaw-desktop --desktop-sharing \
--artifacts-dir "${RUNNER_TEMP}/linux-desktop-sharing" \
2>&1 | tee "${RUNNER_TEMP}/linux-desktop-sharing/run.log"
- name: Upload native desktop sharing proof
if: always() && steps.desktop-sharing.outcome != 'skipped'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: linux-desktop-sharing
retention-days: 14
if-no-files-found: warn
path: ${{ runner.temp }}/linux-desktop-sharing
- name: Test native Quick Chat
id: quick-chat
if: github.event_name == 'workflow_dispatch'
shell: bash
run: |
mkdir -p "${RUNNER_TEMP}/linux-quick-chat"
xvfb-run -a -s '-screen 0 1440x1080x24' dbus-run-session -- \
/usr/bin/python3 apps/linux/tests/first_run.py \
apps/linux/src-tauri/target/debug/openclaw-desktop --quick-chat \
--artifacts-dir "${RUNNER_TEMP}/linux-quick-chat" \
2>&1 | tee "${RUNNER_TEMP}/linux-quick-chat/run.log"
- name: Upload native Quick Chat proof
if: always() && steps.quick-chat.outcome != 'skipped'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: linux-quick-chat
retention-days: 14
if-no-files-found: warn
path: ${{ runner.temp }}/linux-quick-chat
- name: Stage AppImage GStreamer plugins
if: github.event_name == 'workflow_dispatch'
run: |
plugins="${RUNNER_TEMP}/openclaw-gstreamer-plugins"
apps/linux/scripts/stage-appimage-gstreamer.sh "$plugins" \
| tee "${RUNNER_TEMP}/openclaw-gstreamer-elements.tsv"
echo "GSTREAMER_PLUGINS_DIR=$plugins" >> "$GITHUB_ENV"
- name: Prepare pinned AppImage tools
if: github.event_name == 'workflow_dispatch'
env:
XDG_CACHE_HOME: ${{ runner.temp }}/openclaw-tauri-cache
run: | # zizmor: ignore[github-env] trusted checked-in helper returns the native runtime path
set -euo pipefail
apps/linux/scripts/tauri-appimage-tools.sh prepare
apps/linux/scripts/tauri-appimage-tools.sh verify pre-build
runtime_file=$(apps/linux/scripts/tauri-appimage-tools.sh runtime-path)
printf 'LDAI_RUNTIME_FILE=%s\n' "$runtime_file" >> "$GITHUB_ENV"
- name: Build Linux companion bundles
if: github.event_name == 'workflow_dispatch'
working-directory: apps/linux/src-tauri
env:
# appimagetool strips fail on CI runners; Tauri documents NO_STRIP for Actions.
NO_STRIP: "true"
XDG_CACHE_HOME: ${{ runner.temp }}/openclaw-tauri-cache
# Validation builds have no TAURI_SIGNING_PRIVATE_KEY, and the configured updater
# pubkey makes the bundler hard-require it. Skip updater artifacts here;
# linux-app-release.yml builds the signed updater bundles.
run: pnpm dlx @tauri-apps/cli@2.11.4 build --bundles deb,appimage --config '{"bundle":{"createUpdaterArtifacts":false,"useLocalToolsDir":false}}'
- name: Finalize AppImage
if: github.event_name == 'workflow_dispatch'
env:
XDG_CACHE_HOME: ${{ runner.temp }}/openclaw-tauri-cache
run: apps/linux/scripts/finalize-appimage.sh apps/linux/src-tauri/target/release/bundle/appimage
- name: Test native first-run setup and failed Gateway startup
id: first-run
if: github.event_name == 'workflow_dispatch'
shell: bash
run: |
xvfb-run -a -s '-screen 0 1280x1024x24' dbus-run-session -- \
/usr/bin/python3 apps/linux/tests/first_run.py \
apps/linux/src-tauri/target/release/openclaw-desktop \
2>&1 | tee "${RUNNER_TEMP}/linux-first-run.log"
xvfb-run -a -s '-screen 0 1280x1024x24' dbus-run-session -- \
/usr/bin/python3 apps/linux/tests/first_run.py \
apps/linux/src-tauri/target/release/openclaw-desktop --local-start-failure \
2>&1 | tee -a "${RUNNER_TEMP}/linux-first-run.log"
- name: Upload first-run failure log
if: github.event_name == 'workflow_dispatch' && failure() && steps.first-run.outcome == 'failure'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: linux-first-run-failure
retention-days: 14
if-no-files-found: error
path: ${{ runner.temp }}/linux-first-run.log
- name: Test packaged AppImage runtime
id: packaged-runtime
if: github.event_name == 'workflow_dispatch'
shell: bash
run: |
shopt -s nullglob
appimages=(apps/linux/src-tauri/target/release/bundle/appimage/*.AppImage)
if [[ ${#appimages[@]} -ne 1 ]]; then
echo "Expected one AppImage, found ${#appimages[@]}"
exit 1
fi
xvfb-run -a -s '-screen 0 1280x1024x24' dbus-run-session -- \
/usr/bin/python3 apps/linux/tests/packaged_runtime_smoke.py \
"${appimages[0]}" \
--output "${RUNNER_TEMP}/linux-packaged-runtime" \
--require-fuse \
--shell-container \
"archlinux@sha256:82b1b08faae9d61e3e7e13d562f4d09114d939105b0d59ff34140f3bd418593a"
- name: Upload packaged runtime failure evidence
if: github.event_name == 'workflow_dispatch' && failure() && steps.packaged-runtime.outcome == 'failure'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: linux-packaged-runtime-failure
retention-days: 14
if-no-files-found: warn
path: ${{ runner.temp }}/linux-packaged-runtime
- name: Upload bundles
if: github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: openclaw-linux-companion
retention-days: 14
if-no-files-found: error
path: |
apps/linux/src-tauri/target/release/bundle/deb/*.deb
apps/linux/src-tauri/target/release/bundle/appimage/*.AppImage
test-macos:
name: Test macOS companion
# This app also ships macOS desktop-test bundles, but the only job that
# compiles them (linux-app-release.yml build_macos) runs behind a manual
# dispatch input. Without a per-PR check, a macOS-gated Tauri API can break
# the macOS target for weeks unnoticed - `WebviewWindowBuilder::transparent`
# did exactly that.
#
# macos-15, not macos-14: tauri-plugin-notifications' Swift sources use
# typed throws (`throws(FFIResult)`), which needs Swift 6. The macos-14
# image still ships Swift 5.x and fails to parse them.
runs-on: macos-15
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
- name: Install Rust
run: rustup toolchain install stable --profile minimal
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
cache-mode: restore
install-bun: "false"
- name: Cache Cargo
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/registry
~/.cargo/git
apps/linux/src-tauri/target
key: macos-app-${{ runner.os }}-${{ hashFiles('apps/linux/src-tauri/Cargo.lock') }}
restore-keys: |
macos-app-${{ runner.os }}-
- name: Test macOS companion
working-directory: apps/linux/src-tauri
# `test` rather than `check`: it additionally links and runs the
# binaries, which is the only way macOS link-time breakage (a missing
# Swift runtime rpath, say) shows up at all. `--all-targets` keeps the
# compile coverage `check --all-targets` used to give.
run: cargo +stable test --locked --all-targets
- name: Verify native macOS power lifecycle
working-directory: apps/linux/src-tauri
run: cargo +stable test --locked --test native_keep_awake native_power_lifecycle -- --nocapture
test-windows-power:
name: Test Windows desktop
# Match the regular Windows installer build runner.
runs-on: windows-2022
timeout-minutes: 105
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
- name: Install Rust
run: rustup toolchain install stable --profile minimal
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
cache-mode: off
install-bun: "false"
- name: Cache Cargo
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/registry
~/.cargo/git
apps/linux/src-tauri/target
key: windows-app-${{ runner.os }}-${{ hashFiles('apps/linux/src-tauri/Cargo.lock') }}
restore-keys: |
windows-app-${{ runner.os }}-
- name: Verify native Windows power lifecycle
working-directory: apps/linux/src-tauri
run: cargo +stable test --locked --test native_keep_awake native_power_lifecycle -- --nocapture
- name: Verify Windows runtime, installer, and desktop boundaries
working-directory: apps/linux/src-tauri
run: |
node --test ../scripts/test-stage-runtime.mjs ../scripts/test-stage-package.mjs
cargo +stable test --locked --bin openclaw-desktop
- name: Build Windows installer without publishing
working-directory: apps/linux/src-tauri
env:
# Cold Windows declarations can exceed the package builder's 45-minute default.
OPENCLAW_DOCKER_PACKAGE_BUILD_TIMEOUT_MS: "3600000"
run: pnpm dlx @tauri-apps/cli@2.11.4 build --bundles nsis --config '{"bundle":{"createUpdaterArtifacts":false}}'
- name: Upload Windows installer
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: openclaw-windows-desktop
retention-days: 14
if-no-files-found: error
path: apps/linux/src-tauri/target/release/bundle/nsis/*.exe