name: PR Checks on: pull_request: types: - opened - synchronize - reopened - ready_for_review concurrency: group: pr-checks-${{ github.event.pull_request.number }} cancel-in-progress: true permissions: contents: read jobs: static_analysis: name: static analysis runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 persist-credentials: false - uses: ./.github/actions/install-node-dependencies - name: Lint run: pnpm exec oxlint --format github - name: Enforce focused code-quality plugins run: pnpm run audit:code-quality:native - name: Enforce type-aware code-quality baseline run: pnpm run audit:code-quality:type-aware - name: Enforce changed-code quality run: pnpm run check:code-quality:changed -- "${{ github.event.pull_request.base.sha }}" - name: Enforce React Doctor on changed lines run: pnpm run check:react-doctor:changed -- "${{ github.event.pull_request.base.sha }}" - name: Check Zustand selector fan-out budget run: pnpm run check:zustand-selector-fanout - name: Check reliability gate manifest run: pnpm run check:reliability-gates - name: Enforce max-lines ratchet run: pnpm run check:max-lines-ratchet - name: Verify bundled skill guides run: pnpm run verify:bundled-skill-guides - name: Verify skill freshness manifest run: pnpm run verify:skill-bundle-manifest - name: Verify localization catalog run: pnpm run verify:localization-catalog # Why: extraction writes sorted evidence to an isolated temporary path, # so feature PRs need one normalized AST pass rather than a three-OS matrix. - name: Verify localization extraction run: pnpm run verify:localization-extraction - name: Verify localization coverage run: pnpm run verify:localization-coverage # Why: project-owned type declarations must live in .ts so tsc # actually checks them. TypeScript's skipLibCheck: true (inherited # from @electron-toolkit/tsconfig) silently widens unresolved names # in .d.ts to `any`, which is how #1186 shipped a broken IPC signature # past typecheck. See docs/preload-typecheck-hole.md. - name: Guard against project-owned .d.ts in preload/shared run: | matches=$(find src/preload src/shared -name '*.d.ts' 2>/dev/null || true) if [ -n "$matches" ]; then echo "::error::Project-owned .d.ts files are not allowed under src/preload or src/shared." echo "Move type declarations into a .ts file so skipLibCheck does not hide errors." echo "See docs/preload-typecheck-hole.md." echo "Found:" echo "$matches" exit 1 fi - name: Check feature wall asset budget run: pnpm check:feature-wall-assets - name: Verify macOS entitlements run: pnpm verify:macos-entitlements typecheck: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: persist-credentials: false - uses: ./.github/actions/install-node-dependencies - run: pnpm typecheck git_compatibility: name: Git compatibility runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: persist-credentials: false - uses: ./.github/actions/install-node-dependencies - name: Verify Git binary compatibility matrix run: | pids=() ( archive="$RUNNER_TEMP/git-2.25.5.tar.gz" source="$RUNNER_TEMP/git-2.25.5" curl -fsSL https://www.kernel.org/pub/software/scm/git/git-2.25.5.tar.gz -o "$archive" echo "41662c52fc16fec4963bfc41075e71f8ead6b5e386797eb6f9a1111ff95a8ddf $archive" \ | sha256sum --check mkdir -p "$source" tar -xzf "$archive" -C "$source" --strip-components=1 make -C "$source" -j"$(nproc)" \ NO_GETTEXT=YesPlease NO_TCLTK=YesPlease NO_PYTHON=YesPlease git ORCA_GIT_COMPAT_BINARY="$source/git" ORCA_GIT_COMPAT_VERSION="2.25.5" \ pnpm exec vitest run --config config/vitest.config.ts \ src/shared/git-binary-compatibility.test.ts ) & pids+=("$!") for spec in \ "alpine/git:edge-2.38.1|2.38.1" \ "alpine/git:v2.49.1|2.49.1"; do ( image="${spec%%|*}" version="${spec#*|}" ORCA_GIT_COMPAT_IMAGE="$image" ORCA_GIT_COMPAT_VERSION="$version" \ pnpm exec vitest run --config config/vitest.config.ts \ src/shared/git-binary-compatibility.test.ts ) & pids+=("$!") done status=0 for pid in "${pids[@]}"; do wait "$pid" || status=1 done exit "$status" shell_contracts: name: shell contracts runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: persist-credentials: false - name: Install zsh run: sudo apt-get update && sudo apt-get install -y zsh - uses: ./.github/actions/install-node-dependencies with: native-runtime: node - name: Test real shell contracts run: | pnpm exec vitest run --config config/vitest.config.ts \ src/main/daemon/shell-ready.test.ts \ src/main/daemon/node-pty-fd-leak.test.ts \ src/main/providers/local-pty-shell-ready.test.ts \ src/main/providers/__tests__/shell-ready-framework-example.test.ts \ src/main/pty/omp-shell-wrapper.node-pty.test.ts \ src/shared/posix-command-path-lookup.test.ts test: name: tests node ${{ matrix.node }} ${{ matrix.shard }}/${{ matrix.shard_total }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: node: ['24', '26'] shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] shard_total: [16] steps: - name: Checkout uses: actions/checkout@v6 with: persist-credentials: false - uses: ./.github/actions/install-node-dependencies with: native-runtime: node node-version: ${{ matrix.node }} - name: Install Electron package binary for tests run: node config/scripts/install-electron-package-binary.mjs - name: Test shard run: | pnpm exec vitest run --config config/vitest.config.ts \ --exclude=src/main/daemon/shell-ready.test.ts \ --exclude=src/main/daemon/node-pty-fd-leak.test.ts \ --exclude=src/main/providers/local-pty-shell-ready.test.ts \ --exclude=src/main/providers/__tests__/shell-ready-framework-example.test.ts \ --exclude=src/main/pty/omp-shell-wrapper.node-pty.test.ts \ --exclude=src/shared/posix-command-path-lookup.test.ts \ --shard=${{ matrix.shard }}/${{ matrix.shard_total }} package: name: package runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: persist-credentials: false - name: Cache electron-builder downloads uses: actions/cache@v5 with: path: | ~/.cache/electron ~/.cache/electron-builder key: electron-builder-linux-${{ hashFiles('pnpm-lock.yaml') }} restore-keys: | electron-builder-linux- - uses: ./.github/actions/install-node-dependencies with: native-runtime: electron - name: Build package inputs run: | status=0 pnpm run build:cli || status=1 scripts=(build:relay build:electron-vite:parallel) pids=() for script in "${scripts[@]}"; do pnpm run "$script" & pids+=("$!") done for pid in "${pids[@]}"; do wait "$pid" || status=1 done exit "$status" - name: Project web client from renderer build run: pnpm run build:web-from-renderer - name: Build native components run: pnpm run build:native - name: Package unpacked app env: ORCA_REUSE_PREPARED_NATIVE_RUNTIME: '1' run: pnpm exec electron-builder --config config/electron-builder.config.cjs --dir - name: Smoke packaged CLI run: node config/scripts/smoke-packaged-cli.mjs --app-dir=dist/linux-unpacked # Why: regression specs under tests/e2e/** used to merge green without ever # running — e2e.yml only fired on schedule/release (#10518). Path-filter so # ordinary PRs stay light; any E2E suite change still gets a full shard run. e2e-paths: name: detect e2e path changes runs-on: ubuntu-latest if: github.event.pull_request.draft != true # Why: detector only needs to read the checkout; do not inherit repo defaults. permissions: contents: read outputs: should_run: ${{ steps.filter.outputs.should_run }} steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 persist-credentials: false - name: Filter E2E-relevant paths id: filter run: | set -euo pipefail BASE="${{ github.event.pull_request.base.sha }}" HEAD="${{ github.event.pull_request.head.sha }}" # Why: capture first so a failed git diff does not look like "no matches" # (pipeline status in `if` is not aborted by set -e). Merge-base limits the # list to files this PR introduced, not base-branch drift. CHANGED="$(git diff --name-only --merge-base "$BASE" "$HEAD")" # Why: tests/playwright.config.ts sits beside tests/e2e/, not inside it, so # it needs its own pattern — a bare `playwright.` prefix matches no tracked # file and would silently skip E2E when the runner config changes. if printf '%s\n' "$CHANGED" | grep -E '^(tests/e2e/|tests/playwright\.|\.github/workflows/e2e\.yml$)' >/dev/null; then echo "should_run=true" >> "$GITHUB_OUTPUT" echo "E2E path changes detected" else echo "should_run=false" >> "$GITHUB_OUTPUT" echo "No E2E path changes" fi e2e: name: e2e needs: e2e-paths if: needs.e2e-paths.outputs.should_run == 'true' # Why: reusable e2e.yml only checkouts, builds, and uploads artifacts. permissions: contents: read uses: ./.github/workflows/e2e.yml verify: if: always() needs: - static_analysis - typecheck - git_compatibility - shell_contracts - test - package runs-on: ubuntu-latest steps: # Why: e2e is deliberately absent from needs. The suite is currently red on # main (every scheduled run), so gating merges on it would block any PR that # touches tests/e2e/** — including the ones fixing the suite. Until it is # green the job runs and reports for E2E-path PRs without blocking. To flip # it on: add `e2e` to needs, add E2E to the env below, and require # `"$E2E" = success || skipped` after the loop — skipped is the normal # result for a path-filtered job and must keep passing, so it has to be # checked outside the loop or it would excuse the jobs above. - name: Require successful checks env: STATIC_ANALYSIS: ${{ needs.static_analysis.result }} TYPECHECK: ${{ needs.typecheck.result }} GIT_COMPATIBILITY: ${{ needs.git_compatibility.result }} SHELL_CONTRACTS: ${{ needs.shell_contracts.result }} TEST: ${{ needs.test.result }} PACKAGE: ${{ needs.package.result }} run: | for result in \ "$STATIC_ANALYSIS" \ "$TYPECHECK" \ "$GIT_COMPATIBILITY" \ "$SHELL_CONTRACTS" \ "$TEST" \ "$PACKAGE"; do if [ "$result" != "success" ]; then exit 1 fi done