name: CI on: workflow_dispatch: inputs: target_ref: description: Optional branch, tag, or full commit SHA to validate instead of the workflow ref required: false default: "" type: string include_android: description: Run Android lanes for this manual CI dispatch. required: false default: false type: boolean push: branches: [main] paths-ignore: - "**/*.md" - "docs/**" pull_request: types: [opened, reopened, synchronize, ready_for_review, converted_to_draft] permissions: contents: read concurrency: group: ${{ github.event_name == 'workflow_dispatch' && format('{0}-manual-v1-{1}', github.workflow, github.run_id) || (github.event_name == 'pull_request' && format('{0}-v7-{1}', github.workflow, github.event.pull_request.number) || (github.repository == 'openclaw/openclaw' && format('{0}-v7-{1}', github.workflow, github.ref) || format('{0}-v7-{1}-{2}', github.workflow, github.ref, github.sha))) }} cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }} 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: ubuntu-24.04 timeout-minutes: 20 outputs: checkout_revision: ${{ steps.checkout_ref.outputs.sha }} 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 }} run_build_artifacts: ${{ steps.manifest.outputs.run_build_artifacts }} run_checks_fast_core: ${{ steps.manifest.outputs.run_checks_fast_core }} run_checks_fast: ${{ steps.manifest.outputs.run_checks_fast }} checks_fast_core_matrix: ${{ steps.manifest.outputs.checks_fast_core_matrix }} run_plugin_contracts_shards: ${{ steps.manifest.outputs.run_plugin_contracts_shards }} plugin_contracts_matrix: ${{ steps.manifest.outputs.plugin_contracts_matrix }} channel_contracts_matrix: ${{ steps.manifest.outputs.channel_contracts_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_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: ref: ${{ inputs.target_ref || github.sha }} fetch-depth: 1 fetch-tags: false persist-credentials: false submodules: false - name: Resolve checkout SHA id: checkout_ref run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - name: Ensure preflight base commit if: github.event_name != 'workflow_dispatch' 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 if: github.event_name != 'workflow_dispatch' uses: ./.github/actions/detect-docs-changes - name: Detect changed scopes id: changed_scope if: github.event_name != 'workflow_dispatch' && 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: Build CI manifest id: manifest env: OPENCLAW_CI_DOCS_ONLY: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.docs_scope.outputs.docs_only }} OPENCLAW_CI_DOCS_CHANGED: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.docs_scope.outputs.docs_changed }} OPENCLAW_CI_RUN_NODE: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_node || 'false' }} OPENCLAW_CI_RUN_MACOS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_macos || 'false' }} OPENCLAW_CI_RUN_ANDROID: ${{ github.event_name == 'workflow_dispatch' && inputs.include_android && 'true' || steps.changed_scope.outputs.run_android || 'false' }} OPENCLAW_CI_RUN_WINDOWS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_windows || 'false' }} OPENCLAW_CI_RUN_NODE_FAST_ONLY: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_only || 'false' }} OPENCLAW_CI_RUN_NODE_FAST_PLUGIN_CONTRACTS: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_plugin_contracts || 'false' }} OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_ci_routing || 'false' }} OPENCLAW_CI_RUN_SKILLS_PYTHON: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_skills_python || 'false' }} OPENCLAW_CI_RUN_CONTROL_UI_I18N: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_control_ui_i18n || 'false' }} OPENCLAW_CI_CHECKOUT_REVISION: ${{ steps.checkout_ref.outputs.sha }} 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 { createPluginContractTestShards, } from "./scripts/lib/plugin-contract-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 createMatrix = (include) => ({ include }); const outputPath = process.env.GITHUB_OUTPUT; 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 runNodeFastOnly = runNode && parseBoolean(process.env.OPENCLAW_CI_RUN_NODE_FAST_ONLY); const runNodeFull = runNode && !runNodeFastOnly; const runNodeFastPluginContracts = runNode && parseBoolean(process.env.OPENCLAW_CI_RUN_NODE_FAST_PLUGIN_CONTRACTS); const runNodeFastCiRouting = runNode && parseBoolean(process.env.OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING); const runPluginContractShards = runNodeFull || runNodeFastPluginContracts; 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 && !runNodeFastOnly && 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 checksFastCoreTasks = []; if (runNodeFull) { checksFastCoreTasks.push( { check_name: "checks-fast-bundled", runtime: "node", task: "bundled" }, ); } else { if (runNodeFastCiRouting) { checksFastCoreTasks.push({ check_name: "checks-fast-ci-routing", runtime: "node", task: "ci-routing", }); } } const nodeTestShards = runNodeFull ? createNodeTestShards({ includeReleaseOnlyPluginShards: false, }).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, runner: shard.runner, })) : []; 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, run_build_artifacts: runNodeFull, run_checks_fast_core: checksFastCoreTasks.length > 0, run_checks_fast: runNodeFull, checks_fast_core_matrix: createMatrix(checksFastCoreTasks), run_plugin_contracts_shards: runPluginContractShards, plugin_contracts_matrix: createMatrix( runPluginContractShards ? createPluginContractTestShards() : [], ), channel_contracts_matrix: createMatrix( runNodeFull ? createChannelContractTestShards() : [], ), run_checks: runNodeFull, checks_matrix: createMatrix( runNodeFull ? [ { check_name: "checks-node-channels", runtime: "node", task: "channels" }, ] : [], ), 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_check: runNodeFull, run_check_additional: runNodeFull, run_build_smoke: runNodeFull, 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" }, ] : [], ), }; 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: ubuntu-24.04 timeout-minutes: 20 env: PRE_COMMIT_HOME: .cache/pre-commit-security-fast steps: - name: Checkout uses: actions/checkout@v6 with: ref: ${{ inputs.target_ref || github.sha }} fetch-depth: 1 fetch-tags: false persist-credentials: false submodules: false - name: Ensure security base commit if: github.event_name != 'workflow_dispatch' 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 }} BASE_REF: ${{ github.event.pull_request.base.ref }} run: | set -euo pipefail trusted_config="$RUNNER_TEMP/pre-commit-base.yaml" if git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null && git cat-file -e "${BASE_SHA}:.pre-commit-config.yaml" 2>/dev/null; then git show "${BASE_SHA}:.pre-commit-config.yaml" > "$trusted_config" elif git show "refs/remotes/origin/${BASE_REF}:.pre-commit-config.yaml" \ > "$trusted_config" 2>/dev/null; then echo "Base SHA ${BASE_SHA} does not expose .pre-commit-config.yaml; using origin/${BASE_REF} instead." else echo "::warning title=trusted pre-commit config unavailable::Could not read .pre-commit-config.yaml from ${BASE_SHA} or origin/${BASE_REF}; falling back to the checked-out config." rm -f "$trusted_config" exit 0 fi 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: ubuntu-24.04 timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v6 with: ref: ${{ inputs.target_ref || github.sha }} 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: ${{ !cancelled() && always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }} runs-on: 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-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 20 outputs: channels-result: ${{ steps.built_artifact_checks.outputs['channels-result'] }} core-support-boundary-result: ${{ steps.built_artifact_checks.outputs['core-support-boundary-result'] }} gateway-watch-result: ${{ steps.built_artifact_checks.outputs['gateway-watch-result'] }} steps: - name: Checkout shell: bash env: CHECKOUT_REPO: ${{ github.repository }} CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }} 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}/5 succeeded" } for attempt in 1 2 3 4 5; do if checkout_attempt "$attempt"; then exit 0 fi echo "checkout attempt ${attempt}/5 failed" sleep $((attempt * 5)) done echo "checkout failed after 5 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: Check Control UI i18n if: needs.preflight.outputs.run_control_ui_i18n == 'true' run: pnpm ui:i18n:check - name: Cache dist build uses: actions/cache@v5 with: path: | dist/ dist-runtime/ key: ${{ runner.os }}-dist-build-${{ needs.preflight.outputs.checkout_revision }} - name: Pack built runtime artifacts run: tar --posix -cf dist-runtime-build.tar.zst --use-compress-program zstdmt dist dist-runtime - name: Upload built runtime artifacts uses: actions/upload-artifact@v7 with: name: dist-runtime-build path: dist-runtime-build.tar.zst 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 - 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 - name: Run built artifact checks id: built_artifact_checks if: needs.preflight.outputs.run_checks == 'true' || needs.preflight.outputs.run_checks_node_core_dist == 'true' || needs.preflight.outputs.run_check_additional == 'true' env: RUN_CHANNELS: ${{ needs.preflight.outputs.run_checks }} RUN_CORE_SUPPORT_BOUNDARY: ${{ needs.preflight.outputs.run_checks_node_core_dist }} RUN_GATEWAY_WATCH: ${{ needs.preflight.outputs.run_check_additional }} shell: bash run: | set -uo pipefail names=() pids=() logs=() declare -A results=( ["channels"]="skipped" ["core-support-boundary"]="skipped" ["gateway-watch"]="skipped" ) start_check() { local name="$1" shift local log="${RUNNER_TEMP}/${name}.log" names+=("$name") logs+=("$log") echo "starting ${name}: $*" "$@" >"$log" 2>&1 & pids+=("$!") } if [ "$RUN_CHANNELS" = "true" ]; then start_check "channels" env \ NODE_OPTIONS=--max-old-space-size=6144 \ OPENCLAW_VITEST_MAX_WORKERS=1 \ pnpm test:channels fi if [ "$RUN_CORE_SUPPORT_BOUNDARY" = "true" ]; then start_check "core-support-boundary" env \ NODE_OPTIONS=--max-old-space-size=6144 \ OPENCLAW_VITEST_MAX_WORKERS=2 \ node scripts/run-vitest.mjs run --config test/vitest/vitest.full-core-support-boundary.config.ts fi if [ "$RUN_GATEWAY_WATCH" = "true" ]; then start_check "gateway-watch" node scripts/check-gateway-watch-regression.mjs --skip-build --ready-timeout-ms 5000 fi for index in "${!pids[@]}"; do name="${names[$index]}" log="${logs[$index]}" pid="${pids[$index]}" if wait "$pid"; then result="success" else result="failure" fi echo "::group::${name} log" cat "$log" echo "::endgroup::" results["$name"]="$result" done for name in channels core-support-boundary gateway-watch; do echo "${name}-result=${results[$name]}" >> "$GITHUB_OUTPUT" done - name: Upload gateway watch regression artifacts if: always() && needs.preflight.outputs.run_check_additional == 'true' uses: actions/upload-artifact@v7 with: name: gateway-watch-regression path: .local/gateway-watch-regression/ retention-days: 7 checks-fast-core: permissions: contents: read name: ${{ matrix.check_name }} needs: [preflight] if: needs.preflight.outputs.run_checks_fast_core == 'true' runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-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: ${{ needs.preflight.outputs.checkout_revision }} 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}/5 succeeded" } for attempt in 1 2 3 4 5; do if checkout_attempt "$attempt"; then exit 0 fi echo "checkout attempt ${attempt}/5 failed" sleep $((attempt * 5)) done echo "checkout failed after 5 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 ;; contracts-plugins-ci-routing) pnpm test:contracts:plugins pnpm test src/commands/status.scan-result.test.ts src/scripts/ci-changed-scope.test.ts test/scripts/test-projects.test.ts ;; ci-routing) pnpm test src/commands/status.scan-result.test.ts src/scripts/ci-changed-scope.test.ts test/scripts/test-projects.test.ts ;; *) echo "Unsupported checks-fast task: $TASK" >&2 exit 1 ;; esac checks-fast-plugin-contracts-shard: permissions: contents: read name: ${{ matrix.checkName }} needs: [preflight] if: needs.preflight.outputs.run_plugin_contracts_shards == 'true' runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 60 strategy: fail-fast: false matrix: ${{ fromJson(needs.preflight.outputs.plugin_contracts_matrix) }} steps: - name: Checkout shell: bash env: CHECKOUT_REPO: ${{ github.repository }} CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }} 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}/5 succeeded" } for attempt in 1 2 3 4 5; do if checkout_attempt "$attempt"; then exit 0 fi echo "checkout attempt ${attempt}/5 failed" sleep $((attempt * 5)) done echo "checkout failed after 5 attempts" >&2 exit 1 - name: Setup Node environment uses: ./.github/actions/setup-node-env with: install-bun: "false" - name: Run plugin contract shard env: OPENCLAW_CONTRACT_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }} shell: bash run: | set -euo pipefail include_file="$RUNNER_TEMP/plugin-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 plugin 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:plugins checks-fast-plugin-contracts: permissions: contents: read name: checks-fast-contracts-plugins needs: [preflight, checks-fast-plugin-contracts-shard] if: ${{ !cancelled() && always() && needs.preflight.outputs.run_plugin_contracts_shards == 'true' }} runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - name: Verify plugin contract shards env: SHARD_RESULT: ${{ needs.checks-fast-plugin-contracts-shard.result }} run: | if [ "$SHARD_RESULT" = "cancelled" ]; then echo "Plugin contract shards were cancelled, usually because a newer commit superseded this run." >&2 exit 1 fi if [ "$SHARD_RESULT" != "success" ]; then echo "Plugin contract shards failed: $SHARD_RESULT" >&2 exit 1 fi checks-fast-channel-contracts-shard: permissions: contents: read name: ${{ matrix.checkName }} needs: [preflight] if: needs.preflight.outputs.run_checks_fast == 'true' runs-on: 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: ${{ needs.preflight.outputs.checkout_revision }} 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}/5 succeeded" } for attempt in 1 2 3 4 5; do if checkout_attempt "$attempt"; then exit 0 fi echo "checkout attempt ${attempt}/5 failed" sleep $((attempt * 5)) done echo "checkout failed after 5 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: ${{ !cancelled() && always() && needs.preflight.outputs.run_checks_fast == 'true' }} runs-on: 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: ubuntu-24.04 timeout-minutes: 30 steps: - name: Checkout shell: bash env: CHECKOUT_REPO: ${{ github.repository }} CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }} 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}/5 succeeded" } for attempt in 1 2 3 4 5; do if checkout_attempt "$attempt"; then exit 0 fi echo "checkout attempt ${attempt}/5 failed" sleep $((attempt * 5)) done echo "checkout failed after 5 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: permissions: contents: read name: ${{ matrix.check_name }} needs: [preflight, build-artifacts] if: ${{ !cancelled() && always() && needs.preflight.outputs.run_checks == 'true' && needs.build-artifacts.result == 'success' }} runs-on: ubuntu-24.04 timeout-minutes: 5 strategy: fail-fast: false matrix: ${{ fromJson(needs.preflight.outputs.checks_matrix) }} steps: - name: Verify ${{ matrix.task }} (${{ matrix.runtime }}) env: TASK: ${{ matrix.task }} CHANNELS_RESULT: ${{ needs.build-artifacts.outputs['channels-result'] }} shell: bash run: | set -euo pipefail case "$TASK" in channels) if [ "$CHANNELS_RESULT" != "success" ]; then echo "Channel tests failed in build-artifacts: $CHANNELS_RESULT" >&2 exit 1 fi ;; *) echo "Unsupported checks task: $TASK" >&2 exit 1 ;; esac checks-node-compat: permissions: contents: read name: checks-node-compat-node22 needs: [preflight] if: needs.preflight.outputs.run_build_artifacts == 'true' && github.event_name == 'workflow_dispatch' runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 60 steps: - name: Checkout shell: bash env: CHECKOUT_REPO: ${{ github.repository }} CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }} 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}/5 succeeded" } for attempt in 1 2 3 4 5; do if checkout_attempt "$attempt"; then exit 0 fi echo "checkout attempt ${attempt}/5 failed" sleep $((attempt * 5)) done echo "checkout failed after 5 attempts" >&2 exit 1 - name: Setup Node environment uses: ./.github/actions/setup-node-env with: node-version: "22.18.0" cache-key-suffix: "node22" install-bun: "false" - name: Configure Node test resources run: echo "OPENCLAW_VITEST_MAX_WORKERS=2" >> "$GITHUB_ENV" - name: Run Node 22 compatibility env: NODE_OPTIONS: --max-old-space-size=6144 run: | pnpm build pnpm ui:build node openclaw.mjs --help node openclaw.mjs status --json --timeout 1 pnpm test:build:singleton 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' && (matrix.runner || 'ubuntu-24.04') || '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: ${{ needs.preflight.outputs.checkout_revision }} 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}/5 succeeded" } for attempt in 1 2 3 4 5; do if checkout_attempt "$attempt"; then exit 0 fi echo "checkout attempt ${attempt}/5 failed" sleep $((attempt * 5)) done echo "checkout failed after 5 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) }} OPENCLAW_VITEST_SHARD_NAME: ${{ matrix.shard_name }} OPENCLAW_TEST_PROJECTS_PARALLEL: "2" 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"; 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; } const result = spawnSync("pnpm", ["exec", "node", "scripts/test-projects.mjs", ...configs], { 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: ${{ !cancelled() && always() && needs.preflight.outputs.run_checks_node_core_dist == 'true' && needs.build-artifacts.result == 'success' }} runs-on: ubuntu-24.04 timeout-minutes: 5 strategy: fail-fast: false matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_dist_matrix) }} steps: - name: Verify Node test shard env: CORE_SUPPORT_BOUNDARY_RESULT: ${{ needs.build-artifacts.outputs['core-support-boundary-result'] }} SHARD_NAME: ${{ matrix.shard_name }} shell: bash run: | set -euo pipefail case "$SHARD_NAME" in core-support-boundary) if [ "$CORE_SUPPORT_BOUNDARY_RESULT" != "success" ]; then echo "Core support boundary shard failed in build-artifacts: $CORE_SUPPORT_BOUNDARY_RESULT" >&2 exit 1 fi ;; *) echo "Unsupported built-artifact shard: $SHARD_NAME" >&2 exit 1 ;; esac 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: ${{ !cancelled() && always() && needs.preflight.outputs.run_checks == 'true' }} runs-on: 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 # Types, lint, and format check shards. check-shard: permissions: contents: read name: ${{ matrix.check_name }} needs: [preflight] if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check == 'true' }} runs-on: ${{ github.repository == 'openclaw/openclaw' && matrix.runner || 'ubuntu-24.04' }} timeout-minutes: 20 strategy: fail-fast: false matrix: include: - check_name: check-preflight-guards task: preflight-guards runner: ubuntu-24.04 - check_name: check-prod-types task: prod-types runner: blacksmith-4vcpu-ubuntu-2404 - check_name: check-lint task: lint runner: blacksmith-16vcpu-ubuntu-2404 - check_name: check-dependencies task: dependencies runner: ubuntu-24.04 - check_name: check-policy-guards task: policy-guards runner: ubuntu-24.04 - check_name: check-test-types task: test-types runner: blacksmith-4vcpu-ubuntu-2404 - check_name: check-strict-smoke task: strict-smoke runner: ubuntu-24.04 steps: - name: Checkout shell: bash env: CHECKOUT_REPO: ${{ github.repository }} CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }} 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}/5 succeeded" } for attempt in 1 2 3 4 5; do if checkout_attempt "$attempt"; then exit 0 fi echo "checkout attempt ${attempt}/5 failed" sleep $((attempt * 5)) done echo "checkout failed after 5 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 --threads=8 ;; dependencies) pnpm deadcode:dependencies pnpm deadcode:unused-files ;; 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) # build-artifacts already runs the tsdown/runtime build for the same Node-relevant changes. pnpm build:plugin-sdk:strict-smoke ;; *) echo "Unsupported check task: $TASK" >&2 exit 1 ;; esac check: permissions: contents: read name: "check" needs: [preflight, check-shard] if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check == 'true' }} runs-on: 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: ${{ !cancelled() && always() && needs.preflight.outputs.run_check_additional == 'true' }} runs-on: 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 group: extension-package-boundary - check_name: check-additional-runtime-topology-architecture group: runtime-topology-architecture steps: - name: Checkout shell: bash env: CHECKOUT_REPO: ${{ github.repository }} CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }} 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}/5 succeeded" } for attempt in 1 2 3 4 5; do if checkout_attempt "$attempt"; then exit 0 fi echo "checkout attempt ${attempt}/5 failed" sleep $((attempt * 5)) done echo "checkout failed after 5 attempts" >&2 exit 1 - name: Setup Node environment uses: ./.github/actions/setup-node-env with: install-bun: "false" - name: Cache extension package boundary artifacts id: extension-package-boundary-cache if: matrix.group == 'extension-package-boundary' uses: actions/cache@v5 with: path: | dist/plugin-sdk packages/plugin-sdk/dist extensions/*/dist/.boundary-tsc.tsbuildinfo extensions/*/dist/.boundary-tsc.stamp key: ${{ runner.os }}-extension-package-boundary-v1-${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'scripts/check-extension-package-tsc-boundary.mjs', 'scripts/prepare-extension-package-boundary-artifacts.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mjs', 'src/plugin-sdk/**', 'src/auto-reply/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'src/types/**', 'extensions/**', 'extensions/tsconfig.package-boundary*.json', 'package.json', 'pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-extension-package-boundary-v1- - name: Preserve extension package boundary cache hit if: matrix.group == 'extension-package-boundary' && steps.extension-package-boundary-cache.outputs.cache-hit == 'true' shell: bash run: | set -euo pipefail find extensions \ -path '*/dist' -prune -o \ -path '*/node_modules' -prune -o \ -type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' -o -name '*.js' -o -name '*.mjs' -o -name '*.json' \) \ -exec touch -t 200001010000 {} + find src \ -type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' -o -name '*.js' -o -name '*.mjs' -o -name '*.json' \) \ -exec touch -t 200001010000 {} + touch -t 200001010000 \ tsconfig.json \ tsconfig.plugin-sdk.dts.json \ packages/plugin-sdk/tsconfig.json \ scripts/check-extension-package-tsc-boundary.mjs \ scripts/prepare-extension-package-boundary-artifacts.mjs \ scripts/write-plugin-sdk-entry-dts.ts \ scripts/lib/plugin-sdk-entrypoints.json \ scripts/lib/plugin-sdk-entries.mjs \ package.json \ pnpm-lock.yaml - name: Run additional check shard env: ADDITIONAL_CHECK_GROUP: ${{ matrix.group }} RUN_CONTROL_UI_I18N: ${{ needs.preflight.outputs.run_control_ui_i18n }} OPENCLAW_ADDITIONAL_BOUNDARY_CONCURRENCY: 4 OPENCLAW_EXTENSION_BOUNDARY_CONCURRENCY: 6 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) node scripts/run-additional-boundary-checks.mjs ;; 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) run_check "test:extensions:package-boundary:compile" pnpm run test:extensions:package-boundary:compile run_check "test:extensions:package-boundary:canary" pnpm run test:extensions:package-boundary:canary ;; runtime-topology-architecture) run_check "check:architecture" pnpm check:architecture ;; *) echo "Unsupported additional check group: $ADDITIONAL_CHECK_GROUP" >&2 exit 1 ;; esac exit "$failures" check-additional: permissions: contents: read name: "check-additional" needs: [preflight, check-additional-shard, build-artifacts] if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check_additional == 'true' }} runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - name: Verify additional check shards env: SHARD_RESULT: ${{ needs.check-additional-shard.result }} BUILD_ARTIFACTS_RESULT: ${{ needs.build-artifacts.result }} GATEWAY_RESULT: ${{ needs.build-artifacts.outputs.gateway-watch-result }} run: | if [ "$SHARD_RESULT" != "success" ]; then echo "Additional check shards failed: $SHARD_RESULT" >&2 exit 1 fi if [ "$BUILD_ARTIFACTS_RESULT" != "success" ]; then echo "Build artifact job failed: $BUILD_ARTIFACTS_RESULT" >&2 exit 1 fi if [ "$GATEWAY_RESULT" != "success" ]; then echo "Gateway topology check failed: $GATEWAY_RESULT" >&2 exit 1 fi build-smoke: permissions: contents: read name: "build-smoke" needs: [preflight, build-artifacts] if: ${{ !cancelled() && always() && needs.preflight.outputs.run_build_smoke == 'true' && (github.event_name != 'push' || needs.build-artifacts.result == 'success') }} runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - name: Verify build smoke env: BUILD_ARTIFACTS_RESULT: ${{ needs.build-artifacts.result }} run: | if [ "$BUILD_ARTIFACTS_RESULT" != "success" ]; then echo "Build smoke checks failed in build-artifacts: $BUILD_ARTIFACTS_RESULT" >&2 exit 1 fi # 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: ubuntu-24.04 timeout-minutes: 20 steps: - name: Checkout shell: bash env: CHECKOUT_REPO: ${{ github.repository }} CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }} 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}/5 succeeded" } for attempt in 1 2 3 4 5; do if checkout_attempt "$attempt"; then exit 0 fi echo "checkout attempt ${attempt}/5 failed" sleep $((attempt * 5)) done echo "checkout failed after 5 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: ubuntu-24.04 timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@v6 with: ref: ${{ needs.preflight.outputs.checkout_revision }} 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] if: needs.preflight.outputs.run_checks_windows == 'true' runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-windows-2025' || 'windows-2025' }} timeout-minutes: 60 env: NODE_OPTIONS: --max-old-space-size=6144 # Keep total concurrency predictable on the smaller Windows 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: ref: ${{ needs.preflight.outputs.checkout_revision }} 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 id: pnpm-cache uses: ./.github/actions/setup-pnpm-store-cache with: pnpm-version: "10.33.0" 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: Save pnpm store cache if: steps.pnpm-cache.outputs.cache-enabled == 'true' && steps.pnpm-cache.outputs.cache-hit != 'true' uses: actions/cache/save@v5 continue-on-error: true with: path: ${{ steps.pnpm-cache.outputs.store-path }} key: ${{ steps.pnpm-cache.outputs.primary-key }} - 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] if: ${{ !cancelled() && always() && needs.preflight.outputs.run_macos_node == 'true' }} 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: ref: ${{ needs.preflight.outputs.checkout_revision }} persist-credentials: false submodules: false - name: Setup Node environment uses: ./.github/actions/setup-node-env with: install-bun: "false" - 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: ref: ${{ needs.preflight.outputs.checkout_revision }} 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 id: swift-build-cache uses: actions/cache@v5 with: path: apps/macos/.build key: ${{ runner.os }}-swift-build-v2-${{ steps.swift-toolchain.outputs.key }}-${{ hashFiles('apps/macos/Package.swift', 'apps/macos/Package.resolved', 'apps/macos/Sources/**', 'apps/macos/Tests/**', 'apps/shared/OpenClawKit/Package.swift', 'apps/shared/OpenClawKit/Sources/**', 'Swabble/Package.swift', 'Swabble/Sources/**') }} restore-keys: | ${{ runner.os }}-swift-build-v2-${{ steps.swift-toolchain.outputs.key }}- - name: Preserve Swift build cache hit if: steps.swift-build-cache.outputs.cache-hit == 'true' run: | set -euo pipefail # Exact source-hash cache hits already match these inputs; checkout # mtimes are the only reason SwiftPM rebuilds cached products. find apps/macos/Sources apps/macos/Tests apps/shared/OpenClawKit/Sources Swabble/Sources apps/macos/.build/checkouts \ -type f -exec touch -t 200001010000 {} + touch -t 200001010000 \ apps/macos/Package.swift \ apps/macos/Package.resolved \ apps/shared/OpenClawKit/Package.swift \ Swabble/Package.swift - 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-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 20 strategy: fail-fast: false matrix: ${{ fromJson(needs.preflight.outputs.android_matrix) }} steps: - name: Checkout shell: bash env: CHECKOUT_REPO: ${{ github.repository }} CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }} 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 -x "$workdir/apps/android/gradlew" || return 1 echo "checkout attempt ${attempt}/5 succeeded" } for attempt in 1 2 3 4 5; do if checkout_attempt "$attempt"; then exit 0 fi echo "checkout attempt ${attempt}/5 failed" sleep $((attempt * 5)) done echo "checkout failed after 5 attempts" >&2 exit 1 - 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 cache: gradle cache-dependency-path: | apps/android/**/*.gradle* apps/android/**/gradle-wrapper.properties apps/android/gradle/libs.versions.toml - name: Cache Android SDK uses: actions/cache@v5 with: path: ~/.android-sdk key: ${{ runner.os }}-android-sdk-v1-cmdline-12266719-platform-36-build-tools-36.0.0 restore-keys: | ${{ runner.os }}-android-sdk-v1- - 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}" if [ ! -x "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" ]; then 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" fi 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: 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 --build-cache :app:testPlayDebugUnitTest ;; test-third-party) ./gradlew --no-daemon --build-cache :app:testThirdPartyDebugUnitTest ;; build-play) ./gradlew --no-daemon --build-cache :app:assemblePlayDebug ;; *) echo "Unsupported Android task: $TASK" >&2 exit 1 ;; esac