mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:50:42 +00:00
ci: split remaining slow CI lanes
This commit is contained in:
514
.github/workflows/ci.yml
vendored
514
.github/workflows/ci.yml
vendored
@@ -10,8 +10,8 @@ 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' }}
|
||||
group: ${{ github.event_name == 'pull_request' && format('{0}-{1}', github.workflow, github.event.pull_request.number) || (github.repository == 'openclaw/openclaw' && format('{0}-{1}', github.workflow, github.ref) || format('{0}-{1}-{2}', github.workflow, github.ref, github.sha)) }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 20
|
||||
outputs:
|
||||
docs_only: ${{ steps.manifest.outputs.docs_only }}
|
||||
@@ -39,10 +39,14 @@ jobs:
|
||||
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 }}
|
||||
channel_contracts_matrix: ${{ steps.manifest.outputs.channel_contracts_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_checks_node_core_nondist: ${{ steps.manifest.outputs.run_checks_node_core_nondist }}
|
||||
checks_node_core_nondist_matrix: ${{ steps.manifest.outputs.checks_node_core_nondist_matrix }}
|
||||
run_checks_node_core_dist: ${{ steps.manifest.outputs.run_checks_node_core_dist }}
|
||||
checks_node_core_dist_matrix: ${{ steps.manifest.outputs.checks_node_core_dist_matrix }}
|
||||
run_extension_fast: ${{ steps.manifest.outputs.run_extension_fast }}
|
||||
extension_fast_matrix: ${{ steps.manifest.outputs.extension_fast_matrix }}
|
||||
run_check: ${{ steps.manifest.outputs.run_check }}
|
||||
@@ -134,12 +138,16 @@ jobs:
|
||||
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":[]}' }}
|
||||
OPENCLAW_CI_REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
node --input-type=module <<'EOF'
|
||||
import { appendFileSync } from "node:fs";
|
||||
import {
|
||||
createNodeTestShards,
|
||||
} from "./scripts/lib/ci-node-test-plan.mjs";
|
||||
import {
|
||||
createChannelContractTestShards,
|
||||
} from "./scripts/lib/channel-contract-test-plan.mjs";
|
||||
import {
|
||||
createExtensionTestShards,
|
||||
DEFAULT_EXTENSION_TEST_SHARD_COUNT,
|
||||
@@ -165,12 +173,16 @@ jobs:
|
||||
const outputPath = process.env.GITHUB_OUTPUT;
|
||||
const eventName = process.env.GITHUB_EVENT_NAME ?? "pull_request";
|
||||
const isPush = eventName === "push";
|
||||
const isCanonicalRepository = process.env.OPENCLAW_CI_REPOSITORY === "openclaw/openclaw";
|
||||
const docsOnly = parseBoolean(process.env.OPENCLAW_CI_DOCS_ONLY);
|
||||
const docsChanged = parseBoolean(process.env.OPENCLAW_CI_DOCS_CHANGED);
|
||||
const runNode = parseBoolean(process.env.OPENCLAW_CI_RUN_NODE) && !docsOnly;
|
||||
const 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 runMacos =
|
||||
parseBoolean(process.env.OPENCLAW_CI_RUN_MACOS) && !docsOnly && isCanonicalRepository;
|
||||
const runAndroid =
|
||||
parseBoolean(process.env.OPENCLAW_CI_RUN_ANDROID) && !docsOnly && isCanonicalRepository;
|
||||
const runWindows =
|
||||
parseBoolean(process.env.OPENCLAW_CI_RUN_WINDOWS) && !docsOnly && isCanonicalRepository;
|
||||
const runSkillsPython = parseBoolean(process.env.OPENCLAW_CI_RUN_SKILLS_PYTHON) && !docsOnly;
|
||||
const runControlUiI18n =
|
||||
parseBoolean(process.env.OPENCLAW_CI_RUN_CONTROL_UI_I18N) && !docsOnly;
|
||||
@@ -179,10 +191,13 @@ jobs:
|
||||
const changedExtensionsMatrix = hasChangedExtensions
|
||||
? parseJson(process.env.OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX, { include: [] })
|
||||
: { include: [] };
|
||||
const extensionTestShardCount = isCanonicalRepository
|
||||
? DEFAULT_EXTENSION_TEST_SHARD_COUNT
|
||||
: Math.max(DEFAULT_EXTENSION_TEST_SHARD_COUNT, 36);
|
||||
const extensionShardMatrix = createMatrix(
|
||||
runNode
|
||||
? createExtensionTestShards({
|
||||
shardCount: DEFAULT_EXTENSION_TEST_SHARD_COUNT,
|
||||
shardCount: extensionTestShardCount,
|
||||
}).map((shard) => ({
|
||||
check_name: shard.checkName,
|
||||
extensions_csv: shard.extensionIds.join(","),
|
||||
@@ -191,6 +206,19 @@ jobs:
|
||||
}))
|
||||
: [],
|
||||
);
|
||||
const nodeTestShards = runNode
|
||||
? createNodeTestShards().map((shard) => ({
|
||||
check_name: shard.checkName,
|
||||
runtime: "node",
|
||||
task: "test-shard",
|
||||
shard_name: shard.shardName,
|
||||
configs: shard.configs,
|
||||
includePatterns: shard.includePatterns,
|
||||
requires_dist: shard.requiresDist,
|
||||
}))
|
||||
: [];
|
||||
const nodeTestNonDistShards = nodeTestShards.filter((shard) => !shard.requires_dist);
|
||||
const nodeTestDistShards = nodeTestShards.filter((shard) => shard.requires_dist);
|
||||
|
||||
const manifest = {
|
||||
docs_only: docsOnly,
|
||||
@@ -209,13 +237,14 @@ jobs:
|
||||
? [
|
||||
{ check_name: "checks-fast-bundled", runtime: "node", task: "bundled" },
|
||||
{
|
||||
check_name: "checks-fast-contracts-protocol",
|
||||
check_name: "checks-fast-contracts-plugins",
|
||||
runtime: "node",
|
||||
task: "contracts",
|
||||
task: "contracts-plugins",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
),
|
||||
channel_contracts_matrix: createMatrix(runNode ? createChannelContractTestShards() : []),
|
||||
checks_node_extensions_matrix: extensionShardMatrix,
|
||||
run_checks: runNode,
|
||||
checks_matrix: createMatrix(
|
||||
@@ -236,18 +265,10 @@ jobs:
|
||||
]
|
||||
: [],
|
||||
),
|
||||
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_checks_node_core_nondist: nodeTestNonDistShards.length > 0,
|
||||
checks_node_core_nondist_matrix: createMatrix(nodeTestNonDistShards),
|
||||
run_checks_node_core_dist: nodeTestDistShards.length > 0,
|
||||
checks_node_core_dist_matrix: createMatrix(nodeTestDistShards),
|
||||
run_extension_fast: hasChangedExtensions,
|
||||
extension_fast_matrix: createMatrix(
|
||||
hasChangedExtensions
|
||||
@@ -302,7 +323,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
PRE_COMMIT_CACHE_KEY_SUFFIX: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.sha }}
|
||||
@@ -402,7 +423,7 @@ jobs:
|
||||
contents: read
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_build_artifacts == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -499,7 +520,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_checks_fast == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -569,8 +590,11 @@ jobs:
|
||||
bundled)
|
||||
pnpm test:bundled
|
||||
;;
|
||||
contracts)
|
||||
pnpm test:contracts
|
||||
contracts-channels)
|
||||
pnpm test:contracts:channels
|
||||
;;
|
||||
contracts-plugins)
|
||||
pnpm test:contracts:plugins
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported checks-fast task: $TASK" >&2
|
||||
@@ -578,13 +602,115 @@ jobs:
|
||||
;;
|
||||
esac
|
||||
|
||||
checks-fast-channel-contracts-shard:
|
||||
permissions:
|
||||
contents: read
|
||||
name: ${{ matrix.checkName }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_checks_fast == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.channel_contracts_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
shell: bash
|
||||
env:
|
||||
CHECKOUT_REPO: ${{ github.repository }}
|
||||
CHECKOUT_SHA: ${{ 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"
|
||||
|
||||
- name: Run channel contract shard
|
||||
env:
|
||||
OPENCLAW_CONTRACT_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
include_file="$RUNNER_TEMP/channel-contract-include.json"
|
||||
INCLUDE_FILE="$include_file" node --input-type=module <<'EOF'
|
||||
import { writeFileSync } from "node:fs";
|
||||
|
||||
const includePatterns = JSON.parse(process.env.OPENCLAW_CONTRACT_INCLUDE_PATTERNS_JSON ?? "[]");
|
||||
if (!Array.isArray(includePatterns) || includePatterns.length === 0) {
|
||||
console.error("Missing channel contract include patterns");
|
||||
process.exit(1);
|
||||
}
|
||||
writeFileSync(process.env.INCLUDE_FILE, JSON.stringify(includePatterns), "utf8");
|
||||
EOF
|
||||
OPENCLAW_VITEST_INCLUDE_FILE="$include_file" pnpm test:contracts:channels
|
||||
|
||||
checks-fast-channel-contracts:
|
||||
permissions:
|
||||
contents: read
|
||||
name: checks-fast-contracts-channels
|
||||
needs: [preflight, checks-fast-channel-contracts-shard]
|
||||
if: always() && needs.preflight.outputs.run_checks_fast == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Verify channel contract shards
|
||||
env:
|
||||
SHARD_RESULT: ${{ needs.checks-fast-channel-contracts-shard.result }}
|
||||
run: |
|
||||
if [ "$SHARD_RESULT" != "success" ]; then
|
||||
echo "Channel contract shards failed: $SHARD_RESULT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
checks-fast-protocol:
|
||||
permissions:
|
||||
contents: read
|
||||
name: "checks-fast-protocol"
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_checks_fast == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -649,7 +775,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_checks_fast == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -719,7 +845,7 @@ jobs:
|
||||
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
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Verify extension shards
|
||||
@@ -737,7 +863,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight, build-artifacts]
|
||||
if: always() && needs.preflight.outputs.run_checks == 'true' && needs.build-artifacts.result == 'success'
|
||||
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -809,17 +935,14 @@ jobs:
|
||||
env:
|
||||
TASK: ${{ matrix.task }}
|
||||
run: |
|
||||
{
|
||||
echo "OPENCLAW_VITEST_MAX_WORKERS=2"
|
||||
if [ "$TASK" = "test" ]; then
|
||||
echo "OPENCLAW_TEST_PROJECTS_LEAF_SHARDS=1"
|
||||
echo "OPENCLAW_TEST_PROJECTS_PARALLEL=4"
|
||||
echo "OPENCLAW_TEST_SKIP_FULL_EXTENSIONS_SHARD=1"
|
||||
fi
|
||||
if [ "$TASK" = "channels" ]; then
|
||||
echo "OPENCLAW_VITEST_MAX_WORKERS=1"
|
||||
fi
|
||||
} >> "$GITHUB_ENV"
|
||||
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'
|
||||
@@ -863,17 +986,141 @@ jobs:
|
||||
;;
|
||||
esac
|
||||
|
||||
checks-node-core-test-shard:
|
||||
checks-node-core-test-nondist-shard:
|
||||
permissions:
|
||||
contents: read
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_checks_node_core_nondist == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_nondist_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
shell: bash
|
||||
env:
|
||||
CHECKOUT_REPO: ${{ github.repository }}
|
||||
CHECKOUT_SHA: ${{ 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"
|
||||
|
||||
- name: Configure Node test resources
|
||||
run: echo "OPENCLAW_VITEST_MAX_WORKERS=2" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Run Node test shard
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=6144
|
||||
OPENCLAW_NODE_TEST_CONFIGS_JSON: ${{ toJson(matrix.configs) }}
|
||||
OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node --input-type=module <<'EOF'
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
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);
|
||||
}
|
||||
const includePatterns = JSON.parse(process.env.OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON ?? "null");
|
||||
const childEnv = { ...process.env };
|
||||
if (Array.isArray(includePatterns) && includePatterns.length > 0) {
|
||||
const includeFile = join(
|
||||
process.env.RUNNER_TEMP ?? ".",
|
||||
`node-test-include-${process.env.GITHUB_JOB ?? "local"}-${Date.now()}.json`,
|
||||
);
|
||||
writeFileSync(includeFile, JSON.stringify(includePatterns), "utf8");
|
||||
childEnv.OPENCLAW_VITEST_INCLUDE_FILE = includeFile;
|
||||
}
|
||||
|
||||
for (const config of configs) {
|
||||
console.error(`[test] starting ${config}`);
|
||||
const result = spawnSync(
|
||||
"pnpm",
|
||||
[
|
||||
"exec",
|
||||
"node",
|
||||
...resolveVitestNodeArgs(process.env),
|
||||
resolveVitestCliEntry(),
|
||||
"run",
|
||||
"--config",
|
||||
config,
|
||||
],
|
||||
{
|
||||
env: childEnv,
|
||||
stdio: "inherit",
|
||||
},
|
||||
);
|
||||
if ((result.status ?? 1) !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
checks-node-core-test-dist-shard:
|
||||
permissions:
|
||||
contents: read
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight, build-artifacts]
|
||||
if: always() && needs.preflight.outputs.run_checks == 'true' && needs.build-artifacts.result == 'success'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
if: always() && needs.preflight.outputs.run_checks_node_core_dist == 'true' && needs.build-artifacts.result == 'success'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_test_matrix) }}
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_dist_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
shell: bash
|
||||
@@ -935,14 +1182,13 @@ jobs:
|
||||
|
||||
- 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'
|
||||
if: steps.dist-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: dist-build
|
||||
@@ -958,11 +1204,14 @@ jobs:
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=6144
|
||||
OPENCLAW_NODE_TEST_CONFIGS_JSON: ${{ toJson(matrix.configs) }}
|
||||
OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node --input-type=module <<'EOF'
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { resolveVitestCliEntry, resolveVitestNodeArgs } from "./scripts/run-vitest.mjs";
|
||||
|
||||
const configs = JSON.parse(process.env.OPENCLAW_NODE_TEST_CONFIGS_JSON ?? "[]");
|
||||
@@ -970,6 +1219,16 @@ jobs:
|
||||
console.error("Missing node test shard configs");
|
||||
process.exit(1);
|
||||
}
|
||||
const includePatterns = JSON.parse(process.env.OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON ?? "null");
|
||||
const childEnv = { ...process.env };
|
||||
if (Array.isArray(includePatterns) && includePatterns.length > 0) {
|
||||
const includeFile = join(
|
||||
process.env.RUNNER_TEMP ?? ".",
|
||||
`node-test-include-${process.env.GITHUB_JOB ?? "local"}-${Date.now()}.json`,
|
||||
);
|
||||
writeFileSync(includeFile, JSON.stringify(includePatterns), "utf8");
|
||||
childEnv.OPENCLAW_VITEST_INCLUDE_FILE = includeFile;
|
||||
}
|
||||
|
||||
for (const config of configs) {
|
||||
console.error(`[test] starting ${config}`);
|
||||
@@ -985,7 +1244,7 @@ jobs:
|
||||
config,
|
||||
],
|
||||
{
|
||||
env: process.env,
|
||||
env: childEnv,
|
||||
stdio: "inherit",
|
||||
},
|
||||
);
|
||||
@@ -999,17 +1258,24 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
name: checks-node-core
|
||||
needs: [preflight, checks-node-core-test-shard]
|
||||
needs: [preflight, checks-node-core-test-nondist-shard, checks-node-core-test-dist-shard]
|
||||
if: always() && needs.preflight.outputs.run_checks == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Verify node test shards
|
||||
env:
|
||||
SHARD_RESULT: ${{ needs.checks-node-core-test-shard.result }}
|
||||
DIST_SHARD_RESULT: ${{ needs.checks-node-core-test-dist-shard.result }}
|
||||
NONDIST_SHARD_RESULT: ${{ needs.checks-node-core-test-nondist-shard.result }}
|
||||
RUN_DIST_SHARDS: ${{ needs.preflight.outputs.run_checks_node_core_dist }}
|
||||
RUN_NONDIST_SHARDS: ${{ needs.preflight.outputs.run_checks_node_core_nondist }}
|
||||
run: |
|
||||
if [ "$SHARD_RESULT" != "success" ]; then
|
||||
echo "Node test shards failed: $SHARD_RESULT" >&2
|
||||
if [ "$RUN_NONDIST_SHARDS" = "true" ] && [ "$NONDIST_SHARD_RESULT" != "success" ]; then
|
||||
echo "Node non-dist test shards failed: $NONDIST_SHARD_RESULT" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$RUN_DIST_SHARDS" = "true" ] && [ "$DIST_SHARD_RESULT" != "success" ]; then
|
||||
echo "Node dist test shards failed: $DIST_SHARD_RESULT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -1019,7 +1285,7 @@ jobs:
|
||||
name: "extension-fast"
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_extension_fast == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -1083,15 +1349,31 @@ jobs:
|
||||
OPENCLAW_CHANGED_EXTENSION: ${{ matrix.extension }}
|
||||
run: pnpm test:extension "$OPENCLAW_CHANGED_EXTENSION"
|
||||
|
||||
# Types, lint, and format check.
|
||||
check:
|
||||
# Types, lint, and format check shards.
|
||||
check-shard:
|
||||
permissions:
|
||||
contents: read
|
||||
name: "check"
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: always() && needs.preflight.outputs.run_check == 'true'
|
||||
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- check_name: check-preflight-guards
|
||||
task: preflight-guards
|
||||
- check_name: check-prod-types
|
||||
task: prod-types
|
||||
- check_name: check-lint
|
||||
task: lint
|
||||
- check_name: check-policy-guards
|
||||
task: policy-guards
|
||||
- check_name: check-test-types
|
||||
task: test-types
|
||||
- check_name: check-strict-smoke
|
||||
task: strict-smoke
|
||||
steps:
|
||||
- name: Checkout
|
||||
shell: bash
|
||||
@@ -1146,18 +1428,60 @@ jobs:
|
||||
with:
|
||||
install-bun: "false"
|
||||
|
||||
- name: Check types and lint and oxfmt
|
||||
- name: Run check shard
|
||||
env:
|
||||
OPENCLAW_LOCAL_CHECK: "0"
|
||||
run: pnpm check
|
||||
TASK: ${{ matrix.task }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "$TASK" in
|
||||
preflight-guards)
|
||||
pnpm check:no-conflict-markers
|
||||
pnpm tool-display:check
|
||||
pnpm check:host-env-policy:swift
|
||||
;;
|
||||
prod-types)
|
||||
pnpm tsgo:prod
|
||||
;;
|
||||
lint)
|
||||
pnpm lint
|
||||
;;
|
||||
policy-guards)
|
||||
pnpm lint:webhook:no-low-level-body-read
|
||||
pnpm lint:auth:no-pairing-store-group
|
||||
pnpm lint:auth:pairing-account-scope
|
||||
pnpm check:import-cycles
|
||||
;;
|
||||
test-types)
|
||||
pnpm check:test-types
|
||||
;;
|
||||
strict-smoke)
|
||||
pnpm build:strict-smoke
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported check task: $TASK" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Check test types
|
||||
check:
|
||||
permissions:
|
||||
contents: read
|
||||
name: "check"
|
||||
needs: [preflight, check-shard]
|
||||
if: always() && needs.preflight.outputs.run_check == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Verify check shards
|
||||
env:
|
||||
OPENCLAW_LOCAL_CHECK: "0"
|
||||
run: pnpm check:test-types
|
||||
|
||||
- name: Strict TS build smoke
|
||||
run: pnpm build:strict-smoke
|
||||
SHARD_RESULT: ${{ needs.check-shard.result }}
|
||||
run: |
|
||||
if [ "$SHARD_RESULT" != "success" ]; then
|
||||
echo "Check shards failed: $SHARD_RESULT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
check-additional-shard:
|
||||
permissions:
|
||||
@@ -1165,7 +1489,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: always() && needs.preflight.outputs.run_check_additional == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -1173,10 +1497,18 @@ jobs:
|
||||
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
|
||||
- check_name: check-additional-extension-channels
|
||||
group: extension-channels
|
||||
- check_name: check-additional-extension-bundled
|
||||
group: extension-bundled
|
||||
- check_name: check-additional-extension-package-boundary-compile
|
||||
group: extension-package-boundary-compile
|
||||
- check_name: check-additional-extension-package-boundary-canary
|
||||
group: extension-package-boundary-canary
|
||||
- check_name: check-additional-runtime-topology-gateway
|
||||
group: runtime-topology-gateway
|
||||
- check_name: check-additional-runtime-topology-architecture
|
||||
group: runtime-topology-architecture
|
||||
steps:
|
||||
- name: Checkout
|
||||
shell: bash
|
||||
@@ -1276,16 +1608,25 @@ jobs:
|
||||
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)
|
||||
extension-channels)
|
||||
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)
|
||||
extension-bundled)
|
||||
run_check "lint:extensions:bundled" pnpm run lint:extensions:bundled
|
||||
;;
|
||||
extension-package-boundary-compile)
|
||||
run_check "test:extensions:package-boundary:compile" pnpm run test:extensions:package-boundary:compile
|
||||
;;
|
||||
extension-package-boundary-canary)
|
||||
run_check "test:extensions:package-boundary:canary" pnpm run test:extensions:package-boundary:canary
|
||||
;;
|
||||
runtime-topology-gateway)
|
||||
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
|
||||
;;
|
||||
runtime-topology-architecture)
|
||||
run_check "check:architecture" pnpm check:architecture
|
||||
;;
|
||||
*)
|
||||
@@ -1297,7 +1638,7 @@ jobs:
|
||||
exit "$failures"
|
||||
|
||||
- name: Upload gateway watch regression artifacts
|
||||
if: always() && matrix.group == 'runtime-topology'
|
||||
if: always() && matrix.group == 'runtime-topology-gateway'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gateway-watch-regression
|
||||
@@ -1310,7 +1651,7 @@ jobs:
|
||||
name: "check-additional"
|
||||
needs: [preflight, check-additional-shard]
|
||||
if: always() && needs.preflight.outputs.run_check_additional == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Verify additional check shards
|
||||
@@ -1328,7 +1669,7 @@ jobs:
|
||||
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
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -1424,7 +1765,7 @@ jobs:
|
||||
contents: read
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_check_docs == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -1488,7 +1829,7 @@ jobs:
|
||||
contents: read
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_skills_python_job == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -1519,7 +1860,7 @@ jobs:
|
||||
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
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-32vcpu-windows-2025' || 'windows-2025' }}
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=6144
|
||||
@@ -1581,9 +1922,6 @@ jobs:
|
||||
- name: Capture node path
|
||||
run: |
|
||||
node_bin="$(dirname "$(node -p 'process.execPath')")"
|
||||
if command -v cygpath >/dev/null 2>&1; then
|
||||
node_bin="$(cygpath -u "$node_bin")"
|
||||
fi
|
||||
echo "NODE_BIN=$node_bin" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -1636,7 +1974,7 @@ jobs:
|
||||
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
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-6vcpu-macos-latest' || 'macos-latest' }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -1715,7 +2053,7 @@ jobs:
|
||||
name: "macos-swift"
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_macos_swift == 'true'
|
||||
runs-on: blacksmith-12vcpu-macos-latest
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-latest' || 'macos-latest' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -1826,7 +2164,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_android_job == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
43
docs/ci.md
43
docs/ci.md
@@ -12,25 +12,26 @@ The CI runs on every push to `main` and every pull request. It uses smart scopin
|
||||
|
||||
## Job Overview
|
||||
|
||||
| Job | Purpose | When it runs |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------- | ----------------------------------- |
|
||||
| `preflight` | Detect docs-only changes, changed scopes, changed extensions, and build the CI manifest | Always on non-draft pushes and PRs |
|
||||
| `security-fast` | Private key detection, workflow audit via `zizmor`, production dependency audit | Always on non-draft pushes and PRs |
|
||||
| `build-artifacts` | Build `dist/` and the Control UI once, upload reusable artifacts for downstream jobs | Node-relevant changes |
|
||||
| `checks-fast-core` | Fast Linux correctness lanes such as bundled/plugin-contract/protocol checks | Node-relevant changes |
|
||||
| `checks-node-extensions` | Full bundled-plugin test shards across the extension suite | Node-relevant changes |
|
||||
| `checks-node-core-test` | Core Node test shards, excluding channel, bundled, contract, and extension lanes | Node-relevant changes |
|
||||
| `extension-fast` | Focused tests for only the changed bundled plugins | When extension changes are detected |
|
||||
| `check` | Main local gate in CI: `pnpm check`, `pnpm check:test-types`, and `pnpm build:strict-smoke` | Node-relevant changes |
|
||||
| `check-additional` | Architecture, boundary, import-cycle guards plus the gateway watch regression harness | Node-relevant changes |
|
||||
| `build-smoke` | Built-CLI smoke tests and startup-memory smoke | Node-relevant changes |
|
||||
| `checks` | Remaining Linux Node lanes: channel tests and push-only Node 22 compatibility | Node-relevant changes |
|
||||
| `check-docs` | Docs formatting, lint, and broken-link checks | Docs changed |
|
||||
| `skills-python` | Ruff + pytest for Python-backed skills | Python-skill-relevant changes |
|
||||
| `checks-windows` | Windows-specific test lanes | Windows-relevant changes |
|
||||
| `macos-node` | macOS TypeScript test lane using the shared built artifacts | macOS-relevant changes |
|
||||
| `macos-swift` | Swift lint, build, and tests for the macOS app | macOS-relevant changes |
|
||||
| `android` | Android build and test matrix | Android-relevant changes |
|
||||
| Job | Purpose | When it runs |
|
||||
| -------------------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------- |
|
||||
| `preflight` | Detect docs-only changes, changed scopes, changed extensions, and build the CI manifest | Always on non-draft pushes and PRs |
|
||||
| `security-fast` | Private key detection, workflow audit via `zizmor`, production dependency audit | Always on non-draft pushes and PRs |
|
||||
| `build-artifacts` | Build `dist/` and the Control UI once, upload reusable artifacts for downstream jobs | Node-relevant changes |
|
||||
| `checks-fast-core` | Fast Linux correctness lanes such as bundled/plugin-contract/protocol checks | Node-relevant changes |
|
||||
| `checks-fast-contracts-channels` | Sharded channel contract checks with a stable aggregate check result | Node-relevant changes |
|
||||
| `checks-node-extensions` | Full bundled-plugin test shards across the extension suite | Node-relevant changes |
|
||||
| `checks-node-core-test` | Core Node test shards, excluding channel, bundled, contract, and extension lanes | Node-relevant changes |
|
||||
| `extension-fast` | Focused tests for only the changed bundled plugins | When extension changes are detected |
|
||||
| `check` | Sharded main local gate equivalent: prod types, lint, guards, test types, and strict smoke | Node-relevant changes |
|
||||
| `check-additional` | Architecture, boundary, extension-surface guards, package-boundary, and gateway-watch shards | Node-relevant changes |
|
||||
| `build-smoke` | Built-CLI smoke tests and startup-memory smoke | Node-relevant changes |
|
||||
| `checks` | Remaining Linux Node lanes: channel tests and push-only Node 22 compatibility | Node-relevant changes |
|
||||
| `check-docs` | Docs formatting, lint, and broken-link checks | Docs changed |
|
||||
| `skills-python` | Ruff + pytest for Python-backed skills | Python-skill-relevant changes |
|
||||
| `checks-windows` | Windows-specific test lanes | Windows-relevant changes |
|
||||
| `macos-node` | macOS TypeScript test lane using the shared built artifacts | macOS-relevant changes |
|
||||
| `macos-swift` | Swift lint, build, and tests for the macOS app | macOS-relevant changes |
|
||||
| `android` | Android build and test matrix | Android-relevant changes |
|
||||
|
||||
## Fail-Fast Order
|
||||
|
||||
@@ -39,7 +40,7 @@ Jobs are ordered so cheap checks fail before expensive ones run:
|
||||
1. `preflight` decides which lanes exist at all. The `docs-scope` and `changed-scope` logic are steps inside this job, not standalone jobs.
|
||||
2. `security-fast`, `check`, `check-additional`, `check-docs`, and `skills-python` fail quickly without waiting on the heavier artifact and platform matrix jobs.
|
||||
3. `build-artifacts` overlaps with the fast Linux lanes so downstream consumers can start as soon as the shared build is ready.
|
||||
4. Heavier platform and runtime lanes fan out after that: `checks-fast-core`, `checks-node-extensions`, `checks-node-core-test`, `extension-fast`, `checks`, `checks-windows`, `macos-node`, `macos-swift`, and `android`.
|
||||
4. Heavier platform and runtime lanes fan out after that: `checks-fast-core`, `checks-fast-contracts-channels`, `checks-node-extensions`, `checks-node-core-test`, `extension-fast`, `checks`, `checks-windows`, `macos-node`, `macos-swift`, and `android`.
|
||||
|
||||
Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests in `src/scripts/ci-changed-scope.test.ts`.
|
||||
The separate `install-smoke` workflow reuses the same scope script through its own `preflight` job. It computes `run_install_smoke` from the narrower changed-smoke signal, so Docker/install smoke only runs for install, packaging, and container-relevant changes.
|
||||
@@ -48,6 +49,8 @@ Local changed-lane logic lives in `scripts/changed-lanes.mjs` and is executed by
|
||||
|
||||
On pushes, the `checks` matrix adds the push-only `compat-node22` lane. On pull requests, that lane is skipped and the matrix stays focused on the normal test/channel lanes.
|
||||
|
||||
The slowest Node test families are split into include-file shards so each job stays small: channel contracts split registry/core/extension coverage into focused shards, and auto-reply reply tests split each large prefix group into two include-pattern shards. `check-additional` also separates package-boundary compile/canary work from runtime topology gateway/architecture work.
|
||||
|
||||
## Runners
|
||||
|
||||
| Runner | Jobs |
|
||||
|
||||
54
scripts/lib/channel-contract-test-plan.mjs
Normal file
54
scripts/lib/channel-contract-test-plan.mjs
Normal file
@@ -0,0 +1,54 @@
|
||||
import { existsSync, readdirSync } from "node:fs";
|
||||
import { join, relative } from "node:path";
|
||||
|
||||
function listContractTestFiles(rootDir = "src/channels/plugins/contracts") {
|
||||
if (!existsSync(rootDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return readdirSync(rootDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".test.ts"))
|
||||
.map((entry) => join(rootDir, entry.name).replaceAll("\\", "/"))
|
||||
.toSorted((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
export function createChannelContractTestShards() {
|
||||
const rootDir = "src/channels/plugins/contracts";
|
||||
const groups = {
|
||||
"checks-fast-contracts-channels-registry-a": [],
|
||||
"checks-fast-contracts-channels-registry-b": [],
|
||||
"checks-fast-contracts-channels-core-a": [],
|
||||
"checks-fast-contracts-channels-core-b": [],
|
||||
"checks-fast-contracts-channels-extensions": [],
|
||||
};
|
||||
const pushBalanced = (firstKey, secondKey, file) => {
|
||||
const target = groups[firstKey].length <= groups[secondKey].length ? firstKey : secondKey;
|
||||
groups[target].push(file);
|
||||
};
|
||||
|
||||
for (const file of listContractTestFiles(rootDir)) {
|
||||
const name = relative(rootDir, file).replaceAll("\\", "/");
|
||||
if (name.startsWith("plugins-core-extension.")) {
|
||||
groups["checks-fast-contracts-channels-extensions"].push(file);
|
||||
} else if (name.startsWith("plugins-core.") || name.startsWith("plugin.")) {
|
||||
pushBalanced(
|
||||
"checks-fast-contracts-channels-core-a",
|
||||
"checks-fast-contracts-channels-core-b",
|
||||
file,
|
||||
);
|
||||
} else {
|
||||
pushBalanced(
|
||||
"checks-fast-contracts-channels-registry-a",
|
||||
"checks-fast-contracts-channels-registry-b",
|
||||
file,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.entries(groups).map(([checkName, includePatterns]) => ({
|
||||
checkName,
|
||||
includePatterns,
|
||||
task: "contracts-channels",
|
||||
runtime: "node",
|
||||
}));
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { existsSync, readdirSync } from "node:fs";
|
||||
import { join, relative } from "node:path";
|
||||
import { fullSuiteVitestShards } from "../../test/vitest/vitest.test-shards.mjs";
|
||||
|
||||
const EXCLUDED_FULL_SUITE_SHARDS = new Set([
|
||||
@@ -7,7 +9,137 @@ const EXCLUDED_FULL_SUITE_SHARDS = new Set([
|
||||
]);
|
||||
|
||||
const EXCLUDED_PROJECT_CONFIGS = new Set(["test/vitest/vitest.channels.config.ts"]);
|
||||
function listTestFiles(rootDir) {
|
||||
if (!existsSync(rootDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const files = [];
|
||||
const visit = (dir) => {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
visit(path);
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile() && entry.name.endsWith(".test.ts")) {
|
||||
files.push(path.replaceAll("\\", "/"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
visit(rootDir);
|
||||
return files.toSorted((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function createAutoReplyReplySplitShards() {
|
||||
const files = listTestFiles("src/auto-reply/reply");
|
||||
const groups = {
|
||||
"auto-reply-reply-agent-runner": [],
|
||||
"auto-reply-reply-commands": [],
|
||||
"auto-reply-reply-dispatch": [],
|
||||
"auto-reply-reply-state-routing": [],
|
||||
};
|
||||
|
||||
for (const file of files) {
|
||||
const name = relative("src/auto-reply/reply", file).replaceAll("\\", "/");
|
||||
if (
|
||||
name.startsWith("agent-runner") ||
|
||||
name.startsWith("acp-") ||
|
||||
name === "abort.test.ts" ||
|
||||
name === "bash-command.stop.test.ts" ||
|
||||
name.startsWith("block-")
|
||||
) {
|
||||
groups["auto-reply-reply-agent-runner"].push(file);
|
||||
} else if (name.startsWith("commands")) {
|
||||
groups["auto-reply-reply-commands"].push(file);
|
||||
} else if (
|
||||
name.startsWith("directive-") ||
|
||||
name.startsWith("dispatch") ||
|
||||
name.startsWith("followup-") ||
|
||||
name.startsWith("get-reply")
|
||||
) {
|
||||
groups["auto-reply-reply-dispatch"].push(file);
|
||||
} else {
|
||||
groups["auto-reply-reply-state-routing"].push(file);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.entries(groups).flatMap(([groupName, includePatterns]) => {
|
||||
const midpoint = Math.ceil(includePatterns.length / 2);
|
||||
return [
|
||||
{
|
||||
shardName: `${groupName}-a`,
|
||||
configs: ["test/vitest/vitest.auto-reply-reply.config.ts"],
|
||||
includePatterns: includePatterns.slice(0, midpoint),
|
||||
requiresDist: false,
|
||||
},
|
||||
{
|
||||
shardName: `${groupName}-b`,
|
||||
configs: ["test/vitest/vitest.auto-reply-reply.config.ts"],
|
||||
includePatterns: includePatterns.slice(midpoint),
|
||||
requiresDist: false,
|
||||
},
|
||||
].filter((shard) => shard.includePatterns.length > 0);
|
||||
});
|
||||
}
|
||||
|
||||
const SPLIT_NODE_SHARDS = new Map([
|
||||
[
|
||||
"core-runtime",
|
||||
[
|
||||
{
|
||||
shardName: "core-runtime-infra",
|
||||
configs: [
|
||||
"test/vitest/vitest.infra.config.ts",
|
||||
"test/vitest/vitest.hooks.config.ts",
|
||||
"test/vitest/vitest.runtime-config.config.ts",
|
||||
"test/vitest/vitest.secrets.config.ts",
|
||||
"test/vitest/vitest.logging.config.ts",
|
||||
"test/vitest/vitest.process.config.ts",
|
||||
],
|
||||
requiresDist: true,
|
||||
},
|
||||
{
|
||||
shardName: "core-runtime-media-ui",
|
||||
configs: [
|
||||
"test/vitest/vitest.media.config.ts",
|
||||
"test/vitest/vitest.media-understanding.config.ts",
|
||||
"test/vitest/vitest.tui.config.ts",
|
||||
"test/vitest/vitest.ui.config.ts",
|
||||
"test/vitest/vitest.wizard.config.ts",
|
||||
],
|
||||
requiresDist: true,
|
||||
},
|
||||
{
|
||||
shardName: "core-runtime-shared",
|
||||
configs: [
|
||||
"test/vitest/vitest.acp.config.ts",
|
||||
"test/vitest/vitest.cron.config.ts",
|
||||
"test/vitest/vitest.shared-core.config.ts",
|
||||
"test/vitest/vitest.tasks.config.ts",
|
||||
"test/vitest/vitest.utils.config.ts",
|
||||
],
|
||||
requiresDist: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"auto-reply",
|
||||
[
|
||||
{
|
||||
shardName: "auto-reply-core",
|
||||
configs: ["test/vitest/vitest.auto-reply-core.config.ts"],
|
||||
requiresDist: false,
|
||||
},
|
||||
{
|
||||
shardName: "auto-reply-top-level",
|
||||
configs: ["test/vitest/vitest.auto-reply-top-level.config.ts"],
|
||||
requiresDist: false,
|
||||
},
|
||||
...createAutoReplyReplySplitShards(),
|
||||
],
|
||||
],
|
||||
[
|
||||
"agentic",
|
||||
[
|
||||
@@ -77,6 +209,7 @@ export function createNodeTestShards() {
|
||||
checkName: formatNodeTestShardCheckName(splitShard.shardName),
|
||||
shardName: splitShard.shardName,
|
||||
configs: splitConfigs,
|
||||
...(splitShard.includePatterns ? { includePatterns: splitShard.includePatterns } : {}),
|
||||
requiresDist: splitShard.requiresDist,
|
||||
},
|
||||
];
|
||||
|
||||
62
test/scripts/channel-contract-test-plan.test.ts
Normal file
62
test/scripts/channel-contract-test-plan.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { existsSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createChannelContractTestShards } from "../../scripts/lib/channel-contract-test-plan.mjs";
|
||||
|
||||
function listContractTests(rootDir = "src/channels/plugins/contracts"): string[] {
|
||||
if (!existsSync(rootDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return readdirSync(rootDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".test.ts"))
|
||||
.map((entry) => join(rootDir, entry.name).replaceAll("\\", "/"))
|
||||
.toSorted((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
describe("scripts/lib/channel-contract-test-plan.mjs", () => {
|
||||
it("splits channel contracts into focused shards", () => {
|
||||
expect(
|
||||
createChannelContractTestShards().map((shard) => ({
|
||||
checkName: shard.checkName,
|
||||
runtime: shard.runtime,
|
||||
task: shard.task,
|
||||
})),
|
||||
).toEqual([
|
||||
{
|
||||
checkName: "checks-fast-contracts-channels-registry-a",
|
||||
runtime: "node",
|
||||
task: "contracts-channels",
|
||||
},
|
||||
{
|
||||
checkName: "checks-fast-contracts-channels-registry-b",
|
||||
runtime: "node",
|
||||
task: "contracts-channels",
|
||||
},
|
||||
{
|
||||
checkName: "checks-fast-contracts-channels-core-a",
|
||||
runtime: "node",
|
||||
task: "contracts-channels",
|
||||
},
|
||||
{
|
||||
checkName: "checks-fast-contracts-channels-core-b",
|
||||
runtime: "node",
|
||||
task: "contracts-channels",
|
||||
},
|
||||
{
|
||||
checkName: "checks-fast-contracts-channels-extensions",
|
||||
runtime: "node",
|
||||
task: "contracts-channels",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("covers every channel contract test exactly once", () => {
|
||||
const actual = createChannelContractTestShards()
|
||||
.flatMap((shard) => shard.includePatterns)
|
||||
.toSorted((a, b) => a.localeCompare(b));
|
||||
|
||||
expect(actual).toEqual(listContractTests());
|
||||
expect(new Set(actual).size).toBe(actual.length);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,29 @@
|
||||
import { existsSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createNodeTestShards } from "../../scripts/lib/ci-node-test-plan.mjs";
|
||||
|
||||
function listTestFiles(rootDir: string): string[] {
|
||||
if (!existsSync(rootDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const files: string[] = [];
|
||||
const visit = (dir: string) => {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
visit(path);
|
||||
} else if (entry.isFile() && entry.name.endsWith(".test.ts")) {
|
||||
files.push(path.replaceAll("\\", "/"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
visit(rootDir);
|
||||
return files.toSorted((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
describe("scripts/lib/ci-node-test-plan.mjs", () => {
|
||||
it("names the node shard checks as core test lanes", () => {
|
||||
const shards = createNodeTestShards();
|
||||
@@ -32,11 +55,60 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => {
|
||||
|
||||
expect(requiresDistShardNames).toEqual([
|
||||
"core-support-boundary",
|
||||
"core-runtime",
|
||||
"core-runtime-infra",
|
||||
"core-runtime-media-ui",
|
||||
"core-runtime-shared",
|
||||
"agentic-agents-plugins",
|
||||
]);
|
||||
});
|
||||
|
||||
it("splits core runtime configs into smaller dist-dependent shards", () => {
|
||||
const runtimeShards = createNodeTestShards()
|
||||
.filter((shard) => shard.shardName.startsWith("core-runtime-"))
|
||||
.map((shard) => ({
|
||||
configs: shard.configs,
|
||||
requiresDist: shard.requiresDist,
|
||||
shardName: shard.shardName,
|
||||
}));
|
||||
|
||||
expect(runtimeShards).toEqual([
|
||||
{
|
||||
configs: [
|
||||
"test/vitest/vitest.infra.config.ts",
|
||||
"test/vitest/vitest.hooks.config.ts",
|
||||
"test/vitest/vitest.runtime-config.config.ts",
|
||||
"test/vitest/vitest.secrets.config.ts",
|
||||
"test/vitest/vitest.logging.config.ts",
|
||||
"test/vitest/vitest.process.config.ts",
|
||||
],
|
||||
requiresDist: true,
|
||||
shardName: "core-runtime-infra",
|
||||
},
|
||||
{
|
||||
configs: [
|
||||
"test/vitest/vitest.media.config.ts",
|
||||
"test/vitest/vitest.media-understanding.config.ts",
|
||||
"test/vitest/vitest.tui.config.ts",
|
||||
"test/vitest/vitest.ui.config.ts",
|
||||
"test/vitest/vitest.wizard.config.ts",
|
||||
],
|
||||
requiresDist: true,
|
||||
shardName: "core-runtime-media-ui",
|
||||
},
|
||||
{
|
||||
configs: [
|
||||
"test/vitest/vitest.acp.config.ts",
|
||||
"test/vitest/vitest.cron.config.ts",
|
||||
"test/vitest/vitest.shared-core.config.ts",
|
||||
"test/vitest/vitest.tasks.config.ts",
|
||||
"test/vitest/vitest.utils.config.ts",
|
||||
],
|
||||
requiresDist: true,
|
||||
shardName: "core-runtime-shared",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("splits the agentic lane into control-plane, commands, and agent/plugin shards", () => {
|
||||
const shards = createNodeTestShards();
|
||||
const controlPlaneShard = shards.find((shard) => shard.shardName === "agentic-control-plane");
|
||||
@@ -77,4 +149,89 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => {
|
||||
requiresDist: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("splits auto-reply into independent core, top-level, and reply subtree shards", () => {
|
||||
const shards = createNodeTestShards();
|
||||
const autoReplyShards = shards
|
||||
.filter((shard) => shard.shardName.startsWith("auto-reply"))
|
||||
.map((shard) => ({
|
||||
checkName: shard.checkName,
|
||||
configs: shard.configs,
|
||||
requiresDist: shard.requiresDist,
|
||||
shardName: shard.shardName,
|
||||
}));
|
||||
|
||||
expect(autoReplyShards).toEqual([
|
||||
{
|
||||
checkName: "checks-node-auto-reply-core",
|
||||
configs: ["test/vitest/vitest.auto-reply-core.config.ts"],
|
||||
requiresDist: false,
|
||||
shardName: "auto-reply-core",
|
||||
},
|
||||
{
|
||||
checkName: "checks-node-auto-reply-top-level",
|
||||
configs: ["test/vitest/vitest.auto-reply-top-level.config.ts"],
|
||||
requiresDist: false,
|
||||
shardName: "auto-reply-top-level",
|
||||
},
|
||||
{
|
||||
checkName: "checks-node-auto-reply-reply-agent-runner-a",
|
||||
configs: ["test/vitest/vitest.auto-reply-reply.config.ts"],
|
||||
requiresDist: false,
|
||||
shardName: "auto-reply-reply-agent-runner-a",
|
||||
},
|
||||
{
|
||||
checkName: "checks-node-auto-reply-reply-agent-runner-b",
|
||||
configs: ["test/vitest/vitest.auto-reply-reply.config.ts"],
|
||||
requiresDist: false,
|
||||
shardName: "auto-reply-reply-agent-runner-b",
|
||||
},
|
||||
{
|
||||
checkName: "checks-node-auto-reply-reply-commands-a",
|
||||
configs: ["test/vitest/vitest.auto-reply-reply.config.ts"],
|
||||
requiresDist: false,
|
||||
shardName: "auto-reply-reply-commands-a",
|
||||
},
|
||||
{
|
||||
checkName: "checks-node-auto-reply-reply-commands-b",
|
||||
configs: ["test/vitest/vitest.auto-reply-reply.config.ts"],
|
||||
requiresDist: false,
|
||||
shardName: "auto-reply-reply-commands-b",
|
||||
},
|
||||
{
|
||||
checkName: "checks-node-auto-reply-reply-dispatch-a",
|
||||
configs: ["test/vitest/vitest.auto-reply-reply.config.ts"],
|
||||
requiresDist: false,
|
||||
shardName: "auto-reply-reply-dispatch-a",
|
||||
},
|
||||
{
|
||||
checkName: "checks-node-auto-reply-reply-dispatch-b",
|
||||
configs: ["test/vitest/vitest.auto-reply-reply.config.ts"],
|
||||
requiresDist: false,
|
||||
shardName: "auto-reply-reply-dispatch-b",
|
||||
},
|
||||
{
|
||||
checkName: "checks-node-auto-reply-reply-state-routing-a",
|
||||
configs: ["test/vitest/vitest.auto-reply-reply.config.ts"],
|
||||
requiresDist: false,
|
||||
shardName: "auto-reply-reply-state-routing-a",
|
||||
},
|
||||
{
|
||||
checkName: "checks-node-auto-reply-reply-state-routing-b",
|
||||
configs: ["test/vitest/vitest.auto-reply-reply.config.ts"],
|
||||
requiresDist: false,
|
||||
shardName: "auto-reply-reply-state-routing-b",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("covers every auto-reply reply test exactly once across split shards", () => {
|
||||
const actual = createNodeTestShards()
|
||||
.filter((shard) => shard.shardName.startsWith("auto-reply-reply-"))
|
||||
.flatMap((shard) => shard.includePatterns ?? [])
|
||||
.toSorted((a, b) => a.localeCompare(b));
|
||||
|
||||
expect(actual).toEqual(listTestFiles("src/auto-reply/reply"));
|
||||
expect(new Set(actual).size).toBe(actual.length);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user