Fix Telegram polling ingress under event-loop stalls (#81746) #51
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| 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 == 'pull_request' }} | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| jobs: | |
| # Preflight: establish routing truth and job matrices once, then let real | |
| # work fan out from a single source of truth. | |
| preflight: | |
| permissions: | |
| contents: read | |
| if: github.event_name != 'pull_request' || !github.event.pull_request.draft | |
| runs-on: 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"; | |
| 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 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-16vcpu-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 | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=8192 | |
| 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 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 | |
| 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=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 | |
| - 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: ${{ 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 }} | |
| 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-pnpm11" | |
| 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.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-pnpm11' }}" | |
| 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_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 | |
| pnpm dup:check:coverage | |
| pnpm deps:patches:check | |
| ;; | |
| 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 | |
| ;; | |
| 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 | |
| - 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: | |
| 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: ${{ 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-b | |
| group: boundaries | |
| boundary_shard: 2/4 | |
| - check_name: check-additional-boundaries-c | |
| group: boundaries | |
| boundary_shard: 3/4 | |
| - check_name: check-additional-boundaries-d | |
| group: boundaries | |
| boundary_shard: 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 }} | |
| 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 }} | |
| 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" | |
| 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: Checkout ClawHub docs source | |
| uses: actions/checkout@v6 | |
| with: | |
| repository: openclaw/clawhub | |
| path: clawhub-source | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - 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: 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 --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.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 | |
| 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: "11.0.8" | |
| cache-key-suffix: "node24-pnpm11" | |
| 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/**', '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.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 |