refactor(test): split docker e2e planner

This commit is contained in:
Peter Steinberger
2026-04-26 23:36:19 +01:00
parent c9c0ab3a44
commit a2adb05f74
20 changed files with 1056 additions and 677 deletions

View File

@@ -29,16 +29,13 @@ docker_e2e_prepare_package_tgz() {
return 0
fi
echo "Building OpenClaw package artifacts..."
run_logged "$label-host-build" pnpm build
echo "Writing package inventory and packing OpenClaw once..."
run_logged "$label-inventory" node --import tsx --input-type=module -e 'const { writePackageDistInventory } = await import("./src/infra/package-dist-inventory.ts"); await writePackageDistInventory(process.cwd());'
local pack_dir
pack_dir="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-docker-e2e-pack.XXXXXX")"
run_logged "$label-pack" npm pack --ignore-scripts --pack-destination "$pack_dir"
package_tgz="$(find "$pack_dir" -maxdepth 1 -name 'openclaw-*.tgz' -print -quit)"
package_tgz="$(
node "$ROOT_DIR/scripts/package-openclaw-for-docker.mjs" \
--output-dir "$pack_dir" \
--output-name openclaw-current.tgz
)"
if [ -z "$package_tgz" ]; then
echo "missing packed OpenClaw tarball" >&2
return 1
@@ -61,3 +58,7 @@ docker_e2e_package_mount_args() {
local target="${2:-/tmp/openclaw-current.tgz}"
DOCKER_E2E_PACKAGE_ARGS=(-v "$package_tgz:$target:ro" -e "OPENCLAW_CURRENT_PACKAGE_TGZ=$target")
}
docker_e2e_harness_mount_args() {
DOCKER_E2E_HARNESS_ARGS=(-v "$ROOT_DIR/scripts/e2e:/app/scripts/e2e:ro")
}

View File

@@ -0,0 +1,236 @@
// Docker E2E scheduler planning helpers.
// This module turns the scenario catalog plus env-driven inputs into a concrete
// lane plan. It intentionally does not define scenario commands.
import {
DEFAULT_LIVE_RETRIES,
allReleasePathLanes,
mainLanes,
releasePathChunkLanes,
tailLanes,
} from "./docker-e2e-scenarios.mjs";
export { DEFAULT_LIVE_RETRIES };
export const DEFAULT_E2E_BARE_IMAGE = "openclaw-docker-e2e-bare:local";
export const DEFAULT_E2E_FUNCTIONAL_IMAGE = "openclaw-docker-e2e-functional:local";
export const DEFAULT_E2E_IMAGE = DEFAULT_E2E_FUNCTIONAL_IMAGE;
export const DEFAULT_PARALLELISM = 10;
export const DEFAULT_PROFILE = "all";
export const DEFAULT_RESOURCE_LIMITS = {
docker: DEFAULT_PARALLELISM,
live: 9,
"live:claude": 4,
"live:codex": 4,
"live:droid": 4,
"live:gemini": 4,
"live:opencode": 4,
npm: 10,
service: 7,
};
export const DEFAULT_TAIL_PARALLELISM = 10;
export const RELEASE_PATH_PROFILE = "release-path";
export function parseLaneSelection(raw) {
if (!raw) {
return [];
}
return [
...new Set(
String(raw)
.split(/[,\s]+/u)
.map((token) => token.trim())
.filter(Boolean),
),
];
}
export function dedupeLanes(poolLanes) {
const byName = new Map();
for (const poolLane of poolLanes) {
if (!byName.has(poolLane.name)) {
byName.set(poolLane.name, poolLane);
}
}
return [...byName.values()];
}
export function selectNamedLanes(poolLanes, selectedNames, label) {
const byName = new Map(poolLanes.map((poolLane) => [poolLane.name, poolLane]));
const missing = selectedNames.filter((name) => !byName.has(name));
if (missing.length > 0) {
throw new Error(
`${label} unknown lane(s): ${missing.join(", ")}. Available lanes: ${[...byName.keys()]
.toSorted((a, b) => a.localeCompare(b))
.join(", ")}`,
);
}
return selectedNames.map((name) => byName.get(name));
}
export function parseLiveMode(raw) {
const mode = raw || "all";
if (mode === "all" || mode === "skip" || mode === "only") {
return mode;
}
throw new Error(
`OPENCLAW_DOCKER_ALL_LIVE_MODE must be one of: all, skip, only. Got: ${JSON.stringify(raw)}`,
);
}
export function parseProfile(raw) {
const profile = raw || DEFAULT_PROFILE;
if (profile === DEFAULT_PROFILE || profile === RELEASE_PATH_PROFILE) {
return profile;
}
throw new Error(
`OPENCLAW_DOCKER_ALL_PROFILE must be one of: ${DEFAULT_PROFILE}, ${RELEASE_PATH_PROFILE}. Got: ${JSON.stringify(raw)}`,
);
}
export function applyLiveMode(poolLanes, mode) {
if (mode === "all") {
return poolLanes;
}
return poolLanes.filter((poolLane) => (mode === "only" ? poolLane.live : !poolLane.live));
}
export function applyLiveRetries(poolLanes, retries) {
return poolLanes.map((poolLane) => (poolLane.live ? { ...poolLane, retries } : poolLane));
}
export function laneWeight(poolLane) {
return Math.max(1, poolLane.weight ?? 1);
}
export function laneResources(poolLane) {
return ["docker", ...(poolLane.resources ?? [])];
}
export function laneSummary(poolLane) {
const resources = laneResources(poolLane).join(",");
const timeout = poolLane.timeoutMs ? ` timeout=${Math.round(poolLane.timeoutMs / 1000)}s` : "";
const retries = poolLane.retries > 0 ? ` retries=${poolLane.retries}` : "";
const cache = poolLane.cacheKey ? ` cache=${poolLane.cacheKey}` : "";
const image = poolLane.e2eImageKind ? ` image=${poolLane.e2eImageKind}` : "";
return `${poolLane.name}(w=${laneWeight(poolLane)} r=${resources}${timeout}${retries}${cache}${image})`;
}
export function lanesNeedE2eImageKind(poolLanes, kind) {
return poolLanes.some((poolLane) => poolLane.e2eImageKind === kind);
}
export function lanesNeedOpenClawPackage(poolLanes) {
return poolLanes.some((poolLane) => poolLane.e2eImageKind);
}
export function findLaneByName(name) {
return dedupeLanes([
...allReleasePathLanes({ includeOpenWebUI: true }),
...mainLanes,
...tailLanes,
]).find((poolLane) => poolLane.name === name);
}
export function laneCredentialRequirements(poolLane) {
const credentials = [];
if (poolLane.name === "install-e2e") {
credentials.push("openai", "anthropic");
}
if (poolLane.name === "openwebui" || poolLane.name === "openai-web-search-minimal") {
credentials.push("openai");
}
return credentials;
}
function unique(values) {
return [...new Set(values.filter(Boolean))];
}
export function buildPlanJson(params) {
const scheduledLanes = [...params.orderedLanes, ...params.orderedTailLanes];
const imageKinds = unique(scheduledLanes.map((poolLane) => poolLane.e2eImageKind)).toSorted(
(a, b) => a.localeCompare(b),
);
return {
chunk: params.releaseChunk || undefined,
credentials: unique(scheduledLanes.flatMap(laneCredentialRequirements)).toSorted((a, b) =>
a.localeCompare(b),
),
imageKinds,
includeOpenWebUI: params.includeOpenWebUI,
lanes: scheduledLanes.map((poolLane) => ({
command: poolLane.command,
imageKind: poolLane.e2eImageKind,
live: poolLane.live,
name: poolLane.name,
resources: laneResources(poolLane),
timeoutMs: poolLane.timeoutMs,
weight: laneWeight(poolLane),
})),
mainLanes: params.orderedLanes.map((poolLane) => poolLane.name),
needs: {
bareImage: imageKinds.includes("bare"),
e2eImage: imageKinds.length > 0,
functionalImage: imageKinds.includes("functional"),
liveImage: scheduledLanes.some((poolLane) => poolLane.live),
package: lanesNeedOpenClawPackage(scheduledLanes),
},
profile: params.profile,
selectedLanes: params.selectedLaneNames,
tailLanes: params.orderedTailLanes.map((poolLane) => poolLane.name),
version: 1,
};
}
export function resolveDockerE2ePlan(options) {
const retriedMainLanes = applyLiveRetries(mainLanes, options.liveRetries);
const retriedTailLanes = applyLiveRetries(tailLanes, options.liveRetries);
const releaseLanes =
options.selectedLaneNames.length === 0 && options.profile === RELEASE_PATH_PROFILE
? options.planReleaseAll
? allReleasePathLanes({ includeOpenWebUI: options.includeOpenWebUI })
: releasePathChunkLanes(options.releaseChunk, {
includeOpenWebUI: options.includeOpenWebUI,
})
: undefined;
const selectedLanes =
options.selectedLaneNames.length > 0
? selectNamedLanes(
dedupeLanes([
...allReleasePathLanes({ includeOpenWebUI: options.includeOpenWebUI }),
...retriedMainLanes,
...retriedTailLanes,
]),
options.selectedLaneNames,
"OPENCLAW_DOCKER_ALL_LANES",
)
: undefined;
const configuredLanes = selectedLanes
? selectedLanes
: releaseLanes
? releaseLanes
: options.liveMode === "only"
? applyLiveMode([...retriedMainLanes, ...retriedTailLanes], options.liveMode)
: applyLiveMode(retriedMainLanes, options.liveMode);
const configuredTailLanes =
selectedLanes || releaseLanes
? []
: options.liveMode === "only"
? []
: applyLiveMode(retriedTailLanes, options.liveMode);
const orderedLanes = options.orderLanes(configuredLanes, options.timingStore);
const orderedTailLanes = options.orderLanes(configuredTailLanes, options.timingStore);
return {
orderedLanes,
orderedTailLanes,
plan: buildPlanJson({
includeOpenWebUI: options.includeOpenWebUI,
orderedLanes,
orderedTailLanes,
profile: options.profile,
releaseChunk: options.releaseChunk,
selectedLaneNames: options.selectedLaneNames,
}),
scheduledLanes: [...orderedLanes, ...orderedTailLanes],
};
}

View File

@@ -0,0 +1,417 @@
// Docker E2E scenario catalog.
// Keep lane names, commands, image kind, timeout, resources, and release chunks
// here. Planning and execution live in separate modules.
const BUNDLED_UPDATE_TIMEOUT_MS = 20 * 60 * 1000;
export const DEFAULT_LIVE_RETRIES = 1;
const LIVE_ACP_TIMEOUT_MS = 20 * 60 * 1000;
const LIVE_CLI_TIMEOUT_MS = 20 * 60 * 1000;
const LIVE_PROFILE_TIMEOUT_MS = 20 * 60 * 1000;
const OPENWEBUI_TIMEOUT_MS = 20 * 60 * 1000;
export const LIVE_RETRY_PATTERNS = [
/529\b/i,
/overloaded/i,
/capacity/i,
/rate.?limit/i,
/gateway closed \(1000 normal closure\)/i,
/ECONNRESET|ETIMEDOUT|ENOTFOUND/i,
];
const bundledChannelLaneCommand =
"OPENCLAW_SKIP_DOCKER_BUILD=1 OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_LOAD_FAILURE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_DISABLED_CONFIG_SCENARIO=0 pnpm test:docker:bundled-channel-deps";
function lane(name, command, options = {}) {
return {
cacheKey: options.cacheKey,
command,
e2eImageKind: options.e2eImageKind ?? (options.live ? undefined : "functional"),
estimateSeconds: options.estimateSeconds,
live: options.live === true,
name,
retryPatterns: options.retryPatterns ?? [],
retries: options.retries ?? 0,
resources: options.resources ?? [],
timeoutMs: options.timeoutMs,
weight: options.weight ?? 1,
};
}
function liveProviderResource(provider) {
if (!provider) {
return undefined;
}
if (provider === "claude-cli" || provider === "claude") {
return "live:claude";
}
if (provider === "codex-cli" || provider === "codex") {
return "live:codex";
}
if (provider === "droid") {
return "live:droid";
}
if (provider === "google-gemini-cli" || provider === "gemini") {
return "live:gemini";
}
if (provider === "opencode") {
return "live:opencode";
}
if (provider === "openai") {
return "live:openai";
}
return `live:${provider}`;
}
function liveProviderResources(options) {
const providers = options.providers ?? (options.provider ? [options.provider] : []);
return providers.map(liveProviderResource).filter(Boolean);
}
function liveLane(name, command, options = {}) {
return lane(name, command, {
...options,
live: true,
resources: ["live", ...liveProviderResources(options), ...(options.resources ?? [])],
retryPatterns: options.retryPatterns ?? LIVE_RETRY_PATTERNS,
retries: options.retries ?? DEFAULT_LIVE_RETRIES,
weight: options.weight ?? 3,
});
}
function npmLane(name, command, options = {}) {
return lane(name, command, {
...options,
e2eImageKind: options.e2eImageKind ?? "bare",
resources: ["npm", ...(options.resources ?? [])],
weight: options.weight ?? 2,
});
}
function serviceLane(name, command, options = {}) {
return lane(name, command, {
...options,
resources: ["service", ...(options.resources ?? [])],
weight: options.weight ?? 2,
});
}
function bundledChannelScenarioLane(name, env, options = {}) {
return npmLane(
name,
`${env} OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:bundled-channel-deps`,
options,
);
}
const bundledScenarioLanes = [
...["telegram", "discord", "slack", "feishu", "memory-lancedb"].map((channel) =>
npmLane(
`bundled-channel-${channel}`,
`OPENCLAW_BUNDLED_CHANNELS=${channel} ${bundledChannelLaneCommand}`,
),
),
...["telegram", "discord", "slack", "feishu", "memory-lancedb", "acpx"].map((target) =>
bundledChannelScenarioLane(
`bundled-channel-update-${target}`,
`OPENCLAW_BUNDLED_CHANNEL_SCENARIOS=0 OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO=1 OPENCLAW_BUNDLED_CHANNEL_UPDATE_TARGETS=${target} OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_LOAD_FAILURE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_DISABLED_CONFIG_SCENARIO=0`,
{ timeoutMs: BUNDLED_UPDATE_TIMEOUT_MS },
),
),
bundledChannelScenarioLane(
"bundled-channel-root-owned",
"OPENCLAW_BUNDLED_CHANNEL_SCENARIOS=0 OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO=1 OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_LOAD_FAILURE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_DISABLED_CONFIG_SCENARIO=0",
),
bundledChannelScenarioLane(
"bundled-channel-setup-entry",
"OPENCLAW_BUNDLED_CHANNEL_SCENARIOS=0 OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO=1 OPENCLAW_BUNDLED_CHANNEL_LOAD_FAILURE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_DISABLED_CONFIG_SCENARIO=0",
),
bundledChannelScenarioLane(
"bundled-channel-load-failure",
"OPENCLAW_BUNDLED_CHANNEL_SCENARIOS=0 OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_LOAD_FAILURE_SCENARIO=1 OPENCLAW_BUNDLED_CHANNEL_DISABLED_CONFIG_SCENARIO=0",
),
bundledChannelScenarioLane(
"bundled-channel-disabled-config",
"OPENCLAW_BUNDLED_CHANNEL_SCENARIOS=0 OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_LOAD_FAILURE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_DISABLED_CONFIG_SCENARIO=1",
),
];
export const mainLanes = [
liveLane("live-models", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-models", {
providers: ["claude-cli", "codex-cli", "google-gemini-cli"],
timeoutMs: LIVE_PROFILE_TIMEOUT_MS,
weight: 4,
}),
liveLane("live-gateway", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-gateway", {
providers: ["claude-cli", "codex-cli", "google-gemini-cli"],
timeoutMs: LIVE_PROFILE_TIMEOUT_MS,
weight: 4,
}),
liveLane(
"live-cli-backend-claude",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-cli-backend:claude",
{
cacheKey: "cli-backend-claude",
provider: "claude-cli",
resources: ["npm"],
timeoutMs: LIVE_CLI_TIMEOUT_MS,
weight: 3,
},
),
liveLane(
"live-cli-backend-gemini",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-cli-backend:gemini",
{
cacheKey: "cli-backend-gemini",
provider: "google-gemini-cli",
resources: ["npm"],
timeoutMs: LIVE_CLI_TIMEOUT_MS,
weight: 3,
},
),
serviceLane("openwebui", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:openwebui", {
timeoutMs: OPENWEBUI_TIMEOUT_MS,
weight: 5,
}),
serviceLane("onboard", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:onboard", {
weight: 2,
}),
npmLane(
"npm-onboard-channel-agent",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:npm-onboard-channel-agent",
{ resources: ["service"], weight: 3 },
),
serviceLane("gateway-network", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:gateway-network"),
serviceLane(
"agents-delete-shared-workspace",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:agents-delete-shared-workspace",
),
serviceLane("mcp-channels", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:mcp-channels", {
resources: ["npm"],
weight: 3,
}),
lane("pi-bundle-mcp-tools", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:pi-bundle-mcp-tools"),
lane("crestodian-rescue", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:crestodian-rescue"),
lane("crestodian-planner", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:crestodian-planner"),
serviceLane(
"cron-mcp-cleanup",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:cron-mcp-cleanup",
{ resources: ["npm"], weight: 3 },
),
npmLane("doctor-switch", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:doctor-switch", {
weight: 3,
}),
npmLane(
"update-channel-switch",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:update-channel-switch",
{
timeoutMs: 30 * 60 * 1000,
weight: 3,
},
),
lane("plugins", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:plugins", {
resources: ["npm", "service"],
weight: 6,
}),
npmLane("plugin-update", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:plugin-update"),
serviceLane("config-reload", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:config-reload"),
...bundledScenarioLanes,
lane("openai-image-auth", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:openai-image-auth"),
lane(
"crestodian-first-run",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:crestodian-first-run",
),
lane(
"session-runtime-context",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:session-runtime-context",
),
lane("qr", "pnpm test:docker:qr"),
];
export const tailLanes = [
serviceLane(
"openai-web-search-minimal",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:openai-web-search-minimal",
{ timeoutMs: 8 * 60 * 1000 },
),
liveLane(
"live-codex-harness",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-codex-harness",
{
cacheKey: "codex-harness",
provider: "codex-cli",
resources: ["npm"],
timeoutMs: LIVE_ACP_TIMEOUT_MS,
weight: 3,
},
),
liveLane("live-codex-bind", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-codex-bind", {
cacheKey: "codex-harness",
provider: "codex-cli",
resources: ["npm"],
timeoutMs: LIVE_ACP_TIMEOUT_MS,
weight: 3,
}),
liveLane(
"live-cli-backend-codex",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-cli-backend:codex",
{
cacheKey: "cli-backend-codex",
provider: "codex-cli",
resources: ["npm"],
timeoutMs: LIVE_CLI_TIMEOUT_MS,
weight: 3,
},
),
liveLane(
"live-acp-bind-claude",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-acp-bind:claude",
{
cacheKey: "acp-bind-claude",
provider: "claude-cli",
resources: ["npm"],
timeoutMs: LIVE_ACP_TIMEOUT_MS,
weight: 3,
},
),
liveLane(
"live-acp-bind-codex",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-acp-bind:codex",
{
cacheKey: "acp-bind-codex",
provider: "codex-cli",
resources: ["npm"],
timeoutMs: LIVE_ACP_TIMEOUT_MS,
weight: 3,
},
),
liveLane(
"live-acp-bind-droid",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-acp-bind:droid",
{
cacheKey: "acp-bind-droid",
provider: "droid",
resources: ["npm"],
timeoutMs: LIVE_ACP_TIMEOUT_MS,
weight: 3,
},
),
liveLane(
"live-acp-bind-gemini",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-acp-bind:gemini",
{
cacheKey: "acp-bind-gemini",
provider: "google-gemini-cli",
resources: ["npm"],
timeoutMs: LIVE_ACP_TIMEOUT_MS,
weight: 3,
},
),
liveLane(
"live-acp-bind-opencode",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-acp-bind:opencode",
{
cacheKey: "acp-bind-opencode",
provider: "opencode",
resources: ["npm"],
timeoutMs: LIVE_ACP_TIMEOUT_MS,
weight: 3,
},
),
];
const releasePathChunks = {
core: [
lane("qr", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:qr"),
serviceLane("onboard", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:onboard", {
weight: 2,
}),
serviceLane("gateway-network", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:gateway-network"),
serviceLane("config-reload", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:config-reload"),
lane(
"session-runtime-context",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:session-runtime-context",
),
lane(
"pi-bundle-mcp-tools",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:pi-bundle-mcp-tools",
),
serviceLane("mcp-channels", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:mcp-channels", {
resources: ["npm"],
weight: 3,
}),
],
"package-update": [
npmLane("install-e2e", "OPENCLAW_E2E_MODELS=both pnpm test:install:e2e", {
resources: ["service"],
weight: 4,
}),
npmLane(
"npm-onboard-channel-agent",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:npm-onboard-channel-agent",
{ resources: ["service"], weight: 3 },
),
npmLane("doctor-switch", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:doctor-switch", {
weight: 3,
}),
npmLane(
"update-channel-switch",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:update-channel-switch",
{
timeoutMs: 30 * 60 * 1000,
weight: 3,
},
),
],
"plugins-integrations": [
lane("plugins", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:plugins", {
resources: ["npm", "service"],
weight: 6,
}),
npmLane("plugin-update", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:plugin-update"),
npmLane(
"bundled-channel-deps",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:bundled-channel-deps",
{ resources: ["service"], weight: 3 },
),
serviceLane(
"cron-mcp-cleanup",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:cron-mcp-cleanup",
{
resources: ["npm"],
weight: 3,
},
),
serviceLane(
"openai-web-search-minimal",
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:openai-web-search-minimal",
{ timeoutMs: 8 * 60 * 1000 },
),
],
};
export function releasePathChunkLanes(chunk, options = {}) {
const base = releasePathChunks[chunk];
if (!base) {
throw new Error(
`OPENCLAW_DOCKER_ALL_CHUNK must be one of: ${Object.keys(releasePathChunks).join(", ")}. Got: ${JSON.stringify(chunk)}`,
);
}
if (chunk !== "plugins-integrations" || !options.includeOpenWebUI) {
return base;
}
return [
...base,
serviceLane("openwebui", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:openwebui", {
timeoutMs: OPENWEBUI_TIMEOUT_MS,
weight: 5,
}),
];
}
export function allReleasePathLanes(options = {}) {
return Object.keys(releasePathChunks).flatMap((chunk) =>
releasePathChunkLanes(chunk, {
includeOpenWebUI: chunk === "plugins-integrations" && options.includeOpenWebUI,
}),
);
}