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]
    paths-ignore:
      - "CHANGELOG.md"

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 == 'pull_request' || (github.event_name == 'push' && github.repository == 'openclaw/openclaw' && github.ref == 'refs/heads/main') }}

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.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || '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 }}
      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 }}
      run_check: ${{ steps.manifest.outputs.run_check }}
      run_check_additional: ${{ steps.manifest.outputs.run_check_additional }}
      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
        env:
          CHECKOUT_REPO: ${{ github.repository }}
          CHECKOUT_REF: ${{ inputs.target_ref || github.sha }}
          CHECKOUT_FALLBACK_REF: ${{ github.sha }}
          GITHUB_EVENT_NAME: ${{ github.event_name }}
        run: |
          set -euo pipefail
          git init "$GITHUB_WORKSPACE"
          git -C "$GITHUB_WORKSPACE" config gc.auto 0
          git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
          fetch_checkout_ref() {
            local ref="$1"
            local fetch_status
            for attempt in 1 2 3; do
              timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
                -c protocol.version=2 \
                fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
                "+${ref}:refs/remotes/origin/checkout" && return 0
              fetch_status="$?"
              if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
                return "$fetch_status"
              fi
              if [ "$attempt" = "3" ]; then
                return "$fetch_status"
              fi
              echo "::warning::checkout fetch for '$ref' timed out on attempt $attempt; retrying"
              sleep 5
            done
          }
          if fetch_checkout_ref "$CHECKOUT_REF"; then
            :
          else
            fetch_status="$?"
            if [ "$fetch_status" = "124" ] || [ "$fetch_status" = "137" ]; then
              echo "::error::checkout fetch for '$CHECKOUT_REF' timed out"
              exit "$fetch_status"
            fi
            if [ "$GITHUB_EVENT_NAME" != "workflow_dispatch" ] || [ "$CHECKOUT_REF" = "$CHECKOUT_FALLBACK_REF" ]; then
              exit "$fetch_status"
            fi
            echo "::warning::workflow_dispatch target_ref '$CHECKOUT_REF' is unavailable; falling back to head SHA '$CHECKOUT_FALLBACK_REF'"
            fetch_checkout_ref "$CHECKOUT_FALLBACK_REF"
          fi
          git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout

      - 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 }}
          OPENCLAW_CI_EVENT_NAME: ${{ github.event_name }}
        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";

          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 { createPluginContractTestShards } = await import(
            "./scripts/lib/plugin-contract-test-plan.mjs"
          ).catch((error) => {
            if (error?.code !== "ERR_MODULE_NOT_FOUND") {
              throw error;
            }
            return {
              createPluginContractTestShards: () => [
                {
                  checkName: "checks-fast-contracts-plugins-legacy",
                  includePatterns: ["src/plugins/contracts/**/*.test.ts"],
                  runtime: "node",
                  task: "contracts-plugins",
                },
              ],
            };
          });

          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 eventName = process.env.OPENCLAW_CI_EVENT_NAME ?? "";
          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-protocol", runtime: "node", task: "bundled-protocol" },
              { check_name: "checks-fast-bun-launcher", runtime: "bun", task: "bun-launcher" },
            );
          } 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,
            run_checks_node_core_nondist: nodeTestNonDistShards.length > 0,
            checks_node_core_nondist_matrix: createMatrix(nodeTestNonDistShards),
            run_checks_node_core_dist: nodeTestDistShards.length > 0,
            run_check: runNodeFull,
            run_check_additional: runNodeFull,
            run_check_docs: docsChanged && eventName !== "push",
            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 dependency-free security checks in parallel with scope detection so the
  # main Node jobs do not have to wait for Python/pre-commit setup.
  security-fast:
    permissions:
      contents: read
    if: github.event_name != 'pull_request' || !github.event.pull_request.draft
    runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
    timeout-minutes: 20
    env:
      PRE_COMMIT_HOME: .cache/pre-commit-security-fast
    steps:
      - name: Checkout
        env:
          CHECKOUT_REPO: ${{ github.repository }}
          CHECKOUT_REF: ${{ inputs.target_ref || github.sha }}
          CHECKOUT_FALLBACK_REF: ${{ github.sha }}
          GITHUB_EVENT_NAME: ${{ github.event_name }}
        run: |
          set -euo pipefail
          git init "$GITHUB_WORKSPACE"
          git -C "$GITHUB_WORKSPACE" config gc.auto 0
          git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
          fetch_checkout_ref() {
            local ref="$1"
            local fetch_status
            for attempt in 1 2 3; do
              timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
                -c protocol.version=2 \
                fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
                "+${ref}:refs/remotes/origin/checkout" && return 0
              fetch_status="$?"
              if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
                return "$fetch_status"
              fi
              if [ "$attempt" = "3" ]; then
                return "$fetch_status"
              fi
              echo "::warning::checkout fetch for '$ref' timed out on attempt $attempt; retrying"
              sleep 5
            done
          }
          if fetch_checkout_ref "$CHECKOUT_REF"; then
            :
          else
            fetch_status="$?"
            if [ "$fetch_status" = "124" ] || [ "$fetch_status" = "137" ]; then
              echo "::error::checkout fetch for '$CHECKOUT_REF' timed out"
              exit "$fetch_status"
            fi
            if [ "$GITHUB_EVENT_NAME" != "workflow_dispatch" ] || [ "$CHECKOUT_REF" = "$CHECKOUT_FALLBACK_REF" ]; then
              exit "$fetch_status"
            fi
            echo "::warning::workflow_dispatch target_ref '$CHECKOUT_REF' is unavailable; falling back to head SHA '$CHECKOUT_FALLBACK_REF'"
            fetch_checkout_ref "$CHECKOUT_FALLBACK_REF"
          fi
          git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout

      - 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: Resolve Python runtime
        id: setup-python
        run: |
          set -euo pipefail
          python3 --version
          version="$(python3 - <<'PY'
          import platform
          print(platform.python_version())
          PY
          )"
          echo "python-version=${version}" >> "$GITHUB_OUTPUT"

      - name: Install pre-commit
        run: python3 -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[@]}"

      - name: Setup Node.js
        env:
          REQUESTED_NODE_VERSION: "24.x"
        run: |
          set -euo pipefail
          source .github/actions/setup-pnpm-store-cache/ensure-node.sh
          openclaw_ensure_node "$REQUESTED_NODE_VERSION"

      - name: Audit production dependencies
        run: node scripts/pre-commit/pnpm-audit-prod.mjs --audit-level=high

  # Warm the lockfile- and pnpm-pinned store without blocking Linux Node shards.
  # On a cold key this job owns the save for later workflow runs.
  pnpm-store-warmup:
    permissions:
      contents: read
    needs: [preflight]
    if: needs.preflight.outputs.run_node == 'true' || needs.preflight.outputs.run_check_docs == 'true'
    runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
    timeout-minutes: 20
    steps:
      - name: Checkout
        shell: bash
        env:
          CHECKOUT_REPO: ${{ github.repository }}
          CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
        run: |
          set -euo pipefail

          workdir="$GITHUB_WORKSPACE"
          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"
            git -C "$workdir" config gc.auto 0

            timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
              -c protocol.version=2 \
              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"
          save-actions-cache: "true"

  # 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.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-32vcpu-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 }}
        run: |
          set -euo pipefail

          workdir="$GITHUB_WORKSPACE"
          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"
            git -C "$workdir" config gc.auto 0

            timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
              -c protocol.version=2 \
              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: Restore build-all step cache
        uses: actions/cache@v5
        with:
          path: .artifacts/build-all-cache
          key: ${{ runner.os }}-build-all-v3-${{ hashFiles('package.json', 'pnpm-lock.yaml', 'npm-shrinkwrap.json', 'packages/plugin-sdk/package.json', 'packages/llm-core/package.json', 'packages/model-catalog-core/package.json', 'packages/memory-host-sdk/package.json', 'scripts/build-all.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entries.mjs', 'tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'src/plugin-sdk/**', 'packages/llm-core/src/**', 'packages/model-catalog-core/src/**', 'packages/memory-host-sdk/src/**', 'src/types/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'scripts/copy-export-html-templates.ts', 'scripts/lib/copy-assets.ts', 'src/auto-reply/reply/export-html/**') }}
          restore-keys: |
            ${{ runner.os }}-build-all-v3-

      - name: Build dist
        env:
          NODE_OPTIONS: --max-old-space-size=8192
        run: pnpm build:ci-artifacts

      - 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 bundled plugin asset artifacts
        uses: actions/upload-artifact@v7
        with:
          name: bundled-plugin-assets
          path: |
            extensions/*/src/host/**/.bundle.hash
            extensions/*/src/host/**/*.bundle.js
          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: Check CLI startup memory
        shell: bash
        run: |
          set +e
          pnpm test:startup:memory
          status=$?
          if [[ -f .artifacts/startup-memory/summary.md ]]; then
            cat .artifacts/startup-memory/summary.md >> "$GITHUB_STEP_SUMMARY"
          fi
          exit "$status"

      - name: Upload startup memory report
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: startup-memory
          path: .artifacts/startup-memory/
          if-no-files-found: ignore
          retention-days: 7

      - 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=8192 \
              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=8192 \
              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

          failures=0
          for name in channels core-support-boundary gateway-watch; do
            if [ "${results[$name]}" = "failure" ]; then
              echo "::error title=${name} failed::${name} failed"
              failures=1
            fi
          done
          exit "$failures"

      - 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.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (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 }}
        run: |
          set -euo pipefail

          workdir="$GITHUB_WORKSPACE"
          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"
            git -C "$workdir" config gc.auto 0

            timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
              -c protocol.version=2 \
              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: ${{ matrix.task == 'bun-launcher' && 'true' || '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-protocol)
              pnpm test:bundled
              pnpm protocol:check
              ;;
            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/changed-lanes.test.ts test/scripts/run-vitest.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/changed-lanes.test.ts test/scripts/run-vitest.test.ts test/scripts/test-projects.test.ts
              ;;
            bun-launcher)
              OPENCLAW_TEST_BUN_LAUNCHER=1 pnpm test test/openclaw-launcher.e2e.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.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (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 }}
        run: |
          set -euo pipefail

          workdir="$GITHUB_WORKSPACE"
          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"
            git -C "$workdir" config gc.auto 0

            timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
              -c protocol.version=2 \
              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-channel-contracts-shard:
    permissions:
      contents: read
    name: ${{ matrix.checkName }}
    needs: [preflight]
    if: needs.preflight.outputs.run_checks_fast == 'true'
    runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-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: ${{ needs.preflight.outputs.checkout_revision }}
        run: |
          set -euo pipefail

          workdir="$GITHUB_WORKSPACE"
          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"
            git -C "$workdir" config gc.auto 0

            timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
              -c protocol.version=2 \
              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-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.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (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 }}
        run: |
          set -euo pipefail

          workdir="$GITHUB_WORKSPACE"
          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"
            git -C "$workdir" config gc.auto 0

            timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
              -c protocol.version=2 \
              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.19.0"
          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=8192
        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.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (matrix.runner || 'blacksmith-8vcpu-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: ${{ needs.preflight.outputs.checkout_revision }}
        run: |
          set -euo pipefail

          workdir="$GITHUB_WORKSPACE"
          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"
            git -C "$workdir" config gc.auto 0

            timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
              -c protocol.version=2 \
              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' }}"
          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=8192
          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_VITEST_NO_OUTPUT_TIMEOUT_MS: "900000"
          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

  # 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.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (matrix.runner || 'blacksmith-4vcpu-ubuntu-2404') || 'ubuntu-24.04') }}
    timeout-minutes: 20
    strategy:
      fail-fast: false
      matrix:
        include:
          - check_name: check-guards
            task: guards
            runner: blacksmith-4vcpu-ubuntu-2404
          - 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: blacksmith-8vcpu-ubuntu-2404
          - check_name: check-test-types
            task: test-types
            runner: blacksmith-4vcpu-ubuntu-2404
    steps:
      - name: Checkout
        shell: bash
        env:
          CHECKOUT_REPO: ${{ github.repository }}
          CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
        run: |
          set -euo pipefail

          workdir="$GITHUB_WORKSPACE"
          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"
            git -C "$workdir" config gc.auto 0

            timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
              -c protocol.version=2 \
              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
            guards)
              pnpm check:no-conflict-markers
              pnpm tool-display:check
              pnpm check:host-env-policy:swift
              pnpm dup:check:coverage
              pnpm deps:shrinkwrap:check
              pnpm deps:patches:check
              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
              # build-artifacts already runs the tsdown/runtime build for the same Node-relevant changes.
              NODE_OPTIONS=--max-old-space-size=8192 pnpm build:plugin-sdk:strict-smoke
              ;;
            prod-types)
              pnpm tsgo:prod
              ;;
            lint)
              pnpm lint --threads=8
              ;;
            dependencies)
              if pnpm run --silent 2>/dev/null | grep -q '^  deadcode:dependencies$'; then
                pnpm deadcode:dependencies
                pnpm deadcode:unused-files
                pnpm deadcode:report:ci:ts-unused
              else
                pnpm deadcode:ci
              fi
              ;;
            test-types)
              pnpm check:test-types
              ;;
            *)
              echo "Unsupported check task: $TASK" >&2
              exit 1
              ;;
          esac

      - name: Upload deadcode reports
        if: ${{ always() && matrix.task == 'dependencies' }}
        uses: actions/upload-artifact@v7
        with:
          name: deadcode-reports
          path: .artifacts/deadcode
          if-no-files-found: ignore

  check-additional-shard:
    permissions:
      contents: read
    name: ${{ matrix.check_name }}
    needs: [preflight]
    if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check_additional == 'true' }}
    runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
    timeout-minutes: 20
    strategy:
      fail-fast: false
      matrix:
        include:
          - check_name: check-additional-boundaries-a
            group: boundaries
            boundary_shard: 1/4
          - check_name: check-additional-boundaries-bcd
            group: boundaries
            boundary_shard: 2/4,3/4,4/4
          - 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 }}
        run: |
          set -euo pipefail

          workdir="$GITHUB_WORKSPACE"
          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"
            git -C "$workdir" config gc.auto 0

            timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
              -c protocol.version=2 \
              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', 'packages/llm-core/package.json', 'packages/model-catalog-core/package.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/**', 'packages/llm-core/src/**', 'packages/model-catalog-core/src/**', '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 {} +
          if [ -d packages/llm-core/src ]; then
            find packages/llm-core/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 {} +
          fi
          if [ -d packages/model-catalog-core/src ]; then
            find packages/model-catalog-core/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 {} +
          fi
          cache_inputs=(
            tsconfig.json \
            tsconfig.plugin-sdk.dts.json \
            packages/plugin-sdk/tsconfig.json \
            packages/llm-core/package.json \
            packages/model-catalog-core/package.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
          )
          for cache_input in "${cache_inputs[@]}"; do
            if [ -e "$cache_input" ]; then
              touch -t 200001010000 "$cache_input"
            fi
          done

      - name: Run additional check shard
        env:
          ADDITIONAL_CHECK_GROUP: ${{ matrix.group }}
          OPENCLAW_ADDITIONAL_BOUNDARY_SHARD: ${{ matrix.boundary_shard || '' }}
          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"

  # 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.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
    timeout-minutes: 20
    steps:
      - name: Checkout
        shell: bash
        env:
          CHECKOUT_REPO: ${{ github.repository }}
          CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
        run: |
          set -euo pipefail

          workdir="$GITHUB_WORKSPACE"
          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"
            git -C "$workdir" config gc.auto 0

            timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
              -c protocol.version=2 \
              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: Checkout ClawHub docs source
        run: |
          set -euo pipefail

          workdir="$GITHUB_WORKSPACE/clawhub-source"
          started_at="$(date +%s)"

          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 -C "$workdir" config gc.auto 0
            git -C "$workdir" remote add origin "https://github.com/openclaw/clawhub.git"

            timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
              -c protocol.version=2 \
              fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
              "+refs/heads/main:refs/remotes/origin/checkout" || return 1

            git -C "$workdir" checkout --force --detach refs/remotes/origin/checkout || return 1
            echo "ClawHub checkout attempt ${attempt}/5 succeeded"
          }

          for attempt in 1 2 3 4 5; do
            if checkout_attempt "$attempt"; then
              elapsed="$(( $(date +%s) - started_at ))"
              echo "ClawHub checkout completed in ${elapsed}s"
              exit 0
            fi
            echo "ClawHub checkout attempt ${attempt}/5 failed"
            sleep $((attempt * 5))
          done

          echo "ClawHub checkout failed after 5 attempts" >&2
          exit 1

      - name: Check docs
        env:
          OPENCLAW_DOCS_SYNC_CLAWHUB_REPO: ${{ github.workspace }}/clawhub-source
        run: pnpm check:docs

  skills-python:
    permissions:
      contents: read
    needs: [preflight]
    if: needs.preflight.outputs.run_skills_python_job == 'true'
    runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
    timeout-minutes: 20
    steps:
      - name: Checkout
        env:
          CHECKOUT_REPO: ${{ github.repository }}
          CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
        run: |
          set -euo pipefail
          git init "$GITHUB_WORKSPACE"
          git -C "$GITHUB_WORKSPACE" config gc.auto 0
          git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
          fetch_checkout_ref() {
            local fetch_status
            for attempt in 1 2 3; do
              timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
                -c protocol.version=2 \
                fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
                "+${CHECKOUT_SHA}:refs/remotes/origin/checkout" && return 0
              fetch_status="$?"
              if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
                return "$fetch_status"
              fi
              if [ "$attempt" = "3" ]; then
                return "$fetch_status"
              fi
              echo "::warning::checkout fetch for '$CHECKOUT_SHA' timed out on attempt $attempt; retrying"
              sleep 5
            done
          }
          fetch_checkout_ref
          git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout

      - 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 --config skills/pyproject.toml skills

      - name: Test skill Python scripts
        run: python -m pytest -q -c skills/pyproject.toml skills

  checks-windows:
    permissions:
      contents: read
    name: ${{ matrix.check_name }}
    needs: [preflight]
    if: needs.preflight.outputs.run_checks_windows == 'true'
    runs-on: ${{ github.event_name == 'workflow_dispatch' && 'windows-2025' || (github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-windows-2025' || 'windows-2025') }}
    timeout-minutes: 60
    env:
      NODE_OPTIONS: --max-old-space-size=8192
      # 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
        env:
          CHECKOUT_REPO: ${{ github.repository }}
          CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
        run: |
          set -euo pipefail
          git init "$GITHUB_WORKSPACE"
          git -C "$GITHUB_WORKSPACE" config gc.auto 0
          git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
          fetch_timeout_seconds=90
          fetch_checkout_ref() {
            git -C "$GITHUB_WORKSPACE" \
              -c protocol.version=2 \
              fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
              "+${CHECKOUT_SHA}:refs/remotes/origin/checkout" &
            local fetch_pid="$!"
            local elapsed=0
            while kill -0 "$fetch_pid" 2>/dev/null; do
              if [ "$elapsed" -ge "$fetch_timeout_seconds" ]; then
                kill -TERM "$fetch_pid" 2>/dev/null || true
                sleep 10
                kill -KILL "$fetch_pid" 2>/dev/null || true
                wait "$fetch_pid" || true
                return 124
              fi
              sleep 1
              elapsed=$((elapsed + 1))
            done
            wait "$fetch_pid"
          }
          fetch_checkout_ref
          git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout

      - 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
        env:
          REQUESTED_NODE_VERSION: "22.x"
        run: |
          set -euo pipefail
          source .github/actions/setup-pnpm-store-cache/ensure-node.sh
          openclaw_ensure_node "$REQUESTED_NODE_VERSION"

      - name: Setup pnpm
        uses: ./.github/actions/setup-pnpm-store-cache
        with:
          node-version: 22.x

      - 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: 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.event_name == 'workflow_dispatch' && 'macos-15' || (github.repository == 'openclaw/openclaw' && 'blacksmith-6vcpu-macos-15' || 'macos-15') }}
    timeout-minutes: 20
    strategy:
      fail-fast: false
      matrix: ${{ fromJson(needs.preflight.outputs.macos_node_matrix) }}
    steps:
      - name: Checkout
        env:
          CHECKOUT_REPO: ${{ github.repository }}
          CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
        run: |
          set -euo pipefail
          git init "$GITHUB_WORKSPACE"
          git -C "$GITHUB_WORKSPACE" config gc.auto 0
          git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
          fetch_timeout_seconds=90
          fetch_checkout_ref() {
            git -C "$GITHUB_WORKSPACE" \
              -c protocol.version=2 \
              fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
              "+${CHECKOUT_SHA}:refs/remotes/origin/checkout" &
            local fetch_pid="$!"
            local elapsed=0
            while kill -0 "$fetch_pid" 2>/dev/null; do
              if [ "$elapsed" -ge "$fetch_timeout_seconds" ]; then
                kill -TERM "$fetch_pid" 2>/dev/null || true
                sleep 10
                kill -KILL "$fetch_pid" 2>/dev/null || true
                wait "$fetch_pid" || true
                return 124
              fi
              sleep 1
              elapsed=$((elapsed + 1))
            done
            wait "$fetch_pid"
          }
          fetch_checkout_ref
          git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout

      - 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.event_name == 'workflow_dispatch' && 'macos-26' || (github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-26' || 'macos-26') }}
    timeout-minutes: 20
    steps:
      - name: Checkout
        env:
          CHECKOUT_REPO: ${{ github.repository }}
          CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
        run: |
          set -euo pipefail
          git init "$GITHUB_WORKSPACE"
          git -C "$GITHUB_WORKSPACE" config gc.auto 0
          git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
          fetch_timeout_seconds=90
          fetch_checkout_ref() {
            git -C "$GITHUB_WORKSPACE" \
              -c protocol.version=2 \
              fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
              "+${CHECKOUT_SHA}:refs/remotes/origin/checkout" &
            local fetch_pid="$!"
            local elapsed=0
            while kill -0 "$fetch_pid" 2>/dev/null; do
              if [ "$elapsed" -ge "$fetch_timeout_seconds" ]; then
                kill -TERM "$fetch_pid" 2>/dev/null || true
                sleep 10
                kill -KILL "$fetch_pid" 2>/dev/null || true
                wait "$fetch_pid" || true
                return 124
              fi
              sleep 1
              elapsed=$((elapsed + 1))
            done
            wait "$fetch_pid"
          }
          fetch_checkout_ref
          git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout

      - 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/**', 'apps/swabble/Package.swift', 'apps/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 apps/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 \
            apps/swabble/Package.swift

      - name: Show toolchain
        run: |
          sw_vers
          xcodebuild -version
          swift --version

      - name: Swift lint
        run: |
          swiftlint lint --config config/swiftlint.yml
          swiftformat --lint apps/macos/Sources --config config/swiftformat --exclude '**/OpenClawProtocol,**/HostEnvSecurityPolicy.generated.swift'

      - 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.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (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 }}
        run: |
          set -euo pipefail

          workdir="$GITHUB_WORKSPACE"
          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"
            git -C "$workdir" config gc.auto 0

            timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
              -c protocol.version=2 \
              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

  ci-timings-summary:
    permissions:
      actions: read
      contents: read
    name: ci-timings-summary
    needs:
      - preflight
      - security-fast
      - pnpm-store-warmup
      - build-artifacts
      - checks-fast-core
      - checks-fast-plugin-contracts-shard
      - checks-fast-channel-contracts-shard
      - checks-node-compat
      - checks-node-core-test-nondist-shard
      - check-shard
      - check-additional-shard
      - check-docs
      - skills-python
      - checks-windows
      - macos-node
      - macos-swift
      - android
    if: ${{ !cancelled() && always() && github.event_name != 'push' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
    runs-on: ubuntu-24.04
    timeout-minutes: 5
    steps:
      - name: Checkout timing summary helper
        uses: actions/checkout@v6
        with:
          ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || needs.preflight.outputs.checkout_revision || github.sha }}
          fetch-depth: 1
          fetch-tags: false
          persist-credentials: false
          submodules: false

      - name: Write CI timing summary
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          node scripts/ci-run-timings.mjs "$GITHUB_RUN_ID" --limit 25 > ci-timings-summary.txt
          cat ci-timings-summary.txt >> "$GITHUB_STEP_SUMMARY"

      - name: Upload CI timing summary
        uses: actions/upload-artifact@v7
        with:
          name: ci-timings-summary
          path: ci-timings-summary.txt
          retention-days: 14
