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) || (github.repository == 'openclaw/openclaw' && format('{0}-{1}', github.workflow, github.ref) || format('{0}-{1}-{2}', github.workflow, github.ref, github.sha)) }} cancel-in-progress: true 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: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} 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 }} channel_contracts_matrix: ${{ steps.manifest.outputs.channel_contracts_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 }} run_checks_node_core_nondist: ${{ steps.manifest.outputs.run_checks_node_core_nondist }} checks_node_core_nondist_matrix: ${{ steps.manifest.outputs.checks_node_core_nondist_matrix }} run_checks_node_core_dist: ${{ steps.manifest.outputs.run_checks_node_core_dist }} checks_node_core_dist_matrix: ${{ steps.manifest.outputs.checks_node_core_dist_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" - 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":[]}' }} OPENCLAW_CI_REPOSITORY: ${{ github.repository }} run: | node --input-type=module <<'EOF' import { appendFileSync } from "node:fs"; import { createNodeTestShards, } from "./scripts/lib/ci-node-test-plan.mjs"; import { createChannelContractTestShards, } from "./scripts/lib/channel-contract-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 isCanonicalRepository = process.env.OPENCLAW_CI_REPOSITORY === "openclaw/openclaw"; 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 && isCanonicalRepository; const runAndroid = parseBoolean(process.env.OPENCLAW_CI_RUN_ANDROID) && !docsOnly && isCanonicalRepository; const runWindows = parseBoolean(process.env.OPENCLAW_CI_RUN_WINDOWS) && !docsOnly && isCanonicalRepository; 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 extensionTestShardCount = isCanonicalRepository ? DEFAULT_EXTENSION_TEST_SHARD_COUNT : Math.max(DEFAULT_EXTENSION_TEST_SHARD_COUNT, 36); const extensionShardMatrix = createMatrix( runNode ? createExtensionTestShards({ shardCount: extensionTestShardCount, }).map((shard) => ({ check_name: shard.checkName, extensions_csv: shard.extensionIds.join(","), shard_index: shard.index + 1, task: "extensions-batch", })) : [], ); const nodeTestShards = runNode ? createNodeTestShards().map((shard) => ({ check_name: shard.checkName, runtime: "node", task: "test-shard", shard_name: shard.shardName, configs: shard.configs, includePatterns: shard.includePatterns, requires_dist: shard.requiresDist, })) : []; const nodeTestNonDistShards = nodeTestShards.filter((shard) => !shard.requires_dist); const nodeTestDistShards = nodeTestShards.filter((shard) => shard.requires_dist); 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-plugins", runtime: "node", task: "contracts-plugins", }, ] : [], ), channel_contracts_matrix: createMatrix(runNode ? createChannelContractTestShards() : []), 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", }, ] : []), ] : [], ), run_checks_node_core_nondist: nodeTestNonDistShards.length > 0, checks_node_core_nondist_matrix: createMatrix(nodeTestNonDistShards), run_checks_node_core_dist: nodeTestDistShards.length > 0, checks_node_core_dist_matrix: createMatrix(nodeTestDistShards), 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-scm-fast: permissions: contents: read if: github.event_name != 'pull_request' || !github.event.pull_request.draft runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 20 env: PRE_COMMIT_HOME: .cache/pre-commit-security-fast 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 Python id: setup-python uses: actions/setup-python@v6 with: python-version: "3.12" - name: Restore pre-commit cache uses: actions/cache@v5 with: path: .cache/pre-commit-security-fast key: pre-commit-security-fast-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }} restore-keys: | pre-commit-security-fast-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}- - name: Install pre-commit run: python -m pip install --disable-pip-version-check 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[@]}" security-dependency-audit: permissions: contents: read if: github.event_name != 'pull_request' || !github.event.pull_request.draft runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 1 fetch-tags: false persist-credentials: false submodules: false - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: "24.x" check-latest: false - name: Audit production dependencies run: node scripts/pre-commit/pnpm-audit-prod.mjs --audit-level=high security-fast: permissions: {} needs: [security-scm-fast, security-dependency-audit] if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 5 steps: - name: Verify fast security jobs env: DEPENDENCY_AUDIT_RESULT: ${{ needs.security-dependency-audit.result }} SCM_RESULT: ${{ needs.security-scm-fast.result }} run: | set -euo pipefail failed=0 for result in \ "security-scm-fast=${SCM_RESULT}" \ "security-dependency-audit=${DEPENDENCY_AUDIT_RESULT}" do job="${result%%=*}" status="${result#*=}" if [ "$status" != "success" ]; then echo "::error::${job} ended with ${status}" failed=1 fi done exit "$failed" # 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: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} 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" - 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: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} 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" - 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-channels) pnpm test:contracts:channels ;; contracts-plugins) pnpm test:contracts:plugins ;; *) echo "Unsupported checks-fast task: $TASK" >&2 exit 1 ;; esac checks-fast-channel-contracts-shard: permissions: contents: read name: ${{ matrix.checkName }} needs: [preflight] if: needs.preflight.outputs.run_checks_fast == 'true' runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 60 strategy: fail-fast: false matrix: ${{ fromJson(needs.preflight.outputs.channel_contracts_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" - name: Run channel contract shard env: OPENCLAW_CONTRACT_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }} shell: bash run: | set -euo pipefail include_file="$RUNNER_TEMP/channel-contract-include.json" INCLUDE_FILE="$include_file" node --input-type=module <<'EOF' import { writeFileSync } from "node:fs"; const includePatterns = JSON.parse(process.env.OPENCLAW_CONTRACT_INCLUDE_PATTERNS_JSON ?? "[]"); if (!Array.isArray(includePatterns) || includePatterns.length === 0) { console.error("Missing channel contract include patterns"); process.exit(1); } writeFileSync(process.env.INCLUDE_FILE, JSON.stringify(includePatterns), "utf8"); EOF OPENCLAW_VITEST_INCLUDE_FILE="$include_file" pnpm test:contracts:channels checks-fast-channel-contracts: permissions: contents: read name: checks-fast-contracts-channels needs: [preflight, checks-fast-channel-contracts-shard] if: always() && needs.preflight.outputs.run_checks_fast == 'true' runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 5 steps: - name: Verify channel contract shards env: SHARD_RESULT: ${{ needs.checks-fast-channel-contracts-shard.result }} run: | if [ "$SHARD_RESULT" = "cancelled" ]; then echo "Channel contract shards were cancelled, usually because a newer commit superseded this run." >&2 exit 1 fi if [ "$SHARD_RESULT" != "success" ]; then echo "Channel contract shards failed: $SHARD_RESULT" >&2 exit 1 fi checks-fast-protocol: permissions: contents: read name: "checks-fast-protocol" needs: [preflight] if: needs.preflight.outputs.run_checks_fast == 'true' runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} 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" - 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: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} 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" - 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: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} 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: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} 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" - 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-nondist-shard: permissions: contents: read name: ${{ matrix.check_name }} needs: [preflight] if: needs.preflight.outputs.run_checks_node_core_nondist == 'true' runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 60 strategy: fail-fast: false matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_nondist_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" - name: Configure Node test resources run: echo "OPENCLAW_VITEST_MAX_WORKERS=2" >> "$GITHUB_ENV" - name: Run Node test shard env: NODE_OPTIONS: --max-old-space-size=6144 OPENCLAW_NODE_TEST_CONFIGS_JSON: ${{ toJson(matrix.configs) }} OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }} shell: bash run: | set -euo pipefail node --input-type=module <<'EOF' import { spawnSync } from "node:child_process"; import { writeFileSync } from "node:fs"; import { join } from "node:path"; 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); } const includePatterns = JSON.parse(process.env.OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON ?? "null"); const childEnv = { ...process.env }; if (Array.isArray(includePatterns) && includePatterns.length > 0) { const includeFile = join( process.env.RUNNER_TEMP ?? ".", `node-test-include-${process.env.GITHUB_JOB ?? "local"}-${Date.now()}.json`, ); writeFileSync(includeFile, JSON.stringify(includePatterns), "utf8"); childEnv.OPENCLAW_VITEST_INCLUDE_FILE = includeFile; } for (const config of configs) { console.error(`[test] starting ${config}`); const result = spawnSync( "pnpm", [ "exec", "node", ...resolveVitestNodeArgs(process.env), resolveVitestCliEntry(), "run", "--config", config, ], { env: childEnv, stdio: "inherit", }, ); if ((result.status ?? 1) !== 0) { process.exit(result.status ?? 1); } } EOF checks-node-core-test-dist-shard: permissions: contents: read name: ${{ matrix.check_name }} needs: [preflight, build-artifacts] if: always() && needs.preflight.outputs.run_checks_node_core_dist == 'true' && needs.build-artifacts.result == 'success' runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 60 strategy: fail-fast: false matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_dist_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" - name: Configure Node test resources run: echo "OPENCLAW_VITEST_MAX_WORKERS=2" >> "$GITHUB_ENV" - name: Restore dist cache id: dist-cache uses: actions/cache@v5 with: path: dist/ key: ${{ runner.os }}-dist-build-${{ github.sha }} - name: Download dist artifact if: 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) }} OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }} shell: bash run: | set -euo pipefail node --input-type=module <<'EOF' import { spawnSync } from "node:child_process"; import { writeFileSync } from "node:fs"; import { join } from "node:path"; 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); } const includePatterns = JSON.parse(process.env.OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON ?? "null"); const childEnv = { ...process.env }; if (Array.isArray(includePatterns) && includePatterns.length > 0) { const includeFile = join( process.env.RUNNER_TEMP ?? ".", `node-test-include-${process.env.GITHUB_JOB ?? "local"}-${Date.now()}.json`, ); writeFileSync(includeFile, JSON.stringify(includePatterns), "utf8"); childEnv.OPENCLAW_VITEST_INCLUDE_FILE = includeFile; } for (const config of configs) { console.error(`[test] starting ${config}`); const result = spawnSync( "pnpm", [ "exec", "node", ...resolveVitestNodeArgs(process.env), resolveVitestCliEntry(), "run", "--config", config, ], { env: childEnv, 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-nondist-shard, checks-node-core-test-dist-shard] if: always() && needs.preflight.outputs.run_checks == 'true' runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 5 steps: - name: Verify node test shards env: DIST_SHARD_RESULT: ${{ needs.checks-node-core-test-dist-shard.result }} NONDIST_SHARD_RESULT: ${{ needs.checks-node-core-test-nondist-shard.result }} RUN_DIST_SHARDS: ${{ needs.preflight.outputs.run_checks_node_core_dist }} RUN_NONDIST_SHARDS: ${{ needs.preflight.outputs.run_checks_node_core_nondist }} run: | if [ "$RUN_NONDIST_SHARDS" = "true" ] && [ "$NONDIST_SHARD_RESULT" != "success" ]; then echo "Node non-dist test shards failed: $NONDIST_SHARD_RESULT" >&2 exit 1 fi if [ "$RUN_DIST_SHARDS" = "true" ] && [ "$DIST_SHARD_RESULT" != "success" ]; then echo "Node dist test shards failed: $DIST_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: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} 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" - name: Run changed extension tests env: OPENCLAW_CHANGED_EXTENSION: ${{ matrix.extension }} run: pnpm test:extension "$OPENCLAW_CHANGED_EXTENSION" # Types, lint, and format check shards. check-shard: permissions: contents: read name: ${{ matrix.check_name }} needs: [preflight] if: always() && needs.preflight.outputs.run_check == 'true' runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 20 strategy: fail-fast: false matrix: include: - check_name: check-preflight-guards task: preflight-guards - check_name: check-prod-types task: prod-types - check_name: check-lint task: lint - check_name: check-policy-guards task: policy-guards - check_name: check-test-types task: test-types - check_name: check-strict-smoke task: strict-smoke 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" - name: Run check shard env: OPENCLAW_LOCAL_CHECK: "0" TASK: ${{ matrix.task }} shell: bash run: | set -euo pipefail case "$TASK" in preflight-guards) pnpm check:no-conflict-markers pnpm tool-display:check pnpm check:host-env-policy:swift ;; prod-types) pnpm tsgo:prod ;; lint) pnpm lint ;; policy-guards) pnpm lint:webhook:no-low-level-body-read pnpm lint:auth:no-pairing-store-group pnpm lint:auth:pairing-account-scope pnpm check:import-cycles ;; test-types) pnpm check:test-types ;; strict-smoke) pnpm build:strict-smoke ;; *) echo "Unsupported check task: $TASK" >&2 exit 1 ;; esac check: permissions: contents: read name: "check" needs: [preflight, check-shard] if: always() && needs.preflight.outputs.run_check == 'true' runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 5 steps: - name: Verify check shards env: SHARD_RESULT: ${{ needs.check-shard.result }} run: | if [ "$SHARD_RESULT" != "success" ]; then echo "Check shards failed: $SHARD_RESULT" >&2 exit 1 fi check-additional-shard: permissions: contents: read name: ${{ matrix.check_name }} needs: [preflight] if: always() && needs.preflight.outputs.run_check_additional == 'true' runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 20 strategy: fail-fast: false matrix: include: - check_name: check-additional-boundaries group: boundaries - check_name: check-additional-extension-channels group: extension-channels - check_name: check-additional-extension-bundled group: extension-bundled - check_name: check-additional-extension-package-boundary-compile group: extension-package-boundary-compile - check_name: check-additional-extension-package-boundary-canary group: extension-package-boundary-canary - check_name: check-additional-runtime-topology-gateway group: runtime-topology-gateway - check_name: check-additional-runtime-topology-architecture group: runtime-topology-architecture 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" - 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-channels) run_check "lint:extensions:channels" pnpm run lint:extensions:channels ;; extension-bundled) run_check "lint:extensions:bundled" pnpm run lint:extensions:bundled ;; extension-package-boundary-compile) run_check "test:extensions:package-boundary:compile" pnpm run test:extensions:package-boundary:compile ;; extension-package-boundary-canary) run_check "test:extensions:package-boundary:canary" pnpm run test:extensions:package-boundary:canary ;; runtime-topology-gateway) 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 ;; runtime-topology-architecture) run_check "check:architecture" pnpm check:architecture ;; *) 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-gateway' 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: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} 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: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} 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" - 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: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} 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" - 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: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} 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: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-32vcpu-windows-2025' || '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" use-restore-keys: "false" use-actions-cache: "true" - name: Runtime versions run: | node -v npm -v pnpm -v - name: Capture node path run: | node_bin="$(dirname "$(node -p 'process.execPath')")" if command -v cygpath >/dev/null 2>&1; then node_bin="$(cygpath -u "$node_bin")" fi echo "NODE_BIN=$node_bin" >> "$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: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-6vcpu-macos-latest' || '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: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-latest' || '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: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }} 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