Files
simplex-chat/.github/workflows/desktop-linux.yml
T
spacedandy-agents-simplex a4b9639ef9 ci: guard Claude's Bash with the hook alone, log its decisions, use Sonnet 5.5
In run 38737084248 the credential scrub made Claude Code 2.1.292 require its whole Bash sandbox (bubblewrap and socat) and force the default permission mode, so every ui call failed. The scrub and bubblewrap are dropped: the PreToolUse guard, which runs in -p sessions, keeps Bash to single ui commands, and now logs each decision outside Claude's directory as evidence. The model is Claude Sonnet 5.5 for the last Claude run the session allows. Third Claude run: [claude-test]
2026-10-15 13:01:20 +00:00

463 lines
16 KiB
YAML

name: desktop-linux
on:
push:
branches:
- f/ci-desktop
workflow_dispatch:
permissions:
contents: read
actions: read
concurrency:
group: desktop-linux-${{ github.ref }}
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: find earlier builds
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
backend: ${{ steps.names.outputs.backend }}
backend-run: ${{ steps.backend.outputs.run-id }}
steps:
- name: Checkout code
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
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
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.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
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 an earlier run built it
if: ${{ !cancelled() && !failure() }}
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
dpkg-query -W xvfb xdotool openbox imagemagick tesseract-ocr sqlite3
- 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
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
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
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.link.outcome == 'success' }}
run: scripts/ci/desktop-linux/smoke-test.sh apps/multiplatform/release/main/app/simplex "$RUNNER_TEMP/smoke-test"
- name: Stop test backend
if: ${{ !cancelled() }}
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() }}
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's directory search with the ui helper. Each run spends the Claude
# subscription's usage limits, so it runs only when dispatched or asked for in the head commit's message.
claude:
name: Claude searches the directory in the desktop app
needs: [plan, core, backend]
if: ${{ !cancelled() && !failure() && (github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[claude-test]')) }}
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:
CLAUDE_CODE_VERSION: 2.1.292
CLAUDE_MODEL: claude-sonnet-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
dpkg-query -W xvfb xdotool openbox imagemagick tesseract-ocr sqlite3
- 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
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
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
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 at the chat list
run: scripts/ci/desktop-linux/smoke-test.sh apps/multiplatform/release/main/app/simplex "$RUNNER_TEMP/claude-test/screens" onboarding
# 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() }}
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.guard.log
${{ runner.temp }}/claude-usage.json
retention-days: 7
if-no-files-found: warn
- name: Upload backend logs
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v6
with:
name: desktop-claude-backend-logs
path: ${{ runner.temp }}/backend/*.log
retention-days: 3
if-no-files-found: warn
release:
name: released app under Xvfb
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