tasks: add detached runtime plugin registration contract (#68915) #2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| types: [opened, reopened, synchronize, ready_for_review, converted_to_draft] | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ${{ github.event_name == 'pull_request' && format('{0}-{1}', github.workflow, github.event.pull_request.number) || format('{0}-{1}', github.workflow, github.run_id) }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| jobs: | |
| # Preflight: establish routing truth and job matrices once, then let real | |
| # work fan out from a single source of truth. | |
| preflight: | |
| permissions: | |
| contents: read | |
| if: github.event_name != 'pull_request' || !github.event.pull_request.draft | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| timeout-minutes: 20 | |
| outputs: | |
| docs_only: ${{ steps.manifest.outputs.docs_only }} | |
| docs_changed: ${{ steps.manifest.outputs.docs_changed }} | |
| run_node: ${{ steps.manifest.outputs.run_node }} | |
| run_macos: ${{ steps.manifest.outputs.run_macos }} | |
| run_android: ${{ steps.manifest.outputs.run_android }} | |
| run_skills_python: ${{ steps.manifest.outputs.run_skills_python }} | |
| run_skills_python_job: ${{ steps.manifest.outputs.run_skills_python_job }} | |
| run_windows: ${{ steps.manifest.outputs.run_windows }} | |
| has_changed_extensions: ${{ steps.manifest.outputs.has_changed_extensions }} | |
| changed_extensions_matrix: ${{ steps.manifest.outputs.changed_extensions_matrix }} | |
| run_build_artifacts: ${{ steps.manifest.outputs.run_build_artifacts }} | |
| run_checks_fast: ${{ steps.manifest.outputs.run_checks_fast }} | |
| checks_fast_core_matrix: ${{ steps.manifest.outputs.checks_fast_core_matrix }} | |
| checks_node_extensions_matrix: ${{ steps.manifest.outputs.checks_node_extensions_matrix }} | |
| run_checks: ${{ steps.manifest.outputs.run_checks }} | |
| checks_matrix: ${{ steps.manifest.outputs.checks_matrix }} | |
| checks_node_core_test_matrix: ${{ steps.manifest.outputs.checks_node_core_test_matrix }} | |
| run_extension_fast: ${{ steps.manifest.outputs.run_extension_fast }} | |
| extension_fast_matrix: ${{ steps.manifest.outputs.extension_fast_matrix }} | |
| run_check: ${{ steps.manifest.outputs.run_check }} | |
| run_check_additional: ${{ steps.manifest.outputs.run_check_additional }} | |
| run_build_smoke: ${{ steps.manifest.outputs.run_build_smoke }} | |
| run_check_docs: ${{ steps.manifest.outputs.run_check_docs }} | |
| run_control_ui_i18n: ${{ steps.manifest.outputs.run_control_ui_i18n }} | |
| run_checks_windows: ${{ steps.manifest.outputs.run_checks_windows }} | |
| checks_windows_matrix: ${{ steps.manifest.outputs.checks_windows_matrix }} | |
| run_macos_node: ${{ steps.manifest.outputs.run_macos_node }} | |
| macos_node_matrix: ${{ steps.manifest.outputs.macos_node_matrix }} | |
| run_macos_swift: ${{ steps.manifest.outputs.run_macos_swift }} | |
| run_android_job: ${{ steps.manifest.outputs.run_android_job }} | |
| android_matrix: ${{ steps.manifest.outputs.android_matrix }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 1 | |
| fetch-tags: false | |
| persist-credentials: false | |
| submodules: false | |
| - name: Ensure preflight base commit | |
| uses: ./.github/actions/ensure-base-commit | |
| with: | |
| base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }} | |
| fetch-ref: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }} | |
| - name: Detect docs-only changes | |
| id: docs_scope | |
| uses: ./.github/actions/detect-docs-changes | |
| - name: Detect changed scopes | |
| id: changed_scope | |
| if: steps.docs_scope.outputs.docs_only != 'true' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [ "${{ github.event_name }}" = "push" ]; then | |
| BASE="${{ github.event.before }}" | |
| else | |
| BASE="${{ github.event.pull_request.base.sha }}" | |
| fi | |
| node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD | |
| - name: Setup Node environment | |
| if: steps.docs_scope.outputs.docs_only != 'true' | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| install-deps: "false" | |
| use-sticky-disk: "false" | |
| - name: Detect changed extensions | |
| id: changed_extensions | |
| if: steps.docs_scope.outputs.docs_only != 'true' && steps.changed_scope.outputs.run_node == 'true' | |
| env: | |
| BASE_SHA: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }} | |
| BASE_REF: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }} | |
| run: | | |
| node --input-type=module <<'EOF' | |
| import { appendFileSync } from "node:fs"; | |
| import { listChangedExtensionIds } from "./scripts/lib/changed-extensions.mjs"; | |
| const extensionIds = listChangedExtensionIds({ | |
| base: process.env.BASE_SHA, | |
| head: "HEAD", | |
| fallbackBaseRef: process.env.BASE_REF, | |
| unavailableBaseBehavior: "all", | |
| }); | |
| const matrix = JSON.stringify({ include: extensionIds.map((extension) => ({ extension })) }); | |
| appendFileSync(process.env.GITHUB_OUTPUT, `has_changed_extensions=${extensionIds.length > 0}\n`, "utf8"); | |
| appendFileSync(process.env.GITHUB_OUTPUT, `changed_extensions_matrix=${matrix}\n`, "utf8"); | |
| EOF | |
| - name: Build CI manifest | |
| id: manifest | |
| env: | |
| OPENCLAW_CI_DOCS_ONLY: ${{ steps.docs_scope.outputs.docs_only }} | |
| OPENCLAW_CI_DOCS_CHANGED: ${{ steps.docs_scope.outputs.docs_changed }} | |
| OPENCLAW_CI_RUN_NODE: ${{ steps.changed_scope.outputs.run_node || 'false' }} | |
| OPENCLAW_CI_RUN_MACOS: ${{ steps.changed_scope.outputs.run_macos || 'false' }} | |
| OPENCLAW_CI_RUN_ANDROID: ${{ steps.changed_scope.outputs.run_android || 'false' }} | |
| OPENCLAW_CI_RUN_WINDOWS: ${{ steps.changed_scope.outputs.run_windows || 'false' }} | |
| OPENCLAW_CI_RUN_SKILLS_PYTHON: ${{ steps.changed_scope.outputs.run_skills_python || 'false' }} | |
| OPENCLAW_CI_RUN_CONTROL_UI_I18N: ${{ steps.changed_scope.outputs.run_control_ui_i18n || 'false' }} | |
| OPENCLAW_CI_HAS_CHANGED_EXTENSIONS: ${{ steps.changed_extensions.outputs.has_changed_extensions || 'false' }} | |
| OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX: ${{ steps.changed_extensions.outputs.changed_extensions_matrix || '{"include":[]}' }} | |
| run: | | |
| node --input-type=module <<'EOF' | |
| import { appendFileSync } from "node:fs"; | |
| import { | |
| createNodeTestShards, | |
| } from "./scripts/lib/ci-node-test-plan.mjs"; | |
| import { | |
| createExtensionTestShards, | |
| DEFAULT_EXTENSION_TEST_SHARD_COUNT, | |
| } from "./scripts/lib/extension-test-plan.mjs"; | |
| const parseBoolean = (value, fallback = false) => { | |
| if (value === undefined) return fallback; | |
| const normalized = value.trim().toLowerCase(); | |
| if (normalized === "true" || normalized === "1") return true; | |
| if (normalized === "false" || normalized === "0" || normalized === "") return false; | |
| return fallback; | |
| }; | |
| const parseJson = (value, fallback) => { | |
| try { | |
| return value ? JSON.parse(value) : fallback; | |
| } catch { | |
| return fallback; | |
| } | |
| }; | |
| const createMatrix = (include) => ({ include }); | |
| const outputPath = process.env.GITHUB_OUTPUT; | |
| const eventName = process.env.GITHUB_EVENT_NAME ?? "pull_request"; | |
| const isPush = eventName === "push"; | |
| const docsOnly = parseBoolean(process.env.OPENCLAW_CI_DOCS_ONLY); | |
| const docsChanged = parseBoolean(process.env.OPENCLAW_CI_DOCS_CHANGED); | |
| const runNode = parseBoolean(process.env.OPENCLAW_CI_RUN_NODE) && !docsOnly; | |
| const runMacos = parseBoolean(process.env.OPENCLAW_CI_RUN_MACOS) && !docsOnly; | |
| const runAndroid = parseBoolean(process.env.OPENCLAW_CI_RUN_ANDROID) && !docsOnly; | |
| const runWindows = parseBoolean(process.env.OPENCLAW_CI_RUN_WINDOWS) && !docsOnly; | |
| const runSkillsPython = parseBoolean(process.env.OPENCLAW_CI_RUN_SKILLS_PYTHON) && !docsOnly; | |
| const runControlUiI18n = | |
| parseBoolean(process.env.OPENCLAW_CI_RUN_CONTROL_UI_I18N) && !docsOnly; | |
| const hasChangedExtensions = | |
| parseBoolean(process.env.OPENCLAW_CI_HAS_CHANGED_EXTENSIONS) && !docsOnly; | |
| const changedExtensionsMatrix = hasChangedExtensions | |
| ? parseJson(process.env.OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX, { include: [] }) | |
| : { include: [] }; | |
| const extensionShardMatrix = createMatrix( | |
| runNode | |
| ? createExtensionTestShards({ | |
| shardCount: DEFAULT_EXTENSION_TEST_SHARD_COUNT, | |
| }).map((shard) => ({ | |
| check_name: shard.checkName, | |
| extensions_csv: shard.extensionIds.join(","), | |
| shard_index: shard.index + 1, | |
| task: "extensions-batch", | |
| })) | |
| : [], | |
| ); | |
| const manifest = { | |
| docs_only: docsOnly, | |
| docs_changed: docsChanged, | |
| run_node: runNode, | |
| run_macos: runMacos, | |
| run_android: runAndroid, | |
| run_skills_python: runSkillsPython, | |
| run_windows: runWindows, | |
| has_changed_extensions: hasChangedExtensions, | |
| changed_extensions_matrix: changedExtensionsMatrix, | |
| run_build_artifacts: runNode, | |
| run_checks_fast: runNode, | |
| checks_fast_core_matrix: createMatrix( | |
| runNode | |
| ? [ | |
| { check_name: "checks-fast-bundled", runtime: "node", task: "bundled" }, | |
| { | |
| check_name: "checks-fast-contracts-protocol", | |
| runtime: "node", | |
| task: "contracts", | |
| }, | |
| ] | |
| : [], | |
| ), | |
| checks_node_extensions_matrix: extensionShardMatrix, | |
| run_checks: runNode, | |
| checks_matrix: createMatrix( | |
| runNode | |
| ? [ | |
| { check_name: "checks-node-channels", runtime: "node", task: "channels" }, | |
| ...(isPush | |
| ? [ | |
| { | |
| check_name: "checks-node-compat-node22", | |
| runtime: "node", | |
| task: "compat-node22", | |
| node_version: "22.18.0", | |
| cache_key_suffix: "node22", | |
| }, | |
| ] | |
| : []), | |
| ] | |
| : [], | |
| ), | |
| checks_node_core_test_matrix: createMatrix( | |
| runNode | |
| ? createNodeTestShards().map((shard) => ({ | |
| check_name: shard.checkName, | |
| runtime: "node", | |
| task: "test-shard", | |
| shard_name: shard.shardName, | |
| configs: shard.configs, | |
| requires_dist: shard.requiresDist, | |
| })) | |
| : [], | |
| ), | |
| run_extension_fast: hasChangedExtensions, | |
| extension_fast_matrix: createMatrix( | |
| hasChangedExtensions | |
| ? (changedExtensionsMatrix.include ?? []).map((entry) => ({ | |
| check_name: `extension-fast-${entry.extension}`, | |
| extension: entry.extension, | |
| })) | |
| : [], | |
| ), | |
| run_check: runNode, | |
| run_check_additional: runNode, | |
| run_build_smoke: runNode, | |
| run_check_docs: docsChanged, | |
| run_control_ui_i18n: runControlUiI18n, | |
| run_skills_python_job: runSkillsPython, | |
| run_checks_windows: runWindows, | |
| checks_windows_matrix: createMatrix( | |
| runWindows | |
| ? [{ check_name: "checks-windows-node-test", runtime: "node", task: "test" }] | |
| : [], | |
| ), | |
| run_macos_node: runMacos, | |
| macos_node_matrix: createMatrix( | |
| runMacos ? [{ check_name: "macos-node", runtime: "node", task: "test" }] : [], | |
| ), | |
| run_macos_swift: runMacos, | |
| run_android_job: runAndroid, | |
| android_matrix: createMatrix( | |
| runAndroid | |
| ? [ | |
| { check_name: "android-test-play", task: "test-play" }, | |
| { check_name: "android-test-third-party", task: "test-third-party" }, | |
| { check_name: "android-build-play", task: "build-play" }, | |
| { check_name: "android-build-third-party", task: "build-third-party" }, | |
| ] | |
| : [], | |
| ), | |
| }; | |
| for (const [key, value] of Object.entries(manifest)) { | |
| appendFileSync( | |
| outputPath, | |
| `${key}=${typeof value === "string" ? value : JSON.stringify(value)}\n`, | |
| "utf8", | |
| ); | |
| } | |
| EOF | |
| # Run the fast security/SCM checks in parallel with scope detection so the | |
| # main Node jobs do not have to wait for Python/pre-commit setup. | |
| security-fast: | |
| permissions: | |
| contents: read | |
| if: github.event_name != 'pull_request' || !github.event.pull_request.draft | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| timeout-minutes: 20 | |
| env: | |
| PRE_COMMIT_CACHE_KEY_SUFFIX: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.sha }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 1 | |
| fetch-tags: false | |
| persist-credentials: false | |
| submodules: false | |
| - name: Ensure security base commit | |
| uses: ./.github/actions/ensure-base-commit | |
| with: | |
| base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }} | |
| fetch-ref: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }} | |
| - name: Prepare trusted pre-commit config | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| run: | | |
| set -euo pipefail | |
| trusted_config="$RUNNER_TEMP/pre-commit-base.yaml" | |
| git show "${BASE_SHA}:.pre-commit-config.yaml" > "$trusted_config" | |
| echo "PRE_COMMIT_CONFIG_PATH=$trusted_config" >> "$GITHUB_ENV" | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| install-deps: "false" | |
| use-sticky-disk: "false" | |
| - name: Setup Python | |
| id: setup-python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| cache: "pip" | |
| cache-dependency-path: | | |
| pyproject.toml | |
| .pre-commit-config.yaml | |
| .github/workflows/ci.yml | |
| - name: Restore pre-commit cache | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.cache/pre-commit | |
| key: pre-commit-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }}-${{ env.PRE_COMMIT_CACHE_KEY_SUFFIX }} | |
| restore-keys: | | |
| pre-commit-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }}- | |
| - name: Install pre-commit | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install pre-commit==4.2.0 | |
| - name: Detect committed private keys | |
| run: pre-commit run --config "${PRE_COMMIT_CONFIG_PATH:-.pre-commit-config.yaml}" --all-files detect-private-key | |
| - name: Audit changed GitHub workflows with zizmor | |
| env: | |
| BASE_SHA: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${BASE_SHA:-}" ] || [ "${BASE_SHA}" = "0000000000000000000000000000000000000000" ]; then | |
| echo "No usable base SHA detected; skipping zizmor." | |
| exit 0 | |
| fi | |
| if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then | |
| echo "Base SHA ${BASE_SHA} is unavailable; skipping zizmor." | |
| exit 0 | |
| fi | |
| mapfile -t workflow_files < <( | |
| git diff --name-only "${BASE_SHA}" HEAD -- '.github/workflows/*.yml' '.github/workflows/*.yaml' | |
| ) | |
| if [ "${#workflow_files[@]}" -eq 0 ]; then | |
| echo "No workflow changes detected; skipping zizmor." | |
| exit 0 | |
| fi | |
| printf 'Auditing workflow files:\n%s\n' "${workflow_files[@]}" | |
| pre-commit run --config "${PRE_COMMIT_CONFIG_PATH:-.pre-commit-config.yaml}" zizmor --files "${workflow_files[@]}" | |
| - name: Audit production dependencies | |
| run: pre-commit run --config "${PRE_COMMIT_CONFIG_PATH:-.pre-commit-config.yaml}" --all-files pnpm-audit-prod | |
| # Build dist once for Node-relevant changes and share it with downstream jobs. | |
| # Keep this overlapping with the fast correctness lanes so green PRs get heavy | |
| # test/build feedback sooner instead of waiting behind a full `check` pass. | |
| build-artifacts: | |
| permissions: | |
| contents: read | |
| needs: [preflight] | |
| if: needs.preflight.outputs.run_build_artifacts == 'true' | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout | |
| shell: bash | |
| env: | |
| CHECKOUT_REPO: ${{ github.repository }} | |
| CHECKOUT_SHA: ${{ github.sha }} | |
| CHECKOUT_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| workdir="$GITHUB_WORKSPACE" | |
| auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')" | |
| reset_checkout_dir() { | |
| mkdir -p "$workdir" | |
| find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} + | |
| } | |
| checkout_attempt() { | |
| local attempt="$1" | |
| reset_checkout_dir | |
| git init "$workdir" >/dev/null | |
| git config --global --add safe.directory "$workdir" | |
| git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}" | |
| git -C "$workdir" config gc.auto 0 | |
| timeout --signal=TERM 30s git -C "$workdir" \ | |
| -c protocol.version=2 \ | |
| -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \ | |
| fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \ | |
| "+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1 | |
| git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1 | |
| test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1 | |
| echo "checkout attempt ${attempt}/2 succeeded" | |
| } | |
| for attempt in 1 2; do | |
| if checkout_attempt "$attempt"; then | |
| exit 0 | |
| fi | |
| echo "checkout attempt ${attempt}/2 failed" | |
| sleep $((attempt * 5)) | |
| done | |
| echo "checkout failed after 2 attempts" >&2 | |
| exit 1 | |
| - name: Ensure secrets base commit (PR fast path) | |
| if: github.event_name == 'pull_request' | |
| uses: ./.github/actions/ensure-base-commit | |
| with: | |
| base-sha: ${{ github.event.pull_request.base.sha }} | |
| fetch-ref: ${{ github.event.pull_request.base.ref }} | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| use-sticky-disk: "false" | |
| - name: Build dist | |
| run: pnpm build:ci-artifacts | |
| - name: Build Control UI | |
| run: pnpm ui:build | |
| - name: Cache dist build | |
| uses: actions/cache@v5 | |
| with: | |
| path: dist/ | |
| key: ${{ runner.os }}-dist-build-${{ github.sha }} | |
| - name: Upload dist artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: dist-build | |
| path: dist/ | |
| compression-level: 0 | |
| retention-days: 1 | |
| - name: Upload A2UI bundle artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: canvas-a2ui-bundle | |
| path: src/canvas-host/a2ui/ | |
| include-hidden-files: true | |
| retention-days: 1 | |
| checks-fast-core: | |
| permissions: | |
| contents: read | |
| name: ${{ matrix.check_name }} | |
| needs: [preflight] | |
| if: needs.preflight.outputs.run_checks_fast == 'true' | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJson(needs.preflight.outputs.checks_fast_core_matrix) }} | |
| steps: | |
| - name: Checkout | |
| shell: bash | |
| env: | |
| CHECKOUT_REPO: ${{ github.repository }} | |
| CHECKOUT_SHA: ${{ github.sha }} | |
| CHECKOUT_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| workdir="$GITHUB_WORKSPACE" | |
| auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')" | |
| reset_checkout_dir() { | |
| mkdir -p "$workdir" | |
| find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} + | |
| } | |
| checkout_attempt() { | |
| local attempt="$1" | |
| reset_checkout_dir | |
| git init "$workdir" >/dev/null | |
| git config --global --add safe.directory "$workdir" | |
| git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}" | |
| git -C "$workdir" config gc.auto 0 | |
| timeout --signal=TERM 30s git -C "$workdir" \ | |
| -c protocol.version=2 \ | |
| -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \ | |
| fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \ | |
| "+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1 | |
| git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1 | |
| test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1 | |
| echo "checkout attempt ${attempt}/2 succeeded" | |
| } | |
| for attempt in 1 2; do | |
| if checkout_attempt "$attempt"; then | |
| exit 0 | |
| fi | |
| echo "checkout attempt ${attempt}/2 failed" | |
| sleep $((attempt * 5)) | |
| done | |
| echo "checkout failed after 2 attempts" >&2 | |
| exit 1 | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| use-sticky-disk: "false" | |
| - name: Run ${{ matrix.task }} (${{ matrix.runtime }}) | |
| env: | |
| OPENCLAW_TEST_PROJECTS_PARALLEL: 3 | |
| TASK: ${{ matrix.task }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| case "$TASK" in | |
| bundled) | |
| pnpm test:bundled | |
| ;; | |
| contracts) | |
| pnpm test:contracts | |
| ;; | |
| *) | |
| echo "Unsupported checks-fast task: $TASK" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| checks-fast-protocol: | |
| permissions: | |
| contents: read | |
| name: "checks-fast-protocol" | |
| needs: [preflight] | |
| if: needs.preflight.outputs.run_checks_fast == 'true' | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout | |
| shell: bash | |
| env: | |
| CHECKOUT_REPO: ${{ github.repository }} | |
| CHECKOUT_SHA: ${{ github.sha }} | |
| CHECKOUT_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| workdir="$GITHUB_WORKSPACE" | |
| auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')" | |
| reset_checkout_dir() { | |
| mkdir -p "$workdir" | |
| find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} + | |
| } | |
| checkout_attempt() { | |
| local attempt="$1" | |
| reset_checkout_dir | |
| git init "$workdir" >/dev/null | |
| git config --global --add safe.directory "$workdir" | |
| git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}" | |
| git -C "$workdir" config gc.auto 0 | |
| timeout --signal=TERM 30s git -C "$workdir" \ | |
| -c protocol.version=2 \ | |
| -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \ | |
| fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \ | |
| "+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1 | |
| git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1 | |
| test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1 | |
| echo "checkout attempt ${attempt}/2 succeeded" | |
| } | |
| for attempt in 1 2; do | |
| if checkout_attempt "$attempt"; then | |
| exit 0 | |
| fi | |
| echo "checkout attempt ${attempt}/2 failed" | |
| sleep $((attempt * 5)) | |
| done | |
| echo "checkout failed after 2 attempts" >&2 | |
| exit 1 | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| use-sticky-disk: "false" | |
| - name: Run protocol check | |
| run: pnpm protocol:check | |
| checks-node-extensions-shard: | |
| permissions: | |
| contents: read | |
| name: ${{ matrix.check_name }} | |
| needs: [preflight] | |
| if: needs.preflight.outputs.run_checks_fast == 'true' | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJson(needs.preflight.outputs.checks_node_extensions_matrix) }} | |
| steps: | |
| - name: Checkout | |
| shell: bash | |
| env: | |
| CHECKOUT_REPO: ${{ github.repository }} | |
| CHECKOUT_SHA: ${{ github.sha }} | |
| CHECKOUT_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| workdir="$GITHUB_WORKSPACE" | |
| auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')" | |
| reset_checkout_dir() { | |
| mkdir -p "$workdir" | |
| find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} + | |
| } | |
| checkout_attempt() { | |
| local attempt="$1" | |
| reset_checkout_dir | |
| git init "$workdir" >/dev/null | |
| git config --global --add safe.directory "$workdir" | |
| git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}" | |
| git -C "$workdir" config gc.auto 0 | |
| timeout --signal=TERM 30s git -C "$workdir" \ | |
| -c protocol.version=2 \ | |
| -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \ | |
| fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \ | |
| "+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1 | |
| git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1 | |
| test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1 | |
| echo "checkout attempt ${attempt}/2 succeeded" | |
| } | |
| for attempt in 1 2; do | |
| if checkout_attempt "$attempt"; then | |
| exit 0 | |
| fi | |
| echo "checkout attempt ${attempt}/2 failed" | |
| sleep $((attempt * 5)) | |
| done | |
| echo "checkout failed after 2 attempts" >&2 | |
| exit 1 | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| use-sticky-disk: "false" | |
| - name: Run extension shard | |
| env: | |
| OPENCLAW_EXTENSION_BATCH: ${{ matrix.extensions_csv }} | |
| run: pnpm test:extensions:batch -- "$OPENCLAW_EXTENSION_BATCH" | |
| checks-node-extensions: | |
| permissions: | |
| contents: read | |
| name: checks-node-extensions | |
| needs: [preflight, checks-node-extensions-shard] | |
| if: always() && needs.preflight.outputs.run_checks_fast == 'true' | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Verify extension shards | |
| env: | |
| SHARD_RESULT: ${{ needs.checks-node-extensions-shard.result }} | |
| run: | | |
| if [ "$SHARD_RESULT" != "success" ]; then | |
| echo "Extension shard checks failed: $SHARD_RESULT" >&2 | |
| exit 1 | |
| fi | |
| checks: | |
| permissions: | |
| contents: read | |
| name: ${{ matrix.check_name }} | |
| needs: [preflight, build-artifacts] | |
| if: always() && needs.preflight.outputs.run_checks == 'true' && needs.build-artifacts.result == 'success' | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJson(needs.preflight.outputs.checks_matrix) }} | |
| steps: | |
| - name: Skip compatibility lanes on pull requests | |
| if: github.event_name == 'pull_request' && matrix.task == 'compat-node22' | |
| run: echo "Skipping push-only lane on pull requests." | |
| - name: Checkout | |
| if: github.event_name != 'pull_request' || matrix.task != 'compat-node22' | |
| shell: bash | |
| env: | |
| CHECKOUT_REPO: ${{ github.repository }} | |
| CHECKOUT_SHA: ${{ github.sha }} | |
| CHECKOUT_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| workdir="$GITHUB_WORKSPACE" | |
| auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')" | |
| reset_checkout_dir() { | |
| mkdir -p "$workdir" | |
| find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} + | |
| } | |
| checkout_attempt() { | |
| local attempt="$1" | |
| reset_checkout_dir | |
| git init "$workdir" >/dev/null | |
| git config --global --add safe.directory "$workdir" | |
| git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}" | |
| git -C "$workdir" config gc.auto 0 | |
| timeout --signal=TERM 30s git -C "$workdir" \ | |
| -c protocol.version=2 \ | |
| -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \ | |
| fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \ | |
| "+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1 | |
| git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1 | |
| test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1 | |
| echo "checkout attempt ${attempt}/2 succeeded" | |
| } | |
| for attempt in 1 2; do | |
| if checkout_attempt "$attempt"; then | |
| exit 0 | |
| fi | |
| echo "checkout attempt ${attempt}/2 failed" | |
| sleep $((attempt * 5)) | |
| done | |
| echo "checkout failed after 2 attempts" >&2 | |
| exit 1 | |
| - name: Setup Node environment | |
| if: github.event_name != 'pull_request' || matrix.task != 'compat-node22' | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| node-version: "${{ matrix.node_version || '24.x' }}" | |
| cache-key-suffix: "${{ matrix.cache_key_suffix || 'node24' }}" | |
| install-bun: "false" | |
| use-sticky-disk: "false" | |
| - name: Configure Node test resources | |
| if: (github.event_name != 'pull_request' || matrix.task != 'compat-node22') && matrix.runtime == 'node' && (matrix.task == 'test' || matrix.task == 'channels' || matrix.task == 'compat-node22') | |
| env: | |
| TASK: ${{ matrix.task }} | |
| run: | | |
| echo "OPENCLAW_VITEST_MAX_WORKERS=2" >> "$GITHUB_ENV" | |
| if [ "$TASK" = "test" ]; then | |
| echo "OPENCLAW_TEST_PROJECTS_LEAF_SHARDS=1" >> "$GITHUB_ENV" | |
| echo "OPENCLAW_TEST_SKIP_FULL_EXTENSIONS_SHARD=1" >> "$GITHUB_ENV" | |
| fi | |
| if [ "$TASK" = "channels" ]; then | |
| echo "OPENCLAW_VITEST_MAX_WORKERS=1" >> "$GITHUB_ENV" | |
| fi | |
| - name: Download dist artifact | |
| if: matrix.task == 'test' | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: dist-build | |
| path: dist/ | |
| - name: Download A2UI bundle artifact | |
| if: matrix.task == 'test' || matrix.task == 'channels' | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: canvas-a2ui-bundle | |
| path: src/canvas-host/a2ui/ | |
| - name: Run ${{ matrix.task }} (${{ matrix.runtime }}) | |
| if: github.event_name != 'pull_request' || matrix.task != 'compat-node22' | |
| env: | |
| TASK: ${{ matrix.task }} | |
| NODE_OPTIONS: --max-old-space-size=6144 | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| case "$TASK" in | |
| test) | |
| pnpm test | |
| ;; | |
| channels) | |
| pnpm test:channels | |
| ;; | |
| compat-node22) | |
| pnpm build | |
| pnpm ui:build | |
| node openclaw.mjs --help | |
| node openclaw.mjs status --json --timeout 1 | |
| pnpm test:build:singleton | |
| ;; | |
| *) | |
| echo "Unsupported checks task: $TASK" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| checks-node-core-test-shard: | |
| permissions: | |
| contents: read | |
| name: ${{ matrix.check_name }} | |
| needs: [preflight, build-artifacts] | |
| if: always() && needs.preflight.outputs.run_checks == 'true' && needs.build-artifacts.result == 'success' | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_test_matrix) }} | |
| steps: | |
| - name: Checkout | |
| shell: bash | |
| env: | |
| CHECKOUT_REPO: ${{ github.repository }} | |
| CHECKOUT_SHA: ${{ github.sha }} | |
| CHECKOUT_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| workdir="$GITHUB_WORKSPACE" | |
| auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')" | |
| reset_checkout_dir() { | |
| mkdir -p "$workdir" | |
| find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} + | |
| } | |
| checkout_attempt() { | |
| local attempt="$1" | |
| reset_checkout_dir | |
| git init "$workdir" >/dev/null | |
| git config --global --add safe.directory "$workdir" | |
| git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}" | |
| git -C "$workdir" config gc.auto 0 | |
| timeout --signal=TERM 30s git -C "$workdir" \ | |
| -c protocol.version=2 \ | |
| -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \ | |
| fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \ | |
| "+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1 | |
| git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1 | |
| test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1 | |
| echo "checkout attempt ${attempt}/2 succeeded" | |
| } | |
| for attempt in 1 2; do | |
| if checkout_attempt "$attempt"; then | |
| exit 0 | |
| fi | |
| echo "checkout attempt ${attempt}/2 failed" | |
| sleep $((attempt * 5)) | |
| done | |
| echo "checkout failed after 2 attempts" >&2 | |
| exit 1 | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| node-version: "${{ matrix.node_version || '24.x' }}" | |
| cache-key-suffix: "${{ matrix.cache_key_suffix || 'node24' }}" | |
| install-bun: "false" | |
| use-sticky-disk: "false" | |
| - name: Configure Node test resources | |
| run: echo "OPENCLAW_VITEST_MAX_WORKERS=2" >> "$GITHUB_ENV" | |
| - name: Restore dist cache | |
| id: dist-cache | |
| if: matrix.requires_dist == true | |
| uses: actions/cache@v5 | |
| with: | |
| path: dist/ | |
| key: ${{ runner.os }}-dist-build-${{ github.sha }} | |
| - name: Download dist artifact | |
| if: matrix.requires_dist == true && steps.dist-cache.outputs.cache-hit != 'true' | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: dist-build | |
| path: dist/ | |
| - name: Download A2UI bundle artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: canvas-a2ui-bundle | |
| path: src/canvas-host/a2ui/ | |
| - name: Run Node test shard | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=6144 | |
| OPENCLAW_NODE_TEST_CONFIGS_JSON: ${{ toJson(matrix.configs) }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| node --input-type=module <<'EOF' | |
| import { spawnSync } from "node:child_process"; | |
| import { resolveVitestCliEntry, resolveVitestNodeArgs } from "./scripts/run-vitest.mjs"; | |
| const configs = JSON.parse(process.env.OPENCLAW_NODE_TEST_CONFIGS_JSON ?? "[]"); | |
| if (!Array.isArray(configs) || configs.length === 0) { | |
| console.error("Missing node test shard configs"); | |
| process.exit(1); | |
| } | |
| for (const config of configs) { | |
| console.error(`[test] starting ${config}`); | |
| const result = spawnSync( | |
| "pnpm", | |
| [ | |
| "exec", | |
| "node", | |
| ...resolveVitestNodeArgs(process.env), | |
| resolveVitestCliEntry(), | |
| "run", | |
| "--config", | |
| config, | |
| ], | |
| { | |
| env: process.env, | |
| stdio: "inherit", | |
| }, | |
| ); | |
| if ((result.status ?? 1) !== 0) { | |
| process.exit(result.status ?? 1); | |
| } | |
| } | |
| EOF | |
| checks-node-core-test: | |
| permissions: | |
| contents: read | |
| name: checks-node-core | |
| needs: [preflight, checks-node-core-test-shard] | |
| if: always() && needs.preflight.outputs.run_checks == 'true' | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Verify node test shards | |
| env: | |
| SHARD_RESULT: ${{ needs.checks-node-core-test-shard.result }} | |
| run: | | |
| if [ "$SHARD_RESULT" != "success" ]; then | |
| echo "Node test shards failed: $SHARD_RESULT" >&2 | |
| exit 1 | |
| fi | |
| extension-fast: | |
| permissions: | |
| contents: read | |
| name: "extension-fast" | |
| needs: [preflight] | |
| if: needs.preflight.outputs.run_extension_fast == 'true' | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJson(needs.preflight.outputs.extension_fast_matrix) }} | |
| steps: | |
| - name: Checkout | |
| shell: bash | |
| env: | |
| CHECKOUT_REPO: ${{ github.repository }} | |
| CHECKOUT_SHA: ${{ github.sha }} | |
| CHECKOUT_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| workdir="$GITHUB_WORKSPACE" | |
| auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')" | |
| reset_checkout_dir() { | |
| mkdir -p "$workdir" | |
| find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} + | |
| } | |
| checkout_attempt() { | |
| local attempt="$1" | |
| reset_checkout_dir | |
| git init "$workdir" >/dev/null | |
| git config --global --add safe.directory "$workdir" | |
| git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}" | |
| git -C "$workdir" config gc.auto 0 | |
| timeout --signal=TERM 30s git -C "$workdir" \ | |
| -c protocol.version=2 \ | |
| -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \ | |
| fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \ | |
| "+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1 | |
| git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1 | |
| test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1 | |
| echo "checkout attempt ${attempt}/2 succeeded" | |
| } | |
| for attempt in 1 2; do | |
| if checkout_attempt "$attempt"; then | |
| exit 0 | |
| fi | |
| echo "checkout attempt ${attempt}/2 failed" | |
| sleep $((attempt * 5)) | |
| done | |
| echo "checkout failed after 2 attempts" >&2 | |
| exit 1 | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| use-sticky-disk: "false" | |
| - name: Run changed extension tests | |
| env: | |
| OPENCLAW_CHANGED_EXTENSION: ${{ matrix.extension }} | |
| run: pnpm test:extension "$OPENCLAW_CHANGED_EXTENSION" | |
| # Types, lint, and format check. | |
| check: | |
| permissions: | |
| contents: read | |
| name: "check" | |
| needs: [preflight] | |
| if: always() && needs.preflight.outputs.run_check == 'true' | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout | |
| shell: bash | |
| env: | |
| CHECKOUT_REPO: ${{ github.repository }} | |
| CHECKOUT_SHA: ${{ github.sha }} | |
| CHECKOUT_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| workdir="$GITHUB_WORKSPACE" | |
| auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')" | |
| reset_checkout_dir() { | |
| mkdir -p "$workdir" | |
| find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} + | |
| } | |
| checkout_attempt() { | |
| local attempt="$1" | |
| reset_checkout_dir | |
| git init "$workdir" >/dev/null | |
| git config --global --add safe.directory "$workdir" | |
| git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}" | |
| git -C "$workdir" config gc.auto 0 | |
| timeout --signal=TERM 30s git -C "$workdir" \ | |
| -c protocol.version=2 \ | |
| -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \ | |
| fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \ | |
| "+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1 | |
| git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1 | |
| test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1 | |
| echo "checkout attempt ${attempt}/2 succeeded" | |
| } | |
| for attempt in 1 2; do | |
| if checkout_attempt "$attempt"; then | |
| exit 0 | |
| fi | |
| echo "checkout attempt ${attempt}/2 failed" | |
| sleep $((attempt * 5)) | |
| done | |
| echo "checkout failed after 2 attempts" >&2 | |
| exit 1 | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| use-sticky-disk: "false" | |
| - name: Check types and lint and oxfmt | |
| env: | |
| OPENCLAW_LOCAL_CHECK: "0" | |
| run: pnpm check | |
| - name: Strict TS build smoke | |
| run: pnpm build:strict-smoke | |
| check-additional-shard: | |
| permissions: | |
| contents: read | |
| name: ${{ matrix.check_name }} | |
| needs: [preflight] | |
| if: always() && needs.preflight.outputs.run_check_additional == 'true' | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| timeout-minutes: 20 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - check_name: check-additional-boundaries | |
| group: boundaries | |
| - check_name: check-additional-extension-surfaces | |
| group: extension-surfaces | |
| - check_name: check-additional-runtime-topology | |
| group: runtime-topology | |
| steps: | |
| - name: Checkout | |
| shell: bash | |
| env: | |
| CHECKOUT_REPO: ${{ github.repository }} | |
| CHECKOUT_SHA: ${{ github.sha }} | |
| CHECKOUT_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| workdir="$GITHUB_WORKSPACE" | |
| auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')" | |
| reset_checkout_dir() { | |
| mkdir -p "$workdir" | |
| find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} + | |
| } | |
| checkout_attempt() { | |
| local attempt="$1" | |
| reset_checkout_dir | |
| git init "$workdir" >/dev/null | |
| git config --global --add safe.directory "$workdir" | |
| git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}" | |
| git -C "$workdir" config gc.auto 0 | |
| timeout --signal=TERM 30s git -C "$workdir" \ | |
| -c protocol.version=2 \ | |
| -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \ | |
| fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \ | |
| "+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1 | |
| git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1 | |
| test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1 | |
| echo "checkout attempt ${attempt}/2 succeeded" | |
| } | |
| for attempt in 1 2; do | |
| if checkout_attempt "$attempt"; then | |
| exit 0 | |
| fi | |
| echo "checkout attempt ${attempt}/2 failed" | |
| sleep $((attempt * 5)) | |
| done | |
| echo "checkout failed after 2 attempts" >&2 | |
| exit 1 | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| use-sticky-disk: "false" | |
| - name: Run additional check shard | |
| env: | |
| ADDITIONAL_CHECK_GROUP: ${{ matrix.group }} | |
| RUN_CONTROL_UI_I18N: ${{ needs.preflight.outputs.run_control_ui_i18n }} | |
| OPENCLAW_EXTENSION_BOUNDARY_CONCURRENCY: 4 | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| failures=0 | |
| run_check() { | |
| local label="$1" | |
| shift | |
| echo "::group::${label}" | |
| if "$@"; then | |
| echo "[ok] ${label}" | |
| else | |
| echo "::error title=${label} failed::${label} failed" | |
| failures=1 | |
| fi | |
| echo "::endgroup::" | |
| } | |
| case "$ADDITIONAL_CHECK_GROUP" in | |
| boundaries) | |
| run_check "plugin-extension-boundary" pnpm run lint:plugins:no-extension-imports | |
| run_check "lint:tmp:no-random-messaging" pnpm run lint:tmp:no-random-messaging | |
| run_check "lint:tmp:channel-agnostic-boundaries" pnpm run lint:tmp:channel-agnostic-boundaries | |
| run_check "lint:tmp:tsgo-core-boundary" pnpm run lint:tmp:tsgo-core-boundary | |
| run_check "lint:tmp:no-raw-channel-fetch" pnpm run lint:tmp:no-raw-channel-fetch | |
| run_check "lint:agent:ingress-owner" pnpm run lint:agent:ingress-owner | |
| run_check "lint:plugins:no-register-http-handler" pnpm run lint:plugins:no-register-http-handler | |
| run_check "lint:plugins:no-monolithic-plugin-sdk-entry-imports" pnpm run lint:plugins:no-monolithic-plugin-sdk-entry-imports | |
| run_check "lint:plugins:no-extension-src-imports" pnpm run lint:plugins:no-extension-src-imports | |
| run_check "lint:plugins:no-extension-test-core-imports" pnpm run lint:plugins:no-extension-test-core-imports | |
| run_check "lint:plugins:plugin-sdk-subpaths-exported" pnpm run lint:plugins:plugin-sdk-subpaths-exported | |
| run_check "web-search-provider-boundary" pnpm run lint:web-search-provider-boundaries | |
| run_check "web-fetch-provider-boundary" pnpm run lint:web-fetch-provider-boundaries | |
| run_check "extension-src-outside-plugin-sdk-boundary" pnpm run lint:extensions:no-src-outside-plugin-sdk | |
| run_check "extension-plugin-sdk-internal-boundary" pnpm run lint:extensions:no-plugin-sdk-internal | |
| run_check "extension-relative-outside-package-boundary" pnpm run lint:extensions:no-relative-outside-package | |
| run_check "lint:ui:no-raw-window-open" pnpm lint:ui:no-raw-window-open | |
| ;; | |
| extension-surfaces) | |
| run_check "lint:extensions:channels" pnpm run lint:extensions:channels | |
| run_check "lint:extensions:bundled" pnpm run lint:extensions:bundled | |
| run_check "test:extensions:package-boundary" pnpm run test:extensions:package-boundary | |
| ;; | |
| runtime-topology) | |
| if [ "$RUN_CONTROL_UI_I18N" = "true" ]; then | |
| run_check "ui:i18n:check" pnpm ui:i18n:check | |
| fi | |
| run_check "gateway-watch-regression" pnpm test:gateway:watch-regression | |
| run_check "check:import-cycles" pnpm check:import-cycles | |
| run_check "check:madge-import-cycles" pnpm check:madge-import-cycles | |
| ;; | |
| *) | |
| echo "Unsupported additional check group: $ADDITIONAL_CHECK_GROUP" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| exit "$failures" | |
| - name: Upload gateway watch regression artifacts | |
| if: always() && matrix.group == 'runtime-topology' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: gateway-watch-regression | |
| path: .local/gateway-watch-regression/ | |
| retention-days: 7 | |
| check-additional: | |
| permissions: | |
| contents: read | |
| name: "check-additional" | |
| needs: [preflight, check-additional-shard] | |
| if: always() && needs.preflight.outputs.run_check_additional == 'true' | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Verify additional check shards | |
| env: | |
| SHARD_RESULT: ${{ needs.check-additional-shard.result }} | |
| run: | | |
| if [ "$SHARD_RESULT" != "success" ]; then | |
| echo "Additional check shards failed: $SHARD_RESULT" >&2 | |
| exit 1 | |
| fi | |
| build-smoke: | |
| permissions: | |
| contents: read | |
| name: "build-smoke" | |
| needs: [preflight, build-artifacts] | |
| if: always() && needs.preflight.outputs.run_build_smoke == 'true' && (github.event_name != 'push' || needs.build-artifacts.result == 'success') | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout | |
| shell: bash | |
| env: | |
| CHECKOUT_REPO: ${{ github.repository }} | |
| CHECKOUT_SHA: ${{ github.sha }} | |
| CHECKOUT_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| workdir="$GITHUB_WORKSPACE" | |
| auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')" | |
| reset_checkout_dir() { | |
| mkdir -p "$workdir" | |
| find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} + | |
| } | |
| checkout_attempt() { | |
| local attempt="$1" | |
| reset_checkout_dir | |
| git init "$workdir" >/dev/null | |
| git config --global --add safe.directory "$workdir" | |
| git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}" | |
| git -C "$workdir" config gc.auto 0 | |
| timeout --signal=TERM 30s git -C "$workdir" \ | |
| -c protocol.version=2 \ | |
| -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \ | |
| fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \ | |
| "+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1 | |
| git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1 | |
| test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1 | |
| echo "checkout attempt ${attempt}/2 succeeded" | |
| } | |
| for attempt in 1 2; do | |
| if checkout_attempt "$attempt"; then | |
| exit 0 | |
| fi | |
| echo "checkout attempt ${attempt}/2 failed" | |
| sleep $((attempt * 5)) | |
| done | |
| echo "checkout failed after 2 attempts" >&2 | |
| exit 1 | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| use-sticky-disk: "false" | |
| - name: Restore dist cache | |
| id: build-smoke-dist-cache | |
| if: github.event_name == 'push' | |
| uses: actions/cache@v5 | |
| with: | |
| path: dist/ | |
| key: ${{ runner.os }}-dist-build-${{ github.sha }} | |
| - name: Download dist artifact | |
| if: github.event_name == 'push' && steps.build-smoke-dist-cache.outputs.cache-hit != 'true' | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: dist-build | |
| path: dist/ | |
| - name: Build dist | |
| if: github.event_name != 'push' | |
| run: pnpm build | |
| - name: Smoke test CLI launcher help | |
| run: node openclaw.mjs --help | |
| - name: Smoke test CLI launcher status json | |
| run: node openclaw.mjs status --json --timeout 1 | |
| - name: Smoke test built bundled plugin singleton | |
| run: pnpm test:build:singleton | |
| - name: Smoke test built bundled runtime deps | |
| run: pnpm test:build:bundled-runtime-deps | |
| - name: Check CLI startup memory | |
| run: pnpm test:startup:memory | |
| # Validate docs (format, lint, broken links) only when docs files changed. | |
| check-docs: | |
| permissions: | |
| contents: read | |
| needs: [preflight] | |
| if: needs.preflight.outputs.run_check_docs == 'true' | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout | |
| shell: bash | |
| env: | |
| CHECKOUT_REPO: ${{ github.repository }} | |
| CHECKOUT_SHA: ${{ github.sha }} | |
| CHECKOUT_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| workdir="$GITHUB_WORKSPACE" | |
| auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')" | |
| reset_checkout_dir() { | |
| mkdir -p "$workdir" | |
| find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} + | |
| } | |
| checkout_attempt() { | |
| local attempt="$1" | |
| reset_checkout_dir | |
| git init "$workdir" >/dev/null | |
| git config --global --add safe.directory "$workdir" | |
| git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}" | |
| git -C "$workdir" config gc.auto 0 | |
| timeout --signal=TERM 30s git -C "$workdir" \ | |
| -c protocol.version=2 \ | |
| -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \ | |
| fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \ | |
| "+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1 | |
| git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1 | |
| test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1 | |
| echo "checkout attempt ${attempt}/2 succeeded" | |
| } | |
| for attempt in 1 2; do | |
| if checkout_attempt "$attempt"; then | |
| exit 0 | |
| fi | |
| echo "checkout attempt ${attempt}/2 failed" | |
| sleep $((attempt * 5)) | |
| done | |
| echo "checkout failed after 2 attempts" >&2 | |
| exit 1 | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| use-sticky-disk: "false" | |
| - name: Check docs | |
| run: pnpm check:docs | |
| skills-python: | |
| permissions: | |
| contents: read | |
| needs: [preflight] | |
| if: needs.preflight.outputs.run_skills_python_job == 'true' | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| submodules: false | |
| - name: Setup Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| - name: Install Python tooling | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install pytest ruff pyyaml | |
| - name: Lint Python skill scripts | |
| run: python -m ruff check skills | |
| - name: Test skill Python scripts | |
| run: python -m pytest -q skills | |
| checks-windows: | |
| permissions: | |
| contents: read | |
| name: ${{ matrix.check_name }} | |
| needs: [preflight, build-artifacts] | |
| if: always() && needs.preflight.outputs.run_checks_windows == 'true' && needs.build-artifacts.result == 'success' | |
| runs-on: blacksmith-32vcpu-windows-2025 | |
| timeout-minutes: 60 | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=6144 | |
| # Keep total concurrency predictable on the 32 vCPU runner. | |
| OPENCLAW_VITEST_MAX_WORKERS: 1 | |
| OPENCLAW_TEST_SKIP_FULL_EXTENSIONS_SHARD: 1 | |
| defaults: | |
| run: | |
| shell: bash | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJson(needs.preflight.outputs.checks_windows_matrix) }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| submodules: false | |
| - name: Try to exclude workspace from Windows Defender (best-effort) | |
| shell: pwsh | |
| run: | | |
| $cmd = Get-Command Add-MpPreference -ErrorAction SilentlyContinue | |
| if (-not $cmd) { | |
| Write-Host "Add-MpPreference not available, skipping Defender exclusions." | |
| exit 0 | |
| } | |
| try { | |
| # Defender sometimes intercepts process spawning (vitest workers). If this fails | |
| # (eg hardened images), keep going and rely on worker limiting above. | |
| Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE" -ErrorAction Stop | |
| Add-MpPreference -ExclusionProcess "node.exe" -ErrorAction Stop | |
| Write-Host "Defender exclusions applied." | |
| } catch { | |
| Write-Warning "Failed to apply Defender exclusions, continuing. $($_.Exception.Message)" | |
| } | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24.x | |
| check-latest: false | |
| - name: Setup pnpm + cache store | |
| uses: ./.github/actions/setup-pnpm-store-cache | |
| with: | |
| pnpm-version: "10.32.1" | |
| cache-key-suffix: "node24" | |
| # Sticky disk mount currently retries/fails on every shard and adds ~50s | |
| # before install while still yielding zero pnpm store reuse. | |
| # Try exact-key actions/cache restores instead to recover store reuse | |
| # without the sticky-disk mount penalty. | |
| use-sticky-disk: "false" | |
| use-restore-keys: "false" | |
| use-actions-cache: "true" | |
| - name: Runtime versions | |
| run: | | |
| node -v | |
| npm -v | |
| pnpm -v | |
| - name: Capture node path | |
| run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV" | |
| - name: Install dependencies | |
| env: | |
| CI: true | |
| run: | | |
| export PATH="$NODE_BIN:$PATH" | |
| which node | |
| node -v | |
| pnpm -v | |
| # Persist Windows-native postinstall outputs in the pnpm store so restored | |
| # caches can skip repeated rebuild/download work on later shards/runs. | |
| pnpm install --frozen-lockfile --prefer-offline --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true --config.side-effects-cache=true || pnpm install --frozen-lockfile --prefer-offline --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true --config.side-effects-cache=true | |
| - name: Download dist artifact | |
| if: matrix.task == 'test' | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: dist-build | |
| path: dist/ | |
| - name: Download A2UI bundle artifact | |
| if: matrix.task == 'test' | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: canvas-a2ui-bundle | |
| path: src/canvas-host/a2ui/ | |
| - name: Run ${{ matrix.task }} (${{ matrix.runtime }}) | |
| env: | |
| TASK: ${{ matrix.task }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| case "$TASK" in | |
| test) | |
| # Linux owns the full repo test suite. Keep the Windows runner focused on | |
| # Windows-native process/path wrappers so platform regressions fail fast. | |
| pnpm test:windows:ci | |
| ;; | |
| *) | |
| echo "Unsupported Windows checks task: $TASK" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| macos-node: | |
| permissions: | |
| contents: read | |
| name: ${{ matrix.check_name }} | |
| needs: [preflight, build-artifacts] | |
| if: always() && needs.preflight.outputs.run_macos_node == 'true' && needs.build-artifacts.result == 'success' | |
| runs-on: blacksmith-6vcpu-macos-latest | |
| timeout-minutes: 20 | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJson(needs.preflight.outputs.macos_node_matrix) }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| submodules: false | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| install-bun: "false" | |
| - name: Download dist artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: dist-build | |
| path: dist/ | |
| - name: Download A2UI bundle artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: canvas-a2ui-bundle | |
| path: src/canvas-host/a2ui/ | |
| - name: Patch mlx-audio-swift manifest | |
| run: | | |
| set -euo pipefail | |
| swift package resolve --package-path apps/macos >/dev/null | |
| chmod u+w apps/macos/.build/checkouts/mlx-audio-swift/Package.swift | |
| python <<'PY' | |
| from pathlib import Path | |
| path = Path("apps/macos/.build/checkouts/mlx-audio-swift/Package.swift") | |
| text = path.read_text() | |
| if "Models/Qwen3/README.md" in text: | |
| print("mlx-audio-swift README excludes already present") | |
| raise SystemExit(0) | |
| needle = ' path: "Sources/MLXAudioTTS"\n' | |
| replacement = """ path: \"Sources/MLXAudioTTS\",\n exclude: [\n \"Models/Llama/README.md\",\n \"Models/Marvis/README.md\",\n \"Models/PocketTTS/README.md\",\n \"Models/Qwen3/README.md\",\n \"Models/Soprano/README.md\",\n ]\n""" | |
| if needle not in text: | |
| raise SystemExit("Could not find MLXAudioTTS target path in mlx-audio-swift Package.swift") | |
| path.write_text(text.replace(needle, replacement, 1)) | |
| print(f"Patched {path}") | |
| PY | |
| - name: TS tests (macOS) | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=4096 | |
| OPENCLAW_VITEST_MAX_WORKERS: 2 | |
| TASK: ${{ matrix.task }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| case "$TASK" in | |
| test) | |
| # Linux owns the full repo test suite. Keep macOS CI focused on | |
| # launchd/Homebrew/runtime path coverage and the process-group wrapper. | |
| pnpm test:macos:ci | |
| ;; | |
| *) | |
| echo "Unsupported macOS node task: $TASK" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| macos-swift: | |
| permissions: | |
| contents: read | |
| name: "macos-swift" | |
| needs: [preflight] | |
| if: needs.preflight.outputs.run_macos_swift == 'true' | |
| runs-on: blacksmith-12vcpu-macos-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| submodules: false | |
| - name: Install XcodeGen / SwiftLint / SwiftFormat | |
| run: brew install xcodegen swiftlint swiftformat | |
| - name: Detect Swift toolchain cache key | |
| id: swift-toolchain | |
| run: | | |
| set -euo pipefail | |
| xcode_version="$(xcodebuild -version | tr '\n' ' ' | sed 's/ */ /g; s/ $//')" | |
| swift_version="$(swift --version | head -n 1)" | |
| toolchain_key="$(printf '%s\n%s\n' "$xcode_version" "$swift_version" | shasum -a 256 | awk '{print $1}')" | |
| echo "key=$toolchain_key" >> "$GITHUB_OUTPUT" | |
| - name: Cache SwiftPM | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/Library/Caches/org.swift.swiftpm | |
| key: ${{ runner.os }}-swiftpm-${{ hashFiles('apps/macos/Package.resolved') }} | |
| restore-keys: | | |
| ${{ runner.os }}-swiftpm- | |
| - name: Cache Swift build directory | |
| uses: actions/cache@v5 | |
| with: | |
| path: apps/macos/.build | |
| key: ${{ runner.os }}-swift-build-v1-${{ steps.swift-toolchain.outputs.key }}-${{ hashFiles('apps/macos/Package.swift', 'apps/macos/Package.resolved', 'apps/shared/OpenClawKit/Package.swift', 'Swabble/Package.swift') }} | |
| restore-keys: | | |
| ${{ runner.os }}-swift-build-v1-${{ steps.swift-toolchain.outputs.key }}- | |
| - name: Patch mlx-audio-swift manifest | |
| run: | | |
| set -euo pipefail | |
| if [ ! -f apps/macos/.build/checkouts/mlx-audio-swift/Package.swift ]; then | |
| swift package resolve --package-path apps/macos >/dev/null | |
| fi | |
| if [ ! -f apps/macos/.build/checkouts/mlx-audio-swift/Package.swift ]; then | |
| echo "mlx-audio-swift checkout missing after swift package resolve" >&2 | |
| exit 1 | |
| fi | |
| chmod u+w apps/macos/.build/checkouts/mlx-audio-swift/Package.swift | |
| python <<'PY' | |
| from pathlib import Path | |
| path = Path("apps/macos/.build/checkouts/mlx-audio-swift/Package.swift") | |
| text = path.read_text() | |
| if "Models/Qwen3/README.md" in text: | |
| print("mlx-audio-swift README excludes already present") | |
| raise SystemExit(0) | |
| needle = ' path: "Sources/MLXAudioTTS"\n' | |
| replacement = """ path: \"Sources/MLXAudioTTS\",\n exclude: [\n \"Models/Llama/README.md\",\n \"Models/Marvis/README.md\",\n \"Models/PocketTTS/README.md\",\n \"Models/Qwen3/README.md\",\n \"Models/Soprano/README.md\",\n ]\n""" | |
| if needle not in text: | |
| raise SystemExit("Could not find MLXAudioTTS target path in mlx-audio-swift Package.swift") | |
| path.write_text(text.replace(needle, replacement, 1)) | |
| print(f"Patched {path}") | |
| PY | |
| - name: Show toolchain | |
| run: | | |
| sw_vers | |
| xcodebuild -version | |
| swift --version | |
| - name: Swift lint | |
| run: | | |
| swiftlint --config .swiftlint.yml | |
| swiftformat --lint apps/macos/Sources --config .swiftformat | |
| - name: Swift build (release) | |
| run: | | |
| set -euo pipefail | |
| for attempt in 1 2 3; do | |
| # The macOS lane validates the desktop app build; the CLI product is | |
| # intentionally left to its own narrower surfaces instead of making | |
| # this lane rebuild the whole package graph. | |
| if swift build --package-path apps/macos --product OpenClaw --configuration release; then | |
| exit 0 | |
| fi | |
| echo "swift build failed (attempt $attempt/3). Retrying…" | |
| sleep $((attempt * 20)) | |
| done | |
| exit 1 | |
| - name: Swift test | |
| run: | | |
| set -euo pipefail | |
| for attempt in 1 2 3; do | |
| if swift test --package-path apps/macos --parallel --enable-code-coverage --show-codecov-path; then | |
| exit 0 | |
| fi | |
| echo "swift test failed (attempt $attempt/3). Retrying…" | |
| sleep $((attempt * 20)) | |
| done | |
| exit 1 | |
| android: | |
| permissions: | |
| contents: read | |
| name: ${{ matrix.check_name }} | |
| needs: [preflight] | |
| if: needs.preflight.outputs.run_android_job == 'true' | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| timeout-minutes: 20 | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJson(needs.preflight.outputs.android_matrix) }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| submodules: false | |
| - name: Setup Java | |
| uses: actions/setup-java@v5 | |
| with: | |
| distribution: temurin | |
| # Keep sdkmanager on the stable JDK path for Linux CI runners. | |
| java-version: 17 | |
| - name: Setup Android SDK cmdline-tools | |
| run: | | |
| set -euo pipefail | |
| ANDROID_SDK_ROOT="$HOME/.android-sdk" | |
| CMDLINE_TOOLS_VERSION="12266719" | |
| ARCHIVE="commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip" | |
| URL="https://dl.google.com/android/repository/${ARCHIVE}" | |
| mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools" | |
| curl -fsSL "$URL" -o "/tmp/${ARCHIVE}" | |
| rm -rf "$ANDROID_SDK_ROOT/cmdline-tools/latest" | |
| unzip -q "/tmp/${ARCHIVE}" -d "$ANDROID_SDK_ROOT/cmdline-tools" | |
| mv "$ANDROID_SDK_ROOT/cmdline-tools/cmdline-tools" "$ANDROID_SDK_ROOT/cmdline-tools/latest" | |
| echo "ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT" >> "$GITHUB_ENV" | |
| echo "ANDROID_HOME=$ANDROID_SDK_ROOT" >> "$GITHUB_ENV" | |
| echo "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin" >> "$GITHUB_PATH" | |
| echo "$ANDROID_SDK_ROOT/platform-tools" >> "$GITHUB_PATH" | |
| - name: Setup Gradle | |
| uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5 | |
| with: | |
| gradle-version: 8.11.1 | |
| - name: Install Android SDK packages | |
| run: | | |
| yes | sdkmanager --sdk_root="${ANDROID_SDK_ROOT}" --licenses >/dev/null | |
| sdkmanager --sdk_root="${ANDROID_SDK_ROOT}" --install \ | |
| "platform-tools" \ | |
| "platforms;android-36" \ | |
| "build-tools;36.0.0" | |
| - name: Run Android ${{ matrix.task }} | |
| working-directory: apps/android | |
| env: | |
| TASK: ${{ matrix.task }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| case "$TASK" in | |
| test-play) | |
| ./gradlew --no-daemon :app:testPlayDebugUnitTest | |
| ;; | |
| test-third-party) | |
| ./gradlew --no-daemon :app:testThirdPartyDebugUnitTest | |
| ;; | |
| build-play) | |
| ./gradlew --no-daemon :app:assemblePlayDebug | |
| ;; | |
| build-third-party) | |
| ./gradlew --no-daemon :app:assembleThirdPartyDebug | |
| ;; | |
| *) | |
| echo "Unsupported Android task: $TASK" >&2 | |
| exit 1 | |
| ;; | |
| esac |