From 24cf6601610f03958227a91a103fa633a8f657f9 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 15 Sep 2026 00:05:47 +0200 Subject: [PATCH] ci: move packaging to main and remove repeated CLI suites PR runners were rebuilding distribution artifacts and executing the entire CLI source suite on all three E2E shards. Keep packaging coverage after merge, let the local E2E runner own its dependency build, and execute source tests once in an existing required check. Root typecheck already validates plugin examples. --- .github/workflows/ci.yml | 72 ++++-------------------- .github/workflows/desktop-packages.yml | 78 ++++++++++++++++++++++++++ .github/workflows/docker.yml | 23 +------- .github/workflows/nix.yml | 25 +-------- docs/testing.md | 6 +- scripts/ci-workflow.test.mjs | 8 ++- 6 files changed, 98 insertions(+), 114 deletions(-) create mode 100644 .github/workflows/desktop-packages.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47ee95f36e..ebe2981bbf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -201,7 +201,7 @@ jobs: - name: Install virtual display if: ${{ runner.os == 'Linux' }} - run: sudo apt-get update && sudo apt-get install -y xvfb xauth rpm xdotool + run: sudo apt-get update && sudo apt-get install -y xvfb xauth xdotool - name: Install Playwright browser if: ${{ runner.os == 'Linux' }} @@ -249,59 +249,6 @@ jobs: if-no-files-found: ignore retention-days: 7 - - name: Build Linux desktop artifacts - if: ${{ runner.os == 'Linux' }} - # Smoke package contents and installation without spending minutes on xz compression. - run: | - printf '{"extends":"%s/packages/desktop/electron-builder.yml","deb":{"compression":"gz"},"rpm":{"fpm":["--rpm-compression=gzip"]}}' "$GITHUB_WORKSPACE" > "$RUNNER_TEMP/desktop-smoke-builder.json" - npm run build:desktop -- --publish never --linux --x64 --config "$RUNNER_TEMP/desktop-smoke-builder.json" - 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 @@ -363,9 +310,6 @@ jobs: - 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 @@ -499,14 +443,18 @@ jobs: - 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 + # The local runner builds its dependencies and shards its own E2E files. + - name: Run CLI E2E tests + run: npm run test:local --workspace=@getpaseo/cli + + # Source tests do not read PASEO_CLI_TEST_SHARD. Run them once, on the + # shortest E2E shard, after its runner has built the server stack. + - name: Run CLI source tests + if: ${{ env.PASEO_CLI_TEST_SHARD == '3' }} + run: npm run test:unit --workspace=@getpaseo/cli cli-tests-2: name: cli-tests (shard 2/3) diff --git a/.github/workflows/desktop-packages.yml b/.github/workflows/desktop-packages.yml new file mode 100644 index 0000000000..8d8a8b7e4c --- /dev/null +++ b/.github/workflows/desktop-packages.yml @@ -0,0 +1,78 @@ +name: Desktop Packages + +on: + push: + branches: [main] + +permissions: + contents: read + +env: + ONNXRUNTIME_NODE_INSTALL: skip + +jobs: + linux: + runs-on: ubuntu-24.04 + timeout-minutes: 30 + 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: Install package smoke dependencies + run: sudo apt-get update && sudo apt-get install -y xvfb xauth rpm + + - name: Build Linux desktop artifacts + # Smoke package contents and installation without spending minutes on xz compression. + run: | + printf '{"extends":"%s/packages/desktop/electron-builder.yml","deb":{"compression":"gz"},"rpm":{"fpm":["--rpm-compression=gzip"]}}' "$GITHUB_WORKSPACE" > "$RUNNER_TEMP/desktop-smoke-builder.json" + npm run build:desktop -- --publish never --linux --x64 --config "$RUNNER_TEMP/desktop-smoke-builder.json" + env: + EP_GH_IGNORE_TIME: true + PASEO_DESKTOP_SMOKE_ARTIFACT_DIR: ${{ runner.temp }}/desktop-smoke + + - name: Smoke installed artifacts with restricted user namespaces + 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 + 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 + 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() }} + with: + name: desktop-packaged-smoke-linux-x64 + path: ${{ runner.temp }}/desktop-smoke + if-no-files-found: ignore + retention-days: 7 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index dc5221fa6e..aa53f3ec81 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,27 +1,6 @@ name: Docker on: - pull_request: - branches: [main] - paths: - - "docker/**" - - ".dockerignore" - - ".github/workflows/docker.yml" - - "package.json" - - "package-lock.json" - - "patches/**" - - "scripts/**" - - "tsconfig.json" - - "tsconfig.base.json" - - "packages/app/**" - - "packages/cli/**" - - "packages/client/**" - - "packages/expo-two-way-audio/**" - - "packages/highlight/**" - - "packages/plugin/**" - - "packages/protocol/**" - - "packages/relay/**" - - "packages/server/**" push: branches: [main] tags: @@ -51,7 +30,7 @@ on: concurrency: group: docker-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + cancel-in-progress: false env: REGISTRY: ghcr.io diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index bf198d1c94..a4a248e1cf 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -1,27 +1,8 @@ name: Nix on: - pull_request: + push: branches: [main] - paths: - - "nix/**" - - "flake.nix" - - "flake.lock" - - "package.json" - - "package-lock.json" - - "packages/app/**" - - "packages/expo-two-way-audio/**" - - "packages/highlight/**" - - "packages/plugin/**" - - "packages/protocol/**" - - "packages/client/**" - - "packages/server/**" - - "packages/relay/**" - - "packages/cli/**" - - "scripts/build-daemon-web-ui.mjs" - - "scripts/update-nix.sh" - - "scripts/fix-lockfile.mjs" - - ".github/workflows/nix.yml" permissions: contents: read @@ -31,8 +12,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - uses: actions/setup-node@v4 with: @@ -110,8 +89,6 @@ jobs: runs-on: macos-14 steps: - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - uses: cachix/install-nix-action@v31 with: diff --git a/docs/testing.md b/docs/testing.md index 933dcba740..f8e83ae553 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -114,7 +114,7 @@ The harness launches the packaged app with isolated user data and daemon state, - the renderer starts a fresh desktop-managed daemon through the normal startup bootstrap; - the bundled CLI can query that daemon and run a terminal command. -Pull-request CI runs the Linux x64 smoke under Xvfb when the cumulative PR diff selects desktop coverage. The Linux job is pinned to Ubuntu 24.04 and runs twice: with AppArmor user namespace restrictions enabled, then with user namespaces available. It installs the real `.deb`, launches the real AppImage via `--appimage-extract-and-run`, and launches the extracted tar archive. It also replaces the Debian installation with the generated RPM through `rpm --install --nodeps` and checks its sandboxed launch under restrictions. Ubuntu supplies the runtime libraries under Debian package names, so this verifies the RPM payload and postinstall rather than Fedora dependency resolution. Each launch verifies the reported sandbox decision; enabled renderers must also have `NoNewPrivs: 1` and `Seccomp: 2` in `/proc`. The desktop release matrix retains its host-native smokes. Linux release builds stay on Ubuntu 22.04 to preserve their native-library baseline; the restricted-host regression runs on Ubuntu 24.04 in PR CI. +The Desktop Packages workflow runs the Linux x64 smoke under Xvfb on main pushes. The Linux job is pinned to Ubuntu 24.04 and runs twice: with AppArmor user namespace restrictions enabled, then with user namespaces available. It installs the real `.deb`, launches the real AppImage via `--appimage-extract-and-run`, and launches the extracted tar archive. It also replaces the Debian installation with the generated RPM through `rpm --install --nodeps` and checks its sandboxed launch under restrictions. Ubuntu supplies the runtime libraries under Debian package names, so this verifies the RPM payload and postinstall rather than Fedora dependency resolution. Each launch verifies the reported sandbox decision; enabled renderers must also have `NoNewPrivs: 1` and `Seccomp: 2` in `/proc`. The desktop release matrix retains its host-native smokes. Linux release builds stay on Ubuntu 22.04 to preserve their native-library baseline; the restricted-host regression runs on Ubuntu 24.04 after merge. Never repair `chrome-sandbox` in the smoke harness. The old unpacked smoke set its mode to 4755 and concealed a broken package installer. Run installer tests as root and launch tests as an ordinary user: a root-run namespace probe does not reproduce Ubuntu's AppArmor policy for desktop users. Preserve both restricted and unrestricted cases; either one alone permits another sandbox regression. @@ -132,7 +132,7 @@ npm run build:desktop -- --publish never --linux --x64 --dir electron-builder packs `node_modules` by walking declared production `dependencies`. A package that imports something it only lists as a `peerDependency` resolves fine in this hoisted workspace, passes every test, and then throws `ERR_MODULE_NOT_FOUND` inside `app.asar` — killing the desktop daemon at startup. That shipped twice from `@replit/codemirror-lang-*` grammars, which are interactive editor extensions published as if they were bare parsers. -The packaged smoke catches it but only runs when a PR touches the `desktop` filter in `.github/ci-paths.yml`. Both offenders landed under `packages/highlight/**`, which maps to `sdk`. +The packaged smoke catches it after merge. PRs need the fast dependency-closure test below because packaging runs on main. `packages/highlight/src/__tests__/dependency-closure.test.ts` replicates the packer's traversal statically and runs with the normal unit tests. It is scoped to `@getpaseo/highlight` on purpose: that tree is small and pure, so the check is exact. Running the same walk over `@getpaseo/server` produces dozens of false positives from optional dependencies loaded behind `try`/`catch`. @@ -204,7 +204,7 @@ PR checks are routed by the behavior each suite proves, using `.github/ci-paths. Required matrix legs are declared as statically named jobs. Their shared steps use YAML anchors, while job-level `if` conditions let GitHub report an unaffected leg as genuinely skipped without allocating a runner or losing the exact required-check name. -The smallest meaningful contract wins over package ownership. Tiny structural invariants such as daemon launch supervision run unconditionally in the always-running routing job instead of maintaining a transitive file list; this check reads source entrypoints and builds no product. Routed integration contracts use stable domain directories. Browser changes select the required Playwright shards; desktop changes select the existing required desktop jobs, with renderer, real-Electron, and packaged-app coverage together in the Ubuntu leg. CLI-side Hub changes select one focused test inside the existing required server jobs. Repository scripts and the shared Vitest configuration run every PR contract because they are cross-cutting toolchain inputs. +The smallest meaningful contract wins over package ownership. Tiny structural invariants such as daemon launch supervision run unconditionally in the always-running routing job instead of maintaining a transitive file list; this check reads source entrypoints and builds no product. Routed integration contracts use stable domain directories. Browser changes select the required Playwright shards; desktop changes select the existing required desktop jobs, with renderer and real-Electron coverage together in the Ubuntu leg. Packaging runs on main in the Desktop Packages, Docker, and Nix workflows. CLI-side Hub changes select one focused test inside the existing required server jobs. The CLI source suite runs once in the third required CLI job; only the separate local E2E runner is sharded, and that runner owns its dependency build. Repository scripts and the shared Vitest configuration run every PR contract because they are cross-cutting toolchain inputs. ## Agent authentication in tests diff --git a/scripts/ci-workflow.test.mjs b/scripts/ci-workflow.test.mjs index 997cb5c7cb..ff58d3832c 100644 --- a/scripts/ci-workflow.test.mjs +++ b/scripts/ci-workflow.test.mjs @@ -281,11 +281,13 @@ test("browser and desktop tests have exclusive, directory-owned suites", () => { ]); }); -test("non-required Docker and Nix workflows avoid runners with workflow path filters", () => { - for (const workflowPath of [dockerWorkflowPath, nixWorkflowPath]) { +test("packaging runs on main without allocating pull-request runners", () => { + const desktopPackagesPath = new URL(".github/workflows/desktop-packages.yml", repoRoot); + for (const workflowPath of [dockerWorkflowPath, nixWorkflowPath, desktopPackagesPath]) { const source = readFileSync(workflowPath, "utf8"); const trigger = source.split("jobs:", 1)[0]; - assert.match(trigger, /^\s+paths:\s*$/m); + assert.match(trigger, /push:\s*\n\s+branches: \[main\]/); + assert.doesNotMatch(trigger, /pull_request/); assert.doesNotMatch(source, /dorny\/paths-filter/); } });