ci: use packaged tarball for docker e2e

This commit is contained in:
Peter Steinberger
2026-04-26 23:10:23 +01:00
parent 1b1eea238c
commit d108110a89
32 changed files with 432 additions and 202 deletions

View File

@@ -1,4 +1,8 @@
# syntax=docker/dockerfile:1.7
#
# Shared Docker E2E image.
# `bare` is a clean Node/Git runner for install/update lanes. `functional`
# installs the prepared OpenClaw npm tarball into /app for built-app lanes.
FROM node:24-bookworm-slim@sha256:e8e2e91b1378f83c5b2dd15f0247f34110e2fe895f6ca7719dbb780f929368eb AS e2e-runner
@@ -7,12 +11,14 @@ RUN apt-get update \
&& rm -rf /var/lib/apt/lists/*
RUN corepack enable
RUN npm install -g tsx@4.21.0 --no-fund --no-audit
RUN useradd --create-home --shell /bin/bash appuser \
&& mkdir -p /app \
&& chown appuser:appuser /app
ENV HOME="/home/appuser"
ENV PATH="/home/appuser/.local/bin:${PATH}"
ENV NODE_OPTIONS="--disable-warning=ExperimentalWarning"
# Docker E2E lanes start many loopback gateways concurrently; mDNS advertising
# is unrelated to those checks and can flap under container CPU/network load.
@@ -21,48 +27,23 @@ ENV OPENCLAW_DISABLE_BONJOUR="1"
USER appuser
WORKDIR /app
FROM e2e-runner AS deps
COPY --chown=appuser:appuser package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
COPY --chown=appuser:appuser ui/package.json ./ui/package.json
COPY --chown=appuser:appuser patches ./patches
COPY --chown=appuser:appuser scripts/postinstall-bundled-plugins.mjs scripts/preinstall-package-manager-warning.mjs scripts/npm-runner.mjs scripts/windows-cmd-helpers.mjs ./scripts/
RUN --mount=type=bind,source=extensions,target=/tmp/extensions,readonly \
find /tmp/extensions -mindepth 2 -maxdepth 2 -name package.json -print | \
while IFS= read -r manifest; do \
dest="${manifest#/tmp/}"; \
mkdir -p "$(dirname "$dest")"; \
cp "$manifest" "$dest"; \
done
RUN --mount=type=cache,id=openclaw-pnpm-store,target=/home/appuser/.local/share/pnpm/store,sharing=locked \
pnpm install --frozen-lockfile
FROM deps AS build
COPY --chown=appuser:appuser .oxlintrc.json tsconfig.json tsconfig.plugin-sdk.dts.json tsconfig.oxlint*.json tsdown.config.ts vitest.config.ts openclaw.mjs ./
COPY --chown=appuser:appuser src ./src
COPY --chown=appuser:appuser test ./test
COPY --chown=appuser:appuser scripts ./scripts
COPY --chown=appuser:appuser docs ./docs
COPY --chown=appuser:appuser packages ./packages
COPY --chown=appuser:appuser skills ./skills
COPY --chown=appuser:appuser ui ./ui
COPY --chown=appuser:appuser extensions ./extensions
COPY --chown=appuser:appuser vendor/a2ui/renderers/lit ./vendor/a2ui/renderers/lit
COPY --chown=appuser:appuser apps/shared/OpenClawKit/Sources/OpenClawKit/Resources ./apps/shared/OpenClawKit/Sources/OpenClawKit/Resources
COPY --chown=appuser:appuser apps/shared/OpenClawKit/Tools/CanvasA2UI ./apps/shared/OpenClawKit/Tools/CanvasA2UI
RUN pnpm build
# Onboard Docker E2E does not exercise the Control UI itself; it only needs the
# asset-existence check to pass so configure/onboard can continue.
RUN mkdir -p dist/control-ui \
&& printf '%s\n' '<!doctype html><title>OpenClaw Control UI</title>' > dist/control-ui/index.html
FROM e2e-runner AS bare
CMD ["bash"]
FROM build AS functional
RUN node scripts/stage-bundled-plugin-runtime-deps.mjs
FROM bare AS build
CMD ["bash"]
FROM bare AS functional
# The app under test enters through the named BuildKit context, not by copying
# checkout sources into the image.
COPY --from=openclaw_package --chown=appuser:appuser openclaw-current.tgz /tmp/openclaw-current.tgz
RUN npm install -g --prefix /tmp/openclaw-prefix /tmp/openclaw-current.tgz --no-fund --no-audit \
&& cp -a /tmp/openclaw-prefix/lib/node_modules/openclaw/. /app/ \
&& mkdir -p "$HOME/.local/bin" \
&& ln -sf /app/openclaw.mjs "$HOME/.local/bin/openclaw" \
&& rm -rf /tmp/openclaw-prefix /tmp/openclaw-current.tgz
CMD ["bash"]

View File

@@ -1,12 +1,16 @@
#!/usr/bin/env bash
# Runs bundled plugin runtime-dependency Docker scenarios from a mounted OpenClaw
# npm tarball. The default image is a clean runner; each scenario installs the
# tarball so package install behavior is what gets tested.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh"
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-bundled-channel-deps-e2e" OPENCLAW_BUNDLED_CHANNEL_DEPS_E2E_IMAGE)"
UPDATE_BASELINE_VERSION="${OPENCLAW_BUNDLED_CHANNEL_UPDATE_BASELINE_VERSION:-2026.4.20}"
DOCKER_TARGET="${OPENCLAW_BUNDLED_CHANNEL_DOCKER_TARGET:-e2e-runner}"
DOCKER_TARGET="${OPENCLAW_BUNDLED_CHANNEL_DOCKER_TARGET:-bare}"
HOST_BUILD="${OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD:-1}"
PACKAGE_TGZ="${OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ:-}"
RUN_CHANNEL_SCENARIOS="${OPENCLAW_BUNDLED_CHANNEL_SCENARIOS:-1}"
@@ -22,32 +26,14 @@ docker_e2e_build_or_reuse "$IMAGE_NAME" bundled-channel-deps "$ROOT_DIR/scripts/
prepare_package_tgz() {
if [ -n "$PACKAGE_TGZ" ]; then
if [ ! -f "$PACKAGE_TGZ" ]; then
echo "OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ does not exist: $PACKAGE_TGZ" >&2
exit 1
fi
PACKAGE_TGZ="$(cd "$(dirname "$PACKAGE_TGZ")" && pwd)/$(basename "$PACKAGE_TGZ")"
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz bundled-channel-deps "$PACKAGE_TGZ")"
return 0
fi
if [ "$HOST_BUILD" != "0" ]; then
echo "Building host package artifacts..."
run_logged bundled-channel-deps-host-build pnpm build
else
echo "Skipping host build (OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD=0)"
fi
echo "Writing package inventory and packing once..."
run_logged bundled-channel-deps-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-bundled-channel-pack.XXXXXX")"
run_logged bundled-channel-deps-pack npm pack --ignore-scripts --pack-destination "$pack_dir"
PACKAGE_TGZ="$(find "$pack_dir" -maxdepth 1 -name 'openclaw-*.tgz' -print -quit)"
if [ -z "$PACKAGE_TGZ" ]; then
echo "missing packed OpenClaw tarball" >&2
if [ "$HOST_BUILD" = "0" ] && [ -z "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}" ]; then
echo "OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD=0 requires OPENCLAW_CURRENT_PACKAGE_TGZ or OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ" >&2
exit 1
fi
PACKAGE_TGZ="$(cd "$(dirname "$PACKAGE_TGZ")" && pwd)/$(basename "$PACKAGE_TGZ")"
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz bundled-channel-deps)"
}
prepare_package_tgz

View File

@@ -1,11 +1,14 @@
// Crestodian first-run Docker harness.
// Imports packaged dist modules so the Docker lane verifies the npm tarball,
// while this small test driver stays mounted from the checkout.
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { runCli, shouldStartCrestodianForBareRoot } from "../../src/cli/run-main.js";
import { clearConfigCache } from "../../src/config/config.js";
import type { OpenClawConfig } from "../../src/config/types.openclaw.js";
import { runCrestodian } from "../../src/crestodian/crestodian.js";
import type { RuntimeEnv } from "../../src/runtime.js";
import { runCli, shouldStartCrestodianForBareRoot } from "../../dist/cli/run-main.js";
import { clearConfigCache } from "../../dist/config/config.js";
import type { OpenClawConfig } from "../../dist/config/types.openclaw.js";
import { runCrestodian } from "../../dist/crestodian/crestodian.js";
import type { RuntimeEnv } from "../../dist/runtime.js";
type CrestodianFirstRunCommand = {
id: string;

View File

@@ -1,4 +1,6 @@
#!/usr/bin/env bash
# Runs the Crestodian first-run Docker smoke against the package-installed
# functional E2E image, with only the test harness mounted from the checkout.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
@@ -16,11 +18,13 @@ trap cleanup EXIT
docker_e2e_build_or_reuse "$IMAGE_NAME" crestodian-first-run
echo "Running in-container Crestodian first-run smoke..."
# Harness files are mounted read-only; the app under test comes from /app/dist.
set +e
docker run --rm \
--name "$CONTAINER_NAME" \
-e "OPENCLAW_STATE_DIR=/tmp/openclaw-state" \
-e "OPENCLAW_CONFIG_PATH=/tmp/openclaw-state/openclaw.json" \
-v "$ROOT_DIR/scripts/e2e:/app/scripts/e2e:ro" \
"$IMAGE_NAME" \
bash -lc "set -euo pipefail
node --import tsx scripts/e2e/crestodian-first-run-docker-client.ts

View File

@@ -1,10 +1,13 @@
// Crestodian planner Docker harness.
// Imports packaged dist modules so the Docker lane verifies the npm tarball,
// while this small test driver stays mounted from the checkout.
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { clearConfigCache } from "../../src/config/config.js";
import type { OpenClawConfig } from "../../src/config/types.openclaw.js";
import { runCrestodian } from "../../src/crestodian/crestodian.js";
import type { RuntimeEnv } from "../../src/runtime.js";
import { clearConfigCache } from "../../dist/config/config.js";
import type { OpenClawConfig } from "../../dist/config/types.openclaw.js";
import { runCrestodian } from "../../dist/crestodian/crestodian.js";
import type { RuntimeEnv } from "../../dist/runtime.js";
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {

View File

@@ -1,4 +1,6 @@
#!/usr/bin/env bash
# Runs the Crestodian planner fallback Docker smoke against the package-installed
# functional E2E image, with only the test harness mounted from the checkout.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
@@ -16,11 +18,13 @@ trap cleanup EXIT
docker_e2e_build_or_reuse "$IMAGE_NAME" crestodian-planner
echo "Running in-container Crestodian planner fallback smoke..."
# Harness files are mounted read-only; the app under test comes from /app/dist.
set +e
docker run --rm \
--name "$CONTAINER_NAME" \
-e "OPENCLAW_STATE_DIR=/tmp/openclaw-state" \
-e "OPENCLAW_CONFIG_PATH=/tmp/openclaw-state/openclaw.json" \
-v "$ROOT_DIR/scripts/e2e:/app/scripts/e2e:ro" \
"$IMAGE_NAME" \
bash -lc "set -euo pipefail
node --import tsx scripts/e2e/crestodian-planner-docker-client.ts

View File

@@ -1,10 +1,13 @@
// Crestodian rescue-message Docker harness.
// Imports packaged dist modules so the Docker lane verifies the npm tarball,
// while this small test driver stays mounted from the checkout.
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { handleCrestodianCommand } from "../../src/auto-reply/reply/commands-crestodian.js";
import { clearConfigCache } from "../../src/config/config.js";
import type { OpenClawConfig } from "../../src/config/types.openclaw.js";
import { runCrestodianRescueMessage } from "../../src/crestodian/rescue-message.js";
import { handleCrestodianCommand } from "../../dist/auto-reply/reply/commands-crestodian.js";
import { clearConfigCache } from "../../dist/config/config.js";
import type { OpenClawConfig } from "../../dist/config/types.openclaw.js";
import { runCrestodianRescueMessage } from "../../dist/crestodian/rescue-message.js";
type CommandResult = Awaited<ReturnType<typeof handleCrestodianCommand>>;

View File

@@ -1,4 +1,6 @@
#!/usr/bin/env bash
# Runs the Crestodian rescue-message Docker smoke against the package-installed
# functional E2E image, with only the test harness mounted from the checkout.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
@@ -16,11 +18,13 @@ trap cleanup EXIT
docker_e2e_build_or_reuse "$IMAGE_NAME" crestodian-rescue
echo "Running in-container Crestodian rescue smoke..."
# Harness files are mounted read-only; the app under test comes from /app/dist.
set +e
docker run --rm \
--name "$CONTAINER_NAME" \
-e "OPENCLAW_STATE_DIR=/tmp/openclaw-state" \
-e "OPENCLAW_CONFIG_PATH=/tmp/openclaw-state/openclaw.json" \
-v "$ROOT_DIR/scripts/e2e:/app/scripts/e2e:ro" \
"$IMAGE_NAME" \
bash -lc "set -euo pipefail
node --import tsx scripts/e2e/crestodian-rescue-docker-client.ts

View File

@@ -1,4 +1,6 @@
#!/usr/bin/env bash
# Starts Gateway plus seeded cron/subagent MCP work in Docker, then verifies MCP
# child-process cleanup through a mounted test harness.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
@@ -18,6 +20,7 @@ trap cleanup EXIT
docker_e2e_build_or_reuse "$IMAGE_NAME" cron-mcp-cleanup
echo "Running in-container cron/subagent MCP cleanup smoke..."
# Harness files are mounted read-only; the app under test comes from /app/dist.
set +e
docker run --rm \
--name "$CONTAINER_NAME" \
@@ -33,6 +36,7 @@ docker run --rm \
-e "GW_URL=ws://127.0.0.1:$PORT" \
-e "GW_TOKEN=$TOKEN" \
-e "OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1" \
-v "$ROOT_DIR/scripts/e2e:/app/scripts/e2e:ro" \
"$IMAGE_NAME" \
bash -lc "set -euo pipefail
entry=dist/index.mjs

View File

@@ -1,8 +1,10 @@
// Shared Docker E2E OpenAI provider config seed helper.
// Uses packaged plugin-sdk runtime modules so seeded configs match the npm tarball.
import {
applyProviderConfigWithDefaultModelPreset,
type ModelDefinitionConfig,
type OpenClawConfig,
} from "../../src/plugin-sdk/provider-onboard.ts";
} from "../../dist/plugin-sdk/provider-onboard.js";
export type { OpenClawConfig };

View File

@@ -1,14 +1,24 @@
#!/usr/bin/env bash
# Verifies doctor/daemon repair switches service entrypoints between package and
# git installs. Both fixtures come from the same prepared OpenClaw npm tarball.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh"
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-doctor-install-switch-e2e" OPENCLAW_DOCTOR_INSTALL_SWITCH_E2E_IMAGE)"
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz doctor-switch "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")"
# Bare lanes mount the package artifact instead of baking app sources into the image.
docker_e2e_package_mount_args "$PACKAGE_TGZ"
docker_e2e_build_or_reuse "$IMAGE_NAME" doctor-switch
docker_e2e_build_or_reuse "$IMAGE_NAME" doctor-switch "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "bare"
echo "Running doctor install switch E2E..."
docker run --rm -e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 "$IMAGE_NAME" bash -lc '
docker run --rm \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
"$IMAGE_NAME" \
bash -lc '
set -euo pipefail
# Keep logs focused; the npm global install step can emit noisy deprecation warnings.
@@ -74,15 +84,23 @@ exit 0
LOGINCTL
chmod +x /tmp/openclaw-bin/loginctl
# Install the npm-global variant from the local /app source.
# `npm pack` can emit script output; keep only the tarball name.
pkg_tgz="$(npm pack --ignore-scripts --silent /app | tail -n 1 | tr -d '\r')"
if [ ! -f "/app/$pkg_tgz" ]; then
echo "npm pack failed (expected /app/$pkg_tgz)"
exit 1
fi
package_tgz="${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_TGZ}"
git_root="/tmp/openclaw-git"
mkdir -p "$git_root"
# The git-style install fixture is unpacked from the tarball so this lane does
# not depend on checkout source files being present in the Docker image.
tar -xzf "$package_tgz" -C "$git_root" --strip-components=1
(
cd "$git_root"
npm install --omit=optional --no-fund --no-audit >/tmp/openclaw-git-install.log 2>&1
git init -q
git config user.email "docker-e2e@openclaw.local"
git config user.name "OpenClaw Docker E2E"
git add -A
git commit -qm "test fixture"
)
npm_log="/tmp/openclaw-doctor-switch-npm-install.log"
if ! npm install -g --prefix /tmp/npm-prefix "/app/$pkg_tgz" >"$npm_log" 2>&1; then
if ! npm install -g --prefix /tmp/npm-prefix "$package_tgz" >"$npm_log" 2>&1; then
cat "$npm_log"
exit 1
fi
@@ -95,12 +113,12 @@ LOGINCTL
npm_entry="$npm_root/dist/index.js"
fi
if [ -f "/app/dist/index.mjs" ]; then
git_entry="/app/dist/index.mjs"
if [ -f "$git_root/dist/index.mjs" ]; then
git_entry="$git_root/dist/index.mjs"
else
git_entry="/app/dist/index.js"
git_entry="$git_root/dist/index.js"
fi
git_cli="/app/openclaw.mjs"
git_cli="$git_root/openclaw.mjs"
assert_entrypoint() {
local unit_path="$1"

View File

@@ -1,4 +1,6 @@
#!/usr/bin/env bash
# Runs a Docker Gateway plus MCP stdio bridge smoke with seeded conversations and
# raw Claude notification-frame assertions.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
@@ -18,6 +20,7 @@ trap cleanup EXIT
docker_e2e_build_or_reuse "$IMAGE_NAME" mcp-channels
echo "Running in-container gateway + MCP smoke..."
# Harness files are mounted read-only; the app under test comes from /app/dist.
set +e
docker run --rm \
--name "$CONTAINER_NAME" \
@@ -33,6 +36,7 @@ docker run --rm \
-e "GW_URL=ws://127.0.0.1:$PORT" \
-e "GW_TOKEN=$TOKEN" \
-e "OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1" \
-v "$ROOT_DIR/scripts/e2e:/app/scripts/e2e:ro" \
"$IMAGE_NAME" \
bash -lc "set -euo pipefail
entry=dist/index.mjs

View File

@@ -1,3 +1,6 @@
// Shared MCP-channel Docker E2E harness helpers.
// The mounted test harness imports packaged dist modules so bridge assertions run
// against the OpenClaw npm tarball installed in the functional image.
import { randomUUID } from "node:crypto";
import { mkdirSync, writeFileSync } from "node:fs";
import process from "node:process";
@@ -6,10 +9,10 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { WebSocket } from "ws";
import { z } from "zod";
import { PROTOCOL_VERSION } from "../../src/gateway/protocol/index.ts";
import { formatErrorMessage } from "../../src/infra/errors.ts";
import { rawDataToString } from "../../src/infra/ws.ts";
import { readStringValue } from "../../src/shared/string-coerce.ts";
import { PROTOCOL_VERSION } from "../../dist/gateway/protocol/index.js";
import { formatErrorMessage } from "../../dist/infra/errors.js";
import { rawDataToString } from "../../dist/infra/ws.js";
import { readStringValue } from "../../dist/shared/string-coerce.js";
export const ClaudeChannelNotificationSchema = z.object({
method: z.literal("notifications/claude/channel"),

View File

@@ -1,11 +1,14 @@
#!/usr/bin/env bash
# Installs a prepared OpenClaw npm tarball in Docker, runs non-interactive
# onboarding for a channel, and verifies one mocked model turn through Gateway.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh"
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-npm-onboard-channel-agent-e2e" OPENCLAW_NPM_ONBOARD_E2E_IMAGE)"
DOCKER_TARGET="${OPENCLAW_NPM_ONBOARD_DOCKER_TARGET:-e2e-runner}"
DOCKER_TARGET="${OPENCLAW_NPM_ONBOARD_DOCKER_TARGET:-bare}"
HOST_BUILD="${OPENCLAW_NPM_ONBOARD_HOST_BUILD:-1}"
PACKAGE_TGZ="${OPENCLAW_NPM_ONBOARD_PACKAGE_TGZ:-}"
CHANNEL="${OPENCLAW_NPM_ONBOARD_CHANNEL:-telegram}"
@@ -22,32 +25,14 @@ docker_e2e_build_or_reuse "$IMAGE_NAME" npm-onboard-channel-agent "$ROOT_DIR/scr
prepare_package_tgz() {
if [ -n "$PACKAGE_TGZ" ]; then
if [ ! -f "$PACKAGE_TGZ" ]; then
echo "OPENCLAW_NPM_ONBOARD_PACKAGE_TGZ does not exist: $PACKAGE_TGZ" >&2
exit 1
fi
PACKAGE_TGZ="$(cd "$(dirname "$PACKAGE_TGZ")" && pwd)/$(basename "$PACKAGE_TGZ")"
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz npm-onboard-channel-agent "$PACKAGE_TGZ")"
return 0
fi
if [ "$HOST_BUILD" != "0" ]; then
echo "Building host package artifacts..."
run_logged npm-onboard-channel-agent-host-build pnpm build
else
echo "Skipping host build (OPENCLAW_NPM_ONBOARD_HOST_BUILD=0)"
fi
echo "Writing package inventory and packing once..."
run_logged npm-onboard-channel-agent-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-npm-onboard-pack.XXXXXX")"
run_logged npm-onboard-channel-agent-pack npm pack --ignore-scripts --pack-destination "$pack_dir"
PACKAGE_TGZ="$(find "$pack_dir" -maxdepth 1 -name 'openclaw-*.tgz' -print -quit)"
if [ -z "$PACKAGE_TGZ" ]; then
echo "missing packed OpenClaw tarball" >&2
if [ "$HOST_BUILD" = "0" ] && [ -z "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}" ]; then
echo "OPENCLAW_NPM_ONBOARD_HOST_BUILD=0 requires OPENCLAW_CURRENT_PACKAGE_TGZ or OPENCLAW_NPM_ONBOARD_PACKAGE_TGZ" >&2
exit 1
fi
PACKAGE_TGZ="$(cd "$(dirname "$PACKAGE_TGZ")" && pwd)/$(basename "$PACKAGE_TGZ")"
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz npm-onboard-channel-agent)"
}
prepare_package_tgz

View File

@@ -1,4 +1,6 @@
#!/usr/bin/env bash
# Installs a published OpenClaw npm package in Docker, performs Telegram
# onboarding/doctor recovery, then runs the Telegram QA live harness.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
@@ -141,9 +143,12 @@ command -v openclaw
openclaw --version
EOF
# Mount only test harness/plugin QA sources; the SUT itself is the npm install.
run_logged docker run --rm \
"${docker_env[@]}" \
-v "$ROOT_DIR/.artifacts:/app/.artifacts" \
-v "$ROOT_DIR/scripts/e2e:/app/scripts/e2e:ro" \
-v "$ROOT_DIR/extensions:/app/extensions:ro" \
-v "$npm_prefix_host:/npm-global" \
-i "$IMAGE_NAME" bash -s <<'EOF'
set -euo pipefail
@@ -171,6 +176,10 @@ trap 'status=$?; dump_hotpath_logs "$status"; exit "$status"' ERR
command -v openclaw
openclaw --version
# The mounted QA harness imports openclaw/plugin-sdk; point that package import
# at the installed npm package without copying source into the test image.
mkdir -p /app/node_modules
ln -sfn /npm-global/lib/node_modules/openclaw /app/node_modules/openclaw
echo "Running installed npm onboarding recovery hot path..."
OPENAI_API_KEY="${OPENAI_API_KEY:-sk-openclaw-npm-telegram-hotpath}" openclaw onboard --non-interactive --accept-risk \

View File

@@ -1,10 +1,12 @@
#!/usr/bin/env -S node --import tsx
// Telegram npm-live Docker harness.
// Runs QA live transport code against the published package installed in Docker.
import fs from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { formatErrorMessage } from "../../dist/infra/errors.js";
import { runTelegramQaLive } from "../../extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts";
import { formatErrorMessage } from "../../src/infra/errors.ts";
function parseBoolean(value: string | undefined) {
const normalized = value?.trim().toLowerCase();

View File

@@ -1,4 +1,6 @@
#!/usr/bin/env bash
# Runs a mocked OpenAI image-generation auth smoke inside Docker against the
# package-installed functional E2E image.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
@@ -10,9 +12,11 @@ SKIP_BUILD="${OPENCLAW_OPENAI_IMAGE_AUTH_E2E_SKIP_BUILD:-0}"
docker_e2e_build_or_reuse "$IMAGE_NAME" openai-image-auth "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "" "$SKIP_BUILD"
echo "Running OpenAI image auth Docker E2E..."
# Harness files are mounted read-only; the app under test comes from /app/dist.
run_logged openai-image-auth docker run --rm \
-e "OPENAI_API_KEY=sk-openclaw-image-auth-e2e" \
-e "OPENCLAW_QA_ALLOW_LOCAL_IMAGE_PROVIDER=1" \
-v "$ROOT_DIR/scripts/e2e:/app/scripts/e2e:ro" \
-i "$IMAGE_NAME" bash -lc '
set -euo pipefail
export HOME="$(mktemp -d "/tmp/openclaw-openai-image-auth.XXXXXX")"

View File

@@ -1,4 +1,6 @@
#!/usr/bin/env bash
# Runs Open WebUI against a Dockerized OpenClaw Gateway and verifies the proxied
# chat path with a real OpenAI-compatible request.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
@@ -55,6 +57,7 @@ echo "Creating Docker network..."
docker_cmd docker network create "$NET_NAME" >/dev/null
echo "Starting gateway container..."
# Harness files are mounted read-only; the app under test comes from /app/dist.
docker_cmd docker run -d \
--name "$GW_NAME" \
--network "$NET_NAME" \
@@ -66,6 +69,7 @@ docker_cmd docker run -d \
-e "OPENCLAW_SKIP_CANVAS_HOST=1" \
-e OPENAI_API_KEY \
${OPENAI_BASE_URL_VALUE:+-e OPENAI_BASE_URL} \
-v "$ROOT_DIR/scripts/e2e:/app/scripts/e2e:ro" \
"$IMAGE_NAME" \
bash -lc '
set -euo pipefail

View File

@@ -1,16 +1,19 @@
// Pi bundle MCP tools Docker harness.
// Imports packaged dist modules so tool materialization is verified against the
// npm tarball installed in the functional image.
import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import { createRequire } from "node:module";
import os from "node:os";
import path from "node:path";
import { materializeBundleMcpToolsForRun } from "../../src/agents/pi-bundle-mcp-materialize.ts";
import { materializeBundleMcpToolsForRun } from "../../dist/agents/pi-bundle-mcp-materialize.js";
import {
disposeAllSessionMcpRuntimes,
getOrCreateSessionMcpRuntime,
} from "../../src/agents/pi-bundle-mcp-runtime.ts";
import { applyFinalEffectiveToolPolicy } from "../../src/agents/pi-embedded-runner/effective-tool-policy.ts";
import type { OpenClawConfig } from "../../src/config/types.openclaw.ts";
import { getPluginToolMeta } from "../../src/plugins/tools.ts";
} from "../../dist/agents/pi-bundle-mcp-runtime.js";
import { applyFinalEffectiveToolPolicy } from "../../dist/agents/pi-embedded-runner/effective-tool-policy.js";
import type { OpenClawConfig } from "../../dist/config/types.openclaw.js";
import { getPluginToolMeta } from "../../dist/plugins/tools.js";
const require = createRequire(import.meta.url);

View File

@@ -1,4 +1,6 @@
#!/usr/bin/env bash
# Verifies embedded Pi bundle MCP tool materialization and tool-policy behavior
# inside the package-installed functional E2E image.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
@@ -16,10 +18,12 @@ trap cleanup EXIT
docker_e2e_build_or_reuse "$IMAGE_NAME" pi-bundle-mcp-tools
echo "Running in-container Pi bundle MCP tool availability smoke..."
# Harness files are mounted read-only; the app under test comes from /app/dist.
set +e
docker run --rm \
--name "$CONTAINER_NAME" \
-e "OPENCLAW_STATE_DIR=/tmp/openclaw-state" \
-v "$ROOT_DIR/scripts/e2e:/app/scripts/e2e:ro" \
"$IMAGE_NAME" \
bash -lc "set -euo pipefail
node --import tsx scripts/e2e/pi-bundle-mcp-tools-docker-client.ts

View File

@@ -1,24 +1,34 @@
#!/usr/bin/env bash
# Verifies `openclaw plugins update` is a no-op for an already-current plugin.
# The CLI under test is installed from the prepared npm tarball in a bare runner.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh"
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-plugin-update-e2e" OPENCLAW_PLUGIN_UPDATE_E2E_IMAGE)"
SKIP_BUILD="${OPENCLAW_PLUGIN_UPDATE_E2E_SKIP_BUILD:-0}"
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz plugin-update "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")"
# Bare lanes mount the package artifact instead of baking app sources into the image.
docker_e2e_package_mount_args "$PACKAGE_TGZ"
docker_e2e_build_or_reuse "$IMAGE_NAME" plugin-update "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "" "$SKIP_BUILD"
docker_e2e_build_or_reuse "$IMAGE_NAME" plugin-update "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "bare" "$SKIP_BUILD"
echo "Running unchanged plugin update smoke..."
docker run --rm \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e OPENCLAW_SKIP_CHANNELS=1 \
-e OPENCLAW_SKIP_PROVIDERS=1 \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
"$IMAGE_NAME" \
bash -lc "set -euo pipefail
entry=dist/index.mjs
[ -f \"\$entry\" ] || entry=dist/index.js
package_tgz=\"\${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_TGZ}\"
npm install -g --prefix /tmp/npm-prefix \"\$package_tgz\" --no-fund --no-audit >/tmp/openclaw-install.log 2>&1
entry=\"/tmp/npm-prefix/lib/node_modules/openclaw/dist/index.mjs\"
[ -f \"\$entry\" ] || entry=/tmp/npm-prefix/lib/node_modules/openclaw/dist/index.js
export NPM_CONFIG_REGISTRY=http://127.0.0.1:4873
export PATH=\"/tmp/npm-prefix/bin:\$PATH\"
mkdir -p \"\$HOME/.openclaw/extensions/lossless-claw\"
cat > \"\$HOME/.openclaw/extensions/lossless-claw/package.json\" <<'JSON'

View File

@@ -1,3 +1,6 @@
// Session runtime-context Docker harness.
// Imports packaged dist modules so transcript behavior is verified against the
// npm tarball installed in the functional image.
import { spawnSync } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
@@ -6,7 +9,7 @@ import { SessionManager } from "@mariozechner/pi-coding-agent";
import {
queueRuntimeContextForNextTurn,
resolveRuntimeContextPromptParts,
} from "../../src/agents/pi-embedded-runner/run/runtime-context-prompt.js";
} from "../../dist/agents/pi-embedded-runner/run/runtime-context-prompt.js";
type TranscriptEntry = {
type?: string;

View File

@@ -1,4 +1,6 @@
#!/usr/bin/env bash
# Verifies hidden runtime context transcript persistence in Docker using the
# package-installed functional E2E image.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
@@ -17,10 +19,12 @@ trap cleanup EXIT
docker_e2e_build_or_reuse "$IMAGE_NAME" session-runtime-context
echo "Running session runtime context Docker E2E..."
# Harness files are mounted read-only; the app under test comes from /app/dist.
set +e
docker run --rm \
--name "$CONTAINER_NAME" \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-v "$ROOT_DIR/scripts/e2e:/app/scripts/e2e:ro" \
"$IMAGE_NAME" \
bash -lc 'set -euo pipefail; node --import tsx scripts/e2e/session-runtime-context-docker-client.ts' \
>"$RUN_LOG" 2>&1

View File

@@ -1,19 +1,26 @@
#!/usr/bin/env bash
# Exercises package-to-git and git-to-package update channel switching in Docker.
# Both package and git fixtures are derived from the same prepared npm tarball.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh"
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-update-channel-switch-e2e" OPENCLAW_UPDATE_CHANNEL_SWITCH_E2E_IMAGE)"
SKIP_BUILD="${OPENCLAW_UPDATE_CHANNEL_SWITCH_E2E_SKIP_BUILD:-0}"
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz update-channel-switch "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")"
# Bare lanes mount the package artifact instead of baking app sources into the image.
docker_e2e_package_mount_args "$PACKAGE_TGZ"
docker_e2e_build_or_reuse "$IMAGE_NAME" update-channel-switch "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "" "$SKIP_BUILD"
docker_e2e_build_or_reuse "$IMAGE_NAME" update-channel-switch "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "bare" "$SKIP_BUILD"
echo "Running update channel switch E2E..."
docker run --rm \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e OPENCLAW_SKIP_CHANNELS=1 \
-e OPENCLAW_SKIP_PROVIDERS=1 \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
"$IMAGE_NAME" \
bash -lc 'set -euo pipefail
@@ -29,32 +36,26 @@ export OPENCLAW_DISABLE_BUNDLED_PLUGINS=1
export OPENCLAW_NO_ONBOARD=1
export OPENCLAW_NO_PROMPT=1
cat > /app/.gitignore <<'"'"'GITIGNORE'"'"'
node_modules
**/node_modules/
dist
dist-runtime
.turbo
coverage
GITIGNORE
node --import tsx scripts/write-package-dist-inventory.ts
package_tgz="${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_TGZ}"
git_root="/tmp/openclaw-git"
mkdir -p "$git_root"
# Build the fake git install from the packed package contents, not the checkout.
tar -xzf "$package_tgz" -C "$git_root" --strip-components=1
(
cd "$git_root"
npm install --omit=optional --no-fund --no-audit >/tmp/openclaw-git-install.log 2>&1
)
git config --global user.email "docker-e2e@openclaw.local"
git config --global user.name "OpenClaw Docker E2E"
git config --global gc.auto 0
git -C /app init -q
git -C /app config gc.auto 0
git -C /app add -A
git -C /app commit -qm "test fixture"
fixture_sha="$(git -C /app rev-parse HEAD)"
git -C "$git_root" init -q
git -C "$git_root" config gc.auto 0
git -C "$git_root" add -A
git -C "$git_root" commit -qm "test fixture"
fixture_sha="$(git -C "$git_root" rev-parse HEAD)"
pkg_tgz="$(npm pack --ignore-scripts --silent --pack-destination /tmp /app | tail -n 1 | tr -d "\r")"
pkg_tgz_path="/tmp/$pkg_tgz"
if [ ! -f "$pkg_tgz_path" ]; then
echo "npm pack failed (expected $pkg_tgz_path)"
exit 1
fi
pkg_tgz_path="$package_tgz"
npm install -g --prefix /tmp/npm-prefix --omit=optional "$pkg_tgz_path"
@@ -70,7 +71,7 @@ cat > "$HOME/.openclaw/openclaw.json" <<'"'"'JSON'"'"'
}
JSON
export OPENCLAW_GIT_DIR=/app
export OPENCLAW_GIT_DIR="$git_root"
export OPENCLAW_UPDATE_DEV_TARGET_REF="$fixture_sha"
echo "==> package -> git dev channel"

View File

@@ -1,10 +1,15 @@
#!/usr/bin/env bash
#
# Shared Docker E2E image resolver/builder.
# Suite-specific scripts call this to resolve overrides, reuse pulled images, or
# build the runner/functional images with the prepared OpenClaw package tarball.
DOCKER_E2E_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="${ROOT_DIR:-$(cd "$DOCKER_E2E_LIB_DIR/../.." && pwd)}"
source "$DOCKER_E2E_LIB_DIR/docker-e2e-logs.sh"
source "$DOCKER_E2E_LIB_DIR/docker-build.sh"
source "$DOCKER_E2E_LIB_DIR/docker-e2e-package.sh"
docker_e2e_resolve_image() {
local default_image="$1"
@@ -34,6 +39,11 @@ docker_e2e_build_or_reuse() {
local context="${4:-$ROOT_DIR}"
local target="${5:-}"
local skip_build="${6:-0}"
if [ -z "$target" ] && [ "$dockerfile" = "$ROOT_DIR/scripts/e2e/Dockerfile" ]; then
# The generic E2E image defaults to the package-installed app image; tests
# that need a clean install runner pass target=bare explicitly.
target="functional"
fi
if [ "${OPENCLAW_SKIP_DOCKER_BUILD:-0}" = "1" ] || [ "$skip_build" = "1" ]; then
echo "Reusing Docker image: $image_name"
@@ -53,6 +63,15 @@ docker_e2e_build_or_reuse() {
if [ -n "$target" ]; then
build_args+=(--target "$target")
fi
if [ "$target" = "functional" ]; then
local package_tgz
local package_context
package_tgz="$(docker_e2e_prepare_package_tgz "$label")"
package_context="$(docker_e2e_prepare_package_context "$package_tgz")"
# The Dockerfile never sees repo sources as app input; functional installs
# exactly this tarball through a named BuildKit context.
build_args+=(--build-context "openclaw_package=$package_context")
fi
build_args+=(-t "$image_name" -f "$dockerfile" "$context")
docker_build_run "$label-build" "${build_args[@]}"
}

View File

@@ -0,0 +1,63 @@
#!/usr/bin/env bash
#
# Shared package helpers for Docker E2E scripts.
# Builds or resolves one OpenClaw npm tarball and exposes mount/build-context
# helpers so Docker lanes test the package artifact instead of repo sources.
DOCKER_E2E_PACKAGE_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="${ROOT_DIR:-$(cd "$DOCKER_E2E_PACKAGE_LIB_DIR/../.." && pwd)}"
if ! declare -F run_logged >/dev/null 2>&1; then
source "$DOCKER_E2E_PACKAGE_LIB_DIR/docker-e2e-logs.sh"
fi
docker_e2e_abs_path() {
local file="$1"
(cd "$(dirname "$file")" && printf '%s/%s\n' "$(pwd)" "$(basename "$file")")
}
docker_e2e_prepare_package_tgz() {
local label="$1"
local package_tgz="${2:-${OPENCLAW_CURRENT_PACKAGE_TGZ:-}}"
if [ -n "$package_tgz" ]; then
if [ ! -f "$package_tgz" ]; then
echo "OpenClaw package tarball does not exist: $package_tgz" >&2
return 1
fi
docker_e2e_abs_path "$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)"
if [ -z "$package_tgz" ]; then
echo "missing packed OpenClaw tarball" >&2
return 1
fi
docker_e2e_abs_path "$package_tgz"
}
docker_e2e_prepare_package_context() {
local package_tgz="$1"
local context_dir
context_dir="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-docker-e2e-package-context.XXXXXX")"
# BuildKit named contexts must be directories, so expose the tarball as a
# stable filename inside a tiny temporary context.
cp "$package_tgz" "$context_dir/openclaw-current.tgz"
printf '%s\n' "$context_dir"
}
docker_e2e_package_mount_args() {
local package_tgz="$1"
local target="${2:-/tmp/openclaw-current.tgz}"
DOCKER_E2E_PACKAGE_ARGS=(-v "$package_tgz:$target:ro" -e "OPENCLAW_CURRENT_PACKAGE_TGZ=$target")
}

View File

@@ -1,3 +1,6 @@
// Docker E2E aggregate scheduler.
// Builds shared Docker images, prepares one OpenClaw npm tarball, assigns lanes
// to bare/functional images, and runs lanes through weighted resource pools.
import { spawn } from "node:child_process";
import fs from "node:fs";
import { mkdir, readFile } from "node:fs/promises";
@@ -661,8 +664,12 @@ function buildLaneRerunCommand(name, baseEnv) {
["OPENCLAW_DOCKER_E2E_IMAGE", image || DEFAULT_E2E_IMAGE],
["OPENCLAW_DOCKER_E2E_BARE_IMAGE", baseEnv.OPENCLAW_DOCKER_E2E_BARE_IMAGE],
["OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE", baseEnv.OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE],
["OPENCLAW_CURRENT_PACKAGE_TGZ", baseEnv.OPENCLAW_CURRENT_PACKAGE_TGZ],
];
return `${env.map(([key, value]) => `${key}=${shellQuote(value)}`).join(" ")} pnpm test:docker:all`;
return `${env
.filter(([, value]) => value !== undefined && value !== "")
.map(([key, value]) => `${key}=${shellQuote(value)}`)
.join(" ")} pnpm test:docker:all`;
}
function findLaneByName(name) {
@@ -805,11 +812,8 @@ function printLaneManifest(label, poolLanes, timingStore) {
}
}
function lanesNeedBundledPackage(poolLanes) {
return poolLanes.some(
(poolLane) =>
poolLane.name === "npm-onboard-channel-agent" || poolLane.name.startsWith("bundled-channel"),
);
function lanesNeedOpenClawPackage(poolLanes) {
return poolLanes.some((poolLane) => poolLane.e2eImageKind);
}
function dockerPreflightContainerNames(raw) {
@@ -1011,30 +1015,33 @@ async function runDockerPreflight(baseEnv, options) {
console.log(`==> Docker preflight run: ${elapsedSeconds}s`);
}
async function prepareBundledChannelPackage(baseEnv, logDir) {
if (baseEnv.OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ) {
console.log(`==> Bundled channel package: ${baseEnv.OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ}`);
async function prepareOpenClawPackage(baseEnv, logDir) {
const existing =
baseEnv.OPENCLAW_CURRENT_PACKAGE_TGZ ||
baseEnv.OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ ||
baseEnv.OPENCLAW_NPM_ONBOARD_PACKAGE_TGZ;
if (existing) {
const packageTgz = path.resolve(existing);
baseEnv.OPENCLAW_CURRENT_PACKAGE_TGZ = packageTgz;
baseEnv.OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ ||= packageTgz;
baseEnv.OPENCLAW_NPM_ONBOARD_PACKAGE_TGZ ||= packageTgz;
baseEnv.OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD = "0";
baseEnv.OPENCLAW_NPM_ONBOARD_HOST_BUILD = "0";
console.log(`==> OpenClaw package: ${packageTgz}`);
return;
}
const packDir = path.join(logDir, "bundled-channel-package");
const packDir = path.join(logDir, "openclaw-package");
await mkdir(packDir, { recursive: true });
const packScript = [
"set -euo pipefail",
"node --import tsx --input-type=module -e \"const { writePackageDistInventory } = await import('./src/infra/package-dist-inventory.ts'); await writePackageDistInventory(process.cwd());\"",
"npm pack --silent --ignore-scripts --pack-destination /tmp/openclaw-pack >/tmp/openclaw-pack.out",
"cat /tmp/openclaw-pack.out",
].join("\n");
await runForeground("Build OpenClaw package artifacts once", "pnpm build", baseEnv);
await runForeground(
"Pack bundled channel package once from bare Docker E2E image",
[
"docker run --rm",
"-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0",
`-v ${shellQuote(packDir)}:/tmp/openclaw-pack`,
shellQuote(baseEnv.OPENCLAW_DOCKER_E2E_BARE_IMAGE),
"bash -lc",
shellQuote(packScript),
].join(" "),
"Write OpenClaw package inventory",
"node --import tsx --input-type=module -e \"const { writePackageDistInventory } = await import('./src/infra/package-dist-inventory.ts'); await writePackageDistInventory(process.cwd());\"",
baseEnv,
);
await runForeground(
"Pack OpenClaw package once",
`npm pack --silent --ignore-scripts --pack-destination ${shellQuote(packDir)}`,
baseEnv,
);
@@ -1045,11 +1052,12 @@ async function prepareBundledChannelPackage(baseEnv, logDir) {
if (!packed) {
throw new Error(`missing packed OpenClaw tarball in ${packDir}`);
}
baseEnv.OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ = path.join(packDir, packed);
baseEnv.OPENCLAW_CURRENT_PACKAGE_TGZ = path.join(packDir, packed);
baseEnv.OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ = baseEnv.OPENCLAW_CURRENT_PACKAGE_TGZ;
baseEnv.OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD = "0";
baseEnv.OPENCLAW_NPM_ONBOARD_PACKAGE_TGZ = baseEnv.OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ;
baseEnv.OPENCLAW_NPM_ONBOARD_PACKAGE_TGZ = baseEnv.OPENCLAW_CURRENT_PACKAGE_TGZ;
baseEnv.OPENCLAW_NPM_ONBOARD_HOST_BUILD = "0";
console.log(`==> Bundled channel package: ${baseEnv.OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ}`);
console.log(`==> OpenClaw package: ${baseEnv.OPENCLAW_CURRENT_PACKAGE_TGZ}`);
}
function laneEnv(poolLane, baseEnv, logDir, cacheKey) {
@@ -1530,10 +1538,17 @@ async function main() {
});
},
);
const scheduledLanes = [...orderedLanes, ...orderedTailLanes];
if (lanesNeedOpenClawPackage(scheduledLanes)) {
await runPhase(phases, "prepare-openclaw-package", {}, async () => {
await prepareOpenClawPackage(baseEnv, logDir);
});
} else {
console.log("==> OpenClaw package: not needed for selected lanes");
}
if (buildEnabled) {
const buildEntries = [];
const scheduledLanes = [...orderedLanes, ...orderedTailLanes];
if (scheduledLanes.some((poolLane) => poolLane.live)) {
buildEntries.push({
command: "pnpm test:docker:live-build",
@@ -1547,7 +1562,7 @@ async function main() {
command: "pnpm test:docker:e2e-build",
env: {
OPENCLAW_DOCKER_E2E_IMAGE: baseEnv.OPENCLAW_DOCKER_E2E_BARE_IMAGE,
OPENCLAW_DOCKER_E2E_TARGET: "build",
OPENCLAW_DOCKER_E2E_TARGET: "bare",
},
label: `shared bare Docker E2E image once: ${baseEnv.OPENCLAW_DOCKER_E2E_BARE_IMAGE}`,
phaseDetails: { image: baseEnv.OPENCLAW_DOCKER_E2E_BARE_IMAGE, imageKind: "bare" },
@@ -1573,13 +1588,6 @@ async function main() {
} else {
console.log(`==> Shared Docker image builds: skipped`);
}
if (lanesNeedBundledPackage([...orderedLanes, ...orderedTailLanes])) {
await runPhase(phases, "prepare-bundled-channel-package", { imageKind: "bare" }, async () => {
await prepareBundledChannelPackage(baseEnv, logDir);
});
} else {
console.log("==> Bundled channel package: not needed for selected lanes");
}
const options = {
...schedulerOptions,