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" # 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' }} env: BACKEND: ${{ steps.backend.outcome }} run: | if [ "$BACKEND" = success ]; then export NAMES_SERVER=$(cat "$RUNNER_TEMP/backend/names-server") cases=backend 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: 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 run: scripts/ci/desktop-linux/smoke-test.sh apps/multiplatform/release/main/app/simplex "$RUNNER_TEMP/claude-test/screens" 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 run: scripts/ci/desktop-linux/claude-test.sh "$RUNNER_TEMP/claude-test" "$RUNNER_TEMP/claude-transcript.jsonl" - 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