From 112f6e16222ac896a8cb016866b96be1ab79a3dd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 23 Apr 2026 05:55:10 +0100 Subject: [PATCH] test: reuse prebuilt docker e2e image --- docs/ci.md | 2 +- docs/help/testing.md | 11 +++- package.json | 3 +- scripts/e2e/build-image.sh | 10 ++++ .../bundled-channel-runtime-deps-docker.sh | 11 ++-- scripts/e2e/config-reload-source-docker.sh | 11 ++-- scripts/e2e/cron-mcp-cleanup-docker.sh | 7 ++- scripts/e2e/doctor-install-switch-docker.sh | 7 ++- scripts/e2e/gateway-network-docker.sh | 11 ++-- scripts/e2e/mcp-channels-docker.sh | 7 ++- .../e2e/npm-onboard-channel-agent-docker.sh | 11 ++-- scripts/e2e/onboard-docker.sh | 7 ++- scripts/e2e/openwebui-docker.sh | 7 ++- scripts/e2e/pi-bundle-mcp-tools-docker.sh | 9 ++-- scripts/e2e/plugin-update-unchanged-docker.sh | 11 ++-- scripts/e2e/plugins-docker.sh | 7 ++- scripts/lib/docker-e2e-image.sh | 54 +++++++++++++++++++ scripts/test-docker-all.sh | 25 +++++++++ 18 files changed, 138 insertions(+), 73 deletions(-) create mode 100644 scripts/e2e/build-image.sh create mode 100644 scripts/lib/docker-e2e-image.sh create mode 100644 scripts/test-docker-all.sh diff --git a/docs/ci.md b/docs/ci.md index 3b76b4dc1c0..950754b00fd 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -47,7 +47,7 @@ Jobs are ordered so cheap checks fail before expensive ones run: Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests in `src/scripts/ci-changed-scope.test.ts`. CI workflow edits validate the Node CI graph plus workflow linting, but do not force Windows, Android, or macOS native builds by themselves; those platform lanes stay scoped to platform source changes. Windows Node checks are scoped to Windows-specific process/path wrappers, npm/pnpm/UI runner helpers, package manager config, and the CI workflow surfaces that execute that lane; unrelated source, plugin, install-smoke, and test-only changes stay on the Linux Node lanes so they do not reserve a 16-vCPU Windows worker for coverage that is already exercised by the normal test shards. -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 runs for install, packaging, container-relevant changes, bundled extension production changes, and the core plugin/channel/gateway/Plugin SDK surfaces that the Docker smoke jobs exercise. Test-only and docs-only edits do not reserve Docker workers. Its QR package smoke forces the Docker `pnpm install` layer to rerun while preserving the BuildKit pnpm store cache, so it still exercises installation without redownloading dependencies on every run. Its gateway-network e2e reuses the runtime image built earlier in the job, so it adds real container-to-container WebSocket coverage without adding another Docker build. A separate `docker-e2e-fast` job runs the bounded bundled-plugin Docker profile under a 120-second command timeout: setup-entry dependency repair plus synthetic bundled-loader failure isolation. The full bundled update/channel matrix remains manual/full-suite because it performs repeated real npm update and doctor repair passes. +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 runs for install, packaging, container-relevant changes, bundled extension production changes, and the core plugin/channel/gateway/Plugin SDK surfaces that the Docker smoke jobs exercise. Test-only and docs-only edits do not reserve Docker workers. Its QR package smoke forces the Docker `pnpm install` layer to rerun while preserving the BuildKit pnpm store cache, so it still exercises installation without redownloading dependencies on every run. Its gateway-network e2e reuses the runtime image built earlier in the job, so it adds real container-to-container WebSocket coverage without adding another Docker build. Local `test:docker:all` similarly prebuilds one shared `scripts/e2e/Dockerfile` built-app image and reuses it across the E2E container smoke runners. A separate `docker-e2e-fast` job runs the bounded bundled-plugin Docker profile under a 120-second command timeout: setup-entry dependency repair plus synthetic bundled-loader failure isolation. The full bundled update/channel matrix remains manual/full-suite because it performs repeated real npm update and doctor repair passes. Local changed-lane logic lives in `scripts/changed-lanes.mjs` and is executed by `scripts/check-changed.mjs`. That local gate is stricter about architecture boundaries than the broad CI platform scope: core production changes run core prod typecheck plus core tests, core test-only changes run only core test typecheck/tests, extension production changes run extension prod typecheck plus extension tests, and extension test-only changes run only extension test typecheck/tests. Public Plugin SDK or plugin-contract changes expand to extension validation because extensions depend on those core contracts. Release metadata-only version bumps run targeted version/config/root-dependency checks. Unknown root/config changes fail safe to all lanes. diff --git a/docs/help/testing.md b/docs/help/testing.md index b037016cebe..f4687bf38af 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -906,7 +906,7 @@ These Docker runners split into two buckets: `OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=45000`, and `OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=90000`. Override those env vars when you explicitly want the larger exhaustive scan. -- `test:docker:all` builds the live Docker image once via `test:docker:live-build`, then reuses it for the two live Docker lanes. +- `test:docker:all` builds the live Docker image once via `test:docker:live-build`, then reuses it for the two live Docker lanes. It also builds one shared `scripts/e2e/Dockerfile` image via `test:docker:e2e-build` and reuses it for the E2E container smoke runners that exercise the built app. - Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:gateway-network`, `test:docker:mcp-channels`, `test:docker:pi-bundle-mcp-tools`, and `test:docker:plugins` boot one or more real containers and verify higher-level integration paths. The live-model Docker runners also bind-mount only the needed CLI auth homes (or all supported ones when the run is not narrowed), then copy them into the container home before the run so external-CLI OAuth can refresh tokens without mutating the host auth store: @@ -928,6 +928,15 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or - Narrow bundled plugin runtime deps while iterating by disabling unrelated scenarios, for example: `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 pnpm test:docker:bundled-channel-deps`. +To prebuild and reuse the shared built-app image manually: + +```bash +OPENCLAW_DOCKER_E2E_IMAGE=openclaw-docker-e2e:local pnpm test:docker:e2e-build +OPENCLAW_DOCKER_E2E_IMAGE=openclaw-docker-e2e:local OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:mcp-channels +``` + +Suite-specific image overrides such as `OPENCLAW_GATEWAY_NETWORK_E2E_IMAGE` still win when set. The QR and installer Docker tests keep their own Dockerfiles because they validate package/install behavior rather than the shared built-app runtime. + The live-model Docker runners also bind-mount the current checkout read-only and stage it into a temporary workdir inside the container. This keeps the runtime image slim while still running Vitest against your exact local source/config. diff --git a/package.json b/package.json index 6b41f850b30..881e801e77e 100644 --- a/package.json +++ b/package.json @@ -1416,12 +1416,13 @@ "test:contracts:plugins": "node scripts/run-vitest.mjs run --config test/vitest/vitest.contracts-plugin.config.ts --maxWorkers=1", "test:coverage": "node scripts/run-vitest.mjs run --config test/vitest/vitest.unit.config.ts --coverage", "test:coverage:changed": "node scripts/run-vitest.mjs run --config test/vitest/vitest.unit.config.ts --coverage --changed origin/main", - "test:docker:all": "pnpm test:docker:live-build && OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-models && OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-gateway && pnpm test:docker:openwebui && pnpm test:docker:onboard && pnpm test:docker:npm-onboard-channel-agent && pnpm test:docker:gateway-network && pnpm test:docker:mcp-channels && pnpm test:docker:pi-bundle-mcp-tools && pnpm test:docker:cron-mcp-cleanup && pnpm test:docker:qr && pnpm test:docker:doctor-switch && pnpm test:docker:plugins && pnpm test:docker:bundled-channel-deps && pnpm test:docker:cleanup", + "test:docker:all": "bash scripts/test-docker-all.sh", "test:docker:bundled-channel-deps": "bash scripts/e2e/bundled-channel-runtime-deps-docker.sh", "test:docker:bundled-channel-deps:fast": "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=1 bash scripts/e2e/bundled-channel-runtime-deps-docker.sh", "test:docker:cleanup": "bash scripts/test-cleanup-docker.sh", "test:docker:cron-mcp-cleanup": "bash scripts/e2e/cron-mcp-cleanup-docker.sh", "test:docker:doctor-switch": "bash scripts/e2e/doctor-install-switch-docker.sh", + "test:docker:e2e-build": "bash scripts/e2e/build-image.sh", "test:docker:gateway-network": "bash scripts/e2e/gateway-network-docker.sh", "test:docker:live-acp-bind": "bash scripts/test-live-acp-bind-docker.sh", "test:docker:live-acp-bind:claude": "OPENCLAW_LIVE_ACP_BIND_AGENT=claude bash scripts/test-live-acp-bind-docker.sh", diff --git a/scripts/e2e/build-image.sh b/scripts/e2e/build-image.sh new file mode 100644 index 00000000000..a2968ff27a6 --- /dev/null +++ b/scripts/e2e/build-image.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" + +IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-docker-e2e:local")" +DOCKER_TARGET="${OPENCLAW_DOCKER_E2E_TARGET:-build}" + +docker_e2e_build_or_reuse "$IMAGE_NAME" docker-e2e "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "$DOCKER_TARGET" diff --git a/scripts/e2e/bundled-channel-runtime-deps-docker.sh b/scripts/e2e/bundled-channel-runtime-deps-docker.sh index cb41f547ee7..0b979f0c214 100644 --- a/scripts/e2e/bundled-channel-runtime-deps-docker.sh +++ b/scripts/e2e/bundled-channel-runtime-deps-docker.sh @@ -2,9 +2,9 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh" +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -IMAGE_NAME="${OPENCLAW_BUNDLED_CHANNEL_DEPS_E2E_IMAGE:-openclaw-bundled-channel-deps-e2e}" +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}" HOST_BUILD="${OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD:-1}" @@ -15,12 +15,7 @@ RUN_ROOT_OWNED_SCENARIO="${OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO:-1}" RUN_SETUP_ENTRY_SCENARIO="${OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO:-1}" RUN_LOAD_FAILURE_SCENARIO="${OPENCLAW_BUNDLED_CHANNEL_LOAD_FAILURE_SCENARIO:-1}" -if [ "${OPENCLAW_SKIP_DOCKER_BUILD:-0}" = "1" ]; then - echo "Reusing Docker image: $IMAGE_NAME (OPENCLAW_SKIP_DOCKER_BUILD=1)" -else - echo "Building Docker image target $DOCKER_TARGET..." - run_logged bundled-channel-deps-build docker build --target "$DOCKER_TARGET" -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" -fi +docker_e2e_build_or_reuse "$IMAGE_NAME" bundled-channel-deps "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "$DOCKER_TARGET" prepare_package_tgz() { if [ -n "$PACKAGE_TGZ" ]; then diff --git a/scripts/e2e/config-reload-source-docker.sh b/scripts/e2e/config-reload-source-docker.sh index 3260848f30e..9351b175569 100755 --- a/scripts/e2e/config-reload-source-docker.sh +++ b/scripts/e2e/config-reload-source-docker.sh @@ -2,9 +2,9 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh" +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -IMAGE_NAME="${OPENCLAW_CONFIG_RELOAD_E2E_IMAGE:-openclaw-config-reload-e2e}" +IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-config-reload-e2e" OPENCLAW_CONFIG_RELOAD_E2E_IMAGE)" SKIP_BUILD="${OPENCLAW_CONFIG_RELOAD_E2E_SKIP_BUILD:-0}" PORT="18789" TOKEN="reload-e2e-token" @@ -15,12 +15,7 @@ cleanup() { } trap cleanup EXIT -if [ "$SKIP_BUILD" = "1" ]; then - echo "Reusing Docker image: $IMAGE_NAME" -else - echo "Building Docker image..." - run_logged config-reload-build docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" -fi +docker_e2e_build_or_reuse "$IMAGE_NAME" config-reload "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "" "$SKIP_BUILD" echo "Starting gateway container..." docker run -d \ diff --git a/scripts/e2e/cron-mcp-cleanup-docker.sh b/scripts/e2e/cron-mcp-cleanup-docker.sh index d3924471751..4051237bebd 100644 --- a/scripts/e2e/cron-mcp-cleanup-docker.sh +++ b/scripts/e2e/cron-mcp-cleanup-docker.sh @@ -2,8 +2,8 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh" -IMAGE_NAME="${OPENCLAW_IMAGE:-openclaw-cron-mcp-cleanup-e2e}" +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" +IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-cron-mcp-cleanup-e2e" OPENCLAW_IMAGE)" PORT="18789" TOKEN="cron-mcp-e2e-$(date +%s)-$$" CONTAINER_NAME="openclaw-cron-mcp-e2e-$$" @@ -15,8 +15,7 @@ cleanup() { } trap cleanup EXIT -echo "Building Docker image..." -run_logged cron-mcp-cleanup-build docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" +docker_e2e_build_or_reuse "$IMAGE_NAME" cron-mcp-cleanup echo "Running in-container cron/subagent MCP cleanup smoke..." set +e diff --git a/scripts/e2e/doctor-install-switch-docker.sh b/scripts/e2e/doctor-install-switch-docker.sh index 086f6c66304..95246043edc 100755 --- a/scripts/e2e/doctor-install-switch-docker.sh +++ b/scripts/e2e/doctor-install-switch-docker.sh @@ -2,11 +2,10 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh" -IMAGE_NAME="openclaw-doctor-install-switch-e2e" +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" +IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-doctor-install-switch-e2e" OPENCLAW_DOCTOR_INSTALL_SWITCH_E2E_IMAGE)" -echo "Building Docker image..." -run_logged doctor-switch-build docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" +docker_e2e_build_or_reuse "$IMAGE_NAME" doctor-switch echo "Running doctor install switch E2E..." docker run --rm -e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 "$IMAGE_NAME" bash -lc ' diff --git a/scripts/e2e/gateway-network-docker.sh b/scripts/e2e/gateway-network-docker.sh index 327941ce38b..b4816bb0716 100644 --- a/scripts/e2e/gateway-network-docker.sh +++ b/scripts/e2e/gateway-network-docker.sh @@ -2,8 +2,8 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh" -IMAGE_NAME="${OPENCLAW_GATEWAY_NETWORK_E2E_IMAGE:-openclaw-gateway-network-e2e}" +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" +IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-gateway-network-e2e" OPENCLAW_GATEWAY_NETWORK_E2E_IMAGE)" SKIP_BUILD="${OPENCLAW_GATEWAY_NETWORK_E2E_SKIP_BUILD:-0}" PORT="18789" @@ -17,12 +17,7 @@ cleanup() { } trap cleanup EXIT -if [ "$SKIP_BUILD" = "1" ]; then - echo "Reusing Docker image: $IMAGE_NAME" -else - echo "Building Docker image..." - run_logged gateway-network-build docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" -fi +docker_e2e_build_or_reuse "$IMAGE_NAME" gateway-network "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "" "$SKIP_BUILD" echo "Creating Docker network..." docker network create "$NET_NAME" >/dev/null diff --git a/scripts/e2e/mcp-channels-docker.sh b/scripts/e2e/mcp-channels-docker.sh index 6189294640c..79169e6ada7 100644 --- a/scripts/e2e/mcp-channels-docker.sh +++ b/scripts/e2e/mcp-channels-docker.sh @@ -2,8 +2,8 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh" -IMAGE_NAME="${OPENCLAW_IMAGE:-openclaw-mcp-channels-e2e}" +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" +IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-mcp-channels-e2e" OPENCLAW_IMAGE)" PORT="18789" TOKEN="mcp-e2e-$(date +%s)-$$" CONTAINER_NAME="openclaw-mcp-e2e-$$" @@ -15,8 +15,7 @@ cleanup() { } trap cleanup EXIT -echo "Building Docker image..." -run_logged mcp-channels-build docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" +docker_e2e_build_or_reuse "$IMAGE_NAME" mcp-channels echo "Running in-container gateway + MCP smoke..." set +e diff --git a/scripts/e2e/npm-onboard-channel-agent-docker.sh b/scripts/e2e/npm-onboard-channel-agent-docker.sh index 38ff53cafc2..04aab0e2199 100644 --- a/scripts/e2e/npm-onboard-channel-agent-docker.sh +++ b/scripts/e2e/npm-onboard-channel-agent-docker.sh @@ -2,9 +2,9 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh" +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -IMAGE_NAME="${OPENCLAW_NPM_ONBOARD_E2E_IMAGE:-openclaw-npm-onboard-channel-agent-e2e}" +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}" HOST_BUILD="${OPENCLAW_NPM_ONBOARD_HOST_BUILD:-1}" PACKAGE_TGZ="${OPENCLAW_NPM_ONBOARD_PACKAGE_TGZ:-}" @@ -18,12 +18,7 @@ case "$CHANNEL" in ;; esac -if [ "${OPENCLAW_SKIP_DOCKER_BUILD:-0}" = "1" ]; then - echo "Reusing Docker image: $IMAGE_NAME (OPENCLAW_SKIP_DOCKER_BUILD=1)" -else - echo "Building Docker image target $DOCKER_TARGET..." - run_logged npm-onboard-channel-agent-build docker build --target "$DOCKER_TARGET" -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" -fi +docker_e2e_build_or_reuse "$IMAGE_NAME" npm-onboard-channel-agent "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "$DOCKER_TARGET" prepare_package_tgz() { if [ -n "$PACKAGE_TGZ" ]; then diff --git a/scripts/e2e/onboard-docker.sh b/scripts/e2e/onboard-docker.sh index 3352132eb59..12e39ed164b 100755 --- a/scripts/e2e/onboard-docker.sh +++ b/scripts/e2e/onboard-docker.sh @@ -2,11 +2,10 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh" -IMAGE_NAME="openclaw-onboard-e2e" +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" +IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-onboard-e2e" OPENCLAW_ONBOARD_E2E_IMAGE)" -echo "Building Docker image..." -run_logged onboard-build docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" +docker_e2e_build_or_reuse "$IMAGE_NAME" onboard echo "Running onboarding E2E..." docker run --rm -t "$IMAGE_NAME" bash -lc ' diff --git a/scripts/e2e/openwebui-docker.sh b/scripts/e2e/openwebui-docker.sh index a8ec259dac9..d757f3700df 100755 --- a/scripts/e2e/openwebui-docker.sh +++ b/scripts/e2e/openwebui-docker.sh @@ -2,9 +2,9 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh" +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -IMAGE_NAME="openclaw-openwebui-e2e" +IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-openwebui-e2e" OPENCLAW_OPENWEBUI_E2E_IMAGE)" OPENWEBUI_IMAGE="${OPENWEBUI_IMAGE:-ghcr.io/open-webui/open-webui:v0.8.10}" # Keep the default on a broadly available non-reasoning OpenAI model for # Open WebUI compatibility smoke. Callers can still override this explicitly. @@ -40,8 +40,7 @@ cleanup() { } trap cleanup EXIT -echo "Building Docker image..." -run_logged openwebui-build docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" +docker_e2e_build_or_reuse "$IMAGE_NAME" openwebui echo "Pulling Open WebUI image: $OPENWEBUI_IMAGE" docker pull "$OPENWEBUI_IMAGE" >/dev/null diff --git a/scripts/e2e/pi-bundle-mcp-tools-docker.sh b/scripts/e2e/pi-bundle-mcp-tools-docker.sh index 0440408b48a..e17294cb619 100755 --- a/scripts/e2e/pi-bundle-mcp-tools-docker.sh +++ b/scripts/e2e/pi-bundle-mcp-tools-docker.sh @@ -2,8 +2,8 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh" -IMAGE_NAME="${OPENCLAW_IMAGE:-openclaw-pi-bundle-mcp-tools-e2e}" +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" +IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-pi-bundle-mcp-tools-e2e" OPENCLAW_IMAGE)" CONTAINER_NAME="openclaw-pi-bundle-mcp-tools-e2e-$$" RUN_LOG="$(mktemp -t openclaw-pi-bundle-mcp-tools-log.XXXXXX)" @@ -13,10 +13,7 @@ cleanup() { } trap cleanup EXIT -if [ "${OPENCLAW_SKIP_DOCKER_BUILD:-0}" != "1" ]; then - echo "Building Docker image..." - run_logged pi-bundle-mcp-tools-build docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" -fi +docker_e2e_build_or_reuse "$IMAGE_NAME" pi-bundle-mcp-tools echo "Running in-container Pi bundle MCP tool availability smoke..." set +e diff --git a/scripts/e2e/plugin-update-unchanged-docker.sh b/scripts/e2e/plugin-update-unchanged-docker.sh index 221e12e1e57..5837784b7b6 100755 --- a/scripts/e2e/plugin-update-unchanged-docker.sh +++ b/scripts/e2e/plugin-update-unchanged-docker.sh @@ -2,17 +2,12 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh" +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -IMAGE_NAME="${OPENCLAW_PLUGIN_UPDATE_E2E_IMAGE:-openclaw-plugin-update-e2e}" +IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-plugin-update-e2e" OPENCLAW_PLUGIN_UPDATE_E2E_IMAGE)" SKIP_BUILD="${OPENCLAW_PLUGIN_UPDATE_E2E_SKIP_BUILD:-0}" -if [ "$SKIP_BUILD" = "1" ]; then - echo "Reusing Docker image: $IMAGE_NAME" -else - echo "Building Docker image..." - run_logged plugin-update-build docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" -fi +docker_e2e_build_or_reuse "$IMAGE_NAME" plugin-update "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "" "$SKIP_BUILD" echo "Running unchanged plugin update smoke..." docker run --rm \ diff --git a/scripts/e2e/plugins-docker.sh b/scripts/e2e/plugins-docker.sh index 505da013084..94979ccadc1 100755 --- a/scripts/e2e/plugins-docker.sh +++ b/scripts/e2e/plugins-docker.sh @@ -2,11 +2,10 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh" -IMAGE_NAME="openclaw-plugins-e2e" +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" +IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-plugins-e2e" OPENCLAW_PLUGINS_E2E_IMAGE)" -echo "Building Docker image..." -run_logged plugins-build docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" +docker_e2e_build_or_reuse "$IMAGE_NAME" plugins DOCKER_ENV_ARGS=(-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0) if [[ -n "${OPENAI_API_KEY:-}" && "${OPENAI_API_KEY:-}" != "undefined" && "${OPENAI_API_KEY:-}" != "null" ]]; then diff --git a/scripts/lib/docker-e2e-image.sh b/scripts/lib/docker-e2e-image.sh new file mode 100644 index 00000000000..e42d67ac39d --- /dev/null +++ b/scripts/lib/docker-e2e-image.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +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" + +docker_e2e_resolve_image() { + local default_image="$1" + shift + + local env_name + for env_name in "$@"; do + local value="${!env_name:-}" + if [ -n "$value" ]; then + printf '%s\n' "$value" + return 0 + fi + done + + if [ -n "${OPENCLAW_DOCKER_E2E_IMAGE:-}" ]; then + printf '%s\n' "$OPENCLAW_DOCKER_E2E_IMAGE" + return 0 + fi + + printf '%s\n' "$default_image" +} + +docker_e2e_build_or_reuse() { + local image_name="$1" + local label="$2" + local dockerfile="${3:-$ROOT_DIR/scripts/e2e/Dockerfile}" + local context="${4:-$ROOT_DIR}" + local target="${5:-}" + local skip_build="${6:-0}" + + if [ "${OPENCLAW_SKIP_DOCKER_BUILD:-0}" = "1" ] || [ "$skip_build" = "1" ]; then + echo "Reusing Docker image: $image_name" + if ! docker image inspect "$image_name" >/dev/null 2>&1; then + echo "Docker image not found: $image_name" >&2 + echo "Build it first or unset OPENCLAW_SKIP_DOCKER_BUILD." >&2 + return 1 + fi + return 0 + fi + + echo "Building Docker image: $image_name" + local build_cmd=(docker build) + if [ -n "$target" ]; then + build_cmd+=(--target "$target") + fi + build_cmd+=(-t "$image_name" -f "$dockerfile" "$context") + run_logged "$label-build" "${build_cmd[@]}" +} diff --git a/scripts/test-docker-all.sh b/scripts/test-docker-all.sh new file mode 100644 index 00000000000..6fce6d451a9 --- /dev/null +++ b/scripts/test-docker-all.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +pnpm test:docker:live-build +OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-models +OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-gateway + +export OPENCLAW_DOCKER_E2E_IMAGE="${OPENCLAW_DOCKER_E2E_IMAGE:-openclaw-docker-e2e:local}" +pnpm test:docker:e2e-build + +OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:openwebui +OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:onboard +OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:npm-onboard-channel-agent +OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:gateway-network +OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:mcp-channels +OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:pi-bundle-mcp-tools +OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:cron-mcp-cleanup +pnpm test:docker:qr +OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:doctor-switch +OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:plugins +OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:bundled-channel-deps +pnpm test:docker:cleanup