diff --git a/.github/workflows/desktop-ci.yml b/.github/workflows/desktop-ci.yml index fc7c1063..a5a5e708 100644 --- a/.github/workflows/desktop-ci.yml +++ b/.github/workflows/desktop-ci.yml @@ -18,12 +18,53 @@ jobs: quality: name: Desktop quality gates runs-on: ubuntu-24.04 + outputs: + desktop_changed: ${{ steps.scope.outputs.desktop_changed }} steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Detect desktop-impacting changes + id: scope + env: + EVENT_NAME: ${{ github.event_name }} + BEFORE_SHA: ${{ github.event.before }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + echo "desktop_changed=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [[ "$EVENT_NAME" == "pull_request" ]]; then + compare_from="$BASE_SHA" + else + compare_from="$BEFORE_SHA" + fi + + if [[ -z "$compare_from" || "$compare_from" =~ ^0+$ ]]; then + echo "desktop_changed=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if ! git cat-file -e "${compare_from}^{commit}" 2>/dev/null; then + echo "desktop_changed=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + git diff --no-renames --name-only -z "$compare_from" "$HEAD_SHA" \ + | python3 scripts/desktop_ci_scope.py >> "$GITHUB_OUTPUT" + + - name: Skip unaffected Desktop checks + if: steps.scope.outputs.desktop_changed != 'true' + run: echo "No Desktop or bundled App Server inputs changed." - name: Install Tauri Linux dependencies + if: steps.scope.outputs.desktop_changed == 'true' run: | sudo apt-get update sudo apt-get install -y \ @@ -38,22 +79,26 @@ jobs: librsvg2-dev - name: Set up Node - uses: actions/setup-node@v4 + if: steps.scope.outputs.desktop_changed == 'true' + uses: actions/setup-node@v5 with: node-version: "22" cache: npm cache-dependency-path: desktop/package-lock.json - name: Set up Rust + if: steps.scope.outputs.desktop_changed == 'true' uses: dtolnay/rust-toolchain@stable with: components: clippy, rustfmt - name: Install frontend dependencies + if: steps.scope.outputs.desktop_changed == 'true' working-directory: desktop run: npm ci - name: Verify frontend and protocol + if: steps.scope.outputs.desktop_changed == 'true' working-directory: desktop run: | npm run check:version @@ -64,10 +109,12 @@ jobs: npm run build - name: Prepare Tauri resources for Rust-only checks + if: steps.scope.outputs.desktop_changed == 'true' working-directory: desktop run: mkdir -p build/sidecar/dist/deepcode-app-server - name: Verify Tauri crate + if: steps.scope.outputs.desktop_changed == 'true' working-directory: desktop/src-tauri run: | cargo fmt --check @@ -77,6 +124,9 @@ jobs: bundle: name: Bundle ${{ matrix.name }} needs: quality + if: >- + needs.quality.result == 'success' && + needs.quality.outputs.desktop_changed == 'true' strategy: fail-fast: false matrix: @@ -97,7 +147,7 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install Tauri Linux dependencies if: runner.os == 'Linux' @@ -116,14 +166,14 @@ jobs: patchelf - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: "22" cache: npm cache-dependency-path: desktop/package-lock.json - name: Set up Python for App Server sidecar - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.12" cache: pip diff --git a/desktop/src/App.test.tsx b/desktop/src/App.test.tsx index 60e24276..8a660abf 100644 --- a/desktop/src/App.test.tsx +++ b/desktop/src/App.test.tsx @@ -1470,9 +1470,20 @@ describe("desktop command center", () => { await screen.findByRole("heading", { name: "Skills" }); fireEvent.click(await screen.findByRole("button", { name: "Create Skill" })); - const composer = (await screen.findByRole("textbox", { - name: "Task instruction", - })) as HTMLTextAreaElement; + // Creating a Skill crosses an async navigation and a thread/start RPC. + // Synchronize on that observable boundary before asserting the destination UI. + await waitFor( + () => + expect( + runtime.requests.some((request) => request.method === "thread/start"), + ).toBe(true), + { timeout: 5_000 }, + ); + const composer = (await screen.findByRole( + "textbox", + { name: "Task instruction" }, + { timeout: 5_000 }, + )) as HTMLTextAreaElement; await waitFor(() => expect(composer.value).toContain("$skill-creator"), ); diff --git a/scripts/desktop_ci_scope.py b/scripts/desktop_ci_scope.py new file mode 100644 index 00000000..a933183e --- /dev/null +++ b/scripts/desktop_ci_scope.py @@ -0,0 +1,65 @@ +"""Classify whether a Git change can affect the Desktop application. + +Desktop bundles the Python App Server, so its CI scope includes both ``desktop`` +and the Python runtime imported by the sidecar. Release-only scripts, tests, and +documentation intentionally stay outside this list. +""" + +from __future__ import annotations + +import os +import sys +from collections.abc import Iterable + + +DESKTOP_IMPACT_PREFIXES = ( + ".github/actions/", + "app_server/", + "cli/", + "core/", + "desktop/", + "prompts/", + "protocol/", + "schema/", + "tools/", + "utils/", + "workflows/", +) + +DESKTOP_IMPACT_FILES = frozenset( + { + ".github/workflows/desktop-ci.yml", + "__init__.py", + "deepcode.py", + "requirements.txt", + "rust-toolchain.toml", + "scripts/desktop_ci_scope.py", + } +) + + +def affects_desktop(paths: Iterable[str]) -> bool: + """Return whether any repository-relative path affects Desktop artifacts.""" + + return any( + path in DESKTOP_IMPACT_FILES or path.startswith(DESKTOP_IMPACT_PREFIXES) + for path in paths + ) + + +def _read_null_delimited_paths() -> list[str]: + return [ + os.fsdecode(raw_path) + for raw_path in sys.stdin.buffer.read().split(b"\0") + if raw_path + ] + + +def main() -> int: + changed = affects_desktop(_read_null_delimited_paths()) + print(f"desktop_changed={'true' if changed else 'false'}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_desktop_ci_scope.py b/tests/test_desktop_ci_scope.py new file mode 100644 index 00000000..a9faeebd --- /dev/null +++ b/tests/test_desktop_ci_scope.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import pytest + +from scripts.desktop_ci_scope import affects_desktop + + +@pytest.mark.parametrize( + "path", + [ + "desktop/src/App.tsx", + "desktop/src-tauri/src/main.rs", + "app_server/dispatcher.py", + "core/skills/service.py", + "protocol/app-server.schema.json", + "requirements.txt", + ".github/workflows/desktop-ci.yml", + "scripts/desktop_ci_scope.py", + ], +) +def test_desktop_ci_runs_for_desktop_and_sidecar_inputs(path: str) -> None: + assert affects_desktop([path]) + + +@pytest.mark.parametrize( + "path", + [ + "README.md", + "docs/HEADLESS_AND_AUTOMATION.md", + "tests/test_python_distribution_release.py", + "scripts/verify_python_distribution.py", + ".github/workflows/python-ci.yml", + ".github/workflows/pypi-publish.yml", + "desktop-notes/architecture.md", + ], +) +def test_desktop_ci_skips_release_only_and_documentation_changes(path: str) -> None: + assert not affects_desktop([path]) + + +def test_desktop_ci_runs_when_any_changed_path_has_desktop_impact() -> None: + assert affects_desktop(["README.md", "core/version.py"])