mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
1915 lines
70 KiB
YAML
1915 lines
70 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
types: [opened, reopened, synchronize, ready_for_review, converted_to_draft]
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: ${{ github.event_name == 'pull_request' && format('{0}-{1}', github.workflow, github.event.pull_request.number) || format('{0}-{1}', github.workflow, github.run_id) }}
|
|
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: blacksmith-16vcpu-ubuntu-2404
|
|
timeout-minutes: 20
|
|
outputs:
|
|
docs_only: ${{ steps.manifest.outputs.docs_only }}
|
|
docs_changed: ${{ steps.manifest.outputs.docs_changed }}
|
|
run_node: ${{ steps.manifest.outputs.run_node }}
|
|
run_macos: ${{ steps.manifest.outputs.run_macos }}
|
|
run_android: ${{ steps.manifest.outputs.run_android }}
|
|
run_skills_python: ${{ steps.manifest.outputs.run_skills_python }}
|
|
run_skills_python_job: ${{ steps.manifest.outputs.run_skills_python_job }}
|
|
run_windows: ${{ steps.manifest.outputs.run_windows }}
|
|
has_changed_extensions: ${{ steps.manifest.outputs.has_changed_extensions }}
|
|
changed_extensions_matrix: ${{ steps.manifest.outputs.changed_extensions_matrix }}
|
|
run_build_artifacts: ${{ steps.manifest.outputs.run_build_artifacts }}
|
|
run_checks_fast: ${{ steps.manifest.outputs.run_checks_fast }}
|
|
checks_fast_core_matrix: ${{ steps.manifest.outputs.checks_fast_core_matrix }}
|
|
checks_node_extensions_matrix: ${{ steps.manifest.outputs.checks_node_extensions_matrix }}
|
|
run_checks: ${{ steps.manifest.outputs.run_checks }}
|
|
checks_matrix: ${{ steps.manifest.outputs.checks_matrix }}
|
|
checks_node_core_test_matrix: ${{ steps.manifest.outputs.checks_node_core_test_matrix }}
|
|
run_extension_fast: ${{ steps.manifest.outputs.run_extension_fast }}
|
|
extension_fast_matrix: ${{ steps.manifest.outputs.extension_fast_matrix }}
|
|
run_check: ${{ steps.manifest.outputs.run_check }}
|
|
run_check_additional: ${{ steps.manifest.outputs.run_check_additional }}
|
|
run_build_smoke: ${{ steps.manifest.outputs.run_build_smoke }}
|
|
run_check_docs: ${{ steps.manifest.outputs.run_check_docs }}
|
|
run_control_ui_i18n: ${{ steps.manifest.outputs.run_control_ui_i18n }}
|
|
run_checks_windows: ${{ steps.manifest.outputs.run_checks_windows }}
|
|
checks_windows_matrix: ${{ steps.manifest.outputs.checks_windows_matrix }}
|
|
run_macos_node: ${{ steps.manifest.outputs.run_macos_node }}
|
|
macos_node_matrix: ${{ steps.manifest.outputs.macos_node_matrix }}
|
|
run_macos_swift: ${{ steps.manifest.outputs.run_macos_swift }}
|
|
run_android_job: ${{ steps.manifest.outputs.run_android_job }}
|
|
android_matrix: ${{ steps.manifest.outputs.android_matrix }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 1
|
|
fetch-tags: false
|
|
persist-credentials: false
|
|
submodules: false
|
|
|
|
- name: Ensure preflight base commit
|
|
uses: ./.github/actions/ensure-base-commit
|
|
with:
|
|
base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
|
|
fetch-ref: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }}
|
|
|
|
- name: Detect docs-only changes
|
|
id: docs_scope
|
|
uses: ./.github/actions/detect-docs-changes
|
|
|
|
- name: Detect changed scopes
|
|
id: changed_scope
|
|
if: steps.docs_scope.outputs.docs_only != 'true'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if [ "${{ github.event_name }}" = "push" ]; then
|
|
BASE="${{ github.event.before }}"
|
|
else
|
|
BASE="${{ github.event.pull_request.base.sha }}"
|
|
fi
|
|
|
|
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
|
|
|
|
- name: Setup Node environment
|
|
if: steps.docs_scope.outputs.docs_only != 'true'
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
install-deps: "false"
|
|
use-sticky-disk: "false"
|
|
|
|
- name: Detect changed extensions
|
|
id: changed_extensions
|
|
if: steps.docs_scope.outputs.docs_only != 'true' && steps.changed_scope.outputs.run_node == 'true'
|
|
env:
|
|
BASE_SHA: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
|
|
BASE_REF: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }}
|
|
run: |
|
|
node --input-type=module <<'EOF'
|
|
import { appendFileSync } from "node:fs";
|
|
import { listChangedExtensionIds } from "./scripts/lib/changed-extensions.mjs";
|
|
|
|
const extensionIds = listChangedExtensionIds({
|
|
base: process.env.BASE_SHA,
|
|
head: "HEAD",
|
|
fallbackBaseRef: process.env.BASE_REF,
|
|
unavailableBaseBehavior: "all",
|
|
});
|
|
const matrix = JSON.stringify({ include: extensionIds.map((extension) => ({ extension })) });
|
|
|
|
appendFileSync(process.env.GITHUB_OUTPUT, `has_changed_extensions=${extensionIds.length > 0}\n`, "utf8");
|
|
appendFileSync(process.env.GITHUB_OUTPUT, `changed_extensions_matrix=${matrix}\n`, "utf8");
|
|
EOF
|
|
|
|
- name: Build CI manifest
|
|
id: manifest
|
|
env:
|
|
OPENCLAW_CI_DOCS_ONLY: ${{ steps.docs_scope.outputs.docs_only }}
|
|
OPENCLAW_CI_DOCS_CHANGED: ${{ steps.docs_scope.outputs.docs_changed }}
|
|
OPENCLAW_CI_RUN_NODE: ${{ steps.changed_scope.outputs.run_node || 'false' }}
|
|
OPENCLAW_CI_RUN_MACOS: ${{ steps.changed_scope.outputs.run_macos || 'false' }}
|
|
OPENCLAW_CI_RUN_ANDROID: ${{ steps.changed_scope.outputs.run_android || 'false' }}
|
|
OPENCLAW_CI_RUN_WINDOWS: ${{ steps.changed_scope.outputs.run_windows || 'false' }}
|
|
OPENCLAW_CI_RUN_SKILLS_PYTHON: ${{ steps.changed_scope.outputs.run_skills_python || 'false' }}
|
|
OPENCLAW_CI_RUN_CONTROL_UI_I18N: ${{ steps.changed_scope.outputs.run_control_ui_i18n || 'false' }}
|
|
OPENCLAW_CI_HAS_CHANGED_EXTENSIONS: ${{ steps.changed_extensions.outputs.has_changed_extensions || 'false' }}
|
|
OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX: ${{ steps.changed_extensions.outputs.changed_extensions_matrix || '{"include":[]}' }}
|
|
run: |
|
|
node --input-type=module <<'EOF'
|
|
import { appendFileSync } from "node:fs";
|
|
import {
|
|
createNodeTestShards,
|
|
} from "./scripts/lib/ci-node-test-plan.mjs";
|
|
import {
|
|
createExtensionTestShards,
|
|
DEFAULT_EXTENSION_TEST_SHARD_COUNT,
|
|
} from "./scripts/lib/extension-test-plan.mjs";
|
|
|
|
const parseBoolean = (value, fallback = false) => {
|
|
if (value === undefined) return fallback;
|
|
const normalized = value.trim().toLowerCase();
|
|
if (normalized === "true" || normalized === "1") return true;
|
|
if (normalized === "false" || normalized === "0" || normalized === "") return false;
|
|
return fallback;
|
|
};
|
|
|
|
const parseJson = (value, fallback) => {
|
|
try {
|
|
return value ? JSON.parse(value) : fallback;
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
};
|
|
|
|
const createMatrix = (include) => ({ include });
|
|
const outputPath = process.env.GITHUB_OUTPUT;
|
|
const eventName = process.env.GITHUB_EVENT_NAME ?? "pull_request";
|
|
const isPush = eventName === "push";
|
|
const docsOnly = parseBoolean(process.env.OPENCLAW_CI_DOCS_ONLY);
|
|
const docsChanged = parseBoolean(process.env.OPENCLAW_CI_DOCS_CHANGED);
|
|
const runNode = parseBoolean(process.env.OPENCLAW_CI_RUN_NODE) && !docsOnly;
|
|
const runMacos = parseBoolean(process.env.OPENCLAW_CI_RUN_MACOS) && !docsOnly;
|
|
const runAndroid = parseBoolean(process.env.OPENCLAW_CI_RUN_ANDROID) && !docsOnly;
|
|
const runWindows = parseBoolean(process.env.OPENCLAW_CI_RUN_WINDOWS) && !docsOnly;
|
|
const runSkillsPython = parseBoolean(process.env.OPENCLAW_CI_RUN_SKILLS_PYTHON) && !docsOnly;
|
|
const runControlUiI18n =
|
|
parseBoolean(process.env.OPENCLAW_CI_RUN_CONTROL_UI_I18N) && !docsOnly;
|
|
const hasChangedExtensions =
|
|
parseBoolean(process.env.OPENCLAW_CI_HAS_CHANGED_EXTENSIONS) && !docsOnly;
|
|
const changedExtensionsMatrix = hasChangedExtensions
|
|
? parseJson(process.env.OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX, { include: [] })
|
|
: { include: [] };
|
|
const extensionShardMatrix = createMatrix(
|
|
runNode
|
|
? createExtensionTestShards({
|
|
shardCount: DEFAULT_EXTENSION_TEST_SHARD_COUNT,
|
|
}).map((shard) => ({
|
|
check_name: shard.checkName,
|
|
extensions_csv: shard.extensionIds.join(","),
|
|
shard_index: shard.index + 1,
|
|
task: "extensions-batch",
|
|
}))
|
|
: [],
|
|
);
|
|
|
|
const manifest = {
|
|
docs_only: docsOnly,
|
|
docs_changed: docsChanged,
|
|
run_node: runNode,
|
|
run_macos: runMacos,
|
|
run_android: runAndroid,
|
|
run_skills_python: runSkillsPython,
|
|
run_windows: runWindows,
|
|
has_changed_extensions: hasChangedExtensions,
|
|
changed_extensions_matrix: changedExtensionsMatrix,
|
|
run_build_artifacts: runNode,
|
|
run_checks_fast: runNode,
|
|
checks_fast_core_matrix: createMatrix(
|
|
runNode
|
|
? [
|
|
{ check_name: "checks-fast-bundled", runtime: "node", task: "bundled" },
|
|
{
|
|
check_name: "checks-fast-contracts-protocol",
|
|
runtime: "node",
|
|
task: "contracts",
|
|
},
|
|
]
|
|
: [],
|
|
),
|
|
checks_node_extensions_matrix: extensionShardMatrix,
|
|
run_checks: runNode,
|
|
checks_matrix: createMatrix(
|
|
runNode
|
|
? [
|
|
{ check_name: "checks-node-channels", runtime: "node", task: "channels" },
|
|
...(isPush
|
|
? [
|
|
{
|
|
check_name: "checks-node-compat-node22",
|
|
runtime: "node",
|
|
task: "compat-node22",
|
|
node_version: "22.18.0",
|
|
cache_key_suffix: "node22",
|
|
},
|
|
]
|
|
: []),
|
|
]
|
|
: [],
|
|
),
|
|
checks_node_core_test_matrix: createMatrix(
|
|
runNode
|
|
? createNodeTestShards().map((shard) => ({
|
|
check_name: shard.checkName,
|
|
runtime: "node",
|
|
task: "test-shard",
|
|
shard_name: shard.shardName,
|
|
configs: shard.configs,
|
|
requires_dist: shard.requiresDist,
|
|
}))
|
|
: [],
|
|
),
|
|
run_extension_fast: hasChangedExtensions,
|
|
extension_fast_matrix: createMatrix(
|
|
hasChangedExtensions
|
|
? (changedExtensionsMatrix.include ?? []).map((entry) => ({
|
|
check_name: `extension-fast-${entry.extension}`,
|
|
extension: entry.extension,
|
|
}))
|
|
: [],
|
|
),
|
|
run_check: runNode,
|
|
run_check_additional: runNode,
|
|
run_build_smoke: runNode,
|
|
run_check_docs: docsChanged,
|
|
run_control_ui_i18n: runControlUiI18n,
|
|
run_skills_python_job: runSkillsPython,
|
|
run_checks_windows: runWindows,
|
|
checks_windows_matrix: createMatrix(
|
|
runWindows
|
|
? [{ check_name: "checks-windows-node-test", runtime: "node", task: "test" }]
|
|
: [],
|
|
),
|
|
run_macos_node: runMacos,
|
|
macos_node_matrix: createMatrix(
|
|
runMacos ? [{ check_name: "macos-node", runtime: "node", task: "test" }] : [],
|
|
),
|
|
run_macos_swift: runMacos,
|
|
run_android_job: runAndroid,
|
|
android_matrix: createMatrix(
|
|
runAndroid
|
|
? [
|
|
{ check_name: "android-test-play", task: "test-play" },
|
|
{ check_name: "android-test-third-party", task: "test-third-party" },
|
|
{ check_name: "android-build-play", task: "build-play" },
|
|
{ check_name: "android-build-third-party", task: "build-third-party" },
|
|
]
|
|
: [],
|
|
),
|
|
};
|
|
|
|
for (const [key, value] of Object.entries(manifest)) {
|
|
appendFileSync(
|
|
outputPath,
|
|
`${key}=${typeof value === "string" ? value : JSON.stringify(value)}\n`,
|
|
"utf8",
|
|
);
|
|
}
|
|
EOF
|
|
|
|
# Run the fast security/SCM checks in parallel with scope detection so the
|
|
# main Node jobs do not have to wait for Python/pre-commit setup.
|
|
security-fast:
|
|
permissions:
|
|
contents: read
|
|
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
timeout-minutes: 20
|
|
env:
|
|
PRE_COMMIT_CACHE_KEY_SUFFIX: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.sha }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 1
|
|
fetch-tags: false
|
|
persist-credentials: false
|
|
submodules: false
|
|
|
|
- name: Ensure security base commit
|
|
uses: ./.github/actions/ensure-base-commit
|
|
with:
|
|
base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
|
|
fetch-ref: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }}
|
|
|
|
- name: Prepare trusted pre-commit config
|
|
if: github.event_name == 'pull_request'
|
|
env:
|
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
trusted_config="$RUNNER_TEMP/pre-commit-base.yaml"
|
|
git show "${BASE_SHA}:.pre-commit-config.yaml" > "$trusted_config"
|
|
echo "PRE_COMMIT_CONFIG_PATH=$trusted_config" >> "$GITHUB_ENV"
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
install-deps: "false"
|
|
use-sticky-disk: "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') }}-${{ env.PRE_COMMIT_CACHE_KEY_SUFFIX }}
|
|
restore-keys: |
|
|
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==4.2.0
|
|
|
|
- name: Detect committed private keys
|
|
run: pre-commit run --config "${PRE_COMMIT_CONFIG_PATH:-.pre-commit-config.yaml}" --all-files detect-private-key
|
|
|
|
- name: Audit changed GitHub workflows with zizmor
|
|
env:
|
|
BASE_SHA: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if [ -z "${BASE_SHA:-}" ] || [ "${BASE_SHA}" = "0000000000000000000000000000000000000000" ]; then
|
|
echo "No usable base SHA detected; skipping zizmor."
|
|
exit 0
|
|
fi
|
|
|
|
if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
|
|
echo "Base SHA ${BASE_SHA} is unavailable; skipping zizmor."
|
|
exit 0
|
|
fi
|
|
|
|
mapfile -t workflow_files < <(
|
|
git diff --name-only "${BASE_SHA}" HEAD -- '.github/workflows/*.yml' '.github/workflows/*.yaml'
|
|
)
|
|
if [ "${#workflow_files[@]}" -eq 0 ]; then
|
|
echo "No workflow changes detected; skipping zizmor."
|
|
exit 0
|
|
fi
|
|
|
|
printf 'Auditing workflow files:\n%s\n' "${workflow_files[@]}"
|
|
pre-commit run --config "${PRE_COMMIT_CONFIG_PATH:-.pre-commit-config.yaml}" zizmor --files "${workflow_files[@]}"
|
|
|
|
- name: Audit production dependencies
|
|
run: pre-commit run --config "${PRE_COMMIT_CONFIG_PATH:-.pre-commit-config.yaml}" --all-files pnpm-audit-prod
|
|
|
|
# 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: blacksmith-16vcpu-ubuntu-2404
|
|
timeout-minutes: 20
|
|
steps:
|
|
- name: Checkout
|
|
shell: bash
|
|
env:
|
|
CHECKOUT_REPO: ${{ github.repository }}
|
|
CHECKOUT_SHA: ${{ github.sha }}
|
|
CHECKOUT_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
workdir="$GITHUB_WORKSPACE"
|
|
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
|
|
|
|
reset_checkout_dir() {
|
|
mkdir -p "$workdir"
|
|
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
|
}
|
|
|
|
checkout_attempt() {
|
|
local attempt="$1"
|
|
|
|
reset_checkout_dir
|
|
git init "$workdir" >/dev/null
|
|
git config --global --add safe.directory "$workdir"
|
|
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
|
|
git -C "$workdir" config gc.auto 0
|
|
|
|
timeout --signal=TERM 30s git -C "$workdir" \
|
|
-c protocol.version=2 \
|
|
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
|
|
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
|
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
|
|
|
|
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
|
|
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
|
|
echo "checkout attempt ${attempt}/2 succeeded"
|
|
}
|
|
|
|
for attempt in 1 2; do
|
|
if checkout_attempt "$attempt"; then
|
|
exit 0
|
|
fi
|
|
echo "checkout attempt ${attempt}/2 failed"
|
|
sleep $((attempt * 5))
|
|
done
|
|
|
|
echo "checkout failed after 2 attempts" >&2
|
|
exit 1
|
|
|
|
- name: Ensure secrets base commit (PR fast path)
|
|
if: github.event_name == 'pull_request'
|
|
uses: ./.github/actions/ensure-base-commit
|
|
with:
|
|
base-sha: ${{ github.event.pull_request.base.sha }}
|
|
fetch-ref: ${{ github.event.pull_request.base.ref }}
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
use-sticky-disk: "false"
|
|
|
|
- name: Build dist
|
|
run: pnpm build:ci-artifacts
|
|
|
|
- name: Build Control UI
|
|
run: pnpm ui:build
|
|
|
|
- name: Cache dist build
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: dist/
|
|
key: ${{ runner.os }}-dist-build-${{ github.sha }}
|
|
|
|
- name: Upload dist artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: dist-build
|
|
path: dist/
|
|
compression-level: 0
|
|
retention-days: 1
|
|
|
|
- name: Upload A2UI bundle artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: canvas-a2ui-bundle
|
|
path: src/canvas-host/a2ui/
|
|
include-hidden-files: true
|
|
retention-days: 1
|
|
|
|
checks-fast-core:
|
|
permissions:
|
|
contents: read
|
|
name: ${{ matrix.check_name }}
|
|
needs: [preflight]
|
|
if: needs.preflight.outputs.run_checks_fast == 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
timeout-minutes: 60
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJson(needs.preflight.outputs.checks_fast_core_matrix) }}
|
|
steps:
|
|
- name: Checkout
|
|
shell: bash
|
|
env:
|
|
CHECKOUT_REPO: ${{ github.repository }}
|
|
CHECKOUT_SHA: ${{ github.sha }}
|
|
CHECKOUT_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
workdir="$GITHUB_WORKSPACE"
|
|
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
|
|
|
|
reset_checkout_dir() {
|
|
mkdir -p "$workdir"
|
|
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
|
}
|
|
|
|
checkout_attempt() {
|
|
local attempt="$1"
|
|
|
|
reset_checkout_dir
|
|
git init "$workdir" >/dev/null
|
|
git config --global --add safe.directory "$workdir"
|
|
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
|
|
git -C "$workdir" config gc.auto 0
|
|
|
|
timeout --signal=TERM 30s git -C "$workdir" \
|
|
-c protocol.version=2 \
|
|
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
|
|
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
|
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
|
|
|
|
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
|
|
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
|
|
echo "checkout attempt ${attempt}/2 succeeded"
|
|
}
|
|
|
|
for attempt in 1 2; do
|
|
if checkout_attempt "$attempt"; then
|
|
exit 0
|
|
fi
|
|
echo "checkout attempt ${attempt}/2 failed"
|
|
sleep $((attempt * 5))
|
|
done
|
|
|
|
echo "checkout failed after 2 attempts" >&2
|
|
exit 1
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
use-sticky-disk: "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)
|
|
pnpm test:contracts
|
|
;;
|
|
*)
|
|
echo "Unsupported checks-fast task: $TASK" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
checks-fast-protocol:
|
|
permissions:
|
|
contents: read
|
|
name: "checks-fast-protocol"
|
|
needs: [preflight]
|
|
if: needs.preflight.outputs.run_checks_fast == 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
timeout-minutes: 30
|
|
steps:
|
|
- name: Checkout
|
|
shell: bash
|
|
env:
|
|
CHECKOUT_REPO: ${{ github.repository }}
|
|
CHECKOUT_SHA: ${{ github.sha }}
|
|
CHECKOUT_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
workdir="$GITHUB_WORKSPACE"
|
|
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
|
|
|
|
reset_checkout_dir() {
|
|
mkdir -p "$workdir"
|
|
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
|
}
|
|
|
|
checkout_attempt() {
|
|
local attempt="$1"
|
|
|
|
reset_checkout_dir
|
|
git init "$workdir" >/dev/null
|
|
git config --global --add safe.directory "$workdir"
|
|
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
|
|
git -C "$workdir" config gc.auto 0
|
|
|
|
timeout --signal=TERM 30s git -C "$workdir" \
|
|
-c protocol.version=2 \
|
|
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
|
|
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
|
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
|
|
|
|
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
|
|
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
|
|
echo "checkout attempt ${attempt}/2 succeeded"
|
|
}
|
|
|
|
for attempt in 1 2; do
|
|
if checkout_attempt "$attempt"; then
|
|
exit 0
|
|
fi
|
|
echo "checkout attempt ${attempt}/2 failed"
|
|
sleep $((attempt * 5))
|
|
done
|
|
|
|
echo "checkout failed after 2 attempts" >&2
|
|
exit 1
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
use-sticky-disk: "false"
|
|
|
|
- name: Run protocol check
|
|
run: pnpm protocol:check
|
|
|
|
checks-node-extensions-shard:
|
|
permissions:
|
|
contents: read
|
|
name: ${{ matrix.check_name }}
|
|
needs: [preflight]
|
|
if: needs.preflight.outputs.run_checks_fast == 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
timeout-minutes: 60
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJson(needs.preflight.outputs.checks_node_extensions_matrix) }}
|
|
steps:
|
|
- name: Checkout
|
|
shell: bash
|
|
env:
|
|
CHECKOUT_REPO: ${{ github.repository }}
|
|
CHECKOUT_SHA: ${{ github.sha }}
|
|
CHECKOUT_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
workdir="$GITHUB_WORKSPACE"
|
|
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
|
|
|
|
reset_checkout_dir() {
|
|
mkdir -p "$workdir"
|
|
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
|
}
|
|
|
|
checkout_attempt() {
|
|
local attempt="$1"
|
|
|
|
reset_checkout_dir
|
|
git init "$workdir" >/dev/null
|
|
git config --global --add safe.directory "$workdir"
|
|
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
|
|
git -C "$workdir" config gc.auto 0
|
|
|
|
timeout --signal=TERM 30s git -C "$workdir" \
|
|
-c protocol.version=2 \
|
|
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
|
|
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
|
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
|
|
|
|
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
|
|
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
|
|
echo "checkout attempt ${attempt}/2 succeeded"
|
|
}
|
|
|
|
for attempt in 1 2; do
|
|
if checkout_attempt "$attempt"; then
|
|
exit 0
|
|
fi
|
|
echo "checkout attempt ${attempt}/2 failed"
|
|
sleep $((attempt * 5))
|
|
done
|
|
|
|
echo "checkout failed after 2 attempts" >&2
|
|
exit 1
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
use-sticky-disk: "false"
|
|
|
|
- name: Run extension shard
|
|
env:
|
|
OPENCLAW_EXTENSION_BATCH: ${{ matrix.extensions_csv }}
|
|
run: pnpm test:extensions:batch -- "$OPENCLAW_EXTENSION_BATCH"
|
|
|
|
checks-node-extensions:
|
|
permissions:
|
|
contents: read
|
|
name: checks-node-extensions
|
|
needs: [preflight, checks-node-extensions-shard]
|
|
if: always() && needs.preflight.outputs.run_checks_fast == 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
timeout-minutes: 5
|
|
steps:
|
|
- name: Verify extension shards
|
|
env:
|
|
SHARD_RESULT: ${{ needs.checks-node-extensions-shard.result }}
|
|
run: |
|
|
if [ "$SHARD_RESULT" != "success" ]; then
|
|
echo "Extension shard checks failed: $SHARD_RESULT" >&2
|
|
exit 1
|
|
fi
|
|
|
|
checks:
|
|
permissions:
|
|
contents: read
|
|
name: ${{ matrix.check_name }}
|
|
needs: [preflight, build-artifacts]
|
|
if: always() && needs.preflight.outputs.run_checks == 'true' && needs.build-artifacts.result == 'success'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
timeout-minutes: 60
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJson(needs.preflight.outputs.checks_matrix) }}
|
|
steps:
|
|
- name: Skip compatibility lanes on pull requests
|
|
if: github.event_name == 'pull_request' && matrix.task == 'compat-node22'
|
|
run: echo "Skipping push-only lane on pull requests."
|
|
|
|
- name: Checkout
|
|
if: github.event_name != 'pull_request' || matrix.task != 'compat-node22'
|
|
shell: bash
|
|
env:
|
|
CHECKOUT_REPO: ${{ github.repository }}
|
|
CHECKOUT_SHA: ${{ github.sha }}
|
|
CHECKOUT_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
workdir="$GITHUB_WORKSPACE"
|
|
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
|
|
|
|
reset_checkout_dir() {
|
|
mkdir -p "$workdir"
|
|
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
|
}
|
|
|
|
checkout_attempt() {
|
|
local attempt="$1"
|
|
|
|
reset_checkout_dir
|
|
git init "$workdir" >/dev/null
|
|
git config --global --add safe.directory "$workdir"
|
|
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
|
|
git -C "$workdir" config gc.auto 0
|
|
|
|
timeout --signal=TERM 30s git -C "$workdir" \
|
|
-c protocol.version=2 \
|
|
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
|
|
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
|
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
|
|
|
|
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
|
|
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
|
|
echo "checkout attempt ${attempt}/2 succeeded"
|
|
}
|
|
|
|
for attempt in 1 2; do
|
|
if checkout_attempt "$attempt"; then
|
|
exit 0
|
|
fi
|
|
echo "checkout attempt ${attempt}/2 failed"
|
|
sleep $((attempt * 5))
|
|
done
|
|
|
|
echo "checkout failed after 2 attempts" >&2
|
|
exit 1
|
|
|
|
- name: Setup Node environment
|
|
if: github.event_name != 'pull_request' || matrix.task != 'compat-node22'
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
node-version: "${{ matrix.node_version || '24.x' }}"
|
|
cache-key-suffix: "${{ matrix.cache_key_suffix || 'node24' }}"
|
|
install-bun: "false"
|
|
use-sticky-disk: "false"
|
|
|
|
- name: Configure Node test resources
|
|
if: (github.event_name != 'pull_request' || matrix.task != 'compat-node22') && matrix.runtime == 'node' && (matrix.task == 'test' || matrix.task == 'channels' || matrix.task == 'compat-node22')
|
|
env:
|
|
TASK: ${{ matrix.task }}
|
|
run: |
|
|
echo "OPENCLAW_VITEST_MAX_WORKERS=2" >> "$GITHUB_ENV"
|
|
if [ "$TASK" = "test" ]; then
|
|
echo "OPENCLAW_TEST_PROJECTS_LEAF_SHARDS=1" >> "$GITHUB_ENV"
|
|
echo "OPENCLAW_TEST_SKIP_FULL_EXTENSIONS_SHARD=1" >> "$GITHUB_ENV"
|
|
fi
|
|
if [ "$TASK" = "channels" ]; then
|
|
echo "OPENCLAW_VITEST_MAX_WORKERS=1" >> "$GITHUB_ENV"
|
|
fi
|
|
|
|
- name: Download dist artifact
|
|
if: matrix.task == 'test'
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: dist-build
|
|
path: dist/
|
|
|
|
- name: Download A2UI bundle artifact
|
|
if: matrix.task == 'test' || matrix.task == 'channels'
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: canvas-a2ui-bundle
|
|
path: src/canvas-host/a2ui/
|
|
|
|
- name: Run ${{ matrix.task }} (${{ matrix.runtime }})
|
|
if: github.event_name != 'pull_request' || matrix.task != 'compat-node22'
|
|
env:
|
|
TASK: ${{ matrix.task }}
|
|
NODE_OPTIONS: --max-old-space-size=6144
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
case "$TASK" in
|
|
test)
|
|
pnpm test
|
|
;;
|
|
channels)
|
|
pnpm test:channels
|
|
;;
|
|
compat-node22)
|
|
pnpm build
|
|
pnpm ui:build
|
|
node openclaw.mjs --help
|
|
node openclaw.mjs status --json --timeout 1
|
|
pnpm test:build:singleton
|
|
;;
|
|
*)
|
|
echo "Unsupported checks task: $TASK" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
checks-node-core-test-shard:
|
|
permissions:
|
|
contents: read
|
|
name: ${{ matrix.check_name }}
|
|
needs: [preflight, build-artifacts]
|
|
if: always() && needs.preflight.outputs.run_checks == 'true' && needs.build-artifacts.result == 'success'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
timeout-minutes: 60
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_test_matrix) }}
|
|
steps:
|
|
- name: Checkout
|
|
shell: bash
|
|
env:
|
|
CHECKOUT_REPO: ${{ github.repository }}
|
|
CHECKOUT_SHA: ${{ github.sha }}
|
|
CHECKOUT_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
workdir="$GITHUB_WORKSPACE"
|
|
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
|
|
|
|
reset_checkout_dir() {
|
|
mkdir -p "$workdir"
|
|
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
|
}
|
|
|
|
checkout_attempt() {
|
|
local attempt="$1"
|
|
|
|
reset_checkout_dir
|
|
git init "$workdir" >/dev/null
|
|
git config --global --add safe.directory "$workdir"
|
|
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
|
|
git -C "$workdir" config gc.auto 0
|
|
|
|
timeout --signal=TERM 30s git -C "$workdir" \
|
|
-c protocol.version=2 \
|
|
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
|
|
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
|
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
|
|
|
|
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
|
|
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
|
|
echo "checkout attempt ${attempt}/2 succeeded"
|
|
}
|
|
|
|
for attempt in 1 2; do
|
|
if checkout_attempt "$attempt"; then
|
|
exit 0
|
|
fi
|
|
echo "checkout attempt ${attempt}/2 failed"
|
|
sleep $((attempt * 5))
|
|
done
|
|
|
|
echo "checkout failed after 2 attempts" >&2
|
|
exit 1
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
node-version: "${{ matrix.node_version || '24.x' }}"
|
|
cache-key-suffix: "${{ matrix.cache_key_suffix || 'node24' }}"
|
|
install-bun: "false"
|
|
use-sticky-disk: "false"
|
|
|
|
- name: Configure Node test resources
|
|
run: echo "OPENCLAW_VITEST_MAX_WORKERS=2" >> "$GITHUB_ENV"
|
|
|
|
- name: Restore dist cache
|
|
id: dist-cache
|
|
if: matrix.requires_dist == true
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: dist/
|
|
key: ${{ runner.os }}-dist-build-${{ github.sha }}
|
|
|
|
- name: Download dist artifact
|
|
if: matrix.requires_dist == true && steps.dist-cache.outputs.cache-hit != 'true'
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: dist-build
|
|
path: dist/
|
|
|
|
- name: Download A2UI bundle artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: canvas-a2ui-bundle
|
|
path: src/canvas-host/a2ui/
|
|
|
|
- name: Run Node test shard
|
|
env:
|
|
NODE_OPTIONS: --max-old-space-size=6144
|
|
OPENCLAW_NODE_TEST_CONFIGS_JSON: ${{ toJson(matrix.configs) }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
node --input-type=module <<'EOF'
|
|
import { spawnSync } from "node:child_process";
|
|
import { resolveVitestCliEntry, resolveVitestNodeArgs } from "./scripts/run-vitest.mjs";
|
|
|
|
const configs = JSON.parse(process.env.OPENCLAW_NODE_TEST_CONFIGS_JSON ?? "[]");
|
|
if (!Array.isArray(configs) || configs.length === 0) {
|
|
console.error("Missing node test shard configs");
|
|
process.exit(1);
|
|
}
|
|
|
|
for (const config of configs) {
|
|
console.error(`[test] starting ${config}`);
|
|
const result = spawnSync(
|
|
"pnpm",
|
|
[
|
|
"exec",
|
|
"node",
|
|
...resolveVitestNodeArgs(process.env),
|
|
resolveVitestCliEntry(),
|
|
"run",
|
|
"--config",
|
|
config,
|
|
],
|
|
{
|
|
env: process.env,
|
|
stdio: "inherit",
|
|
},
|
|
);
|
|
if ((result.status ?? 1) !== 0) {
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
}
|
|
EOF
|
|
|
|
checks-node-core-test:
|
|
permissions:
|
|
contents: read
|
|
name: checks-node-core
|
|
needs: [preflight, checks-node-core-test-shard]
|
|
if: always() && needs.preflight.outputs.run_checks == 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
timeout-minutes: 5
|
|
steps:
|
|
- name: Verify node test shards
|
|
env:
|
|
SHARD_RESULT: ${{ needs.checks-node-core-test-shard.result }}
|
|
run: |
|
|
if [ "$SHARD_RESULT" != "success" ]; then
|
|
echo "Node test shards failed: $SHARD_RESULT" >&2
|
|
exit 1
|
|
fi
|
|
|
|
extension-fast:
|
|
permissions:
|
|
contents: read
|
|
name: "extension-fast"
|
|
needs: [preflight]
|
|
if: needs.preflight.outputs.run_extension_fast == 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
timeout-minutes: 60
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJson(needs.preflight.outputs.extension_fast_matrix) }}
|
|
steps:
|
|
- name: Checkout
|
|
shell: bash
|
|
env:
|
|
CHECKOUT_REPO: ${{ github.repository }}
|
|
CHECKOUT_SHA: ${{ github.sha }}
|
|
CHECKOUT_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
workdir="$GITHUB_WORKSPACE"
|
|
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
|
|
|
|
reset_checkout_dir() {
|
|
mkdir -p "$workdir"
|
|
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
|
}
|
|
|
|
checkout_attempt() {
|
|
local attempt="$1"
|
|
|
|
reset_checkout_dir
|
|
git init "$workdir" >/dev/null
|
|
git config --global --add safe.directory "$workdir"
|
|
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
|
|
git -C "$workdir" config gc.auto 0
|
|
|
|
timeout --signal=TERM 30s git -C "$workdir" \
|
|
-c protocol.version=2 \
|
|
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
|
|
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
|
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
|
|
|
|
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
|
|
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
|
|
echo "checkout attempt ${attempt}/2 succeeded"
|
|
}
|
|
|
|
for attempt in 1 2; do
|
|
if checkout_attempt "$attempt"; then
|
|
exit 0
|
|
fi
|
|
echo "checkout attempt ${attempt}/2 failed"
|
|
sleep $((attempt * 5))
|
|
done
|
|
|
|
echo "checkout failed after 2 attempts" >&2
|
|
exit 1
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
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:
|
|
permissions:
|
|
contents: read
|
|
name: "check"
|
|
needs: [preflight]
|
|
if: always() && needs.preflight.outputs.run_check == 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
timeout-minutes: 20
|
|
steps:
|
|
- name: Checkout
|
|
shell: bash
|
|
env:
|
|
CHECKOUT_REPO: ${{ github.repository }}
|
|
CHECKOUT_SHA: ${{ github.sha }}
|
|
CHECKOUT_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
workdir="$GITHUB_WORKSPACE"
|
|
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
|
|
|
|
reset_checkout_dir() {
|
|
mkdir -p "$workdir"
|
|
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
|
}
|
|
|
|
checkout_attempt() {
|
|
local attempt="$1"
|
|
|
|
reset_checkout_dir
|
|
git init "$workdir" >/dev/null
|
|
git config --global --add safe.directory "$workdir"
|
|
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
|
|
git -C "$workdir" config gc.auto 0
|
|
|
|
timeout --signal=TERM 30s git -C "$workdir" \
|
|
-c protocol.version=2 \
|
|
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
|
|
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
|
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
|
|
|
|
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
|
|
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
|
|
echo "checkout attempt ${attempt}/2 succeeded"
|
|
}
|
|
|
|
for attempt in 1 2; do
|
|
if checkout_attempt "$attempt"; then
|
|
exit 0
|
|
fi
|
|
echo "checkout attempt ${attempt}/2 failed"
|
|
sleep $((attempt * 5))
|
|
done
|
|
|
|
echo "checkout failed after 2 attempts" >&2
|
|
exit 1
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
use-sticky-disk: "false"
|
|
|
|
- name: Check types and lint and oxfmt
|
|
env:
|
|
OPENCLAW_LOCAL_CHECK: "0"
|
|
run: pnpm check
|
|
|
|
- name: Check test types
|
|
env:
|
|
OPENCLAW_LOCAL_CHECK: "0"
|
|
run: pnpm check:test-types
|
|
|
|
- name: Strict TS build smoke
|
|
run: pnpm build:strict-smoke
|
|
|
|
check-additional-shard:
|
|
permissions:
|
|
contents: read
|
|
name: ${{ matrix.check_name }}
|
|
needs: [preflight]
|
|
if: always() && needs.preflight.outputs.run_check_additional == 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
timeout-minutes: 20
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- check_name: check-additional-boundaries
|
|
group: boundaries
|
|
- check_name: check-additional-extension-surfaces
|
|
group: extension-surfaces
|
|
- check_name: check-additional-runtime-topology
|
|
group: runtime-topology
|
|
steps:
|
|
- name: Checkout
|
|
shell: bash
|
|
env:
|
|
CHECKOUT_REPO: ${{ github.repository }}
|
|
CHECKOUT_SHA: ${{ github.sha }}
|
|
CHECKOUT_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
workdir="$GITHUB_WORKSPACE"
|
|
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
|
|
|
|
reset_checkout_dir() {
|
|
mkdir -p "$workdir"
|
|
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
|
}
|
|
|
|
checkout_attempt() {
|
|
local attempt="$1"
|
|
|
|
reset_checkout_dir
|
|
git init "$workdir" >/dev/null
|
|
git config --global --add safe.directory "$workdir"
|
|
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
|
|
git -C "$workdir" config gc.auto 0
|
|
|
|
timeout --signal=TERM 30s git -C "$workdir" \
|
|
-c protocol.version=2 \
|
|
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
|
|
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
|
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
|
|
|
|
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
|
|
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
|
|
echo "checkout attempt ${attempt}/2 succeeded"
|
|
}
|
|
|
|
for attempt in 1 2; do
|
|
if checkout_attempt "$attempt"; then
|
|
exit 0
|
|
fi
|
|
echo "checkout attempt ${attempt}/2 failed"
|
|
sleep $((attempt * 5))
|
|
done
|
|
|
|
echo "checkout failed after 2 attempts" >&2
|
|
exit 1
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
use-sticky-disk: "false"
|
|
|
|
- name: Run additional check shard
|
|
env:
|
|
ADDITIONAL_CHECK_GROUP: ${{ matrix.group }}
|
|
RUN_CONTROL_UI_I18N: ${{ needs.preflight.outputs.run_control_ui_i18n }}
|
|
OPENCLAW_EXTENSION_BOUNDARY_CONCURRENCY: 4
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
failures=0
|
|
|
|
run_check() {
|
|
local label="$1"
|
|
shift
|
|
|
|
echo "::group::${label}"
|
|
if "$@"; then
|
|
echo "[ok] ${label}"
|
|
else
|
|
echo "::error title=${label} failed::${label} failed"
|
|
failures=1
|
|
fi
|
|
echo "::endgroup::"
|
|
}
|
|
|
|
case "$ADDITIONAL_CHECK_GROUP" in
|
|
boundaries)
|
|
run_check "plugin-extension-boundary" pnpm run lint:plugins:no-extension-imports
|
|
run_check "lint:tmp:no-random-messaging" pnpm run lint:tmp:no-random-messaging
|
|
run_check "lint:tmp:channel-agnostic-boundaries" pnpm run lint:tmp:channel-agnostic-boundaries
|
|
run_check "lint:tmp:tsgo-core-boundary" pnpm run lint:tmp:tsgo-core-boundary
|
|
run_check "lint:tmp:no-raw-channel-fetch" pnpm run lint:tmp:no-raw-channel-fetch
|
|
run_check "lint:agent:ingress-owner" pnpm run lint:agent:ingress-owner
|
|
run_check "lint:plugins:no-register-http-handler" pnpm run lint:plugins:no-register-http-handler
|
|
run_check "lint:plugins:no-monolithic-plugin-sdk-entry-imports" pnpm run lint:plugins:no-monolithic-plugin-sdk-entry-imports
|
|
run_check "lint:plugins:no-extension-src-imports" pnpm run lint:plugins:no-extension-src-imports
|
|
run_check "lint:plugins:no-extension-test-core-imports" pnpm run lint:plugins:no-extension-test-core-imports
|
|
run_check "lint:plugins:plugin-sdk-subpaths-exported" pnpm run lint:plugins:plugin-sdk-subpaths-exported
|
|
run_check "web-search-provider-boundary" pnpm run lint:web-search-provider-boundaries
|
|
run_check "web-fetch-provider-boundary" pnpm run lint:web-fetch-provider-boundaries
|
|
run_check "extension-src-outside-plugin-sdk-boundary" pnpm run lint:extensions:no-src-outside-plugin-sdk
|
|
run_check "extension-plugin-sdk-internal-boundary" pnpm run lint:extensions:no-plugin-sdk-internal
|
|
run_check "extension-relative-outside-package-boundary" pnpm run lint:extensions:no-relative-outside-package
|
|
run_check "lint:ui:no-raw-window-open" pnpm lint:ui:no-raw-window-open
|
|
;;
|
|
extension-surfaces)
|
|
run_check "lint:extensions:channels" pnpm run lint:extensions:channels
|
|
run_check "lint:extensions:bundled" pnpm run lint:extensions:bundled
|
|
run_check "test:extensions:package-boundary" pnpm run test:extensions:package-boundary
|
|
;;
|
|
runtime-topology)
|
|
if [ "$RUN_CONTROL_UI_I18N" = "true" ]; then
|
|
run_check "ui:i18n:check" pnpm ui:i18n:check
|
|
fi
|
|
run_check "gateway-watch-regression" pnpm test:gateway:watch-regression
|
|
run_check "check:architecture" pnpm check:architecture
|
|
;;
|
|
*)
|
|
echo "Unsupported additional check group: $ADDITIONAL_CHECK_GROUP" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
exit "$failures"
|
|
|
|
- name: Upload gateway watch regression artifacts
|
|
if: always() && matrix.group == 'runtime-topology'
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: gateway-watch-regression
|
|
path: .local/gateway-watch-regression/
|
|
retention-days: 7
|
|
|
|
check-additional:
|
|
permissions:
|
|
contents: read
|
|
name: "check-additional"
|
|
needs: [preflight, check-additional-shard]
|
|
if: always() && needs.preflight.outputs.run_check_additional == 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
timeout-minutes: 5
|
|
steps:
|
|
- name: Verify additional check shards
|
|
env:
|
|
SHARD_RESULT: ${{ needs.check-additional-shard.result }}
|
|
run: |
|
|
if [ "$SHARD_RESULT" != "success" ]; then
|
|
echo "Additional check shards failed: $SHARD_RESULT" >&2
|
|
exit 1
|
|
fi
|
|
|
|
build-smoke:
|
|
permissions:
|
|
contents: read
|
|
name: "build-smoke"
|
|
needs: [preflight, build-artifacts]
|
|
if: always() && needs.preflight.outputs.run_build_smoke == 'true' && (github.event_name != 'push' || needs.build-artifacts.result == 'success')
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
timeout-minutes: 20
|
|
steps:
|
|
- name: Checkout
|
|
shell: bash
|
|
env:
|
|
CHECKOUT_REPO: ${{ github.repository }}
|
|
CHECKOUT_SHA: ${{ github.sha }}
|
|
CHECKOUT_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
workdir="$GITHUB_WORKSPACE"
|
|
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
|
|
|
|
reset_checkout_dir() {
|
|
mkdir -p "$workdir"
|
|
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
|
}
|
|
|
|
checkout_attempt() {
|
|
local attempt="$1"
|
|
|
|
reset_checkout_dir
|
|
git init "$workdir" >/dev/null
|
|
git config --global --add safe.directory "$workdir"
|
|
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
|
|
git -C "$workdir" config gc.auto 0
|
|
|
|
timeout --signal=TERM 30s git -C "$workdir" \
|
|
-c protocol.version=2 \
|
|
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
|
|
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
|
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
|
|
|
|
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
|
|
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
|
|
echo "checkout attempt ${attempt}/2 succeeded"
|
|
}
|
|
|
|
for attempt in 1 2; do
|
|
if checkout_attempt "$attempt"; then
|
|
exit 0
|
|
fi
|
|
echo "checkout attempt ${attempt}/2 failed"
|
|
sleep $((attempt * 5))
|
|
done
|
|
|
|
echo "checkout failed after 2 attempts" >&2
|
|
exit 1
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
use-sticky-disk: "false"
|
|
|
|
- name: Restore dist cache
|
|
id: build-smoke-dist-cache
|
|
if: github.event_name == 'push'
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: dist/
|
|
key: ${{ runner.os }}-dist-build-${{ github.sha }}
|
|
|
|
- name: Download dist artifact
|
|
if: github.event_name == 'push' && steps.build-smoke-dist-cache.outputs.cache-hit != 'true'
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: dist-build
|
|
path: dist/
|
|
|
|
- name: Build dist
|
|
if: github.event_name != 'push'
|
|
run: pnpm build
|
|
|
|
- name: Smoke test CLI launcher help
|
|
run: node openclaw.mjs --help
|
|
|
|
- name: Smoke test CLI launcher status json
|
|
run: node openclaw.mjs status --json --timeout 1
|
|
|
|
- name: Smoke test built bundled plugin singleton
|
|
run: pnpm test:build:singleton
|
|
|
|
- name: Smoke test built bundled runtime deps
|
|
run: pnpm test:build:bundled-runtime-deps
|
|
|
|
- name: Check CLI startup memory
|
|
run: pnpm test:startup:memory
|
|
|
|
# Validate docs (format, lint, broken links) only when docs files changed.
|
|
check-docs:
|
|
permissions:
|
|
contents: read
|
|
needs: [preflight]
|
|
if: needs.preflight.outputs.run_check_docs == 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
timeout-minutes: 20
|
|
steps:
|
|
- name: Checkout
|
|
shell: bash
|
|
env:
|
|
CHECKOUT_REPO: ${{ github.repository }}
|
|
CHECKOUT_SHA: ${{ github.sha }}
|
|
CHECKOUT_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
workdir="$GITHUB_WORKSPACE"
|
|
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
|
|
|
|
reset_checkout_dir() {
|
|
mkdir -p "$workdir"
|
|
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
|
}
|
|
|
|
checkout_attempt() {
|
|
local attempt="$1"
|
|
|
|
reset_checkout_dir
|
|
git init "$workdir" >/dev/null
|
|
git config --global --add safe.directory "$workdir"
|
|
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
|
|
git -C "$workdir" config gc.auto 0
|
|
|
|
timeout --signal=TERM 30s git -C "$workdir" \
|
|
-c protocol.version=2 \
|
|
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
|
|
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
|
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
|
|
|
|
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
|
|
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
|
|
echo "checkout attempt ${attempt}/2 succeeded"
|
|
}
|
|
|
|
for attempt in 1 2; do
|
|
if checkout_attempt "$attempt"; then
|
|
exit 0
|
|
fi
|
|
echo "checkout attempt ${attempt}/2 failed"
|
|
sleep $((attempt * 5))
|
|
done
|
|
|
|
echo "checkout failed after 2 attempts" >&2
|
|
exit 1
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
use-sticky-disk: "false"
|
|
|
|
- name: Check docs
|
|
run: pnpm check:docs
|
|
|
|
skills-python:
|
|
permissions:
|
|
contents: read
|
|
needs: [preflight]
|
|
if: needs.preflight.outputs.run_skills_python_job == 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
timeout-minutes: 20
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
submodules: false
|
|
|
|
- name: Setup Python
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Install Python tooling
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
python -m pip install pytest ruff pyyaml
|
|
|
|
- name: Lint Python skill scripts
|
|
run: python -m ruff check skills
|
|
|
|
- name: Test skill Python scripts
|
|
run: python -m pytest -q skills
|
|
|
|
checks-windows:
|
|
permissions:
|
|
contents: read
|
|
name: ${{ matrix.check_name }}
|
|
needs: [preflight, build-artifacts]
|
|
if: always() && needs.preflight.outputs.run_checks_windows == 'true' && needs.build-artifacts.result == 'success'
|
|
runs-on: blacksmith-32vcpu-windows-2025
|
|
timeout-minutes: 60
|
|
env:
|
|
NODE_OPTIONS: --max-old-space-size=6144
|
|
# Keep total concurrency predictable on the 32 vCPU runner.
|
|
OPENCLAW_VITEST_MAX_WORKERS: 1
|
|
OPENCLAW_TEST_SKIP_FULL_EXTENSIONS_SHARD: 1
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJson(needs.preflight.outputs.checks_windows_matrix) }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
submodules: false
|
|
|
|
- name: Try to exclude workspace from Windows Defender (best-effort)
|
|
shell: pwsh
|
|
run: |
|
|
$cmd = Get-Command Add-MpPreference -ErrorAction SilentlyContinue
|
|
if (-not $cmd) {
|
|
Write-Host "Add-MpPreference not available, skipping Defender exclusions."
|
|
exit 0
|
|
}
|
|
|
|
try {
|
|
# Defender sometimes intercepts process spawning (vitest workers). If this fails
|
|
# (eg hardened images), keep going and rely on worker limiting above.
|
|
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE" -ErrorAction Stop
|
|
Add-MpPreference -ExclusionProcess "node.exe" -ErrorAction Stop
|
|
Write-Host "Defender exclusions applied."
|
|
} catch {
|
|
Write-Warning "Failed to apply Defender exclusions, continuing. $($_.Exception.Message)"
|
|
}
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24.x
|
|
check-latest: false
|
|
|
|
- name: Setup pnpm + cache store
|
|
uses: ./.github/actions/setup-pnpm-store-cache
|
|
with:
|
|
pnpm-version: "10.32.1"
|
|
cache-key-suffix: "node24"
|
|
# 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: Download dist artifact
|
|
if: matrix.task == 'test'
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: dist-build
|
|
path: dist/
|
|
|
|
- name: Download A2UI bundle artifact
|
|
if: matrix.task == 'test'
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: canvas-a2ui-bundle
|
|
path: src/canvas-host/a2ui/
|
|
|
|
- name: Run ${{ matrix.task }} (${{ matrix.runtime }})
|
|
env:
|
|
TASK: ${{ matrix.task }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
case "$TASK" in
|
|
test)
|
|
# Linux owns the full repo test suite. Keep the Windows runner focused on
|
|
# Windows-native process/path wrappers so platform regressions fail fast.
|
|
pnpm test:windows:ci
|
|
;;
|
|
*)
|
|
echo "Unsupported Windows checks task: $TASK" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
macos-node:
|
|
permissions:
|
|
contents: read
|
|
name: ${{ matrix.check_name }}
|
|
needs: [preflight, build-artifacts]
|
|
if: always() && needs.preflight.outputs.run_macos_node == 'true' && needs.build-artifacts.result == 'success'
|
|
runs-on: blacksmith-6vcpu-macos-latest
|
|
timeout-minutes: 20
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJson(needs.preflight.outputs.macos_node_matrix) }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
submodules: false
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
install-bun: "false"
|
|
|
|
- name: Download dist artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: dist-build
|
|
path: dist/
|
|
|
|
- name: Download A2UI bundle artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: canvas-a2ui-bundle
|
|
path: src/canvas-host/a2ui/
|
|
|
|
- name: Patch mlx-audio-swift manifest
|
|
run: |
|
|
set -euo pipefail
|
|
swift package resolve --package-path apps/macos >/dev/null
|
|
chmod u+w apps/macos/.build/checkouts/mlx-audio-swift/Package.swift
|
|
python <<'PY'
|
|
from pathlib import Path
|
|
|
|
path = Path("apps/macos/.build/checkouts/mlx-audio-swift/Package.swift")
|
|
text = path.read_text()
|
|
if "Models/Qwen3/README.md" in text:
|
|
print("mlx-audio-swift README excludes already present")
|
|
raise SystemExit(0)
|
|
|
|
needle = ' path: "Sources/MLXAudioTTS"\n'
|
|
replacement = """ path: \"Sources/MLXAudioTTS\",\n exclude: [\n \"Models/Llama/README.md\",\n \"Models/Marvis/README.md\",\n \"Models/PocketTTS/README.md\",\n \"Models/Qwen3/README.md\",\n \"Models/Soprano/README.md\",\n ]\n"""
|
|
|
|
if needle not in text:
|
|
raise SystemExit("Could not find MLXAudioTTS target path in mlx-audio-swift Package.swift")
|
|
|
|
path.write_text(text.replace(needle, replacement, 1))
|
|
print(f"Patched {path}")
|
|
PY
|
|
|
|
- name: TS tests (macOS)
|
|
env:
|
|
NODE_OPTIONS: --max-old-space-size=4096
|
|
OPENCLAW_VITEST_MAX_WORKERS: 2
|
|
TASK: ${{ matrix.task }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
case "$TASK" in
|
|
test)
|
|
# Linux owns the full repo test suite. Keep macOS CI focused on
|
|
# launchd/Homebrew/runtime path coverage and the process-group wrapper.
|
|
pnpm test:macos:ci
|
|
;;
|
|
*)
|
|
echo "Unsupported macOS node task: $TASK" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
macos-swift:
|
|
permissions:
|
|
contents: read
|
|
name: "macos-swift"
|
|
needs: [preflight]
|
|
if: needs.preflight.outputs.run_macos_swift == 'true'
|
|
runs-on: blacksmith-12vcpu-macos-latest
|
|
timeout-minutes: 20
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
submodules: false
|
|
|
|
- name: Install XcodeGen / SwiftLint / SwiftFormat
|
|
run: brew install xcodegen swiftlint swiftformat
|
|
|
|
- name: Detect Swift toolchain cache key
|
|
id: swift-toolchain
|
|
run: |
|
|
set -euo pipefail
|
|
xcode_version="$(xcodebuild -version | tr '\n' ' ' | sed 's/ */ /g; s/ $//')"
|
|
swift_version="$(swift --version | head -n 1)"
|
|
toolchain_key="$(printf '%s\n%s\n' "$xcode_version" "$swift_version" | shasum -a 256 | awk '{print $1}')"
|
|
echo "key=$toolchain_key" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Cache SwiftPM
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: ~/Library/Caches/org.swift.swiftpm
|
|
key: ${{ runner.os }}-swiftpm-${{ hashFiles('apps/macos/Package.resolved') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-swiftpm-
|
|
|
|
- name: Cache Swift build directory
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: apps/macos/.build
|
|
key: ${{ runner.os }}-swift-build-v1-${{ steps.swift-toolchain.outputs.key }}-${{ hashFiles('apps/macos/Package.swift', 'apps/macos/Package.resolved', 'apps/shared/OpenClawKit/Package.swift', 'Swabble/Package.swift') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-swift-build-v1-${{ steps.swift-toolchain.outputs.key }}-
|
|
|
|
- name: Patch mlx-audio-swift manifest
|
|
run: |
|
|
set -euo pipefail
|
|
if [ ! -f apps/macos/.build/checkouts/mlx-audio-swift/Package.swift ]; then
|
|
swift package resolve --package-path apps/macos >/dev/null
|
|
fi
|
|
if [ ! -f apps/macos/.build/checkouts/mlx-audio-swift/Package.swift ]; then
|
|
echo "mlx-audio-swift checkout missing after swift package resolve" >&2
|
|
exit 1
|
|
fi
|
|
chmod u+w apps/macos/.build/checkouts/mlx-audio-swift/Package.swift
|
|
python <<'PY'
|
|
from pathlib import Path
|
|
|
|
path = Path("apps/macos/.build/checkouts/mlx-audio-swift/Package.swift")
|
|
text = path.read_text()
|
|
if "Models/Qwen3/README.md" in text:
|
|
print("mlx-audio-swift README excludes already present")
|
|
raise SystemExit(0)
|
|
|
|
needle = ' path: "Sources/MLXAudioTTS"\n'
|
|
replacement = """ path: \"Sources/MLXAudioTTS\",\n exclude: [\n \"Models/Llama/README.md\",\n \"Models/Marvis/README.md\",\n \"Models/PocketTTS/README.md\",\n \"Models/Qwen3/README.md\",\n \"Models/Soprano/README.md\",\n ]\n"""
|
|
|
|
if needle not in text:
|
|
raise SystemExit("Could not find MLXAudioTTS target path in mlx-audio-swift Package.swift")
|
|
|
|
path.write_text(text.replace(needle, replacement, 1))
|
|
print(f"Patched {path}")
|
|
PY
|
|
|
|
- name: Show toolchain
|
|
run: |
|
|
sw_vers
|
|
xcodebuild -version
|
|
swift --version
|
|
|
|
- name: Swift lint
|
|
run: |
|
|
swiftlint --config .swiftlint.yml
|
|
swiftformat --lint apps/macos/Sources --config .swiftformat
|
|
|
|
- name: Swift build (release)
|
|
run: |
|
|
set -euo pipefail
|
|
for attempt in 1 2 3; do
|
|
# The macOS lane validates the desktop app build; the CLI product is
|
|
# intentionally left to its own narrower surfaces instead of making
|
|
# this lane rebuild the whole package graph.
|
|
if swift build --package-path apps/macos --product OpenClaw --configuration release; then
|
|
exit 0
|
|
fi
|
|
echo "swift build failed (attempt $attempt/3). Retrying…"
|
|
sleep $((attempt * 20))
|
|
done
|
|
exit 1
|
|
|
|
- name: Swift test
|
|
run: |
|
|
set -euo pipefail
|
|
for attempt in 1 2 3; do
|
|
if swift test --package-path apps/macos --parallel --enable-code-coverage --show-codecov-path; then
|
|
exit 0
|
|
fi
|
|
echo "swift test failed (attempt $attempt/3). Retrying…"
|
|
sleep $((attempt * 20))
|
|
done
|
|
exit 1
|
|
|
|
android:
|
|
permissions:
|
|
contents: read
|
|
name: ${{ matrix.check_name }}
|
|
needs: [preflight]
|
|
if: needs.preflight.outputs.run_android_job == 'true'
|
|
runs-on: blacksmith-16vcpu-ubuntu-2404
|
|
timeout-minutes: 20
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJson(needs.preflight.outputs.android_matrix) }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
submodules: false
|
|
|
|
- name: Setup Java
|
|
uses: actions/setup-java@v5
|
|
with:
|
|
distribution: temurin
|
|
# Keep sdkmanager on the stable JDK path for Linux CI runners.
|
|
java-version: 17
|
|
|
|
- name: Setup Android SDK cmdline-tools
|
|
run: |
|
|
set -euo pipefail
|
|
ANDROID_SDK_ROOT="$HOME/.android-sdk"
|
|
CMDLINE_TOOLS_VERSION="12266719"
|
|
ARCHIVE="commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip"
|
|
URL="https://dl.google.com/android/repository/${ARCHIVE}"
|
|
|
|
mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools"
|
|
curl -fsSL "$URL" -o "/tmp/${ARCHIVE}"
|
|
rm -rf "$ANDROID_SDK_ROOT/cmdline-tools/latest"
|
|
unzip -q "/tmp/${ARCHIVE}" -d "$ANDROID_SDK_ROOT/cmdline-tools"
|
|
mv "$ANDROID_SDK_ROOT/cmdline-tools/cmdline-tools" "$ANDROID_SDK_ROOT/cmdline-tools/latest"
|
|
|
|
echo "ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT" >> "$GITHUB_ENV"
|
|
echo "ANDROID_HOME=$ANDROID_SDK_ROOT" >> "$GITHUB_ENV"
|
|
echo "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin" >> "$GITHUB_PATH"
|
|
echo "$ANDROID_SDK_ROOT/platform-tools" >> "$GITHUB_PATH"
|
|
|
|
- name: Setup Gradle
|
|
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5
|
|
with:
|
|
gradle-version: 8.11.1
|
|
|
|
- name: Install Android SDK packages
|
|
run: |
|
|
yes | sdkmanager --sdk_root="${ANDROID_SDK_ROOT}" --licenses >/dev/null
|
|
sdkmanager --sdk_root="${ANDROID_SDK_ROOT}" --install \
|
|
"platform-tools" \
|
|
"platforms;android-36" \
|
|
"build-tools;36.0.0"
|
|
|
|
- name: Run Android ${{ matrix.task }}
|
|
working-directory: apps/android
|
|
env:
|
|
TASK: ${{ matrix.task }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
case "$TASK" in
|
|
test-play)
|
|
./gradlew --no-daemon :app:testPlayDebugUnitTest
|
|
;;
|
|
test-third-party)
|
|
./gradlew --no-daemon :app:testThirdPartyDebugUnitTest
|
|
;;
|
|
build-play)
|
|
./gradlew --no-daemon :app:assemblePlayDebug
|
|
;;
|
|
build-third-party)
|
|
./gradlew --no-daemon :app:assembleThirdPartyDebug
|
|
;;
|
|
*)
|
|
echo "Unsupported Android task: $TASK" >&2
|
|
exit 1
|
|
;;
|
|
esac
|