diff --git a/.github/actions/ci-cache/action.yml b/.github/actions/ci-cache/action.yml new file mode 100644 index 00000000000..016e1e9853f --- /dev/null +++ b/.github/actions/ci-cache/action.yml @@ -0,0 +1,54 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: Publish CI caches +description: Publish compiler and source outputs before behavior tests. +inputs: + compiled_key: + description: Compiler cache key. + required: true + compiled_hit: + description: Whether the exact compiler cache already exists. + default: "false" + compiled_writer: + description: Whether this worker owns compiler cache publication. + default: "false" + downloads_key: + description: Source cache key. + required: true + downloads_hit: + description: Whether the exact source cache already exists. + default: "false" + downloads_writer: + description: Whether this worker owns source cache publication. + default: "false" +runs: + using: composite + steps: + - uses: actions/cache/save@v6 + if: (github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rust-preview'))) && inputs.compiled_writer == 'true' && inputs.compiled_hit != 'true' + with: + path: target + key: ${{ inputs.compiled_key }} + - uses: actions/cache/save@v6 + if: (github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rust-preview'))) && inputs.downloads_writer == 'true' && inputs.downloads_hit != 'true' + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + key: ${{ inputs.downloads_key }} diff --git a/.github/actions/ci-setup/action.yml b/.github/actions/ci-setup/action.yml new file mode 100644 index 00000000000..9e0442c06aa --- /dev/null +++ b/.github/actions/ci-setup/action.yml @@ -0,0 +1,137 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: CI setup +description: Prepare a suite worker and restore its Cargo caches. +inputs: + role: + description: Stable worker identity. + required: true + workspace: + description: Owning Cargo workspace. + default: root + dependencies: + description: Linux native dependency group. + default: "" + javascript: + description: Install locked npm dependencies. + default: "false" + compile: + description: Restore compiler outputs and use mbx on Linux. + default: "true" + nextest: + description: Producer version for archive consumers; otherwise resolve latest. + default: "" +outputs: + compiled_key: + description: Compiler cache key for this toolchain and lockfile. + value: ${{ steps.keys.outputs.compiled }} + compiled_hit: + description: Whether the exact compiler cache was restored. + value: ${{ steps.compiled.outputs.cache-hit }} + compiled_writer: + description: Whether this worker owns compiler cache publication. + value: ${{ steps.keys.outputs.compiled_writer }} + downloads_key: + description: Source cache key for this revision. + value: ${{ steps.keys.outputs.downloads }} + downloads_hit: + description: Whether the exact source cache was restored. + value: ${{ steps.downloads.outputs.cache-hit }} + downloads_writer: + description: Whether this worker owns source cache publication. + value: ${{ steps.keys.outputs.downloads_writer }} +runs: + using: composite + steps: + - uses: actions/setup-node@v7 + with: + node-version: latest + - name: Update Rust + shell: bash + run: rustup update stable --no-self-update + - name: Select Rust tools + id: tools + shell: bash + env: + MAKA_CI_ROLE: ${{ inputs.role }} + MAKA_CI_NEXTEST: ${{ inputs.nextest }} + run: node scripts/ci/setup.mjs tool-list + - uses: taiki-e/install-action@68a389c788b2cd04016ce8c80ee23b80d2ab3d68 # v2.88.1 + if: steps.tools.outputs.tools != '' + with: + tool: cargo-binstall + - name: Install Rust tools + if: steps.tools.outputs.tools != '' + shell: bash + env: + GH_TOKEN: ${{ github.token }} + MAKA_CI_ROLE: ${{ inputs.role }} + MAKA_CI_NEXTEST: ${{ inputs.nextest }} + run: node scripts/ci/setup.mjs tools + - name: Install native dependencies + if: runner.os == 'Linux' && inputs.dependencies != '' + shell: bash + env: + MAKA_CI_DEPENDENCIES: ${{ inputs.dependencies }} + run: node scripts/ci/setup.mjs native + - name: Install mbx + if: runner.os == 'Linux' && inputs.compile == 'true' + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: node scripts/ci/setup.mjs mbx + - name: Identify Cargo caches + id: keys + if: inputs.role != 'replay' + shell: bash + env: + MAKA_CI_ROLE: ${{ inputs.role }} + MAKA_CI_WORKSPACE: ${{ inputs.workspace }} + run: node scripts/ci/setup.mjs keys + - uses: actions/cache/restore@v6 + id: downloads + if: inputs.role != 'replay' + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + key: ${{ steps.keys.outputs.downloads }} + restore-keys: | + ${{ steps.keys.outputs.downloads }} + ${{ steps.keys.outputs.downloads_prefix }}- + - uses: actions/cache/restore@v6 + id: compiled + if: inputs.compile == 'true' + with: + path: target + key: ${{ steps.keys.outputs.compiled }} + restore-keys: | + ${{ steps.keys.outputs.compiled_prefix }}- + - uses: actions/cache@v6 + if: inputs.javascript == 'true' + with: + path: ~/.npm + key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }} + restore-keys: npm-${{ runner.os }}- + - name: Install JavaScript dependencies + if: inputs.javascript == 'true' + shell: bash + run: | + npm install --global --no-audit --no-fund npm@latest + node scripts/setup.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91c7217d163..8b0e50c8a24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,308 +31,201 @@ concurrency: group: ci-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }} +defaults: + run: + shell: bash + +env: + CARGO_INCREMENTAL: "0" + MBX_GC_AUTO: "1" + MBX_TARGET_VIEWS: "0" + MBX_CACHE_LINKS: "0" + MBX_SHARE_OUT_DIR: "0" + jobs: - test: + plan: + runs-on: ubuntu-latest + outputs: + root: ${{ steps.plan.outputs.root }} + workers: ${{ steps.plan.outputs.workers }} + artifact: ${{ steps.upload.outputs.artifact-id }} + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: ./.github/actions/ci-setup + with: + role: plan + compile: "false" + - id: plan + run: node scripts/ci.mjs plan + - uses: actions/upload-artifact@v7 + id: upload + with: + name: ci-plan-${{ github.run_attempt }} + path: ${{ runner.temp }}/ci-plan.json + if-no-files-found: error + + tooling: runs-on: ubuntu-latest - # Allow cold compilation and Host fixtures that reserve multiple CPU slots. timeout-minutes: 180 - env: - MBX_GC_AUTO: "1" - MBX_TARGET_VIEWS: "0" - # Cargo retains linked outputs in target; avoid another copy in mbx's store. - MBX_CACHE_LINKS: "0" steps: - uses: actions/checkout@v7 with: persist-credentials: false - - uses: actions/setup-node@v7 + - uses: ./.github/actions/ci-setup with: - node-version: latest - cache: npm - # NSH headless rendering tests need Vulkan and Mesa software adapters, - # as in third-party/nsh/.github/workflows/gpui.yml. - - name: Install native dependencies - run: | - sudo apt-get update - sudo apt-get install -y bubblewrap ripgrep pkg-config libx11-dev libxi-dev libxtst-dev libxext-dev libxcb1-dev libxkbcommon-dev libxkbcommon-x11-dev libfontconfig1-dev libfreetype6-dev libwayland-dev libvulkan1 mesa-vulkan-drivers - sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - - name: Update Rust - run: rustup update stable --no-self-update - - uses: taiki-e/install-action@68a389c788b2cd04016ce8c80ee23b80d2ab3d68 # v2.88.1 + role: tooling + javascript: "true" + compile: "false" + - run: node scripts/ci.mjs run tooling + + root-build: + needs: plan + if: needs.plan.outputs.root == 'true' + runs-on: ubuntu-latest + timeout-minutes: 180 + outputs: + partitions: ${{ steps.archive.outputs.partitions }} + has_tests: ${{ steps.archive.outputs.has_tests }} + nextest: ${{ steps.archive.outputs.nextest }} + artifact: ${{ steps.upload.outputs.artifact-id }} + steps: + - uses: actions/checkout@v7 with: - tool: cargo-binstall - - name: Install latest Rust tools - env: - GH_TOKEN: ${{ github.token }} - run: | - cargo binstall --no-confirm --force cargo-binstall - cargo binstall --no-confirm --force just cargo-nextest cargo-deny - # mr-boxington-action is not on the ASF allowlist. Install the immutable - # release directly and transport its cache with GitHub's official actions. - - name: Install mbx - id: mbx - env: - GH_TOKEN: ${{ github.token }} - run: | - release="$RUNNER_TEMP/mbx-release.json" - curl --fail --silent --show-error --location --retry 3 \ - --header "Authorization: Bearer $GH_TOKEN" \ - https://api.github.com/repos/jdx/mr-boxington/releases/latest \ - --output "$release" - jq --exit-status '.immutable == true' "$release" > /dev/null - asset='.assets[] | select(.name == "mbx-x86_64-unknown-linux-gnu.tar.gz")' - url=$(jq --exit-status --raw-output "$asset | .browser_download_url" "$release") - digest=$(jq --exit-status --raw-output "$asset | .digest | select(startswith(\"sha256:\")) | ltrimstr(\"sha256:\")" "$release") - archive="$RUNNER_TEMP/mbx.tar.gz" - curl --fail --silent --show-error --location --retry 3 "$url" --output "$archive" - echo "$digest $archive" | sha256sum -c - - mkdir -p "$RUNNER_TEMP/mbx-bin" - tar -xzf "$archive" -C "$RUNNER_TEMP/mbx-bin" - "$RUNNER_TEMP/mbx-bin/mbx" --version - XDG_DATA_HOME="$RUNNER_TEMP/mbx-data" "$RUNNER_TEMP/mbx-bin/mbx" setup --yes - echo "$RUNNER_TEMP/mbx-bin" >> "$GITHUB_PATH" - echo "$RUNNER_TEMP/mbx-data/mbx/bin" >> "$GITHUB_PATH" - echo "key=rust-target-${RUNNER_OS}-${RUNNER_ARCH}-$(rustc -vV | sha256sum | cut -d ' ' -f 1)" >> "$GITHUB_OUTPUT" - - uses: actions/cache/restore@v6 - id: cargo-cache + persist-credentials: false + - uses: ./.github/actions/ci-setup + id: setup with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'third-party/termina/Cargo.lock') }} - restore-keys: cargo-${{ runner.os }}- - - name: Restore compiled Rust cache - id: rust-cache - uses: actions/cache/restore@v6 + role: root + dependencies: headless + javascript: "true" + - uses: actions/download-artifact@v8 with: - path: target - key: ${{ steps.mbx.outputs.key }}-${{ hashFiles('Cargo.lock', 'third-party/termina/Cargo.lock') }}-${{ github.sha }} - restore-keys: | - ${{ steps.mbx.outputs.key }}-${{ hashFiles('Cargo.lock', 'third-party/termina/Cargo.lock') }}- - ${{ steps.mbx.outputs.key }}- - - name: Select npm - run: npm install --global --no-audit --no-fund npm@latest - - run: just setup - - name: Verify Rust dependency inventory before compilation - run: | - node scripts/rust/dependencies.mjs generate - sha256sum products/maka/cli/DEPENDENCIES.rust.tsv - git diff --exit-code -- products/maka/cli/DEPENDENCIES.rust.tsv - - name: Compile Computer Use bridge and tests - run: | - node scripts/rust/check-cua-build.mjs - cargo build --locked -p maka-cli --no-default-features - # Later feature builds replace target/debug/maka. - cp target/debug/maka "$RUNNER_TEMP/maka-bridge" - cargo nextest run --locked -p maka-computer-use --no-default-features --lib --test integration --no-run - - name: Check Termina Windows input - run: | - rustup target add x86_64-pc-windows-msvc - cargo check --locked --manifest-path third-party/termina/Cargo.toml --lib --target x86_64-pc-windows-msvc --target-dir "${CARGO_TARGET_DIR:-target}" - cargo check --locked --manifest-path third-party/termina/Cargo.toml --lib --target x86_64-pc-windows-msvc --target-dir "${CARGO_TARGET_DIR:-target}" --features windows-legacy - - name: Compile workspace tests - run: cargo nextest run --locked --workspace --no-run - env: - NEXTEST_PROFILE: ci - - name: Compile workspace Clippy targets - run: cargo clippy --config 'build.warnings="deny"' --locked --workspace --all-targets - - name: Compile desktop conversation tests - run: cargo nextest run --locked -p maka-cli --no-default-features --features desktop-tests --test integration -E 'test(terminal::desktop_new_session)' --no-run - - name: Compile Termina tests - run: cargo nextest run --locked --manifest-path third-party/termina/Cargo.toml --lib --target-dir "${CARGO_TARGET_DIR:-target}" --no-run - - run: just build - # Publish complete compiler outputs before any behavior tests can fail. - - name: Save compiled Rust cache - if: (github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rust-preview'))) && steps.rust-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v6 + artifact-ids: ${{ needs.plan.outputs.artifact }} + path: ${{ runner.temp }} + merge-multiple: true + - name: Build and archive selected tests + id: archive + run: node scripts/ci.mjs archive + - uses: actions/upload-artifact@v7 + id: upload + if: steps.archive.outputs.has_tests == 'true' with: - path: target - key: ${{ steps.rust-cache.outputs.cache-primary-key }} - - name: Save Cargo downloads - if: (github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rust-preview'))) && steps.cargo-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v6 + name: root-tests-${{ github.run_attempt }} + path: ${{ runner.temp }}/root-tests.tar.zst + compression-level: 0 + if-no-files-found: error + - uses: ./.github/actions/ci-cache with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: ${{ steps.cargo-cache.outputs.cache-primary-key }} - - name: Native CLI smoke - run: | - target/debug/maka --version - target/debug/maka desktop --version - target/debug/maka-desktop-shell --version - - name: Verify Computer Use bridge - run: | - "$RUNNER_TEMP/maka-bridge" --version - cargo nextest run --locked -p maka-computer-use --no-default-features --lib --test integration - - name: Verify model search keyboard selection before full checks - shell: bash - env: - NEXTEST_PROFILE: ci - run: | - filter='package(=maka-cli) & binary(=integration) & test(=terminal::keyboard::connection_filter_preserves_model_search_focus_and_keyboard_selection)' - cargo nextest list --locked --workspace -E "$filter" --message-format json > "$RUNNER_TEMP/model-search-tests.json" - node --input-type=module - "$RUNNER_TEMP/model-search-tests.json" <<'NODE' - import assert from 'node:assert/strict'; - import { readFileSync } from 'node:fs'; - const listing = JSON.parse(readFileSync(process.argv[2], 'utf8')); - const matches = Object.values(listing['rust-suites']).flatMap(suite => - Object.entries(suite.testcases).filter(([, test]) => test['filter-match'].status === 'matches') - .map(([name, test]) => [suite['package-name'], suite['binary-name'], name, test.ignored])); - assert.deepEqual(matches, [['maka-cli', 'integration', - 'terminal::keyboard::connection_filter_preserves_model_search_focus_and_keyboard_selection', false]]); - NODE - cargo nextest run --locked --workspace -E "$filter" - - name: Report storage before checks + compiled_key: ${{ steps.setup.outputs.compiled_key }} + compiled_hit: ${{ steps.setup.outputs.compiled_hit }} + compiled_writer: ${{ steps.setup.outputs.compiled_writer }} + downloads_key: ${{ steps.setup.outputs.downloads_key }} + downloads_hit: ${{ steps.setup.outputs.downloads_hit }} + downloads_writer: ${{ steps.setup.outputs.downloads_writer }} + - name: Report producer storage + if: always() run: | df -h "$GITHUB_WORKSPACE" "$RUNNER_TEMP" - for path in "$GITHUB_WORKSPACE/target" "$HOME/.cache/mbx" "$HOME/.cargo/registry" "$HOME/.cargo/git"; do - if [ -d "$path" ]; then du -sh "$path"; fi - done - - run: just check - env: - NEXTEST_PROFILE: ci - - name: Desktop conversation lifecycle - run: cargo nextest run --locked -p maka-cli --no-default-features --features desktop-tests --test integration -E 'test(terminal::desktop_new_session)' -j 1 - - name: Report storage after checks - if: ${{ always() && steps.mbx.outcome == 'success' }} - run: | - df -h "$GITHUB_WORKSPACE" "$RUNNER_TEMP" - for path in "$GITHUB_WORKSPACE/target" "$HOME/.cache/mbx" "$HOME/.cargo/registry" "$HOME/.cargo/git"; do - if [ -d "$path" ]; then du -sh "$path"; fi - done + if [ -d target ]; then du -sh target; fi - # Root workspace tests compile NSH dependencies but do not select the nested - # workspace's own tests. Run them serially on a fresh runner when dispatched. - nsh-integration: - needs: test - if: ${{ always() && github.event_name == 'workflow_dispatch' && !cancelled() }} + root-tests: + needs: root-build + if: needs.root-build.outputs.has_tests == 'true' runs-on: ubuntu-latest - timeout-minutes: 60 - env: - CARGO_BUILD_JOBS: "2" - CARGO_INCREMENTAL: "0" - CARGO_PROFILE_DEV_DEBUG: "0" - CARGO_PROFILE_TEST_DEBUG: "0" + timeout-minutes: 180 + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.root-build.outputs.partitions) }} steps: - uses: actions/checkout@v7 with: persist-credentials: false - - name: Verify source revision - env: - EXPECTED_SHA: ${{ github.sha }} - run: | - actual=$(git rev-parse HEAD) - echo "NSH validation source: $actual" - test "$actual" = "$EXPECTED_SHA" - echo "CARGO_TARGET_DIR=$RUNNER_TEMP/nsh-target" >> "$GITHUB_ENV" - - name: Update Rust - id: rust - run: | - rustup update stable --no-self-update - echo "key=nsh-target-${RUNNER_OS}-${RUNNER_ARCH}-$(rustc -vV | sha256sum | cut -d ' ' -f 1)" >> "$GITHUB_OUTPUT" - - uses: actions/cache@v6 + - uses: ./.github/actions/ci-setup with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: nsh-cargo-${{ runner.os }}-${{ hashFiles('third-party/nsh/Cargo.lock', 'third-party/nsh/crates/gpui/Cargo.lock') }} - restore-keys: nsh-cargo-${{ runner.os }}- - - name: Restore NSH compiled cache - id: nsh-cache - uses: actions/cache/restore@v6 + role: replay + dependencies: headless + javascript: "true" + compile: "false" + nextest: ${{ needs.root-build.outputs.nextest }} + - uses: actions/download-artifact@v8 with: - path: ${{ env.CARGO_TARGET_DIR }} - key: ${{ steps.rust.outputs.key }}-${{ hashFiles('third-party/nsh/Cargo.lock', 'third-party/nsh/crates/gpui/Cargo.lock') }}-${{ github.sha }} - restore-keys: | - ${{ steps.rust.outputs.key }}-${{ hashFiles('third-party/nsh/Cargo.lock', 'third-party/nsh/crates/gpui/Cargo.lock') }}- - ${{ steps.rust.outputs.key }}- - - uses: taiki-e/install-action@68a389c788b2cd04016ce8c80ee23b80d2ab3d68 # v2.88.1 + artifact-ids: ${{ needs.root-build.outputs.artifact }} + path: ${{ runner.temp }} + merge-multiple: true + - name: Execute the produced test partition + run: node scripts/ci.mjs replay '${{ matrix.partition }}' + + suites: + needs: plan + if: needs.plan.outputs.workers != '[]' + name: ${{ matrix.id }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 180 + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.plan.outputs.workers) }} + steps: + - uses: actions/checkout@v7 with: - tool: cargo-binstall - - name: Install latest nextest - env: - GH_TOKEN: ${{ github.token }} - run: | - cargo binstall --no-confirm --force cargo-binstall - cargo binstall --no-confirm --force cargo-nextest - - name: Test NSH core, JS, headless, terminal and development tooling - run: | - df -h "$RUNNER_TEMP" - df -i "$RUNNER_TEMP" - available=$(df -B1 --output=avail "$RUNNER_TEMP" | tail -n 1 | tr -d ' ') - test "$available" -ge 8589934592 || { echo "NSH tests require 8 GiB free before compilation" >&2; exit 1; } - args=(--locked --manifest-path third-party/nsh/Cargo.toml - -p nsh-core -p nsh-base -p nsh-engine -p nsh-js - -p nsh-testing -p nsh-platform-tui -p nsh-dev -p nsh-bundler - --features nsh-testing/agent,nsh-dev/client,nsh-dev/server) - cargo nextest run "${args[@]}" - cargo test "${args[@]}" --doc - - name: Test portable Web text layout and hit testing - run: | - available=$(df -B1 --output=avail "$RUNNER_TEMP" | tail -n 1 | tr -d ' ') - test "$available" -ge 4294967296 || { echo "Web text tests require 4 GiB free before compilation" >&2; exit 1; } - cargo nextest run --locked --manifest-path third-party/nsh/Cargo.toml \ - -p nsh-platform-web --no-default-features --test text - - name: Test Web shader lifecycle in the installed browser - shell: bash - run: | - chrome=$(command -v google-chrome || command -v chromium || command -v chromium-browser) || { - echo "A preinstalled Chrome/Chromium is required for Web shader lifecycle tests" >&2 - exit 1 - } - "$chrome" --version - node --test third-party/nsh/crates/nsh-platform-web/web/nsh.test.mjs - CHROME="$chrome" node --test --test-reporter=tap \ - third-party/nsh/crates/nsh-platform-web/web/shader-browser.test.mjs \ - | tee "$RUNNER_TEMP/nsh-shader-browser-tests.tap" - grep -Fx '# tests 2' "$RUNNER_TEMP/nsh-shader-browser-tests.tap" - grep -Fx '# pass 2' "$RUNNER_TEMP/nsh-shader-browser-tests.tap" - grep -Fx '# fail 0' "$RUNNER_TEMP/nsh-shader-browser-tests.tap" - grep -Fx '# skipped 0' "$RUNNER_TEMP/nsh-shader-browser-tests.tap" - # This owning workspace has a distinct lock/patch graph; root build - # artifacts are not assumed reusable. Selected tests open no native window. - - name: Check GPUI test build reserve - run: | - available=$(df -B1 --output=avail "$RUNNER_TEMP" | tail -n 1 | tr -d ' ') - inodes=$(df --output=iavail "$RUNNER_TEMP" | tail -n 1 | tr -d ' ') - test "$available" -ge 17179869184 || { echo "GPUI queue tests require 16 GiB free before setup" >&2; exit 1; } - test "$inodes" -ge 100000 || { echo "GPUI queue tests require 100000 free inodes" >&2; exit 1; } - - name: Install GPUI queue test native dependencies - timeout-minutes: 10 - run: | - sudo apt-get update - sudo apt-get install -y \ - clang libclang-dev build-essential pkg-config \ - libasound2-dev libfontconfig1-dev libfreetype6-dev libssl-dev \ - libvulkan1 mesa-vulkan-drivers libwayland-dev libx11-xcb-dev libxcb1-dev \ - libxkbcommon-dev libxkbcommon-x11-dev libzstd-dev - - name: Test GPUI input ownership, window reload and pixel transport - shell: bash - timeout-minutes: 30 - run: | - available=$(df -B1 --output=avail "$RUNNER_TEMP" | tail -n 1 | tr -d ' ') - inodes=$(df --output=iavail "$RUNNER_TEMP" | tail -n 1 | tr -d ' ') - test "$available" -ge 17179869184 || { echo "GPUI queue tests require 16 GiB free before compilation" >&2; exit 1; } - test "$inodes" -ge 100000 || { echo "GPUI queue tests require 100000 free inodes" >&2; exit 1; } - args=(--locked --manifest-path third-party/nsh/crates/gpui/Cargo.toml - -p nsh-platform-gpui -p nsh-gpui-terminal --no-default-features - --features nsh-platform-gpui/js,nsh-platform-gpui/shot - --lib) - cargo nextest run "${args[@]}" -j 1 -E ' - test(native::tests::animated_props_) | - test(=input::tests::nonpressable_focusable_views_join_native_stops_without_stealing_presses) | - test(=tests::windows) | - test(kitty::tests::)' - - name: Report NSH storage - if: ${{ always() }} + persist-credentials: false + - uses: ./.github/actions/ci-setup + id: setup + with: + role: ${{ matrix.id }} + workspace: ${{ matrix.workspace }} + dependencies: ${{ matrix.dependencies }} + javascript: ${{ matrix.javascript }} + compile: ${{ matrix.compile }} + - uses: actions/download-artifact@v8 + with: + artifact-ids: ${{ needs.plan.outputs.artifact }} + path: ${{ runner.temp }} + merge-multiple: true + - name: Compile selected suites + run: node scripts/ci.mjs prepare '${{ matrix.id }}' + - uses: ./.github/actions/ci-cache + with: + compiled_key: ${{ steps.setup.outputs.compiled_key }} + compiled_hit: ${{ steps.setup.outputs.compiled_hit }} + compiled_writer: ${{ steps.setup.outputs.compiled_writer }} + downloads_key: ${{ steps.setup.outputs.downloads_key }} + downloads_hit: ${{ steps.setup.outputs.downloads_hit }} + downloads_writer: ${{ steps.setup.outputs.downloads_writer }} + - name: Verify selected suites + run: node scripts/ci.mjs verify '${{ matrix.id }}' + - name: Report worker storage + if: always() run: | - df -h "$RUNNER_TEMP" - df -i "$RUNNER_TEMP" - if [ -d "$CARGO_TARGET_DIR" ]; then du -sh "$CARGO_TARGET_DIR"; fi - - name: Save NSH compiled cache - if: ${{ !cancelled() && steps.nsh-cache.outcome == 'success' && steps.nsh-cache.outputs.cache-hit != 'true' }} - uses: actions/cache/save@v6 + df -h "$GITHUB_WORKSPACE" "$RUNNER_TEMP" + if [ -d target ]; then du -sh target; fi + + test: + needs: [plan, tooling, root-build, root-tests, suites] + if: always() + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: actions/setup-node@v7 + with: + node-version: latest + - name: Require a successful plan + if: needs.plan.result != 'success' + run: node -e 'throw new Error("CI planning did not succeed")' + - uses: actions/download-artifact@v8 + if: needs.plan.result == 'success' with: - path: ${{ env.CARGO_TARGET_DIR }} - key: ${{ steps.nsh-cache.outputs.cache-primary-key }} + artifact-ids: ${{ needs.plan.outputs.artifact }} + path: ${{ runner.temp }} + merge-multiple: true + - name: Require all planned work to succeed + if: needs.plan.result == 'success' + env: + MAKA_CI_RESULTS: ${{ toJSON(needs) }} + MAKA_CI_HAS_TESTS: ${{ needs.root-build.outputs.has_tests }} + run: node scripts/ci.mjs gate diff --git a/.github/workflows/computer-use.yml b/.github/workflows/computer-use.yml deleted file mode 100644 index 10150b348ba..00000000000 --- a/.github/workflows/computer-use.yml +++ /dev/null @@ -1,75 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -name: Computer Use - -on: - pull_request: - branches: [main, rust-preview] - paths: - - .github/workflows/computer-use.yml - - Cargo.toml - - Cargo.lock - - platform/computer-use/** - push: - branches: [rust-preview] - paths: - - .github/workflows/computer-use.yml - - Cargo.toml - - Cargo.lock - - platform/computer-use/** - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: computer-use-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - check: - runs-on: ${{ matrix.runner }} - timeout-minutes: 45 - strategy: - fail-fast: false - matrix: - runner: [ubuntu-latest, macos-latest, windows-latest] - env: - CARGO_BUILD_JOBS: "2" - CARGO_INCREMENTAL: "0" - CARGO_PROFILE_DEV_DEBUG: "0" - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - name: Update Rust - run: rustup update stable --no-self-update - - uses: actions/cache@v6 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'third-party/termina/Cargo.lock') }} - restore-keys: cargo-${{ runner.os }}- - - name: Install Linux build dependencies - if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install -y clang libclang-dev pkg-config libx11-dev libxi-dev libxtst-dev libxext-dev libxrandr-dev libxfixes-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev libfreetype6-dev libssl-dev - - name: Check native computer-use library - run: cargo check --locked -p maka-computer-use --lib --features native-desktop diff --git a/AGENTS.md b/AGENTS.md index 73ba59ab6f8..7569044cb7e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,3 +35,10 @@ - 让布局本身说话。用结构、层级和交互表达意图,不堆砌 slogan、装饰和解释文案。 - 少写注释,只补充代码无法表达的关键约束、取舍或安全前提;随代码更新或删除过时注释。 - 将 `unsafe` 限制在最小边界,说明安全依据,并封装为安全接口。 + +## CI 维护 + +- 测试套件和执行入口统一维护在 [scripts/ci.mjs](scripts/ci.mjs) 及其模块中,`just` 和 workflow 复用这些定义;新增或移动套件时同步更新路由、归档资源及结果汇总。 +- 修改 SDK、插件、fixture、构建脚本或嵌入资源时,检查实际消费者并更新 Cargo 图之外的输入依赖。内置 skill 的 Markdown 等文件也是产品输入,不能仅按扩展名跳过。 +- 缩小测试编译范围前验证 workspace 的 feature 合并语义。修改归档时检查运行时读取的 `OUT_DIR` 资源及子进程可执行文件,并用独立 runner 上的现有行为测试验收。 +- 修改路由或分片时,验证受影响的传递依赖仍会执行;计划要求的任务被跳过、取消、失败或全量测试意外为空时,统一检查必须失败。 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8b1137755d0..d3e9c8aa4e9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,6 +39,8 @@ just check The [product guide](products/maka/README.md) explains TypeScript development. The [plugin guide](plugins/README.md) covers declarations and composition. Native packaging and V8 requirements live in [scripts/rust](scripts/rust/README.md). The [website](website/README.md) has a separate manifest and lockfile. +`just` and CI share [suite definitions and commands](scripts/ci.mjs). See the [CI commands](scripts/rust/README.md#ci) to inspect an affected-change plan or reproduce a suite locally. + ## Code and documentation - Use `name.rs` with `name/` for child modules. Keep unit tests with their implementation and integration tests under `tests/`. diff --git a/CONTRIBUTING.zh-CN.md b/CONTRIBUTING.zh-CN.md index 8833cb5dcfe..f743cd7e4d2 100644 --- a/CONTRIBUTING.zh-CN.md +++ b/CONTRIBUTING.zh-CN.md @@ -39,6 +39,8 @@ just check [产品指南](products/maka/README.zh-CN.md)说明 TypeScript 开发方式,[插件指南](plugins/README.zh-CN.md)介绍声明与组合。原生打包和 V8 要求见 [scripts/rust](scripts/rust/README.zh-CN.md)。[官网](website/README.md)使用独立的 manifest 和锁文件。 +`just` 与 CI 共用[套件定义和执行命令](scripts/ci.mjs)。通过 [CI 命令](scripts/rust/README.zh-CN.md#ci)查看受影响范围的计划或在本地复现套件。 + ## 代码与文档 - 使用 `name.rs` 和同名 `name/` 目录组织子模块。单元测试与实现放在一起,集成测试位于 `tests/`。 diff --git a/justfile b/justfile index 0afa90db8b1..9132f9d59ce 100644 --- a/justfile +++ b/justfile @@ -54,45 +54,40 @@ fmt: setup # Verify formatting without rewriting files. [group('Checks')] fmt-check: setup - cargo fmt --all -- --check - node node_modules/@biomejs/biome/bin/biome format . + node scripts/ci.mjs check format # Run strict Rust and JavaScript lint checks. [group('Checks')] lint: setup - cargo clippy --config 'build.warnings="deny"' --locked --workspace --all-targets - node node_modules/@biomejs/biome/bin/biome lint . + node scripts/ci.mjs run clippy + node scripts/ci.mjs check lint-js # Verify generated contracts, TypeScript consumers and the workbench UI sources. [group('Checks')] typecheck: setup - npm --workspace @maka-agent/plugin-sdk run schema:check - npm --workspace @maka-agent/plugin-sdk run typecheck - npm --workspace @maka-agent/product run typecheck - dir="$(mktemp -d)/ui-types" && cargo run --locked -q -p maka-workbench --example ui_types -- "$dir" && node node_modules/typescript/bin/tsc -p "$dir/tsconfig.json" + node scripts/ci.mjs run contracts # Run Rust behavior tests; pass nextest filters after the recipe name. [group('Checks')] test *args: setup - cargo nextest run --locked --workspace "$@" + node scripts/ci.mjs run root "$@" # Check the vendored terminal input library with the shared Cargo artifacts. [group('Checks')] test-termina: - cargo nextest run --profile default --locked --manifest-path third-party/termina/Cargo.toml --lib --target-dir "${CARGO_TARGET_DIR:-target}" + node scripts/ci.mjs run termina # Check tooling and the exported product; native CLI composition is a Rust integration test. [group('Checks')] test-js: setup - node --test scripts/*.test.mjs scripts/rust/*.test.mjs products/maka/workbench/tests/*.test.mjs packages/cli/tests/*.test.mjs - npm --workspace @maka-agent/product test + node scripts/ci.mjs check tooling-tests + node scripts/ci.mjs run product # Verify source headers and the reviewed dependency and notice snapshots. [group('Checks')] licenses: setup - node scripts/asf-license-headers.mjs check - node scripts/rust/dependencies.mjs check - node scripts/rust/notices.mjs check + node scripts/ci.mjs check licenses + node scripts/ci.mjs check notices # Build a source-bound native npm package without publishing. [group('Release')] diff --git a/plugins/workspace/workspace-computer/native/src/sessions.rs b/plugins/workspace/workspace-computer/native/src/sessions.rs index 0d149afda50..c628694dcfd 100644 --- a/plugins/workspace/workspace-computer/native/src/sessions.rs +++ b/plugins/workspace/workspace-computer/native/src/sessions.rs @@ -786,14 +786,14 @@ impl Interaction { "Computer epoch retirement worker panicked".into(), )) }); - if result.is_ok() { - if let Some(entry) = entry.upgrade() { - entry - .epochs - .lock() - .unwrap() - .retain(|current| !Arc::ptr_eq(current, &retired)); - } + if result.is_ok() + && let Some(entry) = entry.upgrade() + { + entry + .epochs + .lock() + .unwrap() + .retain(|current| !Arc::ptr_eq(current, &retired)); } ticket.complete(result.clone().map_err(message)); result diff --git a/products/maka/cli/THIRD_PARTY_NOTICES.txt b/products/maka/cli/THIRD_PARTY_NOTICES.txt index 32205ed19cb..a239d989586 100644 --- a/products/maka/cli/THIRD_PARTY_NOTICES.txt +++ b/products/maka/cli/THIRD_PARTY_NOTICES.txt @@ -9936,7 +9936,7 @@ Apache License clipboard-rs@0.3.5 (MIT); authors declared by package: ChurchTao -git+https://github.com/maka-agent/cua?rev=cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4#cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4 :: LICENSE +git+https://github.com/maka-agent/cua?rev=7dfc429ba3db0568c46c49f633b724750f34f451#7dfc429ba3db0568c46c49f633b724750f34f451 :: LICENSE https://crates.io/api/v1/crates/clipboard-rs/0.3.5/download :: LICENSE MIT License @@ -9977,7 +9977,7 @@ platform-windows@0.38.0 (MIT) zbus@5.19.0 (MIT); authors declared by package: Zeeshan Ali Khan git+https://github.com/maka-agent/cua?rev=23221967ce6044c1fed303701f43fa54564f0f53#23221967ce6044c1fed303701f43fa54564f0f53 -git+https://github.com/maka-agent/cua?rev=cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4#cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4 +git+https://github.com/maka-agent/cua?rev=7dfc429ba3db0568c46c49f633b724750f34f451#7dfc429ba3db0568c46c49f633b724750f34f451 Cua Driver Rust SDK and its workspace crates Source: https://github.com/trycua/cua @@ -13312,7 +13312,7 @@ DEALINGS IN THE SOFTWARE. cua-cursor-motion@0.38.0 (MIT); authors declared by package: trycua -git+https://github.com/maka-agent/cua?rev=cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4#cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4 :: LICENSE +git+https://github.com/maka-agent/cua?rev=7dfc429ba3db0568c46c49f633b724750f34f451#7dfc429ba3db0568c46c49f633b724750f34f451 :: LICENSE MIT License @@ -13340,7 +13340,7 @@ SOFTWARE. cua-driver-core@0.38.0 (MIT AND Apache-2.0) -git+https://github.com/maka-agent/cua?rev=cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4#cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4 :: src/browser/extension_assets/maka-ui/LICENSE +git+https://github.com/maka-agent/cua?rev=7dfc429ba3db0568c46c49f633b724750f34f451#7dfc429ba3db0568c46c49f633b724750f34f451 :: src/browser/extension_assets/maka-ui/LICENSE Apache License Version 2.0, January 2004 @@ -13714,7 +13714,7 @@ third-party/gix-discover/{LICENSE-APACHE,LICENSE-MIT,upstream.json,SOURCE.md}. cua-driver-core@0.38.0 (MIT AND Apache-2.0) -git+https://github.com/maka-agent/cua?rev=cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4#cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4 :: src/browser/extension_assets/maka-ui/NOTICE +git+https://github.com/maka-agent/cua?rev=7dfc429ba3db0568c46c49f633b724750f34f451#7dfc429ba3db0568c46c49f633b724750f34f451 :: src/browser/extension_assets/maka-ui/NOTICE Apache Maka (incubating) Copyright 2026 The Apache Software Foundation @@ -49499,7 +49499,7 @@ zbus_xml@5.2.1 (MIT); authors declared by package: Zeeshan Ali Khan zvariant_derive@5.15.1 (MIT); authors declared by package: Zeeshan Ali Khan -git+https://github.com/maka-agent/cua?rev=cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4#cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4 :: LICENSE +git+https://github.com/maka-agent/cua?rev=7dfc429ba3db0568c46c49f633b724750f34f451#7dfc429ba3db0568c46c49f633b724750f34f451 :: LICENSE https://crates.io/api/v1/crates/zbus_macros/5.19.0/download :: LICENSE https://crates.io/api/v1/crates/zbus_names/4.3.5/download :: LICENSE https://crates.io/api/v1/crates/zbus_xml/5.2.1/download :: LICENSE diff --git a/products/maka/cli/notices.json b/products/maka/cli/notices.json index 08a60881728..cd8e2957488 100644 --- a/products/maka/cli/notices.json +++ b/products/maka/cli/notices.json @@ -160,7 +160,7 @@ "cargo:clap_derive@4.6.7 registry+https://github.com/rust-lang/crates.io-index": "82bcc8e413b68e02feff24a3bfa933a3ec902056d5bec77c37a304403297a6cd", "cargo:clap_lex@1.1.1 registry+https://github.com/rust-lang/crates.io-index": "663bce254470eb5563d5b41d8a8b2eceb366748b2784728fe4135b7c0cd4a3dd", "cargo:clap@4.6.7 registry+https://github.com/rust-lang/crates.io-index": "059622eb0e6a6598829c284732c57f80f934f7c818105e7fbacc32306b915823", - "cargo:clipboard-rs@0.3.5 git+https://github.com/maka-agent/cua?rev=cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4#cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4": "15fe46027e37fb314707ed2883c76bfbeae02ebc1af54189264f1aa3f8265564", + "cargo:clipboard-rs@0.3.5 git+https://github.com/maka-agent/cua?rev=7dfc429ba3db0568c46c49f633b724750f34f451#7dfc429ba3db0568c46c49f633b724750f34f451": "0777534e5c0a695c4f159dfcde2003cc6ad6a45c68e6f8e0953d6624030dc7ae", "cargo:clipboard-rs@0.3.5 registry+https://github.com/rust-lang/crates.io-index": "98c2e0d060061cafbb39572bd6c34478ff6fb4c1551b1ac0e894af320ae15ba0", "cargo:clipboard-win@5.4.3 registry+https://github.com/rust-lang/crates.io-index": "47b2bab5712640068cfcbc6166657ecea1a372a43ba2babc660df276011f100d", "cargo:clru@0.6.3 registry+https://github.com/rust-lang/crates.io-index": "e4aa5a19a56dc2922d7bc263a313358f421d2bc5f6943c1cd71cf6cac27b828d", @@ -223,12 +223,12 @@ "cargo:ctor@1.0.13 registry+https://github.com/rust-lang/crates.io-index": "9e1b74aea82adde884c8ea2c566d026f29b166ea0285ac274c729748a674d9f4", "cargo:ctr@0.10.1 registry+https://github.com/rust-lang/crates.io-index": "868d4fbb66e312d0e963f53c6a41cae4a1a9c86592a5a8248875801fee021827", "cargo:ctutils@0.4.5 registry+https://github.com/rust-lang/crates.io-index": "c1fcb338b2eb8b06eb9ae94b40dfc6193cf1faad308c27684aa606e710ece89d", - "cargo:cua-cursor-motion@0.38.0 git+https://github.com/maka-agent/cua?rev=cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4#cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4": "72c26628705a402a910156f76936200bbc687a9d36e32909266a70af5780c470", - "cargo:cua-driver-contract@0.38.0 git+https://github.com/maka-agent/cua?rev=cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4#cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4": "aafdbfc9f93d1fbffb62e4ffe053916c7872a6e6752f1a9d7ec74a9cb5c177a5", - "cargo:cua-driver-core@0.38.0 git+https://github.com/maka-agent/cua?rev=cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4#cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4": "7720498e66afcf3e3acdb500aaf5340e9ac3ae4835497cad6f2d8ab17a101fb9", - "cargo:cua-driver-sdk@0.38.0 git+https://github.com/maka-agent/cua?rev=cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4#cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4": "000ff0c6756c31eb31048e62f9027ea03736c80e851728877c21e8d41270e272", + "cargo:cua-cursor-motion@0.38.0 git+https://github.com/maka-agent/cua?rev=7dfc429ba3db0568c46c49f633b724750f34f451#7dfc429ba3db0568c46c49f633b724750f34f451": "2cf24babd1edbd5ca5c436f3863b37792f6f81cd2d22bcca4789a52c870e2ce3", + "cargo:cua-driver-contract@0.38.0 git+https://github.com/maka-agent/cua?rev=7dfc429ba3db0568c46c49f633b724750f34f451#7dfc429ba3db0568c46c49f633b724750f34f451": "909ef219884cc1d967f2017cb99a1c4b3edd50455bfb5598e4f98c62d942036f", + "cargo:cua-driver-core@0.38.0 git+https://github.com/maka-agent/cua?rev=7dfc429ba3db0568c46c49f633b724750f34f451#7dfc429ba3db0568c46c49f633b724750f34f451": "496c94ec7dbf8ec1b9249ec51e03eea5dbfb03a27cd9b678df5ffd71de58ce01", + "cargo:cua-driver-sdk@0.38.0 git+https://github.com/maka-agent/cua?rev=7dfc429ba3db0568c46c49f633b724750f34f451#7dfc429ba3db0568c46c49f633b724750f34f451": "8594c19123ab2a98debabfbd75e1ec0c9df6f9a264c1d007bf4c77bc02a95e74", "cargo:cursor-icon@1.2.0 registry+https://github.com/rust-lang/crates.io-index": "3ef9b5fb0958ea9a4e1a2d5e9a9d2cb0b16453099e548cbe72ce61bcfd57d347", - "cargo:cursor-overlay@0.38.0 git+https://github.com/maka-agent/cua?rev=cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4#cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4": "95a62b90a86645f7167f92aacd0501d2b6395dbbe4c66f7cf833410db2209324", + "cargo:cursor-overlay@0.38.0 git+https://github.com/maka-agent/cua?rev=7dfc429ba3db0568c46c49f633b724750f34f451#7dfc429ba3db0568c46c49f633b724750f34f451": "17aaf4f5c02da92f83b804e79e8bbb20c5d7224bc549808543ad7fd2cdb15eba", "cargo:curve25519-dalek-derive@0.1.1 registry+https://github.com/rust-lang/crates.io-index": "774e4011c51c5d15fe2618dab4d757e8aa085a8e494565187ad3906ea1f13dbc", "cargo:curve25519-dalek@4.1.3 registry+https://github.com/rust-lang/crates.io-index": "b5988ec3ba7c3f3880c9f67772043cc4eacd9481933ac85ee5bfa59c6db53b4d", "cargo:curve25519-dalek@5.0.0 registry+https://github.com/rust-lang/crates.io-index": "587766614d5a2d7a3d8eeb449f8fe31f58f466817f058edfdf88395c973bba6e", @@ -797,16 +797,16 @@ "cargo:pin-project-internal@1.1.13 registry+https://github.com/rust-lang/crates.io-index": "b182cd7f5ade8cef38090afd2f1d854d3e6e22e08daa709b14a2c9897b03c0b9", "cargo:pin-project-lite@0.2.17 registry+https://github.com/rust-lang/crates.io-index": "189daaf976e3191a47e7177d12bd0c1bc3ae50958cdfdcc7ba0040f6927a887b", "cargo:pin-project@1.1.13 registry+https://github.com/rust-lang/crates.io-index": "3b1bda4e0e723ee3bfe073a4666802f83532afc1cc4c9221a8c41a4854bed4be", - "cargo:pip-preview@0.38.0 git+https://github.com/maka-agent/cua?rev=cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4#cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4": "0280109bf8cc727dea350c470013020c8a584c628b995d82fe751778ea69e528", + "cargo:pip-preview@0.38.0 git+https://github.com/maka-agent/cua?rev=7dfc429ba3db0568c46c49f633b724750f34f451#7dfc429ba3db0568c46c49f633b724750f34f451": "ca6e496dbb20080a532738b0d6ec21c1bdf46b0a6f7bf86cf96d7a255892db07", "cargo:piper@0.2.5 registry+https://github.com/rust-lang/crates.io-index": "50832f7de6299d6c286c168a2d9fcb74833905dbd7bd4ca598d0e8cd83d62af2", "cargo:pkcs1@0.8.0-rc.5 registry+https://github.com/rust-lang/crates.io-index": "449410f98f9b8d7fb4956a308c024c65eef67102502d4d22f6c7d552fc482a28", "cargo:pkcs8@0.10.2 registry+https://github.com/rust-lang/crates.io-index": "bd951867b4c1c4e09877bda17b94468d1fa88a2ebefd6fe6dabc9e02b759f6b1", "cargo:pkcs8@0.11.0 registry+https://github.com/rust-lang/crates.io-index": "8a22cc1f02b22922a8404686ed989afbfba22b1132cfb676c1913cda6717094c", "cargo:pkg-config@0.3.34 registry+https://github.com/rust-lang/crates.io-index": "bf627c9285d9e1d3e269fdb71b6281ff897f769a841a68ff4f10375c82dcc9ac", "cargo:plain@0.2.3 registry+https://github.com/rust-lang/crates.io-index": "bdfe6d39fcbd7d6d2b3c5ee35bd14bf87083a68af033bc8c9aa1f1b52c7b29e9", - "cargo:platform-linux@0.38.0 git+https://github.com/maka-agent/cua?rev=cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4#cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4": "4bb9c41e9c7957a55c9edd7411b0512124b90d671af14172ad23f196cab36c79", - "cargo:platform-macos@0.38.0 git+https://github.com/maka-agent/cua?rev=cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4#cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4": "45f80337095147eb8e7273bbaa1f004f8ff8fb3f69bd72055401f10c7c59e3dd", - "cargo:platform-windows@0.38.0 git+https://github.com/maka-agent/cua?rev=cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4#cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4": "d94358d789dc5be09348b47640454d2372ebad0a2330559e345c1b59a4bd6c17", + "cargo:platform-linux@0.38.0 git+https://github.com/maka-agent/cua?rev=7dfc429ba3db0568c46c49f633b724750f34f451#7dfc429ba3db0568c46c49f633b724750f34f451": "00dd07a00adb72e0ba3877fb5ba58f32636648debb5443e96edfb40e6e041ac0", + "cargo:platform-macos@0.38.0 git+https://github.com/maka-agent/cua?rev=7dfc429ba3db0568c46c49f633b724750f34f451#7dfc429ba3db0568c46c49f633b724750f34f451": "3f4178bb5ab7c895273b1ea136d4c9ecf9e10bafd87346678cda812caa017f9d", + "cargo:platform-windows@0.38.0 git+https://github.com/maka-agent/cua?rev=7dfc429ba3db0568c46c49f633b724750f34f451#7dfc429ba3db0568c46c49f633b724750f34f451": "9a7212adfe6203fe1a34af55a03d835eff6e0642773cacfd651bf646b9865ae4", "cargo:png@0.17.16 registry+https://github.com/rust-lang/crates.io-index": "053e487749a89ba4c8017768d3ebd513b68040c46fb6c1666d000c5225fa40c0", "cargo:png@0.18.1 registry+https://github.com/rust-lang/crates.io-index": "75d96228a3f3ee3ec271b861d405d59782a9395d8cfe810cfd39d9354ad823a5", "cargo:pnp@0.12.12 registry+https://github.com/rust-lang/crates.io-index": "31a84e8c472f391cc7f39effec9b13e7c9f7ad7eb26c7036246a403872dbc70c", @@ -1330,7 +1330,7 @@ "cargo:zbus-lockstep-macros@0.5.2 registry+https://github.com/rust-lang/crates.io-index": "0461205f6fee51a73e7db5a62792447d5375b3e8b23559109eb5ab110ee6f958", "cargo:zbus-lockstep@0.5.2 registry+https://github.com/rust-lang/crates.io-index": "eb5aded81eca8aa9895b225e62c8643d1f3ddbc82d34b670ee92911e1bf70b87", "cargo:zbus-secret-service-keyring-store@1.0.1 registry+https://github.com/rust-lang/crates.io-index": "0166e6b70711931ce989999551870c7644b7db7283abffdbf76c85effa49aec6", - "cargo:zbus@5.19.0 git+https://github.com/maka-agent/cua?rev=cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4#cda1e5c1c7375d7e428e17f3abc75c8cb4d97ae4": "d71fe121f3453b595a7a8198891d4edc971c242e81f2d0264dc36906bb6bd180", + "cargo:zbus@5.19.0 git+https://github.com/maka-agent/cua?rev=7dfc429ba3db0568c46c49f633b724750f34f451#7dfc429ba3db0568c46c49f633b724750f34f451": "d22e9257f8983b2f1856f2b697062a179d06bf12a004468b8b3a762dfc9cd319", "cargo:zcheapstr@1.1.0 registry+https://github.com/rust-lang/crates.io-index": "98fb51b15ba6b432e620ea35d68acb1450b4f9612c69bf686c6e9d1a951bba86", "cargo:zed-font-kit@0.14.1-zed registry+https://github.com/rust-lang/crates.io-index": "96b82424ebb4549bd327c5f4567dba58304c3a463c12edb928dfdc51d53e390f", "cargo:zed-scap@0.0.8-zed registry+https://github.com/rust-lang/crates.io-index": "e092fc2afc9b2f5bb74f6a756fa6fde426876fb7399d70234ec794139607efe0", @@ -1362,5 +1362,5 @@ "cargo:zvariant@5.15.1 registry+https://github.com/rust-lang/crates.io-index": "8606849c24d82d7d012980cba51a766abfe0307b48f936ec59e11b9a05b9c288", "npm:ajv@8.20.0 https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==": "e0b7c0627c7c460c99e8d7b8ac586e4c975ea2a2fae3859fdc94e67ec3aa69cc" }, - "sha256": "b63e66c871a0e660df55ffa5610b0f941f87c85809ab0f24b5739244602bf184" + "sha256": "a635b5166e036313c0cb34f643087be075af213f047ed7b13487c60c63d0dbfe" } diff --git a/scripts/ci.mjs b/scripts/ci.mjs new file mode 100644 index 00000000000..74afc69c3eb --- /dev/null +++ b/scripts/ci.mjs @@ -0,0 +1,180 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { join, resolve } from 'node:path'; +import { command } from './ci/command.mjs'; +import { checkResults, createPlan, documentation, sharedInput } from './ci/plan.mjs'; +import { manifests, suites, workers } from './ci/suites.mjs'; +import { executor } from './ci/run.mjs'; + +const root = fileURLToPath(new URL('../', import.meta.url)); +const temporary = process.env.RUNNER_TEMP ?? join(root, 'target/ci'); +const planFile = join(temporary, 'ci-plan.json'); + +function output(values) { + if (process.env.GITHUB_OUTPUT) + appendFileSync( + process.env.GITHUB_OUTPUT, + Object.entries(values) + .map( + ([key, value]) => `${key}=${typeof value === 'string' ? value : JSON.stringify(value)}\n`, + ) + .join(''), + ); +} + +export async function plan({ baseline = null, event = process.env.GITHUB_EVENT_NAME } = {}) { + const revision = ( + await command('git', ['rev-parse', 'HEAD'], { cwd: root, capture: true }) + ).trim(); + if (process.env.GITHUB_SHA && revision !== process.env.GITHUB_SHA) + throw new Error('Checkout differs from the workflow revision'); + if (event === 'pull_request') { + baseline = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8')).pull_request.base + .sha; + await command('git', ['fetch', '--no-tags', '--depth=1', 'origin', baseline], { cwd: root }); + } + const changes = + baseline === null + ? null + : ( + await command( + 'git', + ['diff', '--name-only', '--no-renames', '-z', baseline, revision, '--'], + { cwd: root, capture: true }, + ) + ) + .split('\0') + .filter(Boolean); + const metadata = {}; + if (changes && !changes.some(sharedInput) && changes.some((path) => !documentation(path))) { + for (const [workspace, manifest] of Object.entries(manifests)) { + metadata[workspace] = JSON.parse( + await command( + 'cargo', + [ + 'metadata', + '--locked', + '--format-version', + '1', + '--all-features', + '--manifest-path', + manifest, + ], + { cwd: root, capture: true }, + ), + ); + } + } + return createPlan({ root, changes, metadata, revision, baseline }); +} + +async function main(args) { + const [action, ...rest] = args; + if (action === 'plan') { + const baseline = rest.length ? rest[1] : null; + if (rest.length && (rest[0] !== '--base' || rest.length !== 2)) + throw new Error('Usage: ci.mjs plan [--base REVISION]'); + const result = await plan({ baseline }); + console.log(JSON.stringify(result, null, 2)); + if (process.env.GITHUB_OUTPUT) { + mkdirSync(temporary, { recursive: true }); + writeFileSync(planFile, JSON.stringify(result)); + output({ root: Boolean(result.suites.root), workers: result.workers }); + } + return; + } + if (action === 'check') { + await executor(root).check(rest[0]); + return; + } + if (action === 'run') { + const [id, ...extra] = rest; + if (!suites[id]) throw new Error(`Unknown suite: ${id}`); + const worker = workers.find((entry) => entry.suites.includes(id)); + const run = executor(root, worker); + await run.prepare(id, suites[id].filter ?? 'all()', extra); + await run.verify(id, suites[id].filter ?? 'all()', extra); + return; + } + if (action === 'gate') { + const results = JSON.parse(process.env.MAKA_CI_RESULTS); + if (results.plan?.result !== 'success') throw new Error('CI planning did not succeed'); + const selected = JSON.parse(readFileSync(planFile, 'utf8')); + const hasTests = process.env.MAKA_CI_HAS_TESTS; + checkResults( + selected, + results, + hasTests === '' || hasTests === undefined ? undefined : JSON.parse(hasTests), + ); + console.log('All planned CI work succeeded'); + return; + } + if (action === 'archive') { + const selected = JSON.parse(readFileSync(planFile, 'utf8')); + const partitions = await executor(root).archive( + selected, + join(temporary, 'root-tests.tar.zst'), + ); + const version = (await command('cargo', ['nextest', '--version'], { capture: true })) + .trim() + .split(/\s+/)[1]; + output({ partitions, has_tests: partitions.length > 0, nextest: version }); + return; + } + if (action === 'replay') { + mkdirSync(join(root, 'target/tmp'), { recursive: true }); + await command( + 'cargo', + [ + 'nextest', + 'run', + '--archive-file', + join(temporary, 'root-tests.tar.zst'), + '--extract-to', + root, + '--extract-overwrite', + '--profile', + 'ci', + '--partition', + rest[0], + ], + { cwd: root }, + ); + return; + } + if (action === 'prepare' || action === 'verify') { + const selected = JSON.parse(readFileSync(planFile, 'utf8')); + const worker = selected.workers.find((entry) => entry.id === rest[0]); + if (!worker) throw new Error(`Worker not selected: ${rest[0]}`); + const run = executor(root, worker); + for (const id of worker.suites) await run[action](id, selected.suites[id].filter); + return; + } + throw new Error('Usage: ci.mjs plan|run|check|prepare|verify|archive|replay|gate'); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + main(process.argv.slice(2)).catch((error) => { + console.error(error); + process.exitCode = 1; + }); +} diff --git a/scripts/ci.test.mjs b/scripts/ci.test.mjs new file mode 100644 index 00000000000..4babdc5304a --- /dev/null +++ b/scripts/ci.test.mjs @@ -0,0 +1,180 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { checkResults, createPlan } from './ci/plan.mjs'; + +const root = '/repo'; + +function workspace(packages) { + return { + packages: packages.map(([name, path, dependencies = []]) => ({ + id: path, + name, + manifest_path: `${root}/${path}/Cargo.toml`, + source: null, + dependencies, + })), + workspace_members: packages.map(([, path]) => path), + resolve: { + nodes: packages.map(([, path, dependencies = []]) => ({ + id: path, + deps: dependencies.map((pkg) => ({ pkg })), + })), + }, + }; +} + +function fixture() { + const metadata = { + root: workspace([ + ['maka-code-runtime', 'platform/code-runtime'], + ['maka-computer-use', 'platform/computer-use', ['platform/code-runtime']], + ['maka-skills-core', 'plugins/skills/core'], + ['nsh-core', 'third-party/nsh/crates/nsh-core'], + [ + 'maka-cli', + 'products/maka/cli', + ['platform/computer-use', 'plugins/skills/core', 'third-party/nsh/crates/nsh-core'], + ], + ['maka-schema', 'tools/schema'], + ['maka-workbench', 'products/maka/workbench'], + ['maka-ui-build', 'tools/ui-build'], + ]), + nsh: workspace([['nsh-core', 'third-party/nsh/crates/nsh-core']]), + gpui: workspace([ + ['nsh-platform-gpui', 'third-party/nsh/crates/gpui/nsh-platform-gpui', ['external-nsh']], + ]), + termina: workspace([['termina', 'third-party/termina']]), + }; + metadata.root.workspace_members = metadata.root.workspace_members.filter( + (id) => !id.startsWith('third-party/'), + ); + // The same path dependency has a different Cargo package ID in this graph. + metadata.gpui.packages.push({ + id: 'external-nsh', + name: 'nsh-core', + manifest_path: `${root}/third-party/nsh/crates/nsh-core/Cargo.toml`, + source: null, + }); + metadata.gpui.resolve.nodes.push({ id: 'external-nsh', deps: [] }); + return (changes) => createPlan({ root, changes, metadata, revision: 'head', baseline: 'base' }); +} + +test('a transitive Computer Use dependency selects all native OS checks and CLI consumers', () => { + const plan = fixture()(['platform/code-runtime/src/lib.rs']); + assert.deepEqual( + plan.workers.filter((worker) => worker.id.startsWith('native-')).map((worker) => worker.runner), + ['ubuntu-latest', 'macos-latest', 'windows-latest'], + ); + assert.equal( + plan.suites.root.filter, + 'package(=maka-cli) | package(=maka-code-runtime) | package(=maka-computer-use)', + ); + assert.ok(plan.suites.bridge); + assert.ok(plan.suites.desktop); + assert.equal(plan.suites.termina, undefined); +}); + +test('a path dependency affects consumers across owning workspaces', () => { + const plan = fixture()(['third-party/nsh/crates/nsh-core/src/lib.rs']); + assert.ok(plan.suites.nsh); + assert.ok(plan.suites.gpui); + assert.equal(plan.suites.root.filter, 'package(=maka-cli)'); + assert.equal(plan.full, false); +}); + +test('SDK runtime inputs select product consumers without a Cargo dependency edge', () => { + const plan = fixture()(['packages/plugin-sdk/src/runtime/client.ts']); + for (const id of ['root', 'bridge', 'desktop', 'clippy', 'contracts', 'product']) + assert.ok(plan.suites[id], id); + assert.equal(plan.suites.root.filter, 'all()'); + assert.equal(plan.suites.native, undefined); +}); + +test('ordinary Rust source inside a product directory retains package routing', () => { + const plan = fixture()(['products/maka/workbench/src/lib.rs']); + assert.equal(plan.suites.root.filter, 'package(=maka-workbench)'); + assert.ok(plan.suites.contracts); + assert.equal(plan.suites.product, undefined); +}); + +test('reviewed notices and distributed dependency licenses select the product verifier', () => { + const select = fixture(); + for (const path of [ + 'products/maka/cli/notices.json', + 'products/maka/cli/THIRD_PARTY_NOTICES.txt', + 'third-party/nsh/crates/nsh-core/LICENSE', + ]) { + const plan = select([path]); + assert.ok(plan.suites.product, path); + assert.equal(plan.full, false, path); + } +}); + +test('embedded Markdown remains an input even when it belongs to another Cargo package', () => { + const plan = fixture()(['platform/computer-use/skills/maka-cua/SKILL.md']); + assert.equal(plan.suites.root.filter, 'all()'); + assert.ok(plan.suites.bridge); + assert.ok(plan.suites.native); + assert.equal(plan.full, false); +}); + +test('documentation is cheap while shared and unknown source inputs request full validation', () => { + const select = fixture(); + const docs = select(['docs/guide.md', 'README.md']); + assert.deepEqual(Object.keys(docs.suites), ['tooling']); + assert.deepEqual(docs.workers, []); + for (const path of [ + '.cargo/config.toml', + 'third-party/nsh/Cargo.lock', + 'new-runtime/source.ts', + ]) { + const plan = select([path]); + assert.equal(plan.full, true, path); + assert.ok(plan.suites.root, path); + assert.ok(plan.suites.gpui, path); + } +}); + +test('the aggregate accepts only successful planned work, including every selected matrix', () => { + const full = createPlan({ root, revision: 'head' }); + const results = Object.fromEntries( + ['plan', 'tooling', 'suites', 'root-build', 'root-tests'].map((id) => [ + id, + { result: 'success' }, + ]), + ); + checkResults(full, results, true); + for (const id of Object.keys(results)) { + for (const result of ['failure', 'skipped', 'cancelled']) { + assert.throws( + () => checkResults(full, { ...results, [id]: { result } }, true), + /did not succeed/, + ); + } + } + assert.throws(() => checkResults(full, results), /did not report/); + assert.throws(() => checkResults(full, results, false), /unexpectedly empty/); + const narrow = fixture()(['tools/schema/src/main.rs']); + checkResults(narrow, { ...results, 'root-tests': { result: 'skipped' } }, false); + const docs = fixture()(['docs/guide.md']); + checkResults(docs, { plan: { result: 'success' }, tooling: { result: 'success' } }); +}); diff --git a/scripts/ci/command.mjs b/scripts/ci/command.mjs new file mode 100644 index 00000000000..836d2e041fc --- /dev/null +++ b/scripts/ci/command.mjs @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { spawn } from 'node:child_process'; + +export async function command(executable, args, { capture = false, ...options } = {}) { + console.error(`+ ${executable} ${args.join(' ')}`); + return await new Promise((resolve, reject) => { + const output = []; + const child = spawn(executable, args, { + stdio: ['inherit', capture ? 'pipe' : 'inherit', 'inherit'], + ...options, + }); + child.stdout?.on('data', (chunk) => output.push(chunk)); + child.on('error', reject); + child.on('close', (code, signal) => { + if (code === 0) resolve(Buffer.concat(output).toString('utf8')); + else reject(new Error(`${executable} failed: ${signal ?? code}`)); + }); + }); +} diff --git a/scripts/ci/plan.mjs b/scripts/ci/plan.mjs new file mode 100644 index 00000000000..5816d41a0cc --- /dev/null +++ b/scripts/ci/plan.mjs @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { dirname, relative, resolve } from 'node:path'; +import { suites, workers } from './suites.mjs'; + +export function contains(directory, path) { + return path === directory || path.startsWith(`${directory}/`); +} + +const shared = [ + '.cargo', + '.config', + '.github/workflows', + '.github/actions', + 'package.json', + 'package-lock.json', + 'justfile', + 'biome.jsonc', + 'scripts/ci.mjs', + 'scripts/ci', +]; + +export function sharedInput(path) { + return /(^|\/)Cargo\.(toml|lock)$/.test(path) || shared.some((entry) => contains(entry, path)); +} + +export function documentation(path) { + return ( + contains('docs', path) || + contains('website', path) || + (!path.includes('/') && (/\.md$/.test(path) || ['LICENSE', 'NOTICE'].includes(path))) + ); +} + +function graph(root, metadata) { + const packages = new Map(); + const reverse = new Map(); + const members = new Map(); + for (const [workspace, data] of Object.entries(metadata)) { + const identities = new Map( + data.packages.map((pkg) => [pkg.id, pkg.source ? pkg.id : resolve(pkg.manifest_path)]), + ); + members.set(workspace, new Set(data.workspace_members.map((id) => identities.get(id)))); + for (const pkg of data.packages) { + const path = relative(root, dirname(pkg.manifest_path)).split('\\').join('/'); + if (!pkg.source && path !== '..' && !path.startsWith('../')) { + packages.set(identities.get(pkg.id), { name: pkg.name, path }); + } + } + for (const node of data.resolve.nodes) { + const consumer = identities.get(node.id); + for (const dependency of node.deps) { + const key = identities.get(dependency.pkg); + if (!reverse.has(key)) reverse.set(key, new Set()); + reverse.get(key).add(consumer); + } + } + } + return { packages, reverse, members }; +} + +function expanded(seeds, reverse) { + const reached = new Set(seeds); + for (const key of reached) { + for (const consumer of reverse.get(key) ?? []) reached.add(consumer); + } + return reached; +} + +function suitePackage(suite, key, packages, members) { + const pkg = packages.get(key); + if (!pkg || !suite.workspace) return false; + return suite.packages === 'workspace' + ? members.get(suite.workspace).has(key) + : suite.packages.includes(pkg.name); +} + +export function createPlan({ root, changes = null, metadata = {}, revision, baseline = null }) { + const selected = {}; + const fullReasons = + changes === null ? ['full validation requested'] : changes.filter(sharedInput); + let full = fullReasons.length > 0; + const inputs = changes?.filter((path) => !documentation(path)) ?? []; + if (!full && inputs.length) { + const { packages, reverse, members } = graph(root, metadata); + const ownership = [...packages].sort((a, b) => b[1].path.length - a[1].path.length); + const owners = new Map( + inputs.map((path) => [path, ownership.find(([, pkg]) => contains(pkg.path, path))?.[0]]), + ); + const extraInput = (suite, path) => + !(owners.get(path) && path.endsWith('.rs')) && + suite.extraInputs?.some((entry) => contains(entry, path)); + const supported = new Set( + [...packages.keys()].filter((key) => + Object.values(suites).some((suite) => suitePackage(suite, key, packages, members)), + ), + ); + const seeds = new Set(); + for (const path of inputs) { + const owner = owners.get(path); + if (owner) seeds.add(owner); + const extra = Object.values(suites).some((suite) => extraInput(suite, path)); + if (!owner && !extra) fullReasons.push(`unmapped input: ${path}`); + if (owner && !extra && ![...expanded([owner], reverse)].some((key) => supported.has(key))) + fullReasons.push(`outside maintained suites: ${path}`); + } + full = fullReasons.length > 0; + if (!full) { + const reached = expanded(seeds, reverse); + for (const [id, suite] of Object.entries(suites)) { + const extras = inputs.filter((path) => extraInput(suite, path)); + const affected = [...reached] + .filter((key) => suitePackage(suite, key, packages, members)) + .map((key) => packages.get(key).name) + .sort(); + if (suite.always || extras.length || affected.length) { + selected[id] = { + filter: + suite.affectedFilter && !extras.length + ? affected.map((name) => `package(=${name})`).join(' | ') + : (suite.filter ?? 'all()'), + reason: suite.always + ? 'always checked' + : extras.length + ? `runtime inputs: ${extras.join(', ')}` + : `affected packages: ${affected.join(', ')}`, + }; + } + } + } + } + if (full) { + for (const [id, suite] of Object.entries(suites)) + selected[id] = { filter: suite.filter ?? 'all()', reason: fullReasons.join(', ') }; + } else { + selected.tooling = { filter: 'all()', reason: 'always checked' }; + } + const matrix = workers.flatMap((worker) => { + const entries = worker.suites.filter((id) => selected[id]); + return entries.length ? [{ ...worker, suites: entries }] : []; + }); + return { revision, baseline, full, reasons: fullReasons, suites: selected, workers: matrix }; +} + +export function checkResults(plan, results, hasRootTests) { + const required = ['plan', 'tooling']; + if (plan.workers.length) required.push('suites'); + if (plan.suites.root) { + if (typeof hasRootTests !== 'boolean') + throw new Error('Root producer did not report whether tests were selected'); + if (!hasRootTests && plan.suites.root.filter === 'all()') + throw new Error('The full root test selection was unexpectedly empty'); + required.push('root-build'); + if (hasRootTests) required.push('root-tests'); + } + const failures = required.filter((id) => results[id]?.result !== 'success'); + if (failures.length) throw new Error(`Required CI jobs did not succeed: ${failures.join(', ')}`); +} diff --git a/scripts/ci/run.mjs b/scripts/ci/run.mjs new file mode 100644 index 00000000000..3c59cd5f799 --- /dev/null +++ b/scripts/ci/run.mjs @@ -0,0 +1,292 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + copyFileSync, + globSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { command } from './command.mjs'; +import { manifests, suites } from './suites.mjs'; + +export function executor(root, worker = {}) { + const target = resolve(root, process.env.CARGO_TARGET_DIR ?? 'target'); + const env = { ...process.env, CARGO_TARGET_DIR: target }; + if (['nsh', 'gpui'].includes(worker.workspace) || worker.id?.startsWith('native-')) { + Object.assign(env, { + CARGO_BUILD_JOBS: '2', + CARGO_INCREMENTAL: '0', + CARGO_PROFILE_DEV_DEBUG: '0', + }); + if (['nsh', 'gpui'].includes(worker.workspace)) env.CARGO_PROFILE_TEST_DEBUG = '0'; + } + const run = (exe, args, options = {}) => command(exe, args, { cwd: root, env, ...options }); + const node = (args) => run(process.execPath, args); + const cargo = (args, options) => run('cargo', args, options); + const nextest = (id, noRun = false, filter = suites[id].filter ?? 'all()', extra = []) => + cargo( + [ + 'nextest', + 'run', + ...suites[id].cargoArgs, + ...(filter === 'all()' ? [] : ['-E', filter]), + ...(noRun ? ['--no-run'] : []), + ...extra, + ], + { env: { ...env, NEXTEST_PROFILE: suites[id].profile ?? 'default' } }, + ); + + async function check(id) { + if (id === 'format') { + await cargo(['fmt', '--all', '--', '--check']); + await node(['node_modules/@biomejs/biome/bin/biome', 'format', '.']); + } else if (id === 'lint-js') { + await node(['node_modules/@biomejs/biome/bin/biome', 'lint', '.']); + } else if (id === 'tooling-tests') { + await node([ + '--test', + ...globSync( + [ + 'scripts/*.test.mjs', + 'scripts/rust/*.test.mjs', + 'products/maka/workbench/tests/*.test.mjs', + 'packages/cli/tests/*.test.mjs', + ], + { cwd: root }, + ).sort(), + ]); + } else if (id === 'licenses') { + for (const script of ['scripts/asf-license-headers.mjs', 'scripts/rust/dependencies.mjs']) + await node([script, 'check']); + } else if (id === 'notices') { + await node(['scripts/rust/notices.mjs', 'check']); + } else { + throw new Error(`Unknown check: ${id}`); + } + } + + async function prepare(id, filter, extra = []) { + if (['root', 'desktop', 'gpui'].includes(id)) { + await nextest(id, true, filter, extra); + } else if (id === 'bridge') { + await node(['scripts/rust/check-cua-build.mjs']); + await cargo(['build', '--locked', '-p', 'maka-cli', '--no-default-features']); + mkdirSync(join(target, 'ci'), { recursive: true }); + copyFileSync(join(target, 'debug/maka'), join(target, 'ci/maka-bridge')); + await nextest(id, true); + } else if (id === 'clippy') { + await cargo([ + 'clippy', + '--config', + 'build.warnings="deny"', + '--locked', + '--workspace', + '--all-targets', + ]); + } else if (id === 'native-build') { + await node(['scripts/rust/build-cli.mjs']); + await run(join(target, 'debug/maka'), ['--version']); + await run(join(target, 'debug/maka'), ['desktop', '--version']); + await run(join(target, 'debug/maka-desktop-shell'), ['--version']); + } else if (id === 'contracts') { + await cargo(['build', '--locked', '-p', 'maka-schema']); + await cargo(['build', '--locked', '-p', 'maka-workbench', '--example', 'ui_types']); + } else if (id === 'product') { + await cargo(['build', '--locked', '-p', 'maka-ui-build', '--bin', 'maka-ui-build']); + } else if (id === 'termina') { + await run('rustup', ['target', 'add', 'x86_64-pc-windows-msvc']); + const args = [ + 'check', + '--locked', + '--manifest-path', + manifests.termina, + '--lib', + '--target', + 'x86_64-pc-windows-msvc', + ]; + await cargo(args); + await cargo([...args, '--features', 'windows-legacy']); + await nextest(id, true); + } else if (id === 'nsh') { + await nextest(id, true); + await cargo(['nextest', 'run', ...webTextArgs, '--no-run']); + } else if (id === 'native') { + await cargo([ + 'check', + '--locked', + '-p', + 'maka-computer-use', + '--lib', + '--features', + 'native-desktop', + ]); + } else if (id !== 'tooling') { + throw new Error(`Unknown suite: ${id}`); + } + } + + async function verify(id, filter, extra = []) { + if (id === 'tooling') { + for (const name of ['format', 'lint-js', 'tooling-tests', 'licenses']) await check(name); + } else if (id === 'root') { + await nextest(id, false, filter, extra); + } else if (id === 'bridge') { + await run(join(target, 'ci/maka-bridge'), ['--version']); + await nextest(id); + } else if (id === 'desktop' || id === 'gpui') { + await nextest(id, false, filter, ['-j', '1', ...extra]); + } else if (id === 'termina') { + await nextest(id); + } else if (id === 'contracts') { + await node(['packages/plugin-sdk/scripts/generate.mjs', 'check']); + await node([ + 'node_modules/typescript/bin/tsc', + '-p', + 'packages/plugin-sdk/tsconfig.json', + '--noEmit', + ]); + await node([ + 'node_modules/typescript/bin/tsc', + '-p', + 'packages/plugin-sdk/tsconfig.acceptance.json', + ]); + await node(['node_modules/typescript/bin/tsc', '-p', 'products/maka/bundle/tsconfig.json']); + const temporary = mkdtempSync(join(tmpdir(), 'maka-ci-types-')); + try { + const output = join(temporary, 'ui-types'); + await cargo([ + 'run', + '--locked', + '-q', + '-p', + 'maka-workbench', + '--example', + 'ui_types', + '--', + output, + ]); + await node(['node_modules/typescript/bin/tsc', '-p', join(output, 'tsconfig.json')]); + } finally { + rmSync(temporary, { recursive: true, force: true }); + } + } else if (id === 'product') { + await node(['scripts/rust/product.mjs', 'test']); + await check('notices'); + } else if (id === 'nsh') { + await nextest(id); + await cargo(['test', ...suites.nsh.cargoArgs, '--doc']); + await cargo(['nextest', 'run', ...webTextArgs]); + const chrome = ( + await run( + 'sh', + ['-c', 'command -v google-chrome || command -v chromium || command -v chromium-browser'], + { capture: true }, + ) + ).trim(); + await run(chrome, ['--version']); + await node(['--test', 'third-party/nsh/crates/nsh-platform-web/web/nsh.test.mjs']); + await run( + process.execPath, + ['--test', 'third-party/nsh/crates/nsh-platform-web/web/shader-browser.test.mjs'], + { env: { ...env, CHROME: chrome } }, + ); + } else if (!['clippy', 'native', 'native-build'].includes(id)) { + throw new Error(`Unknown suite: ${id}`); + } + } + + async function archive(plan, output) { + const args = suites.root.cargoArgs; + const listing = JSON.parse( + await cargo( + ['nextest', 'list', ...args, '-E', plan.suites.root.filter, '--message-format', 'json'], + { capture: true }, + ), + ); + const selected = Object.values(listing['rust-suites']).filter((suite) => + Object.values(suite.testcases).some( + (entry) => !entry.ignored && entry['filter-match'].status === 'matches', + ), + ); + const count = selected + .flatMap((suite) => Object.values(suite.testcases)) + .filter((entry) => !entry.ignored && entry['filter-match'].status === 'matches').length; + if (!count && plan.suites.root.filter === 'all()') + throw new Error('The full root test selection was unexpectedly empty'); + if (!count) return []; + const config = readFileSync(join(root, '.config/nextest.toml'), 'utf8'); + const runtimePackages = new Set( + selected + .filter((suite) => ['maka-cli', 'maka-terminal-shell'].includes(suite['package-name'])) + .map((suite) => suite['package-id']), + ); + const includes = []; + for (const [id, path] of Object.entries(listing['rust-build-meta']['build-script-out-dirs'])) { + if (runtimePackages.has(id)) includes.push(`${path}/product`); + } + const temporary = mkdtempSync(join(tmpdir(), 'maka-ci-archive-')); + try { + const configFile = join(temporary, 'nextest.toml'); + writeFileSync( + configFile, + `${config}\n[profile.ci.archive]\ninclude = [\n${includes.map((path) => `{ path = ${JSON.stringify(path)}, relative-to = "target", depth = "infinite", on-missing = "error" },`).join('\n')}\n]\n`, + ); + mkdirSync(dirname(output), { recursive: true }); + await cargo([ + 'nextest', + 'archive', + ...args, + '--profile', + 'ci', + '--config-file', + configFile, + '-E', + plan.suites.root.filter, + '--archive-file', + output, + ]); + } finally { + rmSync(temporary, { recursive: true, force: true }); + } + // Two runners remove the existing single-runner heavy-fixture bottleneck. + const partitions = Math.min(2, count); + return Array.from({ length: partitions }, (_, index) => ({ + partition: `slice:${index + 1}/${partitions}`, + })); + } + + return { prepare, verify, check, archive, run, target }; +} + +const webTextArgs = [ + '--locked', + '--manifest-path', + manifests.nsh, + '-p', + 'nsh-platform-web', + '--no-default-features', + '--test', + 'text', +]; diff --git a/scripts/ci/setup.mjs b/scripts/ci/setup.mjs new file mode 100644 index 00000000000..460df13cee0 --- /dev/null +++ b/scripts/ci/setup.mjs @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createHash } from 'node:crypto'; +import { appendFileSync, mkdirSync, readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { command } from './command.mjs'; +import { manifests } from './suites.mjs'; + +const role = process.env.MAKA_CI_ROLE; +const workspace = process.env.MAKA_CI_WORKSPACE; +const hash = (input) => createHash('sha256').update(input).digest('hex'); +const version = process.env.MAKA_CI_NEXTEST; +const tools = + role === 'tooling' + ? ['cargo-deny'] + : ['root', 'replay', 'variants', 'termina', 'nsh', 'gpui'].includes(role) + ? [`cargo-nextest${version ? `@${version}` : ''}`] + : []; + +function output(values) { + appendFileSync( + process.env.GITHUB_OUTPUT, + Object.entries(values) + .map(([key, value]) => `${key}=${value}\n`) + .join(''), + ); +} + +async function main(action) { + if (action === 'tool-list') { + output({ tools: tools.join(' ') }); + } else if (action === 'tools') { + await command('cargo', ['binstall', '--no-confirm', '--force', 'cargo-binstall']); + await command('cargo', ['binstall', '--no-confirm', '--force', ...tools]); + } else if (action === 'keys') { + const lock = readFileSync(join(dirname(manifests[workspace]), 'Cargo.lock')); + const compiler = await command('rustc', ['-vV'], { capture: true }); + const sharedRoot = ['root', 'variants', 'clippy', 'product'].includes(role); + const base = `rust-target-${process.env.RUNNER_OS}-${process.env.RUNNER_ARCH}-${hash(compiler)}-${sharedRoot ? 'root' : role}`; + const downloads = `cargo-${process.env.RUNNER_OS}-${workspace}-${hash(lock)}`; + const writer = + workspace !== 'root' || + role === 'root' || + (process.env.RUNNER_OS !== 'Linux' && role.startsWith('native-')); + output({ + compiled: `${base}-${hash(lock)}`, + compiled_prefix: base, + compiled_writer: !sharedRoot || role === 'root', + downloads, + downloads_prefix: `cargo-${process.env.RUNNER_OS}-${workspace}`, + downloads_writer: writer, + }); + } else if (action === 'mbx') { + const release = JSON.parse( + await command('gh', ['api', 'repos/jdx/mr-boxington/releases/latest'], { capture: true }), + ); + if (!release.immutable) throw new Error('The mbx release must be immutable'); + const asset = release.assets.find( + (entry) => entry.name === 'mbx-x86_64-unknown-linux-gnu.tar.gz', + ); + if (!asset) throw new Error('The mbx Linux release asset is missing'); + const temporary = process.env.RUNNER_TEMP; + const archive = join(temporary, 'mbx.tar.gz'); + const bin = join(temporary, 'mbx-bin'); + const data = join(temporary, 'mbx-data'); + mkdirSync(bin, { recursive: true }); + await command('curl', [ + '--fail', + '--silent', + '--show-error', + '--location', + '--retry', + '3', + asset.browser_download_url, + '--output', + archive, + ]); + if (asset.digest !== `sha256:${hash(readFileSync(archive))}`) + throw new Error('mbx release asset digest mismatch'); + await command('tar', ['-xzf', archive, '-C', bin]); + await command(join(bin, 'mbx'), ['--version']); + await command(join(bin, 'mbx'), ['setup', '--yes'], { + env: { ...process.env, XDG_DATA_HOME: data }, + }); + appendFileSync(process.env.GITHUB_PATH, `${bin}\n${join(data, 'mbx/bin')}\n`); + } else if (action === 'native') { + const dependencies = process.env.MAKA_CI_DEPENDENCIES; + const shared = [ + 'clang', + 'libclang-dev', + 'pkg-config', + 'libfontconfig1-dev', + 'libfreetype6-dev', + 'libssl-dev', + ]; + const headless = [ + 'bubblewrap', + 'ripgrep', + 'libx11-dev', + 'libxi-dev', + 'libxtst-dev', + 'libxext-dev', + 'libxcb1-dev', + 'libxkbcommon-dev', + 'libxkbcommon-x11-dev', + 'libwayland-dev', + 'libvulkan1', + 'mesa-vulkan-drivers', + ]; + const packages = + dependencies === 'computer-use' + ? [ + ...shared, + 'libx11-dev', + 'libxi-dev', + 'libxtst-dev', + 'libxext-dev', + 'libxrandr-dev', + 'libxfixes-dev', + 'libxkbcommon-dev', + 'libwayland-dev', + ] + : [ + ...shared, + ...headless, + ...(dependencies === 'gpui' + ? ['build-essential', 'libasound2-dev', 'libx11-xcb-dev', 'libzstd-dev'] + : []), + ]; + await command('sudo', ['apt-get', 'update']); + await command('sudo', ['apt-get', 'install', '-y', ...packages]); + if (dependencies !== 'computer-use') + await command('sudo', ['sysctl', '-w', 'kernel.apparmor_restrict_unprivileged_userns=0']); + } else { + throw new Error(`Unknown CI setup action: ${action}`); + } +} + +main(process.argv[2]).catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/ci/suites.mjs b/scripts/ci/suites.mjs new file mode 100644 index 00000000000..6afbad95e8e --- /dev/null +++ b/scripts/ci/suites.mjs @@ -0,0 +1,242 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { readFileSync } from 'node:fs'; + +export const manifests = { + root: 'Cargo.toml', + termina: 'third-party/termina/Cargo.toml', + nsh: 'third-party/nsh/Cargo.toml', + gpui: 'third-party/nsh/crates/gpui/Cargo.toml', +}; + +const bundledPackages = Object.values( + JSON.parse( + readFileSync(new URL('../../products/maka/bundle/package-inputs.json', import.meta.url)), + ), +); + +export const productInputs = [ + ...bundledPackages, + 'products/maka/bundle', + 'packages/plugin-sdk', + 'plugins/shared', + 'scripts/plugins', + 'scripts/rust/product.mjs', +]; +const embeddedInputs = [ + ...productInputs, + 'skills/maka-plugin-authoring', + 'platform/computer-use/skills', +]; + +export const suites = { + tooling: { always: true }, + root: { + workspace: 'root', + packages: 'workspace', + cargoArgs: ['--locked', '--workspace'], + profile: 'ci', + extraInputs: embeddedInputs, + affectedFilter: true, + }, + bridge: { + workspace: 'root', + packages: ['maka-cli', 'maka-computer-use'], + cargoArgs: [ + '--locked', + '-p', + 'maka-computer-use', + '--no-default-features', + '--lib', + '--test', + 'integration', + ], + extraInputs: embeddedInputs, + }, + desktop: { + workspace: 'root', + packages: ['maka-cli'], + cargoArgs: [ + '--locked', + '-p', + 'maka-cli', + '--no-default-features', + '--features', + 'desktop-tests', + '--test', + 'integration', + ], + filter: 'test(terminal::desktop_new_session)', + extraInputs: embeddedInputs, + }, + 'native-build': { + workspace: 'root', + packages: ['maka-cli', 'maka-desktop-shell', 'maka-python-runtime'], + extraInputs: embeddedInputs, + }, + clippy: { + workspace: 'root', + packages: 'workspace', + extraInputs: embeddedInputs, + }, + contracts: { + workspace: 'root', + packages: ['maka-schema', 'maka-workbench'], + extraInputs: [...productInputs, 'skills/maka-plugin-authoring/assets'], + }, + product: { + workspace: 'root', + packages: ['maka-ui-build', 'maka-cli', 'maka-desktop-shell', 'maka-python-runtime'], + extraInputs: productInputs, + }, + termina: { + workspace: 'termina', + packages: ['termina'], + cargoArgs: ['--locked', '--manifest-path', manifests.termina, '--lib'], + profile: 'default', + }, + nsh: { + workspace: 'nsh', + packages: [ + 'nsh-core', + 'nsh-base', + 'nsh-engine', + 'nsh-js', + 'nsh-testing', + 'nsh-platform-tui', + 'nsh-dev', + 'nsh-bundler', + 'nsh-platform-web', + ], + cargoArgs: [ + '--locked', + '--manifest-path', + manifests.nsh, + '-p', + 'nsh-core', + '-p', + 'nsh-base', + '-p', + 'nsh-engine', + '-p', + 'nsh-js', + '-p', + 'nsh-testing', + '-p', + 'nsh-platform-tui', + '-p', + 'nsh-dev', + '-p', + 'nsh-bundler', + '--features', + 'nsh-testing/agent,nsh-dev/client,nsh-dev/server', + ], + extraInputs: ['third-party/nsh/crates/nsh-platform-web/web'], + }, + gpui: { + workspace: 'gpui', + packages: ['nsh-platform-gpui', 'nsh-gpui-terminal'], + cargoArgs: [ + '--locked', + '--manifest-path', + manifests.gpui, + '-p', + 'nsh-platform-gpui', + '-p', + 'nsh-gpui-terminal', + '--no-default-features', + '--features', + 'nsh-platform-gpui/js,nsh-platform-gpui/shot', + '--lib', + ], + filter: + 'test(native::tests::animated_props_) | test(=input::tests::nonpressable_focusable_views_join_native_stops_without_stealing_presses) | test(=tests::windows) | test(kitty::tests::)', + }, + native: { + workspace: 'root', + packages: ['maka-computer-use'], + }, +}; + +export const workers = [ + { + id: 'variants', + suites: ['bridge', 'desktop', 'native-build'], + runner: 'ubuntu-latest', + workspace: 'root', + dependencies: 'headless', + javascript: true, + compile: true, + }, + { + id: 'clippy', + suites: ['clippy'], + runner: 'ubuntu-latest', + workspace: 'root', + dependencies: 'headless', + javascript: true, + compile: true, + }, + { + id: 'product', + suites: ['contracts', 'product'], + runner: 'ubuntu-latest', + workspace: 'root', + dependencies: 'headless', + javascript: true, + compile: true, + }, + { + id: 'termina', + suites: ['termina'], + runner: 'ubuntu-latest', + workspace: 'termina', + dependencies: '', + javascript: false, + compile: true, + }, + { + id: 'nsh', + suites: ['nsh'], + runner: 'ubuntu-latest', + workspace: 'nsh', + dependencies: 'headless', + javascript: false, + compile: true, + }, + { + id: 'gpui', + suites: ['gpui'], + runner: 'ubuntu-latest', + workspace: 'gpui', + dependencies: 'gpui', + javascript: false, + compile: true, + }, + ...['ubuntu', 'macos', 'windows'].map((os) => ({ + id: `native-${os}`, + suites: ['native'], + runner: `${os}-latest`, + workspace: 'root', + dependencies: os === 'ubuntu' ? 'computer-use' : '', + javascript: false, + compile: true, + })), +]; diff --git a/scripts/rust/README.md b/scripts/rust/README.md index f6f8a6ef683..c92dd06f9a4 100644 --- a/scripts/rust/README.md +++ b/scripts/rust/README.md @@ -65,6 +65,19 @@ python3 scripts/rust/benchmark-long-task.py \ For larger histories, set `MAKA_ENGINE_PERF_FIXTURE_DIR` to a disposable directory and `MAKA_ENGINE_PERF_FIXTURE_TAG` to a revision label. Closed snapshots are keyed by workload settings and fixture source; preserve both when comparing retained executables. Seeding is separate from request latency and may take much longer. Run without competing builds and collect CPU profiles separately. +## CI + +The [suite catalog](../ci/suites.mjs) defines maintained build and test scopes. `just` and the [workflow](../../.github/workflows/ci.yml) use the same [executor](../ci.mjs). Inspect committed changes or reproduce a suite after `just setup`: + +```sh +node scripts/ci.mjs plan --base origin/rust-preview +node scripts/ci.mjs run bridge +``` + +PR plans follow Cargo reverse dependencies and declared runtime inputs; shared or unclassified changes select the full maintained set. Branch pushes and manual runs select the full set. Package filtering preserves each suite's workspace/features build recipe. The GPUI suite covers selected headless platform and terminal tests; standalone host webview/video configurations are outside that scope. + +The root producer creates a nextest archive, including runtime product assets. Test workers execute partitions of that same artifact; the final `test` check requires all selected work to succeed. Cargo caches accelerate builds and are not test artifacts. + ## Windows State Root ownership New State Root directories are created with an explicit current-account owner and a private ACL. An elevated Windows token can otherwise default the owner to the Administrators group. Existing directories retain their permissions until Root validation; nonempty, unrecognized directories are not adopted. diff --git a/scripts/rust/README.zh-CN.md b/scripts/rust/README.zh-CN.md index 340b0a53979..5a4455be4fa 100644 --- a/scripts/rust/README.zh-CN.md +++ b/scripts/rust/README.zh-CN.md @@ -59,6 +59,19 @@ python3 scripts/rust/benchmark-long-task.py \ 更大历史使用一次性目录 `MAKA_ENGINE_PERF_FIXTURE_DIR` 和明确版本标签 `MAKA_ENGINE_PERF_FIXTURE_TAG`。关闭的快照按负载设置与夹具源码索引,对比保留产物时保持两者一致。造数据与请求耗时分开,前者可能长得多。测量时停止竞争编译,CPU 剖析另行采集。 +## CI + +[套件目录](../ci/suites.mjs)定义已维护的构建和测试范围,`just` 与 [workflow](../../.github/workflows/ci.yml)使用同一[执行入口](../ci.mjs)。执行 `just setup` 后,可以查看已提交改动的计划或复现套件: + +```sh +node scripts/ci.mjs plan --base origin/rust-preview +node scripts/ci.mjs run bridge +``` + +PR 根据 Cargo 反向依赖和声明的运行时输入选择范围;共享或无法归属的变更执行完整已维护套件。分支 push 和手动运行也执行全量。包级测试筛选保留各套件原有的 workspace/features 编译配方。GPUI 套件覆盖选定的无窗口 platform 和 terminal 测试,不包含独立 host 的 webview/video 配置。 + +根 producer 生成包含运行时产品资源的 nextest 归档;测试 worker 执行同一产物的分片。最终 `test` 检查要求所有选中工作成功。Cargo 缓存用于加速构建,不代替测试产物。 + ## Windows State Root 所有权 新 State Root 目录在创建时就明确指定当前账号为所有者,并设置私有 ACL。否则,提权后的 Windows 令牌可能默认将所有者设为 Administrators 组。已有目录在 Root 校验前保持原有权限;程序不会接管非空且无法识别的目录。 diff --git a/third-party/nsh/crates/gpui/nsh-platform-gpui/src/tests.rs b/third-party/nsh/crates/gpui/nsh-platform-gpui/src/tests.rs index aadc444e074..b63a94fa4aa 100644 --- a/third-party/nsh/crates/gpui/nsh-platform-gpui/src/tests.rs +++ b/third-party/nsh/crates/gpui/nsh-platform-gpui/src/tests.rs @@ -79,7 +79,7 @@ function App() { loaded.set(`${label}|${policy}|${e.size.width}x${e.size.height}`)} - onError={(e) => loaded.set(e.message)} + onError={() => loaded.set("unavailable")} style={{ width: 32, height: 32 }} /> ();