Files
simplex-chat/.github/workflows/desktop-linux.yml
T
spacedandy-agents-simplex 049cff88c7 ci: restore the desktop Claude search test
With the test backend, the Claude job builds the app against the test directory, sets it up at the chat list and gives Claude the online search test; without it, the onboarding path stays.
2026-10-16 21:02:01 +00:00

526 lines
20 KiB
YAML

name: desktop-linux
# A push runs the jobs only when its head commit's message asks for them: see scripts/ci/desktop-linux/README.md.
on:
push:
branches:
- "**"
# a squash merge's message lists the merged commits' subjects, with their tags
- "!master"
- "!stable"
workflow_dispatch:
inputs:
claude:
description: Also run the Claude job, which spends the Claude subscription's usage limits
type: boolean
default: false
backend:
description: Also start the test backend and test the released app
type: boolean
default: false
permissions:
contents: read
actions: read
concurrency:
# a push that asks for no run doesn't cancel one that does
group: desktop-linux-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[app-ci]') || contains(github.event.head_commit.message, '[app-ci:desktop]') || contains(github.event.head_commit.message, '[claude-test]') || contains(github.event.head_commit.message, '[claude-test:desktop]') }}
cancel-in-progress: true
env:
GHC_VER: 9.6.3
CABAL_VER: 3.10.2.0
JAVA_VER: 17
CORE_PKG: desktop-core-linux-x86_64.tar.gz
BACKEND_PKG: desktop-linux-backend.tar.gz
jobs:
plan:
name: choose jobs, find earlier builds
if: >-
github.event_name == 'workflow_dispatch' ||
contains(github.event.head_commit.message, '[app-ci]') ||
contains(github.event.head_commit.message, '[app-ci:desktop]') ||
contains(github.event.head_commit.message, '[claude-test]') ||
contains(github.event.head_commit.message, '[claude-test:desktop]')
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
run-claude: ${{ steps.jobs.outputs.claude }}
run-backend: ${{ steps.jobs.outputs.backend }}
backend: ${{ steps.names.outputs.backend }}
backend-run: ${{ steps.backend.outputs.run-id }}
steps:
- name: Choose jobs
id: jobs
env:
CLAUDE: ${{ inputs.claude || contains(github.event.head_commit.message, '[claude-test]') || contains(github.event.head_commit.message, '[claude-test:desktop]') }}
BACKEND: ${{ inputs.backend || contains(github.event.head_commit.message, '[ci-backend]') }}
run: |
echo "claude=$CLAUDE" | tee -a "$GITHUB_OUTPUT"
echo "backend=$BACKEND" | tee -a "$GITHUB_OUTPUT"
- name: Checkout code
if: steps.jobs.outputs.backend == 'true'
uses: actions/checkout@v5
# artifacts are named after the sources they are built from, so a run uses one that an earlier run uploaded
- name: Artifact names
id: names
if: steps.jobs.outputs.backend == 'true'
run: echo "backend=desktop-linux-backend-${{ hashFiles('src/**', 'apps/simplex-chat/**', 'apps/simplex-directory-service/Main.hs', 'apps/simplex-directory-service/src/**', 'cabal.project', 'simplex-chat.cabal', 'scripts/ci/desktop-linux/build-backend.sh') }}" >> "$GITHUB_OUTPUT"
- name: Find backend
id: backend
if: steps.jobs.outputs.backend == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: scripts/ci/desktop-linux/earlier-run.sh "${{ steps.names.outputs.backend }}"
backend:
name: test backend for the app
needs: plan
if: needs.plan.outputs.run-backend == 'true' && needs.plan.outputs.backend-run == ''
runs-on: ubuntu-24.04
timeout-minutes: 150
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Show runner
run: |
lsb_release -ds
nproc
free -h
df -h /
- name: Setup Haskell
id: haskell
uses: simplex-chat/setup-haskell-action@v2
with:
ghc-version: ${{ env.GHC_VER }}
cabal-version: ${{ env.CABAL_VER }}
# before the first backend store exists, the core library's has most of the same Hackage dependencies
- name: Restore cabal store
id: cabal-cache
uses: actions/cache/restore@v5
with:
path: |
${{ steps.haskell.outputs.cabal-store }}
dist-newstyle
key: desktop-linux-backend-store-ghc${{ env.GHC_VER }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }}-${{ github.run_id }}
restore-keys: |
desktop-linux-backend-store-ghc${{ env.GHC_VER }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }}-
desktop-linux-backend-store-ghc${{ env.GHC_VER }}-
desktop-linux-core-store-ghc${{ env.GHC_VER }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }}-
- name: Install libraries
run: |
sudo apt-get update
sudo apt-get install -y libffi-dev libgmp-dev libnuma-dev libssl-dev zlib1g-dev
- name: Build backend
run: scripts/ci/desktop-linux/build-backend.sh ~/backend/${{ env.BACKEND_PKG }}
- name: Save cabal store
if: ${{ !cancelled() }}
uses: actions/cache/save@v5
with:
path: |
${{ steps.haskell.outputs.cabal-store }}
dist-newstyle
key: ${{ steps.cabal-cache.outputs.cache-primary-key }}
- name: Upload backend
uses: actions/upload-artifact@v6
with:
name: ${{ needs.plan.outputs.backend }}
path: ~/backend/${{ env.BACKEND_PKG }}
compression-level: 0
retention-days: 30
if-no-files-found: error
core:
name: core library for Linux
needs: plan
runs-on: ubuntu-24.04
timeout-minutes: 150
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Show runner
run: |
lsb_release -ds
nproc
free -h
df -h /
- name: Restore core library
id: core-cache
uses: actions/cache/restore@v5
with:
path: ~/core/${{ env.CORE_PKG }}
key: desktop-linux-core-${{ hashFiles('src/**', 'cabal.project', 'simplex-chat.cabal', 'PRIVACY.md', 'scripts/desktop/build-lib-linux.sh', 'scripts/ci/desktop-linux/build-core.sh') }}
- name: Setup Haskell
id: haskell
if: steps.core-cache.outputs.cache-hit != 'true'
uses: simplex-chat/setup-haskell-action@v2
with:
ghc-version: ${{ env.GHC_VER }}
cabal-version: ${{ env.CABAL_VER }}
- name: Restore cabal store
id: cabal-cache
if: steps.core-cache.outputs.cache-hit != 'true'
uses: actions/cache/restore@v5
with:
path: |
${{ steps.haskell.outputs.cabal-store }}
dist-newstyle
key: desktop-linux-core-store-ghc${{ env.GHC_VER }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }}-${{ github.run_id }}
restore-keys: |
desktop-linux-core-store-ghc${{ env.GHC_VER }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }}-
desktop-linux-core-store-ghc${{ env.GHC_VER }}-
- name: Install libraries
if: steps.core-cache.outputs.cache-hit != 'true'
run: |
sudo apt-get update
sudo apt-get install -y libffi-dev libgmp-dev libnuma-dev libssl-dev zlib1g-dev patchelf
- name: Build core library
if: steps.core-cache.outputs.cache-hit != 'true'
run: scripts/ci/desktop-linux/build-core.sh ~/core/${{ env.CORE_PKG }}
- name: Save cabal store
if: ${{ !cancelled() && steps.core-cache.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v5
with:
path: |
${{ steps.haskell.outputs.cabal-store }}
dist-newstyle
key: ${{ steps.cabal-cache.outputs.cache-primary-key }}
- name: Save core library
if: steps.core-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: ~/core/${{ env.CORE_PKG }}
key: ${{ steps.core-cache.outputs.cache-primary-key }}
- name: Upload core library
uses: actions/upload-artifact@v6
with:
name: desktop-core-linux-x86_64
path: ~/core/${{ env.CORE_PKG }}
compression-level: 0
retention-days: 3
if-no-files-found: error
app:
name: desktop app under Xvfb
needs: [plan, core, backend]
# backend is skipped when it isn't asked for or an earlier run built it
if: ${{ !cancelled() && !failure() && needs.plan.result == 'success' }}
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
submodules: recursive
- name: Show runner
run: |
lsb_release -ds
nproc
free -h
df -h /
echo "ANDROID_HOME=$ANDROID_HOME"
ls "$ANDROID_HOME/platforms" "$ANDROID_HOME/build-tools" || true
- name: Install tools
run: |
sudo apt-get update
sudo apt-get install -y xvfb xdotool openbox imagemagick tesseract-ocr sqlite3 fonts-dejavu-core
dpkg-query -W xvfb xdotool openbox imagemagick tesseract-ocr sqlite3 fonts-dejavu-core
- name: Download core library
uses: actions/download-artifact@v7
with:
name: desktop-core-linux-x86_64
path: ~/core
- name: Install core library
id: core
run: scripts/ci/desktop-linux/install-core.sh ~/core/${{ env.CORE_PKG }}
- name: Download backend
if: needs.plan.outputs.run-backend == 'true'
uses: actions/download-artifact@v7
with:
name: ${{ needs.plan.outputs.backend }}
path: ~/backend
run-id: ${{ needs.plan.outputs.backend-run || github.run_id }}
github-token: ${{ github.token }}
- name: Start test backend
id: backend
if: needs.plan.outputs.run-backend == 'true'
timeout-minutes: 30
run: scripts/ci/desktop-linux/backend.sh start ~/backend/${{ env.BACKEND_PKG }} "$RUNNER_TEMP/backend"
- name: Point the app at the test directory
id: link
if: steps.backend.outcome == 'success'
run: |
link=$(cat "$RUNNER_TEMP/backend/directory-link")
file=apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt
sed -i "s|^const val DIRECTORY_SERVICE_LINK = \".*\"$|const val DIRECTORY_SERVICE_LINK = \"$link\"|" "$file"
grep -F "const val DIRECTORY_SERVICE_LINK = \"$link\"" "$file"
# the app and its tests are still built when the backend fails
- name: Setup Java
if: ${{ !cancelled() && steps.core.outcome == 'success' }}
uses: actions/setup-java@v5
with:
distribution: corretto
java-version: ${{ env.JAVA_VER }}
cache: gradle
- name: Build app
id: build
if: ${{ !cancelled() && steps.core.outcome == 'success' }}
working-directory: apps/multiplatform
run: |
./gradlew --no-daemon --console=plain -Psimplex.assets.dir=../../assets :desktop:createDistributable
app=release/main/app/simplex
ls -la "$app/bin" "$app/lib/app/resources"
du -sh "$app"
LD_LIBRARY_PATH="$app/lib/app/resources" ldd "$app/lib/app/resources/libapp-lib.so"
- name: Run desktop tests
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
working-directory: apps/multiplatform
run: ./gradlew --no-daemon --console=plain -Psimplex.assets.dir=../../assets :common:desktopTest
- name: Report desktop tests
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: scripts/ci/desktop-linux/test-summary.py apps/multiplatform/common/build/test-results/desktopTest
- name: Run smoke test
if: ${{ !cancelled() && steps.build.outcome == 'success' && steps.backend.outcome != 'failure' && steps.link.outcome != 'failure' }}
env:
BACKEND: ${{ steps.backend.outcome }}
run: |
if [ "$BACKEND" = success ]; then
export NAMES_SERVER=$(cat "$RUNNER_TEMP/backend/names-server")
cases=search
fi
scripts/ci/desktop-linux/smoke-test.sh apps/multiplatform/release/main/app/simplex "$RUNNER_TEMP/smoke-test" ${cases:-}
- name: Stop test backend
if: ${{ !cancelled() && steps.backend.outcome != 'skipped' }}
run: scripts/ci/desktop-linux/backend.sh stop "$RUNNER_TEMP/backend"
- name: Upload smoke test results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v6
with:
name: desktop-smoke-test
path: ${{ runner.temp }}/smoke-test
retention-days: 3
if-no-files-found: warn
- name: Upload desktop test report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v6
with:
name: desktop-test-report
path: |
apps/multiplatform/common/build/reports/tests/desktopTest
apps/multiplatform/common/build/test-results/desktopTest
retention-days: 3
if-no-files-found: warn
- name: Upload backend logs
if: ${{ !cancelled() && steps.backend.outcome != 'skipped' }}
uses: actions/upload-artifact@v6
with:
name: desktop-backend-logs
path: ${{ runner.temp }}/backend/*.log
retention-days: 3
if-no-files-found: warn
# Claude Code repeats the smoke test with the ui helper. Each run spends the Claude subscription's usage
# limits, so it runs only when asked for. The app-sim-claude environment's branch rule decides which branches
# may use the token.
claude:
name: Claude tests the desktop app
needs: [plan, core, backend]
if: ${{ !cancelled() && !failure() && needs.plan.outputs.run-claude == 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 60
environment:
name: app-sim-claude
deployment: false
concurrency:
group: desktop-linux-claude-${{ github.ref }}
cancel-in-progress: true
env:
# the first release with Haiku 5.5's token counts right (2.1.293 added the model, 2.1.296 fixed its counts)
CLAUDE_CODE_VERSION: 2.1.296
CLAUDE_MODEL: claude-haiku-5-5
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
submodules: recursive
persist-credentials: false
- name: Install tools
run: |
sudo apt-get update
sudo apt-get install -y xvfb xdotool openbox imagemagick tesseract-ocr sqlite3 fonts-dejavu-core
dpkg-query -W xvfb xdotool openbox imagemagick tesseract-ocr sqlite3 fonts-dejavu-core
- name: Download core library
uses: actions/download-artifact@v7
with:
name: desktop-core-linux-x86_64
path: ~/core
- name: Install core library
run: scripts/ci/desktop-linux/install-core.sh ~/core/${{ env.CORE_PKG }}
- name: Download backend
if: needs.plan.outputs.run-backend == 'true'
uses: actions/download-artifact@v7
with:
name: ${{ needs.plan.outputs.backend }}
path: ~/backend
run-id: ${{ needs.plan.outputs.backend-run || github.run_id }}
github-token: ${{ github.token }}
- name: Start test backend
if: needs.plan.outputs.run-backend == 'true'
timeout-minutes: 30
run: scripts/ci/desktop-linux/backend.sh start ~/backend/${{ env.BACKEND_PKG }} "$RUNNER_TEMP/backend"
- name: Point the app at the test directory
if: needs.plan.outputs.run-backend == 'true'
run: |
link=$(cat "$RUNNER_TEMP/backend/directory-link")
file=apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/DirectorySearch.kt
sed -i "s|^const val DIRECTORY_SERVICE_LINK = \".*\"$|const val DIRECTORY_SERVICE_LINK = \"$link\"|" "$file"
grep -F "const val DIRECTORY_SERVICE_LINK = \"$link\"" "$file"
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: corretto
java-version: ${{ env.JAVA_VER }}
cache: gradle
- name: Build app
working-directory: apps/multiplatform
run: ./gradlew --no-daemon --console=plain -Psimplex.assets.dir=../../assets :desktop:createDistributable
# ui goes on every shell's PATH, where Claude's Bash tool finds it whatever environment it builds
- name: Install Claude Code and the ui command
run: |
npm install --prefix "$RUNNER_TEMP/claude-code" "@anthropic-ai/claude-code@$CLAUDE_CODE_VERSION"
echo "$RUNNER_TEMP/claude-code/node_modules/.bin" >> "$GITHUB_PATH"
printf '#!/bin/sh\nexec %s/scripts/ci/desktop-linux/ui "$@"\n' "$PWD" | sudo tee /usr/local/bin/ui
sudo chmod +x /usr/local/bin/ui
- name: Start the app
env:
BACKEND: ${{ needs.plan.outputs.run-backend }}
run: |
if [ "$BACKEND" = true ]; then
export NAMES_SERVER=$(cat "$RUNNER_TEMP/backend/names-server")
cases=setup
fi
scripts/ci/desktop-linux/smoke-test.sh apps/multiplatform/release/main/app/simplex "$RUNNER_TEMP/claude-test/screens" ${cases:-start}
# Claude's Bash is limited to single ui commands by the guard hook in claude-test.sh. Claude Code's credential
# scrub stays off: on Linux it forces its whole Bash sandbox (bubblewrap, socat), which ui isn't set up for.
- name: Run Claude
id: claude
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1
TEST_PROMPT: scripts/ci/desktop-linux/${{ needs.plan.outputs.run-backend == 'true' && 'claude-search.md' || 'claude-test.md' }}
run: scripts/ci/desktop-linux/claude-test.sh "$RUNNER_TEMP/claude-test" "$RUNNER_TEMP/claude-transcript.jsonl" "$TEST_PROMPT"
- name: Report Claude usage
if: ${{ !cancelled() && steps.claude.outcome != 'skipped' }}
run: scripts/ci/desktop-linux/claude-usage.py "$RUNNER_TEMP/claude-transcript.jsonl" "$RUNNER_TEMP/claude-usage.json"
- name: Stop test backend
if: ${{ !cancelled() && needs.plan.outputs.run-backend == 'true' }}
run: scripts/ci/desktop-linux/backend.sh stop "$RUNNER_TEMP/backend"
- name: Upload Claude test results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v6
with:
name: desktop-claude-test
path: |
${{ runner.temp }}/claude-test
${{ runner.temp }}/claude-transcript.jsonl
${{ runner.temp }}/claude-transcript.jsonl.stderr
${{ runner.temp }}/claude-transcript.jsonl.guard.log
${{ runner.temp }}/claude-usage.json
retention-days: 7
if-no-files-found: warn
- name: Upload backend logs
if: ${{ !cancelled() && needs.plan.outputs.run-backend == 'true' }}
uses: actions/upload-artifact@v6
with:
name: desktop-claude-backend-logs
path: ${{ runner.temp }}/backend/*.log
retention-days: 3
if-no-files-found: warn
# the latest release's onboarding, to tell a branch's own failure from the runner's
release:
name: released app under Xvfb
needs: plan
if: needs.plan.outputs.run-backend == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
filter: tree:0
- name: Download released app
run: scripts/ci/desktop-linux/release-app.sh "$RUNNER_TEMP/release"
- name: Install UI tools
run: |
sudo apt-get update
sudo apt-get install -y xvfb xdotool openbox imagemagick tesseract-ocr
dpkg-query -W xvfb xdotool openbox imagemagick tesseract-ocr
- name: Run smoke test
run: scripts/ci/desktop-linux/smoke-test.sh "$RUNNER_TEMP/release/app/opt/simplex" "$RUNNER_TEMP/smoke-test" onboarding
- name: Upload smoke test results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v6
with:
name: desktop-release-smoke-test
path: ${{ runner.temp }}/smoke-test
retention-days: 3
if-no-files-found: warn