mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-24 08:21:39 +00:00
894 lines
32 KiB
YAML
894 lines
32 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
types: [opened, reopened, synchronize, ready_for_review, converted_to_draft]
|
|
|
|
concurrency:
|
|
group: ${{ github.event_name == 'pull_request' && format('ci-pr-{0}', github.event.pull_request.number) || format('ci-push-{0}', github.run_id) }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
env:
|
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
|
|
|
jobs:
|
|
# Preflight: establish the fast global truth for this revision before the
|
|
# expensive platform and test lanes fan out.
|
|
# Detect docs-only changes to skip heavy jobs (test, build, Windows, macOS, Android).
|
|
# Lint and format always run. Fail-safe: if detection fails, run everything.
|
|
docs-scope:
|
|
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
outputs:
|
|
docs_only: ${{ steps.check.outputs.docs_only }}
|
|
docs_changed: ${{ steps.check.outputs.docs_changed }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 1
|
|
fetch-tags: false
|
|
submodules: false
|
|
|
|
- name: Ensure docs-scope base commit
|
|
uses: ./.github/actions/ensure-base-commit
|
|
with:
|
|
base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
|
|
fetch-ref: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }}
|
|
|
|
- name: Detect docs-only changes
|
|
id: check
|
|
uses: ./.github/actions/detect-docs-changes
|
|
|
|
# Detect which heavy areas are touched so CI can skip unrelated expensive jobs.
|
|
# Fail-safe: if detection fails, downstream jobs run.
|
|
changed-scope:
|
|
needs: [docs-scope]
|
|
if: needs.docs-scope.outputs.docs_only != 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
outputs:
|
|
run_node: ${{ steps.scope.outputs.run_node }}
|
|
run_macos: ${{ steps.scope.outputs.run_macos }}
|
|
run_android: ${{ steps.scope.outputs.run_android }}
|
|
run_skills_python: ${{ steps.scope.outputs.run_skills_python }}
|
|
run_windows: ${{ steps.scope.outputs.run_windows }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 1
|
|
fetch-tags: false
|
|
submodules: false
|
|
|
|
- name: Ensure changed-scope base commit
|
|
uses: ./.github/actions/ensure-base-commit
|
|
with:
|
|
base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
|
|
fetch-ref: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }}
|
|
|
|
- name: Detect changed scopes
|
|
id: scope
|
|
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
|
|
|
|
changed-extensions:
|
|
needs: [docs-scope, changed-scope]
|
|
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
outputs:
|
|
has_changed_extensions: ${{ steps.changed.outputs.has_changed_extensions }}
|
|
changed_extensions_matrix: ${{ steps.changed.outputs.changed_extensions_matrix }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 1
|
|
fetch-tags: false
|
|
submodules: false
|
|
|
|
- name: Ensure changed-extensions base commit
|
|
uses: ./.github/actions/ensure-base-commit
|
|
with:
|
|
base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
|
|
fetch-ref: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }}
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
install-deps: "false"
|
|
use-sticky-disk: "false"
|
|
|
|
- name: Detect changed extensions
|
|
id: changed
|
|
env:
|
|
BASE_SHA: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
|
|
run: |
|
|
node --input-type=module <<'EOF'
|
|
import { appendFileSync } from "node:fs";
|
|
import { listChangedExtensionIds } from "./scripts/test-extension.mjs";
|
|
|
|
const extensionIds = listChangedExtensionIds({ base: process.env.BASE_SHA, head: "HEAD" });
|
|
const matrix = JSON.stringify({ include: extensionIds.map((extension) => ({ extension })) });
|
|
|
|
appendFileSync(process.env.GITHUB_OUTPUT, `has_changed_extensions=${extensionIds.length > 0}\n`, "utf8");
|
|
appendFileSync(process.env.GITHUB_OUTPUT, `changed_extensions_matrix=${matrix}\n`, "utf8");
|
|
EOF
|
|
|
|
secrets:
|
|
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
submodules: false
|
|
|
|
- name: Ensure secrets base commit
|
|
uses: ./.github/actions/ensure-base-commit
|
|
with:
|
|
base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
|
|
fetch-ref: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }}
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
use-sticky-disk: "false"
|
|
install-deps: "false"
|
|
|
|
- name: Setup Python
|
|
id: setup-python
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: "3.12"
|
|
cache: "pip"
|
|
cache-dependency-path: |
|
|
pyproject.toml
|
|
.pre-commit-config.yaml
|
|
.github/workflows/ci.yml
|
|
|
|
- name: Restore pre-commit cache
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: ~/.cache/pre-commit
|
|
key: pre-commit-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }}
|
|
|
|
- name: Install pre-commit
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
python -m pip install pre-commit
|
|
|
|
- name: Detect committed private keys
|
|
run: pre-commit run --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 zizmor --files "${workflow_files[@]}"
|
|
|
|
- name: Audit production dependencies
|
|
run: pre-commit run --all-files pnpm-audit-prod
|
|
|
|
# Preflight hub: collapse setup jobs into one explicit fanout node so the
|
|
# graph matches operator mental model instead of raw YAML ordering.
|
|
preflight:
|
|
needs: [docs-scope, changed-scope, changed-extensions, secrets]
|
|
if: always() && needs.docs-scope.result == 'success' && (needs.changed-scope.result == 'success' || needs.changed-scope.result == 'skipped') && (needs.changed-extensions.result == 'success' || needs.changed-extensions.result == 'skipped') && needs.secrets.result == 'success'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
outputs:
|
|
docs_only: ${{ needs.docs-scope.outputs.docs_only }}
|
|
docs_changed: ${{ needs.docs-scope.outputs.docs_changed }}
|
|
run_node: ${{ needs.changed-scope.outputs.run_node || 'false' }}
|
|
run_macos: ${{ needs.changed-scope.outputs.run_macos || 'false' }}
|
|
run_android: ${{ needs.changed-scope.outputs.run_android || 'false' }}
|
|
run_skills_python: ${{ needs.changed-scope.outputs.run_skills_python || 'false' }}
|
|
run_windows: ${{ needs.changed-scope.outputs.run_windows || 'false' }}
|
|
has_changed_extensions: ${{ needs.changed-extensions.outputs.has_changed_extensions || 'false' }}
|
|
changed_extensions_matrix: ${{ needs.changed-extensions.outputs.changed_extensions_matrix || '{"include":[]}' }}
|
|
steps:
|
|
- name: Preflight complete
|
|
run: echo "Preflight scope complete."
|
|
|
|
# Fanout: downstream lanes branch from preflight outputs instead of waiting
|
|
# on unrelated Linux checks.
|
|
# Build dist once for Node-relevant changes and share it with downstream jobs.
|
|
build-artifacts:
|
|
needs: [preflight]
|
|
if: github.event_name == 'push' && needs.preflight.outputs.docs_only != 'true' && needs.preflight.outputs.run_node == 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
submodules: false
|
|
|
|
- name: Ensure secrets base commit (PR fast path)
|
|
if: github.event_name == 'pull_request'
|
|
uses: ./.github/actions/ensure-base-commit
|
|
with:
|
|
base-sha: ${{ github.event.pull_request.base.sha }}
|
|
fetch-ref: ${{ github.event.pull_request.base.ref }}
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
use-sticky-disk: "false"
|
|
|
|
- name: Build dist
|
|
run: pnpm build
|
|
|
|
- name: Upload dist artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: dist-build
|
|
path: dist/
|
|
retention-days: 1
|
|
|
|
# Validate npm pack contents after build (only on push to main, not PRs).
|
|
release-check:
|
|
needs: [preflight, build-artifacts]
|
|
if: github.event_name == 'push' && needs.preflight.outputs.docs_only != 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
submodules: false
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
use-sticky-disk: "false"
|
|
|
|
- name: Download dist artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: dist-build
|
|
path: dist/
|
|
|
|
- name: Check release contents
|
|
run: pnpm release:check
|
|
|
|
checks:
|
|
needs: [preflight, build-artifacts]
|
|
if: always() && needs.preflight.outputs.docs_only != 'true' && needs.preflight.outputs.run_node == 'true' && (github.event_name != 'push' || needs.build-artifacts.result == 'success')
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- runtime: node
|
|
task: test
|
|
shard_index: 1
|
|
shard_count: 2
|
|
command: pnpm canvas:a2ui:bundle && pnpm test
|
|
- runtime: node
|
|
task: test
|
|
shard_index: 2
|
|
shard_count: 2
|
|
command: pnpm canvas:a2ui:bundle && pnpm test
|
|
- runtime: node
|
|
task: extensions
|
|
command: pnpm test:extensions
|
|
- runtime: node
|
|
task: channels
|
|
command: pnpm test:channels
|
|
- runtime: node
|
|
task: contracts
|
|
command: pnpm test:contracts
|
|
- runtime: node
|
|
task: protocol
|
|
command: pnpm protocol:check
|
|
- runtime: bun
|
|
task: test
|
|
command: pnpm canvas:a2ui:bundle && bunx vitest run --config vitest.unit.config.ts
|
|
- runtime: node
|
|
task: compat-node22
|
|
node_version: "22.x"
|
|
cache_key_suffix: "node22"
|
|
command: |
|
|
pnpm build
|
|
node openclaw.mjs --help
|
|
node openclaw.mjs status --json --timeout 1
|
|
pnpm test:build:singleton
|
|
node scripts/stage-bundled-plugin-runtime-deps.mjs
|
|
node --import tsx scripts/release-check.ts
|
|
steps:
|
|
- name: Skip compatibility lanes on pull requests
|
|
if: github.event_name == 'pull_request' && (matrix.runtime == 'bun' || matrix.task == 'compat-node22')
|
|
run: echo "Skipping push-only lane on pull requests."
|
|
|
|
- name: Checkout
|
|
if: github.event_name != 'pull_request' || (matrix.runtime != 'bun' && matrix.task != 'compat-node22')
|
|
uses: actions/checkout@v6
|
|
with:
|
|
submodules: false
|
|
|
|
- name: Setup Node environment
|
|
if: github.event_name != 'pull_request' || (matrix.runtime != 'bun' && matrix.task != 'compat-node22')
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
node-version: "${{ matrix.node_version || '24.x' }}"
|
|
cache-key-suffix: "${{ matrix.cache_key_suffix || 'node24' }}"
|
|
install-bun: "${{ matrix.runtime == 'bun' }}"
|
|
use-sticky-disk: "false"
|
|
|
|
- name: Configure Node test resources
|
|
if: (github.event_name != 'pull_request' || (matrix.runtime != 'bun' && matrix.task != 'compat-node22')) && matrix.runtime == 'node' && (matrix.task == 'test' || matrix.task == 'compat-node22')
|
|
env:
|
|
SHARD_COUNT: ${{ matrix.shard_count || '' }}
|
|
SHARD_INDEX: ${{ matrix.shard_index || '' }}
|
|
run: |
|
|
# `pnpm test` runs `scripts/test-parallel.mjs`, which spawns multiple Node processes.
|
|
# Default heap limits have been too low on Linux CI (V8 OOM near 4GB).
|
|
echo "OPENCLAW_TEST_WORKERS=2" >> "$GITHUB_ENV"
|
|
echo "OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB=6144" >> "$GITHUB_ENV"
|
|
if [ -n "$SHARD_COUNT" ] && [ -n "$SHARD_INDEX" ]; then
|
|
echo "OPENCLAW_TEST_SHARDS=$SHARD_COUNT" >> "$GITHUB_ENV"
|
|
echo "OPENCLAW_TEST_SHARD_INDEX=$SHARD_INDEX" >> "$GITHUB_ENV"
|
|
fi
|
|
|
|
- name: Download dist artifact
|
|
if: github.event_name == 'push' && matrix.task == 'test'
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: dist-build
|
|
path: dist/
|
|
|
|
- name: Build dist
|
|
if: github.event_name != 'push' && matrix.task == 'test' && matrix.runtime == 'node'
|
|
run: pnpm build
|
|
|
|
- name: Run ${{ matrix.task }} (${{ matrix.runtime }})
|
|
if: github.event_name != 'pull_request' || (matrix.runtime != 'bun' && matrix.task != 'compat-node22')
|
|
run: ${{ matrix.command }}
|
|
|
|
extension-fast:
|
|
name: "extension-fast"
|
|
needs: [preflight]
|
|
if: needs.preflight.outputs.docs_only != 'true' && needs.preflight.outputs.run_node == 'true' && needs.preflight.outputs.has_changed_extensions == 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJson(needs.preflight.outputs.changed_extensions_matrix) }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
submodules: false
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
use-sticky-disk: "false"
|
|
|
|
- name: Run changed extension tests
|
|
env:
|
|
OPENCLAW_CHANGED_EXTENSION: ${{ matrix.extension }}
|
|
run: pnpm test:extension "$OPENCLAW_CHANGED_EXTENSION"
|
|
|
|
# Types, lint, and format check.
|
|
check:
|
|
name: "check"
|
|
needs: [preflight]
|
|
if: needs.preflight.outputs.docs_only != 'true' && needs.preflight.outputs.run_node == 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
submodules: false
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
use-sticky-disk: "false"
|
|
|
|
- name: Check types and lint and oxfmt
|
|
run: pnpm check
|
|
|
|
- name: Strict TS build smoke
|
|
run: pnpm build:strict-smoke
|
|
|
|
check-additional:
|
|
name: "check-additional"
|
|
needs: [preflight]
|
|
if: needs.preflight.outputs.docs_only != 'true' && needs.preflight.outputs.run_node == 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
submodules: false
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
use-sticky-disk: "false"
|
|
|
|
- name: Run plugin extension boundary guard
|
|
id: plugin_extension_boundary
|
|
continue-on-error: true
|
|
run: pnpm run lint:plugins:no-extension-imports
|
|
|
|
- name: Run web search provider boundary guard
|
|
id: web_search_provider_boundary
|
|
continue-on-error: true
|
|
run: pnpm run lint:web-search-provider-boundaries
|
|
|
|
- name: Run extension src boundary guard
|
|
id: extension_src_outside_plugin_sdk_boundary
|
|
continue-on-error: true
|
|
run: pnpm run lint:extensions:no-src-outside-plugin-sdk
|
|
|
|
- name: Run extension plugin-sdk-internal guard
|
|
id: extension_plugin_sdk_internal_boundary
|
|
continue-on-error: true
|
|
run: pnpm run lint:extensions:no-plugin-sdk-internal
|
|
|
|
- name: Enforce safe external URL opening policy
|
|
id: no_raw_window_open
|
|
continue-on-error: true
|
|
run: pnpm lint:ui:no-raw-window-open
|
|
|
|
- name: Run gateway watch regression harness
|
|
id: gateway_watch_regression
|
|
continue-on-error: true
|
|
run: pnpm test:gateway:watch-regression
|
|
|
|
- name: Check config docs drift statefile
|
|
id: config_docs_drift
|
|
continue-on-error: true
|
|
run: pnpm config:docs:check
|
|
|
|
- name: Check plugin SDK API baseline drift
|
|
id: plugin_sdk_api_drift
|
|
continue-on-error: true
|
|
run: pnpm plugin-sdk:api:check
|
|
|
|
- name: Upload gateway watch regression artifacts
|
|
if: always()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: gateway-watch-regression
|
|
path: .local/gateway-watch-regression/
|
|
retention-days: 7
|
|
|
|
- name: Fail if any additional check failed
|
|
if: always()
|
|
env:
|
|
PLUGIN_EXTENSION_BOUNDARY_OUTCOME: ${{ steps.plugin_extension_boundary.outcome }}
|
|
WEB_SEARCH_PROVIDER_BOUNDARY_OUTCOME: ${{ steps.web_search_provider_boundary.outcome }}
|
|
EXTENSION_SRC_OUTSIDE_PLUGIN_SDK_BOUNDARY_OUTCOME: ${{ steps.extension_src_outside_plugin_sdk_boundary.outcome }}
|
|
EXTENSION_PLUGIN_SDK_INTERNAL_BOUNDARY_OUTCOME: ${{ steps.extension_plugin_sdk_internal_boundary.outcome }}
|
|
NO_RAW_WINDOW_OPEN_OUTCOME: ${{ steps.no_raw_window_open.outcome }}
|
|
GATEWAY_WATCH_REGRESSION_OUTCOME: ${{ steps.gateway_watch_regression.outcome }}
|
|
CONFIG_DOCS_DRIFT_OUTCOME: ${{ steps.config_docs_drift.outcome }}
|
|
PLUGIN_SDK_API_DRIFT_OUTCOME: ${{ steps.plugin_sdk_api_drift.outcome }}
|
|
run: |
|
|
failures=0
|
|
for result in \
|
|
"plugin-extension-boundary|$PLUGIN_EXTENSION_BOUNDARY_OUTCOME" \
|
|
"web-search-provider-boundary|$WEB_SEARCH_PROVIDER_BOUNDARY_OUTCOME" \
|
|
"extension-src-outside-plugin-sdk-boundary|$EXTENSION_SRC_OUTSIDE_PLUGIN_SDK_BOUNDARY_OUTCOME" \
|
|
"extension-plugin-sdk-internal-boundary|$EXTENSION_PLUGIN_SDK_INTERNAL_BOUNDARY_OUTCOME" \
|
|
"lint:ui:no-raw-window-open|$NO_RAW_WINDOW_OPEN_OUTCOME" \
|
|
"gateway-watch-regression|$GATEWAY_WATCH_REGRESSION_OUTCOME" \
|
|
"config-docs-drift|$CONFIG_DOCS_DRIFT_OUTCOME" \
|
|
"plugin-sdk-api-drift|$PLUGIN_SDK_API_DRIFT_OUTCOME"; do
|
|
name="${result%%|*}"
|
|
outcome="${result#*|}"
|
|
if [ "$outcome" != "success" ]; then
|
|
echo "::error title=${name} failed::${name} outcome: ${outcome}"
|
|
failures=1
|
|
fi
|
|
done
|
|
|
|
exit "$failures"
|
|
|
|
build-smoke:
|
|
name: "build-smoke"
|
|
needs: [preflight, build-artifacts]
|
|
if: always() && needs.preflight.outputs.docs_only != 'true' && needs.preflight.outputs.run_node == 'true' && (github.event_name != 'push' || needs.build-artifacts.result == 'success')
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
submodules: false
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
use-sticky-disk: "false"
|
|
|
|
- name: Download dist artifact
|
|
if: github.event_name == 'push'
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: dist-build
|
|
path: dist/
|
|
|
|
- name: Build dist
|
|
if: github.event_name != 'push'
|
|
run: pnpm build
|
|
|
|
- name: Smoke test CLI launcher help
|
|
run: node openclaw.mjs --help
|
|
|
|
- name: Smoke test CLI launcher status json
|
|
run: node openclaw.mjs status --json --timeout 1
|
|
|
|
- name: Smoke test built bundled plugin singleton
|
|
run: pnpm test:build:singleton
|
|
|
|
- name: Check CLI startup memory
|
|
run: pnpm test:startup:memory
|
|
|
|
# Validate docs (format, lint, broken links) only when docs files changed.
|
|
check-docs:
|
|
needs: [preflight]
|
|
if: needs.preflight.outputs.docs_changed == 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
submodules: false
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
use-sticky-disk: "false"
|
|
|
|
- name: Check docs
|
|
run: pnpm check:docs
|
|
|
|
skills-python:
|
|
needs: [preflight]
|
|
if: needs.preflight.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.preflight.outputs.run_skills_python == 'true')
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
submodules: false
|
|
|
|
- name: Setup Python
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Install Python tooling
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
python -m pip install pytest ruff pyyaml
|
|
|
|
- name: Lint Python skill scripts
|
|
run: python -m ruff check skills
|
|
|
|
- name: Test skill Python scripts
|
|
run: python -m pytest -q skills
|
|
|
|
checks-windows:
|
|
needs: [preflight, build-artifacts]
|
|
if: always() && needs.preflight.outputs.docs_only != 'true' && needs.preflight.outputs.run_windows == 'true' && (github.event_name != 'push' || needs.build-artifacts.result == 'success')
|
|
runs-on: blacksmith-32vcpu-windows-2025
|
|
timeout-minutes: 45
|
|
env:
|
|
NODE_OPTIONS: --max-old-space-size=6144
|
|
# Keep total concurrency predictable on the 32 vCPU runner.
|
|
# Windows shard 2 has shown intermittent instability at 2 workers.
|
|
OPENCLAW_TEST_WORKERS: 1
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- runtime: node
|
|
task: test
|
|
shard_index: 1
|
|
shard_count: 6
|
|
command: pnpm test
|
|
- runtime: node
|
|
task: test
|
|
shard_index: 2
|
|
shard_count: 6
|
|
command: pnpm test
|
|
- runtime: node
|
|
task: test
|
|
shard_index: 3
|
|
shard_count: 6
|
|
command: pnpm test
|
|
- runtime: node
|
|
task: test
|
|
shard_index: 4
|
|
shard_count: 6
|
|
command: pnpm test
|
|
- runtime: node
|
|
task: test
|
|
shard_index: 5
|
|
shard_count: 6
|
|
command: pnpm test
|
|
- runtime: node
|
|
task: test
|
|
shard_index: 6
|
|
shard_count: 6
|
|
command: pnpm test
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
submodules: false
|
|
|
|
- name: Try to exclude workspace from Windows Defender (best-effort)
|
|
shell: pwsh
|
|
run: |
|
|
$cmd = Get-Command Add-MpPreference -ErrorAction SilentlyContinue
|
|
if (-not $cmd) {
|
|
Write-Host "Add-MpPreference not available, skipping Defender exclusions."
|
|
exit 0
|
|
}
|
|
|
|
try {
|
|
# Defender sometimes intercepts process spawning (vitest workers). If this fails
|
|
# (eg hardened images), keep going and rely on worker limiting above.
|
|
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE" -ErrorAction Stop
|
|
Add-MpPreference -ExclusionProcess "node.exe" -ErrorAction Stop
|
|
Write-Host "Defender exclusions applied."
|
|
} catch {
|
|
Write-Warning "Failed to apply Defender exclusions, continuing. $($_.Exception.Message)"
|
|
}
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24.x
|
|
check-latest: false
|
|
|
|
- name: Setup pnpm + cache store
|
|
uses: ./.github/actions/setup-pnpm-store-cache
|
|
with:
|
|
pnpm-version: "10.23.0"
|
|
cache-key-suffix: "node24"
|
|
# Sticky disk mount currently retries/fails on every shard and adds ~50s
|
|
# before install while still yielding zero pnpm store reuse.
|
|
# Try exact-key actions/cache restores instead to recover store reuse
|
|
# without the sticky-disk mount penalty.
|
|
use-sticky-disk: "false"
|
|
use-restore-keys: "false"
|
|
use-actions-cache: "true"
|
|
|
|
- name: Runtime versions
|
|
run: |
|
|
node -v
|
|
npm -v
|
|
pnpm -v
|
|
|
|
- name: Capture node path
|
|
run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV"
|
|
|
|
- name: Install dependencies
|
|
env:
|
|
CI: true
|
|
run: |
|
|
export PATH="$NODE_BIN:$PATH"
|
|
which node
|
|
node -v
|
|
pnpm -v
|
|
# Persist Windows-native postinstall outputs in the pnpm store so restored
|
|
# caches can skip repeated rebuild/download work on later shards/runs.
|
|
pnpm install --frozen-lockfile --prefer-offline --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true --config.side-effects-cache=true || pnpm install --frozen-lockfile --prefer-offline --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true --config.side-effects-cache=true
|
|
|
|
- name: Configure test shard (Windows)
|
|
if: matrix.task == 'test'
|
|
run: |
|
|
echo "OPENCLAW_TEST_SHARDS=${{ matrix.shard_count }}" >> "$GITHUB_ENV"
|
|
echo "OPENCLAW_TEST_SHARD_INDEX=${{ matrix.shard_index }}" >> "$GITHUB_ENV"
|
|
|
|
- name: Build A2UI bundle (Windows)
|
|
if: matrix.task == 'test'
|
|
run: pnpm canvas:a2ui:bundle
|
|
|
|
- name: Download dist artifact
|
|
if: github.event_name == 'push' && matrix.task == 'test'
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: dist-build
|
|
path: dist/
|
|
|
|
- name: Build dist (Windows)
|
|
if: github.event_name != 'push' && matrix.task == 'test'
|
|
run: pnpm build
|
|
|
|
- name: Run ${{ matrix.task }} (${{ matrix.runtime }})
|
|
run: ${{ matrix.command }}
|
|
|
|
# Consolidated macOS job: runs TS tests + Swift lint/build/test sequentially
|
|
# on a single runner. GitHub limits macOS concurrent jobs to 5 per org;
|
|
# running 4 separate jobs per PR (as before) starved the queue. One job
|
|
# per PR allows 5 PRs to run macOS checks simultaneously.
|
|
macos:
|
|
needs: [preflight]
|
|
if: github.event_name == 'pull_request' && needs.preflight.outputs.docs_only != 'true' && needs.preflight.outputs.run_macos == 'true'
|
|
runs-on: macos-latest
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
submodules: false
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
|
|
- name: Build dist (macOS)
|
|
run: pnpm build
|
|
|
|
# --- Run all checks sequentially (fast gates first) ---
|
|
- name: TS tests (macOS)
|
|
env:
|
|
NODE_OPTIONS: --max-old-space-size=4096
|
|
run: pnpm test
|
|
|
|
# --- Xcode/Swift setup ---
|
|
- name: Select Xcode 26.1
|
|
run: |
|
|
sudo xcode-select -s /Applications/Xcode_26.1.app
|
|
xcodebuild -version
|
|
|
|
- name: Install XcodeGen / SwiftLint / SwiftFormat
|
|
run: brew install xcodegen swiftlint swiftformat
|
|
|
|
- name: Show toolchain
|
|
run: |
|
|
sw_vers
|
|
xcodebuild -version
|
|
swift --version
|
|
|
|
- name: Swift lint
|
|
run: |
|
|
swiftlint --config .swiftlint.yml
|
|
swiftformat --lint apps/macos/Sources --config .swiftformat
|
|
|
|
- name: 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: Swift build (release)
|
|
run: |
|
|
set -euo pipefail
|
|
for attempt in 1 2 3; do
|
|
if swift build --package-path apps/macos --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:
|
|
needs: [preflight]
|
|
if: needs.preflight.outputs.docs_only != 'true' && needs.preflight.outputs.run_android == 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- task: test-play
|
|
command: ./gradlew --no-daemon :app:testPlayDebugUnitTest
|
|
- task: test-third-party
|
|
command: ./gradlew --no-daemon :app:testThirdPartyDebugUnitTest
|
|
- task: build-play
|
|
command: ./gradlew --no-daemon :app:assemblePlayDebug
|
|
- task: build-third-party
|
|
command: ./gradlew --no-daemon :app:assembleThirdPartyDebug
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
submodules: false
|
|
|
|
- name: Setup Java
|
|
uses: actions/setup-java@v5
|
|
with:
|
|
distribution: temurin
|
|
# Keep sdkmanager on the stable JDK path for Linux CI runners.
|
|
java-version: 17
|
|
|
|
- name: Setup Android SDK cmdline-tools
|
|
run: |
|
|
set -euo pipefail
|
|
ANDROID_SDK_ROOT="$HOME/.android-sdk"
|
|
CMDLINE_TOOLS_VERSION="12266719"
|
|
ARCHIVE="commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip"
|
|
URL="https://dl.google.com/android/repository/${ARCHIVE}"
|
|
|
|
mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools"
|
|
curl -fsSL "$URL" -o "/tmp/${ARCHIVE}"
|
|
rm -rf "$ANDROID_SDK_ROOT/cmdline-tools/latest"
|
|
unzip -q "/tmp/${ARCHIVE}" -d "$ANDROID_SDK_ROOT/cmdline-tools"
|
|
mv "$ANDROID_SDK_ROOT/cmdline-tools/cmdline-tools" "$ANDROID_SDK_ROOT/cmdline-tools/latest"
|
|
|
|
echo "ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT" >> "$GITHUB_ENV"
|
|
echo "ANDROID_HOME=$ANDROID_SDK_ROOT" >> "$GITHUB_ENV"
|
|
echo "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin" >> "$GITHUB_PATH"
|
|
echo "$ANDROID_SDK_ROOT/platform-tools" >> "$GITHUB_PATH"
|
|
|
|
- name: Setup Gradle
|
|
uses: gradle/actions/setup-gradle@v5
|
|
with:
|
|
gradle-version: 8.11.1
|
|
|
|
- name: Install Android SDK packages
|
|
run: |
|
|
yes | sdkmanager --sdk_root="${ANDROID_SDK_ROOT}" --licenses >/dev/null
|
|
sdkmanager --sdk_root="${ANDROID_SDK_ROOT}" --install \
|
|
"platform-tools" \
|
|
"platforms;android-36" \
|
|
"build-tools;36.0.0"
|
|
|
|
- name: Run Android ${{ matrix.task }}
|
|
working-directory: apps/android
|
|
run: ${{ matrix.command }}
|