mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 17:52:49 +00:00
* fix(desktop): preserve Linux sandboxing wherever the host allows it Chromium checks its Linux sandbox before main.js, while package installation probes run as root and miss AppArmor restrictions on desktop users. Choose the launch flags before exec and always install the SUID alternative for native packages. Test published artifacts as an ordinary user with namespaces both denied and allowed; never repair permissions in the smoke harness. * fix(desktop): retain the Linux release build baseline * fix(ci): allow canceled desktop smoke uploads to stop * test(desktop): verify the installed RPM sandbox under AppArmor * fix(ci): resolve packaged desktop dependencies before installation * chore(desktop): keep QA evidence out of the repository
533 lines
18 KiB
YAML
533 lines
18 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
branches: [main]
|
|
merge_group:
|
|
workflow_dispatch:
|
|
|
|
concurrency:
|
|
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
env:
|
|
# CI does not use the CUDA execution provider, and the onnxruntime-node
|
|
# postinstall download from NuGet is large enough to make npm ci flaky.
|
|
ONNXRUNTIME_NODE_INSTALL: skip
|
|
|
|
jobs:
|
|
changes:
|
|
name: changes
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
outputs:
|
|
full: ${{ github.event_name != 'pull_request' || steps.filter.outputs.routing != 'false' || steps.filter.outputs.workspace != 'false' || steps.filter.outputs.ci != 'false' }}
|
|
format: ${{ steps.filter.outputs.format }}
|
|
quality: ${{ steps.filter.outputs.quality }}
|
|
hub: ${{ steps.filter.outputs.hub }}
|
|
server: ${{ steps.filter.outputs.server }}
|
|
desktop: ${{ steps.filter.outputs.desktop }}
|
|
app: ${{ steps.filter.outputs.app }}
|
|
sdk: ${{ steps.filter.outputs.sdk }}
|
|
browser: ${{ steps.filter.outputs.browser }}
|
|
relay: ${{ steps.filter.outputs.relay }}
|
|
cli: ${{ steps.filter.outputs.cli }}
|
|
steps:
|
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "22"
|
|
|
|
- name: Detect affected CI contracts
|
|
id: filter
|
|
uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3.0.3
|
|
with:
|
|
filters: .github/ci-paths.yml
|
|
|
|
- name: Validate CI contracts
|
|
run: node --test scripts/ci-workflow.test.mjs scripts/daemon-launch-contract.test.mjs scripts/sync-fdroid-changelogs.test.mjs
|
|
|
|
format:
|
|
name: format
|
|
needs: changes
|
|
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.format != 'false') }}
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "22"
|
|
cache: "npm"
|
|
|
|
- name: Install dependencies
|
|
run: node scripts/npm-retry.mjs ci
|
|
|
|
- name: Check formatting
|
|
run: npm run format:check
|
|
|
|
lint:
|
|
name: lint
|
|
needs: changes
|
|
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.quality != 'false') }}
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "22"
|
|
cache: "npm"
|
|
|
|
- name: Install dependencies
|
|
run: node scripts/npm-retry.mjs ci
|
|
|
|
- name: Lint lockfile
|
|
run: npx --yes lockfile-lint --path package-lock.json --type npm --allowed-hosts npm --validate-https --validate-integrity
|
|
|
|
- name: Verify dependency signatures
|
|
run: npm audit signatures
|
|
|
|
- name: Lint
|
|
run: npm run lint
|
|
|
|
typecheck:
|
|
name: typecheck
|
|
needs: changes
|
|
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.quality != 'false') }}
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "22"
|
|
cache: "npm"
|
|
|
|
- name: Install dependencies
|
|
run: node scripts/npm-retry.mjs ci
|
|
|
|
- name: Build server stack
|
|
run: npm run build:server
|
|
|
|
- name: Typecheck all packages
|
|
run: npm run typecheck
|
|
|
|
- name: Verify public package contents
|
|
run: |
|
|
npm pack --dry-run --ignore-scripts --workspace=@getpaseo/protocol
|
|
npm pack --dry-run --ignore-scripts --workspace=@getpaseo/client
|
|
npm pack --dry-run --ignore-scripts --workspace=@getpaseo/server
|
|
|
|
server-tests-ubuntu:
|
|
name: server-tests (ubuntu-latest)
|
|
needs: changes
|
|
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.server != 'false' || needs.changes.outputs.hub != 'false') }}
|
|
runs-on: ubuntu-latest
|
|
steps: &server_test_steps
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "22"
|
|
cache: "npm"
|
|
|
|
- name: Fetch origin/main (worktree tests)
|
|
if: ${{ needs.changes.outputs.full != 'false' || needs.changes.outputs.server != 'false' }}
|
|
run: git fetch --no-tags origin main:refs/remotes/origin/main
|
|
|
|
- name: Install dependencies
|
|
run: node scripts/npm-retry.mjs ci
|
|
|
|
- name: Install agent CLIs for provider tests
|
|
if: ${{ needs.changes.outputs.full != 'false' || needs.changes.outputs.server != 'false' }}
|
|
run: node scripts/npm-retry.mjs install -g @anthropic-ai/claude-code opencode-ai
|
|
|
|
- name: Build server dependencies
|
|
run: npm run build:server-deps
|
|
|
|
- name: Run server tests
|
|
if: ${{ needs.changes.outputs.full != 'false' || needs.changes.outputs.server != 'false' }}
|
|
run: npm run test --workspace=@getpaseo/server
|
|
env:
|
|
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
|
|
|
- name: Run Hub CLI/daemon contract
|
|
if: ${{ needs.changes.outputs.full == 'false' && needs.changes.outputs.server == 'false' && needs.changes.outputs.hub != 'false' }}
|
|
run: npm run test:hub-cli-contract --workspace=@getpaseo/server
|
|
|
|
server-tests-windows:
|
|
name: server-tests (windows-latest)
|
|
needs: changes
|
|
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.server != 'false' || needs.changes.outputs.hub != 'false') }}
|
|
runs-on: windows-latest
|
|
steps: *server_test_steps
|
|
|
|
desktop-tests-ubuntu:
|
|
name: desktop-tests (ubuntu-latest)
|
|
needs: changes
|
|
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.desktop != 'false') }}
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 30
|
|
permissions:
|
|
contents: read
|
|
steps: &desktop_test_steps
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "22"
|
|
cache: "npm"
|
|
|
|
- name: Install dependencies with retry
|
|
run: node scripts/npm-retry.mjs ci
|
|
|
|
- name: Build server stack
|
|
run: npm run build:server
|
|
|
|
- name: Run desktop tests
|
|
run: npm run test --workspace=@getpaseo/desktop
|
|
|
|
- name: Install virtual display
|
|
if: ${{ runner.os == 'Linux' }}
|
|
run: sudo apt-get update && sudo apt-get install -y xvfb xauth rpm xdotool
|
|
|
|
- name: Install Playwright browser
|
|
if: ${{ runner.os == 'Linux' }}
|
|
run: npx playwright install chromium
|
|
|
|
- name: Verify Windows daemon process control
|
|
if: ${{ runner.os == 'Windows' }}
|
|
run: npx vitest run packages/cli/src/commands/daemon/lifecycle.e2e.test.ts --bail=1 --maxWorkers=1 -t "Windows unbound|managed two-home"
|
|
|
|
- name: Run desktop lifecycle E2E (Linux)
|
|
if: ${{ runner.os == 'Linux' }}
|
|
run: xvfb-run -a --server-args="-screen 0 1280x800x24" npm run test:e2e:lifecycle --workspace=@getpaseo/desktop
|
|
env:
|
|
PASEO_DESKTOP_LIFECYCLE_ARTIFACT_DIR: ${{ runner.temp }}/desktop-lifecycle
|
|
|
|
- name: Upload desktop lifecycle screenshots
|
|
uses: actions/upload-artifact@v4
|
|
if: ${{ !cancelled() && runner.os == 'Linux' }}
|
|
with:
|
|
name: desktop-lifecycle
|
|
path: ${{ runner.temp }}/desktop-lifecycle
|
|
if-no-files-found: ignore
|
|
retention-days: 7
|
|
|
|
- name: Run desktop lifecycle E2E (Windows)
|
|
if: ${{ runner.os == 'Windows' }}
|
|
run: npm run test:e2e:lifecycle --workspace=@getpaseo/desktop
|
|
|
|
- name: Run desktop renderer E2E
|
|
if: ${{ runner.os == 'Linux' }}
|
|
run: npm run test:e2e:renderer --workspace=@getpaseo/desktop
|
|
|
|
- name: Run desktop browser E2E
|
|
if: ${{ runner.os == 'Linux' }}
|
|
run: npm run test:e2e:browser-tabs --workspace=@getpaseo/desktop
|
|
env:
|
|
PASEO_DESKTOP_BROWSER_E2E_ARTIFACT_DIR: ${{ runner.temp }}/desktop-browser-e2e
|
|
|
|
- name: Upload desktop browser diagnostics
|
|
uses: actions/upload-artifact@v4
|
|
if: ${{ failure() && runner.os == 'Linux' }}
|
|
with:
|
|
name: desktop-browser-e2e
|
|
path: ${{ runner.temp }}/desktop-browser-e2e
|
|
if-no-files-found: ignore
|
|
retention-days: 7
|
|
|
|
- name: Build Linux desktop artifacts
|
|
if: ${{ runner.os == 'Linux' }}
|
|
run: npm run build:desktop -- --publish never --linux --x64
|
|
env:
|
|
EP_GH_IGNORE_TIME: true
|
|
PASEO_DESKTOP_SMOKE_ARTIFACT_DIR: ${{ runner.temp }}/desktop-smoke
|
|
|
|
- name: Smoke installed artifacts with restricted user namespaces
|
|
if: ${{ runner.os == 'Linux' }}
|
|
run: |
|
|
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=1
|
|
if unshare --user --map-root-user --net true; then
|
|
echo "Expected the launching user's namespace probe to be denied"
|
|
exit 1
|
|
fi
|
|
sudo apt-get install -y ./packages/desktop/release/*.deb
|
|
node packages/desktop/e2e/linux-artifact-smoke.js packages/desktop/release disabled
|
|
env:
|
|
PASEO_DESKTOP_SMOKE_ARTIFACT_DIR: ${{ runner.temp }}/desktop-smoke/restricted
|
|
|
|
- name: Smoke sandbox-preserving portable launches
|
|
if: ${{ runner.os == 'Linux' }}
|
|
run: |
|
|
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
|
unshare --user --map-root-user --net true
|
|
node packages/desktop/e2e/linux-artifact-smoke.js packages/desktop/release enabled
|
|
env:
|
|
PASEO_DESKTOP_SMOKE_ARTIFACT_DIR: ${{ runner.temp }}/desktop-smoke/userns
|
|
|
|
- name: Smoke installed RPM with restricted user namespaces
|
|
if: ${{ runner.os == 'Linux' }}
|
|
run: |
|
|
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=1
|
|
sudo dpkg --remove paseo
|
|
# Ubuntu supplies the runtime libraries under Debian package names.
|
|
# RPM still performs the real payload installation and postinstall.
|
|
sudo rpm --install --nodeps packages/desktop/release/*.rpm
|
|
node packages/desktop/e2e/linux-artifact-smoke.js packages/desktop/release disabled --installed-only
|
|
env:
|
|
PASEO_DESKTOP_SMOKE_ARTIFACT_DIR: ${{ runner.temp }}/desktop-smoke/rpm
|
|
|
|
- name: Upload packaged smoke diagnostics
|
|
uses: actions/upload-artifact@v4
|
|
if: ${{ !cancelled() && runner.os == 'Linux' }}
|
|
with:
|
|
name: desktop-packaged-smoke-linux-x64
|
|
path: ${{ runner.temp }}/desktop-smoke
|
|
if-no-files-found: ignore
|
|
retention-days: 7
|
|
|
|
desktop-tests-windows:
|
|
name: desktop-tests (windows-latest)
|
|
needs: changes
|
|
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.desktop != 'false') }}
|
|
runs-on: windows-latest
|
|
timeout-minutes: 30
|
|
permissions:
|
|
contents: read
|
|
steps: *desktop_test_steps
|
|
|
|
app-tests:
|
|
name: app-tests
|
|
needs: changes
|
|
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.app != 'false') }}
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "22"
|
|
cache: "npm"
|
|
|
|
- name: Install dependencies with retry
|
|
run: node scripts/npm-retry.mjs ci
|
|
|
|
- name: Install Playwright browsers
|
|
timeout-minutes: 10
|
|
run: npx playwright install chromium
|
|
|
|
- name: Build app dependencies
|
|
run: npm run build:app-deps
|
|
|
|
- name: Run app unit tests
|
|
run: npm run test --workspace=@getpaseo/app
|
|
|
|
sdk-tests:
|
|
name: sdk-tests
|
|
needs: changes
|
|
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.sdk != 'false') }}
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "22"
|
|
cache: "npm"
|
|
|
|
- name: Install dependencies
|
|
run: node scripts/npm-retry.mjs ci
|
|
|
|
- name: Build client dependencies
|
|
run: npm run build:relay && npm run build:client
|
|
|
|
- name: Build plugin SDK
|
|
run: npm run build:plugin
|
|
|
|
- name: Run plugin SDK tests
|
|
run: npm run test --workspace=@getpaseo/plugin
|
|
|
|
- name: Typecheck plugin examples
|
|
run: npm run typecheck --workspace=@getpaseo/plugin
|
|
|
|
- name: Run protocol tests
|
|
run: npm run test --workspace=@getpaseo/protocol
|
|
|
|
- name: Run client tests
|
|
run: npm run test --workspace=@getpaseo/client
|
|
|
|
- name: Run highlight tests
|
|
run: npm run test --workspace=@getpaseo/highlight
|
|
|
|
- name: Typecheck client examples
|
|
run: npm run typecheck:examples --workspace=@getpaseo/client
|
|
|
|
playwright-1:
|
|
name: playwright (shard 1/4)
|
|
needs: changes
|
|
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.browser != 'false') }}
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
PLAYWRIGHT_SHARD: "1/4"
|
|
PLAYWRIGHT_ARTIFACT: "1"
|
|
steps: &playwright_test_steps
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "22"
|
|
cache: "npm"
|
|
|
|
- name: Install dependencies with retry
|
|
run: node scripts/npm-retry.mjs ci
|
|
|
|
- name: Install Playwright browsers
|
|
timeout-minutes: 10
|
|
run: npx playwright install chromium
|
|
|
|
- name: Build server stack
|
|
run: npm run build:server
|
|
|
|
- name: Build native app dependency
|
|
run: npm run build --workspace=@getpaseo/expo-two-way-audio
|
|
|
|
- name: Install agent CLIs for provider tests
|
|
run: node scripts/npm-retry.mjs install -g @anthropic-ai/claude-code @openai/codex@0.105.0 opencode-ai
|
|
|
|
- name: Run Playwright E2E tests
|
|
run: npm run test:e2e --workspace=@getpaseo/app -- --shard=${{ env.PLAYWRIGHT_SHARD }}
|
|
env:
|
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
|
|
- name: Upload test artifacts
|
|
uses: actions/upload-artifact@v4
|
|
if: failure()
|
|
with:
|
|
name: playwright-results-${{ env.PLAYWRIGHT_ARTIFACT }}
|
|
path: |
|
|
packages/app/test-results/
|
|
packages/app/playwright-report/
|
|
retention-days: 7
|
|
|
|
playwright-2:
|
|
name: playwright (shard 2/4)
|
|
needs: changes
|
|
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.browser != 'false') }}
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
PLAYWRIGHT_SHARD: "2/4"
|
|
PLAYWRIGHT_ARTIFACT: "2"
|
|
steps: *playwright_test_steps
|
|
|
|
playwright-3:
|
|
name: playwright (shard 3/4)
|
|
needs: changes
|
|
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.browser != 'false') }}
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
PLAYWRIGHT_SHARD: "3/4"
|
|
PLAYWRIGHT_ARTIFACT: "3"
|
|
steps: *playwright_test_steps
|
|
|
|
playwright-4:
|
|
name: playwright (shard 4/4)
|
|
needs: changes
|
|
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.browser != 'false') }}
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
PLAYWRIGHT_SHARD: "4/4"
|
|
PLAYWRIGHT_ARTIFACT: "4"
|
|
steps: *playwright_test_steps
|
|
|
|
relay-tests:
|
|
name: relay-tests
|
|
needs: changes
|
|
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.relay != 'false') }}
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "22"
|
|
cache: "npm"
|
|
|
|
- name: Install dependencies
|
|
run: node scripts/npm-retry.mjs ci
|
|
|
|
- name: Build relay
|
|
run: npm run build:relay
|
|
|
|
- name: Run relay tests
|
|
run: npm run test --workspace=@getpaseo/relay
|
|
|
|
cli-tests-1:
|
|
name: cli-tests (shard 1/3)
|
|
needs: changes
|
|
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.cli != 'false') }}
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: "0"
|
|
PASEO_DICTATION_ENABLED: "0"
|
|
PASEO_VOICE_MODE_ENABLED: "0"
|
|
PASEO_CLI_TEST_SHARD: "1"
|
|
PASEO_CLI_TEST_SHARD_TOTAL: "3"
|
|
steps: &cli_test_steps
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "22"
|
|
cache: "npm"
|
|
|
|
- name: Install dependencies
|
|
run: node scripts/npm-retry.mjs ci
|
|
|
|
- name: Build server stack
|
|
run: npm run build:server
|
|
|
|
- name: Install agent CLIs for provider tests
|
|
run: node scripts/npm-retry.mjs install -g @anthropic-ai/claude-code @openai/codex@0.105.0 opencode-ai
|
|
|
|
- name: Run CLI tests
|
|
run: npm run test --workspace=@getpaseo/cli
|
|
|
|
cli-tests-2:
|
|
name: cli-tests (shard 2/3)
|
|
needs: changes
|
|
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.cli != 'false') }}
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: "0"
|
|
PASEO_DICTATION_ENABLED: "0"
|
|
PASEO_VOICE_MODE_ENABLED: "0"
|
|
PASEO_CLI_TEST_SHARD: "2"
|
|
PASEO_CLI_TEST_SHARD_TOTAL: "3"
|
|
steps: *cli_test_steps
|
|
|
|
cli-tests-3:
|
|
name: cli-tests (shard 3/3)
|
|
needs: changes
|
|
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.cli != 'false') }}
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: "0"
|
|
PASEO_DICTATION_ENABLED: "0"
|
|
PASEO_VOICE_MODE_ENABLED: "0"
|
|
PASEO_CLI_TEST_SHARD: "3"
|
|
PASEO_CLI_TEST_SHARD_TOTAL: "3"
|
|
steps: *cli_test_steps
|