From d108110a894b48d4e867988f4fd2b2d736df7922 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Apr 2026 23:10:23 +0100 Subject: [PATCH] ci: use packaged tarball for docker e2e --- .agents/skills/openclaw-testing/SKILL.md | 10 +- .../openclaw-live-and-e2e-checks-reusable.yml | 98 ++++++++++++++++++- docs/ci.md | 2 +- docs/help/testing.md | 8 +- docs/reference/test.md | 2 +- scripts/e2e/Dockerfile | 61 ++++-------- .../bundled-channel-runtime-deps-docker.sh | 32 ++---- .../e2e/crestodian-first-run-docker-client.ts | 13 ++- scripts/e2e/crestodian-first-run-docker.sh | 4 + .../e2e/crestodian-planner-docker-client.ts | 11 ++- scripts/e2e/crestodian-planner-docker.sh | 4 + .../e2e/crestodian-rescue-docker-client.ts | 11 ++- scripts/e2e/crestodian-rescue-docker.sh | 4 + scripts/e2e/cron-mcp-cleanup-docker.sh | 4 + scripts/e2e/docker-openai-seed.ts | 4 +- scripts/e2e/doctor-install-switch-docker.sh | 46 ++++++--- scripts/e2e/mcp-channels-docker.sh | 4 + scripts/e2e/mcp-channels-harness.ts | 11 ++- .../e2e/npm-onboard-channel-agent-docker.sh | 31 ++---- scripts/e2e/npm-telegram-live-docker.sh | 9 ++ scripts/e2e/npm-telegram-live-runner.ts | 4 +- scripts/e2e/openai-image-auth-docker.sh | 4 + scripts/e2e/openwebui-docker.sh | 4 + .../e2e/pi-bundle-mcp-tools-docker-client.ts | 13 ++- scripts/e2e/pi-bundle-mcp-tools-docker.sh | 4 + scripts/e2e/plugin-update-unchanged-docker.sh | 16 ++- .../session-runtime-context-docker-client.ts | 5 +- scripts/e2e/session-runtime-context-docker.sh | 4 + scripts/e2e/update-channel-switch-docker.sh | 47 ++++----- scripts/lib/docker-e2e-image.sh | 19 ++++ scripts/lib/docker-e2e-package.sh | 63 ++++++++++++ scripts/test-docker-all.mjs | 82 +++++++++------- 32 files changed, 432 insertions(+), 202 deletions(-) create mode 100644 scripts/lib/docker-e2e-package.sh diff --git a/.agents/skills/openclaw-testing/SKILL.md b/.agents/skills/openclaw-testing/SKILL.md index 61bd39c2441..483e063e6ea 100644 --- a/.agents/skills/openclaw-testing/SKILL.md +++ b/.agents/skills/openclaw-testing/SKILL.md @@ -100,14 +100,18 @@ docker_lanes: install-e2e bundled-channel-update-acpx ``` That skips the three chunk matrix and runs one targeted Docker job against the -prepared GHCR images. Release-path normal mode remains max three Docker chunk -jobs: +prepared GHCR images and the prepared OpenClaw npm tarball. Live-only targeted +reruns skip the E2E images and build only the live-test image. Release-path +normal mode remains max three Docker chunk jobs: - `core` - `package-update` - `plugins-integrations` -Every scheduler run writes `.artifacts/docker-tests/**/summary.json`. Read it +Docker E2E images never copy repo sources as the app under test: the bare image +is a Node/Git runner, and the functional image installs the same prebuilt npm +tarball that bare lanes mount. Every scheduler run writes +`.artifacts/docker-tests/**/summary.json`. Read it before rerunning. Lane entries include `command`, `rerunCommand`, status, timing, timeout state, image kind, and log file path. The summary also includes top-level phase timings for preflight, image build, package prep, lane pools, diff --git a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml index 6eb385ab8e8..6c29fa97330 100644 --- a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml +++ b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml @@ -438,6 +438,7 @@ jobs: OPENCLAW_DOCKER_E2E_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.image }} OPENCLAW_DOCKER_E2E_BARE_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.bare_image }} OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.functional_image }} + OPENCLAW_CURRENT_PACKAGE_TGZ: .artifacts/docker-e2e-package/openclaw-current.tgz OPENCLAW_SKIP_DOCKER_BUILD: "1" INCLUDE_OPENWEBUI: ${{ inputs.include_openwebui }} DOCKER_E2E_CHUNK: ${{ matrix.chunk_id }} @@ -465,6 +466,12 @@ jobs: - name: Hydrate live auth/profile inputs run: bash scripts/ci-hydrate-live-auth.sh + - name: Download OpenClaw Docker E2E package + uses: actions/download-artifact@v8 + with: + name: docker-e2e-package + path: .artifacts/docker-e2e-package + - name: Pull shared Docker E2E image shell: bash run: | @@ -623,6 +630,7 @@ jobs: OPENCLAW_DOCKER_E2E_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.image }} OPENCLAW_DOCKER_E2E_BARE_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.bare_image }} OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.functional_image }} + OPENCLAW_CURRENT_PACKAGE_TGZ: .artifacts/docker-e2e-package/openclaw-current.tgz OPENCLAW_SKIP_DOCKER_BUILD: "1" INCLUDE_OPENWEBUI: ${{ inputs.include_openwebui }} DOCKER_E2E_LANES: ${{ inputs.docker_lanes }} @@ -650,7 +658,31 @@ jobs: - name: Hydrate live auth/profile inputs run: bash scripts/ci-hydrate-live-auth.sh + - name: Detect targeted Docker lane image needs + id: lane_class + shell: bash + run: | + set -euo pipefail + needs_e2e=0 + IFS=', ' read -r -a lanes <<< "${DOCKER_E2E_LANES}" + for lane in "${lanes[@]}"; do + [[ -z "$lane" ]] && continue + if [[ "$lane" != live-* ]]; then + needs_e2e=1 + break + fi + done + echo "needs_e2e=${needs_e2e}" >> "$GITHUB_OUTPUT" + + - name: Download OpenClaw Docker E2E package + if: steps.lane_class.outputs.needs_e2e == '1' + uses: actions/download-artifact@v8 + with: + name: docker-e2e-package + path: .artifacts/docker-e2e-package + - name: Pull shared Docker E2E images + if: steps.lane_class.outputs.needs_e2e == '1' shell: bash run: | set -euo pipefail @@ -691,10 +723,9 @@ jobs: export OPENCLAW_DOCKER_ALL_LOG_DIR=".artifacts/docker-tests/targeted" export OPENCLAW_DOCKER_ALL_TIMINGS_FILE=".artifacts/docker-tests/targeted-timings.json" if [[ "$lanes" == *" live-"* ]]; then - export OPENCLAW_DOCKER_ALL_BUILD=1 - else - export OPENCLAW_DOCKER_ALL_BUILD=0 + pnpm test:docker:live-build fi + export OPENCLAW_DOCKER_ALL_BUILD=0 pnpm test:docker:all @@ -825,7 +856,60 @@ jobs: echo "Shared Docker E2E bare image: \`$bare_image\`" >> "$GITHUB_STEP_SUMMARY" echo "Shared Docker E2E functional image: \`$functional_image\`" >> "$GITHUB_STEP_SUMMARY" + - name: Classify selected Docker lanes + id: lane_class + shell: bash + env: + DOCKER_E2E_LANES: ${{ inputs.docker_lanes }} + INCLUDE_RELEASE_PATH_SUITES: ${{ inputs.include_release_path_suites }} + INCLUDE_OPENWEBUI: ${{ inputs.include_openwebui }} + run: | + set -euo pipefail + needs_e2e=0 + if [[ "${INCLUDE_RELEASE_PATH_SUITES}" == "true" || "${INCLUDE_OPENWEBUI}" == "true" ]]; then + needs_e2e=1 + elif [[ -n "${DOCKER_E2E_LANES}" ]]; then + IFS=', ' read -r -a lanes <<< "${DOCKER_E2E_LANES}" + for lane in "${lanes[@]}"; do + [[ -z "$lane" ]] && continue + if [[ "$lane" != live-* ]]; then + needs_e2e=1 + break + fi + done + fi + echo "needs_e2e=${needs_e2e}" >> "$GITHUB_OUTPUT" + + - name: Setup Node environment + if: steps.lane_class.outputs.needs_e2e == '1' + uses: ./.github/actions/setup-node-env + with: + node-version: ${{ env.NODE_VERSION }} + pnpm-version: ${{ env.PNPM_VERSION }} + install-bun: "true" + + - name: Pack OpenClaw package for Docker E2E + if: steps.lane_class.outputs.needs_e2e == '1' + shell: bash + run: | + set -euo pipefail + mkdir -p .artifacts/docker-e2e-package + pnpm build + 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 .artifacts/docker-e2e-package >/tmp/openclaw-docker-e2e-pack.out + packed="$(tail -n 1 /tmp/openclaw-docker-e2e-pack.out | tr -d '\r')" + mv ".artifacts/docker-e2e-package/$packed" .artifacts/docker-e2e-package/openclaw-current.tgz + + - name: Upload OpenClaw Docker E2E package + if: steps.lane_class.outputs.needs_e2e == '1' + uses: actions/upload-artifact@v7 + with: + name: docker-e2e-package + path: .artifacts/docker-e2e-package/openclaw-current.tgz + if-no-files-found: error + - name: Log in to GHCR + if: steps.lane_class.outputs.needs_e2e == '1' uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 with: registry: ghcr.io @@ -833,15 +917,16 @@ jobs: password: ${{ github.token }} - name: Setup Docker builder + if: steps.lane_class.outputs.needs_e2e == '1' uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1 - name: Build and push bare Docker E2E image - if: inputs.include_release_path_suites || inputs.docker_lanes != '' + if: steps.lane_class.outputs.needs_e2e == '1' && (inputs.include_release_path_suites || inputs.docker_lanes != '') uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . file: ./scripts/e2e/Dockerfile - target: build + target: bare platforms: linux/amd64 cache-from: type=gha,scope=docker-e2e-bare cache-to: type=gha,mode=max,scope=docker-e2e-bare @@ -851,11 +936,14 @@ jobs: push: true - name: Build and push functional Docker E2E image + if: steps.lane_class.outputs.needs_e2e == '1' uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . file: ./scripts/e2e/Dockerfile target: functional + build-contexts: | + openclaw_package=.artifacts/docker-e2e-package platforms: linux/amd64 cache-from: | type=gha,scope=docker-e2e-bare diff --git a/docs/ci.md b/docs/ci.md index 6f584d02980..2f5bf0d94f6 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -92,7 +92,7 @@ Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests 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. CI routing-only edits, selected cheap core-test fixture edits, and narrow plugin contract helper/test-routing edits use a fast Node-only manifest path: preflight, security, and a single `checks-fast-core` task. That path avoids build artifacts, Node 22 compatibility, channel contracts, full core shards, bundled-plugin shards, and additional guard matrices when the changed files are limited to the routing or helper surfaces that the fast task exercises directly. 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 splits smoke coverage into `run_fast_install_smoke` and `run_full_install_smoke`. Pull requests run the fast path for Docker/package surfaces, bundled plugin package/manifest changes, and core plugin/channel/gateway/Plugin SDK surfaces that the Docker smoke jobs exercise. Source-only bundled plugin changes, test-only edits, and docs-only edits do not reserve Docker workers. The fast path builds the root Dockerfile image once, checks the CLI, runs the agents delete shared-workspace CLI smoke, runs the container gateway-network e2e, verifies a bundled extension build arg, and runs the bounded bundled-plugin Docker profile under a 240-second aggregate command timeout with each scenario's Docker run capped separately. The full path keeps QR package install and installer Docker/update coverage for nightly scheduled runs, manual dispatches, workflow-call release checks, and pull requests that truly touch installer/package/Docker surfaces. `main` pushes, including merge commits, do not force the full path; when changed-scope logic would request full coverage on a push, the workflow keeps the fast Docker smoke and leaves the full install smoke to nightly or release validation. The slow Bun global install image-provider smoke is separately gated by `run_bun_global_install_smoke`; it runs on the nightly schedule and from the release checks workflow, and manual `install-smoke` dispatches can opt into it, but pull requests and `main` pushes do not run it. QR and installer Docker tests keep their own install-focused Dockerfiles. Local `test:docker:all` prebuilds one shared live-test image plus two shared `scripts/e2e/Dockerfile` built-app images: a bare image for installer/update/plugin-dependency lanes and a functional image that pre-stages bundled plugin runtime dependencies for normal functionality lanes. The scheduler selects the image per lane with `OPENCLAW_DOCKER_E2E_BARE_IMAGE` and `OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE`, then runs lanes with `OPENCLAW_SKIP_DOCKER_BUILD=1`; tune the default main-pool slot count of 10 with `OPENCLAW_DOCKER_ALL_PARALLELISM` and the provider-sensitive tail-pool slot count of 10 with `OPENCLAW_DOCKER_ALL_TAIL_PARALLELISM`. Heavy lane caps default to `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=6`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=8`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7` so npm install and multi-service lanes do not overcommit Docker while lighter lanes still fill available slots. Lane starts are staggered by 2 seconds by default to avoid local Docker daemon create storms; override with `OPENCLAW_DOCKER_ALL_START_STAGGER_MS=0` or another millisecond value. The local aggregate preflights Docker, removes stale OpenClaw E2E containers, emits active-lane status, persists lane timings for longest-first ordering, and supports `OPENCLAW_DOCKER_ALL_DRY_RUN=1` for scheduler inspection. It stops scheduling new pooled lanes after the first failure by default, and each lane has a 120-minute fallback timeout overrideable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`; selected live/tail lanes use tighter per-lane caps. `OPENCLAW_DOCKER_ALL_LANES=` runs exact scheduler lanes, including release-only lanes such as `install-e2e` and split bundled update lanes such as `bundled-channel-update-acpx`, while skipping the cleanup smoke so agents can reproduce one failed lane. The reusable live/E2E workflow builds and pushes one SHA-tagged bare GHCR Docker E2E image and one SHA-tagged functional GHCR Docker E2E image, then runs the release-path Docker suite as at most three chunked jobs with `OPENCLAW_SKIP_DOCKER_BUILD=1` so each chunk pulls the image kind it needs and executes multiple lanes through the same weighted scheduler (`OPENCLAW_DOCKER_ALL_PROFILE=release-path`, `OPENCLAW_DOCKER_ALL_CHUNK=core|package-update|plugins-integrations`). Each chunk uploads `.artifacts/docker-tests/` with lane logs, timings, `summary.json`, phase timings, and per-lane rerun commands. The workflow `docker_lanes` input runs selected lanes against the prepared images instead of the three chunk jobs, which keeps failed-lane debugging bounded to one targeted Docker job; if a selected lane is a live Docker lane, the targeted job builds the live-test image locally for that rerun. When Open WebUI is requested with the release-path suite, it runs inside the plugins/integrations chunk instead of reserving a fourth Docker worker; Open WebUI keeps a standalone job only for openwebui-only dispatches. The scheduled live/E2E workflow runs the full release-path Docker suite daily. The bundled update matrix is split by update target so repeated npm update and doctor repair passes can shard with other bundled checks. +The separate `install-smoke` workflow reuses the same scope script through its own `preflight` job. It splits smoke coverage into `run_fast_install_smoke` and `run_full_install_smoke`. Pull requests run the fast path for Docker/package surfaces, bundled plugin package/manifest changes, and core plugin/channel/gateway/Plugin SDK surfaces that the Docker smoke jobs exercise. Source-only bundled plugin changes, test-only edits, and docs-only edits do not reserve Docker workers. The fast path builds the root Dockerfile image once, checks the CLI, runs the agents delete shared-workspace CLI smoke, runs the container gateway-network e2e, verifies a bundled extension build arg, and runs the bounded bundled-plugin Docker profile under a 240-second aggregate command timeout with each scenario's Docker run capped separately. The full path keeps QR package install and installer Docker/update coverage for nightly scheduled runs, manual dispatches, workflow-call release checks, and pull requests that truly touch installer/package/Docker surfaces. `main` pushes, including merge commits, do not force the full path; when changed-scope logic would request full coverage on a push, the workflow keeps the fast Docker smoke and leaves the full install smoke to nightly or release validation. The slow Bun global install image-provider smoke is separately gated by `run_bun_global_install_smoke`; it runs on the nightly schedule and from the release checks workflow, and manual `install-smoke` dispatches can opt into it, but pull requests and `main` pushes do not run it. QR and installer Docker tests keep their own install-focused Dockerfiles. Local `test:docker:all` prebuilds one shared live-test image, packs OpenClaw once as an npm tarball, and builds two shared `scripts/e2e/Dockerfile` images: a bare Node/Git runner for installer/update/plugin-dependency lanes and a functional image that installs the same tarball into `/app` for normal functionality lanes. The scheduler selects the image per lane with `OPENCLAW_DOCKER_E2E_BARE_IMAGE` and `OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE`, then runs lanes with `OPENCLAW_SKIP_DOCKER_BUILD=1`; tune the default main-pool slot count of 10 with `OPENCLAW_DOCKER_ALL_PARALLELISM` and the provider-sensitive tail-pool slot count of 10 with `OPENCLAW_DOCKER_ALL_TAIL_PARALLELISM`. Heavy lane caps default to `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=6`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=8`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7` so npm install and multi-service lanes do not overcommit Docker while lighter lanes still fill available slots. Lane starts are staggered by 2 seconds by default to avoid local Docker daemon create storms; override with `OPENCLAW_DOCKER_ALL_START_STAGGER_MS=0` or another millisecond value. The local aggregate preflights Docker, removes stale OpenClaw E2E containers, emits active-lane status, persists lane timings for longest-first ordering, and supports `OPENCLAW_DOCKER_ALL_DRY_RUN=1` for scheduler inspection. It stops scheduling new pooled lanes after the first failure by default, and each lane has a 120-minute fallback timeout overrideable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`; selected live/tail lanes use tighter per-lane caps. `OPENCLAW_DOCKER_ALL_LANES=` runs exact scheduler lanes, including release-only lanes such as `install-e2e` and split bundled update lanes such as `bundled-channel-update-acpx`, while skipping the cleanup smoke so agents can reproduce one failed lane. The reusable live/E2E workflow builds and pushes one SHA-tagged bare GHCR Docker E2E image and one SHA-tagged functional GHCR Docker E2E image, then runs the release-path Docker suite as at most three chunked jobs with `OPENCLAW_SKIP_DOCKER_BUILD=1` so each chunk pulls the image kind it needs and executes multiple lanes through the same weighted scheduler (`OPENCLAW_DOCKER_ALL_PROFILE=release-path`, `OPENCLAW_DOCKER_ALL_CHUNK=core|package-update|plugins-integrations`). Each chunk uploads `.artifacts/docker-tests/` with lane logs, timings, `summary.json`, phase timings, and per-lane rerun commands. The workflow `docker_lanes` input runs selected lanes against the prepared images instead of the three chunk jobs, which keeps failed-lane debugging bounded to one targeted Docker job; if a selected lane is a live Docker lane, the targeted job builds the live-test image locally for that rerun. When Open WebUI is requested with the release-path suite, it runs inside the plugins/integrations chunk instead of reserving a fourth Docker worker; Open WebUI keeps a standalone job only for openwebui-only dispatches. The scheduled live/E2E workflow runs the full release-path Docker suite daily. The bundled update matrix is split by update target so repeated npm update and doctor repair passes can shard with other bundled checks. 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 33b8728efb6..ea02a059a42 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -606,7 +606,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 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. The aggregate uses a weighted local scheduler: `OPENCLAW_DOCKER_ALL_PARALLELISM` controls process slots, while resource caps keep heavy live, npm-install, and multi-service lanes from all starting at once. Defaults are 10 slots, `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=6`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=8`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; tune `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` only when the Docker host has more headroom. The runner performs a Docker preflight by default, removes stale OpenClaw E2E containers, prints status every 30 seconds, stores successful lane timings in `.artifacts/docker-tests/lane-timings.json`, and uses those timings to start longer lanes first on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the weighted lane manifest without building or running Docker. +- `test:docker:all` builds the live Docker image once via `test:docker:live-build`, packs OpenClaw once as an npm tarball, then builds/reuses two `scripts/e2e/Dockerfile` images. The bare image is only the Node/Git runner for install/update/plugin-dependency lanes; those lanes mount the prebuilt tarball. The functional image installs the same tarball into `/app` for built-app functionality lanes. The aggregate uses a weighted local scheduler: `OPENCLAW_DOCKER_ALL_PARALLELISM` controls process slots, while resource caps keep heavy live, npm-install, and multi-service lanes from all starting at once. Defaults are 10 slots, `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=6`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=8`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; tune `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` only when the Docker host has more headroom. The runner performs a Docker preflight by default, removes stale OpenClaw E2E containers, prints status every 30 seconds, stores successful lane timings in `.artifacts/docker-tests/lane-timings.json`, and uses those timings to start longer lanes first on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the weighted lane manifest without building or running Docker. - Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:npm-onboard-channel-agent`, `test:docker:update-channel-switch`, `test:docker:session-runtime-context`, `test:docker:agents-delete-shared-workspace`, `test:docker:gateway-network`, `test:docker:browser-cdp-snapshot`, `test:docker:mcp-channels`, `test:docker:pi-bundle-mcp-tools`, `test:docker:cron-mcp-cleanup`, `test:docker:plugins`, `test:docker:plugin-update`, and `test:docker:config-reload` 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: @@ -639,11 +639,11 @@ 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: +To prebuild and reuse the shared functional 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 +OPENCLAW_DOCKER_E2E_IMAGE=openclaw-docker-e2e-functional:local pnpm test:docker:e2e-build +OPENCLAW_DOCKER_E2E_IMAGE=openclaw-docker-e2e-functional: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. When `OPENCLAW_SKIP_DOCKER_BUILD=1` points at a remote shared image, the scripts pull it if it is not already local. The QR and installer Docker tests keep their own Dockerfiles because they validate package/install behavior rather than the shared built-app runtime. diff --git a/docs/reference/test.md b/docs/reference/test.md index d4219d21e36..5cbf738108d 100644 --- a/docs/reference/test.md +++ b/docs/reference/test.md @@ -33,7 +33,7 @@ title: "Tests" - Gateway integration: opt-in via `OPENCLAW_TEST_INCLUDE_GATEWAY=1 pnpm test` or `pnpm test:gateway`. - `pnpm test:e2e`: Runs gateway end-to-end smoke tests (multi-instance WS/HTTP/node pairing). Defaults to `threads` + `isolate: false` with adaptive workers in `vitest.e2e.config.ts`; tune with `OPENCLAW_E2E_WORKERS=` and set `OPENCLAW_E2E_VERBOSE=1` for verbose logs. - `pnpm test:live`: Runs provider live tests (minimax/zai). Requires API keys and `LIVE=1` (or provider-specific `*_LIVE_TEST=1`) to unskip. -- `pnpm test:docker:all`: Builds the shared live-test image plus two Docker E2E images once, then runs the Docker smoke lanes with `OPENCLAW_SKIP_DOCKER_BUILD=1` through a weighted scheduler. The bare image (`OPENCLAW_DOCKER_E2E_BARE_IMAGE`) is used for installer/update/plugin-dependency lanes; the functional image (`OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE`) pre-stages bundled plugin runtime dependencies for normal functionality lanes. `OPENCLAW_DOCKER_ALL_PARALLELISM=` controls process slots and defaults to 10; `OPENCLAW_DOCKER_ALL_TAIL_PARALLELISM=` controls the provider-sensitive tail pool and defaults to 10. Heavy lane caps default to `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=9`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=10`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; provider caps default to one heavy lane per provider via `OPENCLAW_DOCKER_ALL_LIVE_CLAUDE_LIMIT=4`, `OPENCLAW_DOCKER_ALL_LIVE_CODEX_LIMIT=4`, and `OPENCLAW_DOCKER_ALL_LIVE_GEMINI_LIMIT=4`. Use `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` for larger hosts. Lane starts are staggered by 2 seconds by default to avoid local Docker daemon create storms; override with `OPENCLAW_DOCKER_ALL_START_STAGGER_MS=`. The runner preflights Docker by default, cleans stale OpenClaw E2E containers, emits active-lane status every 30 seconds, shares provider CLI tool caches between compatible lanes, retries transient live-provider failures once by default (`OPENCLAW_DOCKER_ALL_LIVE_RETRIES=`), and stores lane timings in `.artifacts/docker-tests/lane-timings.json` for longest-first ordering on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the lane manifest without running Docker, `OPENCLAW_DOCKER_ALL_STATUS_INTERVAL_MS=` to tune status output, or `OPENCLAW_DOCKER_ALL_TIMINGS=0` to disable timing reuse. Use `OPENCLAW_DOCKER_ALL_LIVE_MODE=skip` for deterministic/local lanes only or `OPENCLAW_DOCKER_ALL_LIVE_MODE=only` for live-provider lanes only; package aliases are `pnpm test:docker:local:all` and `pnpm test:docker:live:all`. Live-only mode merges main and tail live lanes into one longest-first pool so provider buckets can pack Claude, Codex, and Gemini work together. The runner stops scheduling new pooled lanes after the first failure unless `OPENCLAW_DOCKER_ALL_FAIL_FAST=0` is set, and each lane has a 120-minute fallback timeout overrideable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`; selected live/tail lanes use tighter per-lane caps. CLI backend Docker setup commands have their own timeout via `OPENCLAW_LIVE_CLI_BACKEND_SETUP_TIMEOUT_SECONDS` (default 180). Per-lane logs and `summary.json` phase timings are written under `.artifacts/docker-tests//`. +- `pnpm test:docker:all`: Builds the shared live-test image, packs OpenClaw once as an npm tarball, builds/reuses a bare Node/Git runner image plus a functional image that installs that tarball into `/app`, then runs Docker smoke lanes with `OPENCLAW_SKIP_DOCKER_BUILD=1` through a weighted scheduler. The bare image (`OPENCLAW_DOCKER_E2E_BARE_IMAGE`) is used for installer/update/plugin-dependency lanes; those lanes mount the prebuilt tarball instead of using copied repo sources. The functional image (`OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE`) is used for normal built-app functionality lanes. `OPENCLAW_DOCKER_ALL_PARALLELISM=` controls process slots and defaults to 10; `OPENCLAW_DOCKER_ALL_TAIL_PARALLELISM=` controls the provider-sensitive tail pool and defaults to 10. Heavy lane caps default to `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=9`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=10`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; provider caps default to one heavy lane per provider via `OPENCLAW_DOCKER_ALL_LIVE_CLAUDE_LIMIT=4`, `OPENCLAW_DOCKER_ALL_LIVE_CODEX_LIMIT=4`, and `OPENCLAW_DOCKER_ALL_LIVE_GEMINI_LIMIT=4`. Use `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` for larger hosts. Lane starts are staggered by 2 seconds by default to avoid local Docker daemon create storms; override with `OPENCLAW_DOCKER_ALL_START_STAGGER_MS=`. The runner preflights Docker by default, cleans stale OpenClaw E2E containers, emits active-lane status every 30 seconds, shares provider CLI tool caches between compatible lanes, retries transient live-provider failures once by default (`OPENCLAW_DOCKER_ALL_LIVE_RETRIES=`), and stores lane timings in `.artifacts/docker-tests/lane-timings.json` for longest-first ordering on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the lane manifest without running Docker, `OPENCLAW_DOCKER_ALL_STATUS_INTERVAL_MS=` to tune status output, or `OPENCLAW_DOCKER_ALL_TIMINGS=0` to disable timing reuse. Use `OPENCLAW_DOCKER_ALL_LIVE_MODE=skip` for deterministic/local lanes only or `OPENCLAW_DOCKER_ALL_LIVE_MODE=only` for live-provider lanes only; package aliases are `pnpm test:docker:local:all` and `pnpm test:docker:live:all`. Live-only mode merges main and tail live lanes into one longest-first pool so provider buckets can pack Claude, Codex, and Gemini work together. The runner stops scheduling new pooled lanes after the first failure unless `OPENCLAW_DOCKER_ALL_FAIL_FAST=0` is set, and each lane has a 120-minute fallback timeout overrideable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`; selected live/tail lanes use tighter per-lane caps. CLI backend Docker setup commands have their own timeout via `OPENCLAW_LIVE_CLI_BACKEND_SETUP_TIMEOUT_SECONDS` (default 180). Per-lane logs and `summary.json` phase timings are written under `.artifacts/docker-tests//`. - `pnpm test:docker:browser-cdp-snapshot`: Builds a Chromium-backed source E2E container, starts raw CDP plus an isolated Gateway, runs `browser doctor --deep`, and verifies CDP role snapshots include link URLs, cursor-promoted clickables, iframe refs, and frame metadata. - CLI backend live Docker probes can be run as focused lanes, for example `pnpm test:docker:live-cli-backend:codex`, `pnpm test:docker:live-cli-backend:codex:resume`, or `pnpm test:docker:live-cli-backend:codex:mcp`. Claude and Gemini have matching `:resume` and `:mcp` aliases. - `pnpm test:docker:openwebui`: Starts Dockerized OpenClaw + Open WebUI, signs in through Open WebUI, checks `/api/models`, then runs a real proxied chat through `/api/chat/completions`. Requires a usable live model key (for example OpenAI in `~/.profile`), pulls an external Open WebUI image, and is not expected to be CI-stable like the normal unit/e2e suites. diff --git a/scripts/e2e/Dockerfile b/scripts/e2e/Dockerfile index b3edbfd79a1..dbda16a418f 100644 --- a/scripts/e2e/Dockerfile +++ b/scripts/e2e/Dockerfile @@ -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' 'OpenClaw Control UI' > 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"] diff --git a/scripts/e2e/bundled-channel-runtime-deps-docker.sh b/scripts/e2e/bundled-channel-runtime-deps-docker.sh index 978b6922e15..3eff712ab98 100644 --- a/scripts/e2e/bundled-channel-runtime-deps-docker.sh +++ b/scripts/e2e/bundled-channel-runtime-deps-docker.sh @@ -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 diff --git a/scripts/e2e/crestodian-first-run-docker-client.ts b/scripts/e2e/crestodian-first-run-docker-client.ts index 1dd6a3f4f04..a8772d14c42 100644 --- a/scripts/e2e/crestodian-first-run-docker-client.ts +++ b/scripts/e2e/crestodian-first-run-docker-client.ts @@ -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; diff --git a/scripts/e2e/crestodian-first-run-docker.sh b/scripts/e2e/crestodian-first-run-docker.sh index 473f907b1d8..eb1709e5991 100644 --- a/scripts/e2e/crestodian-first-run-docker.sh +++ b/scripts/e2e/crestodian-first-run-docker.sh @@ -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 diff --git a/scripts/e2e/crestodian-planner-docker-client.ts b/scripts/e2e/crestodian-planner-docker-client.ts index bdf420ab25b..5c604b24678 100644 --- a/scripts/e2e/crestodian-planner-docker-client.ts +++ b/scripts/e2e/crestodian-planner-docker-client.ts @@ -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) { diff --git a/scripts/e2e/crestodian-planner-docker.sh b/scripts/e2e/crestodian-planner-docker.sh index debc99a0e7f..a3a9352ee5a 100755 --- a/scripts/e2e/crestodian-planner-docker.sh +++ b/scripts/e2e/crestodian-planner-docker.sh @@ -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 diff --git a/scripts/e2e/crestodian-rescue-docker-client.ts b/scripts/e2e/crestodian-rescue-docker-client.ts index 98ca2c83b99..11e9ae5d713 100644 --- a/scripts/e2e/crestodian-rescue-docker-client.ts +++ b/scripts/e2e/crestodian-rescue-docker-client.ts @@ -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>; diff --git a/scripts/e2e/crestodian-rescue-docker.sh b/scripts/e2e/crestodian-rescue-docker.sh index 4ba9c96ac75..c45b1274937 100755 --- a/scripts/e2e/crestodian-rescue-docker.sh +++ b/scripts/e2e/crestodian-rescue-docker.sh @@ -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 diff --git a/scripts/e2e/cron-mcp-cleanup-docker.sh b/scripts/e2e/cron-mcp-cleanup-docker.sh index d91b41abdfc..7ae872451e6 100644 --- a/scripts/e2e/cron-mcp-cleanup-docker.sh +++ b/scripts/e2e/cron-mcp-cleanup-docker.sh @@ -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 diff --git a/scripts/e2e/docker-openai-seed.ts b/scripts/e2e/docker-openai-seed.ts index bc2b5ef7e93..e876f0be3f5 100644 --- a/scripts/e2e/docker-openai-seed.ts +++ b/scripts/e2e/docker-openai-seed.ts @@ -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 }; diff --git a/scripts/e2e/doctor-install-switch-docker.sh b/scripts/e2e/doctor-install-switch-docker.sh index 12a30fb7938..5f5b1bc3a6c 100755 --- a/scripts/e2e/doctor-install-switch-docker.sh +++ b/scripts/e2e/doctor-install-switch-docker.sh @@ -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" diff --git a/scripts/e2e/mcp-channels-docker.sh b/scripts/e2e/mcp-channels-docker.sh index bf20b92f58b..4b8f2db490b 100644 --- a/scripts/e2e/mcp-channels-docker.sh +++ b/scripts/e2e/mcp-channels-docker.sh @@ -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 diff --git a/scripts/e2e/mcp-channels-harness.ts b/scripts/e2e/mcp-channels-harness.ts index 48c186dcdff..f44e6c34fc7 100644 --- a/scripts/e2e/mcp-channels-harness.ts +++ b/scripts/e2e/mcp-channels-harness.ts @@ -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"), diff --git a/scripts/e2e/npm-onboard-channel-agent-docker.sh b/scripts/e2e/npm-onboard-channel-agent-docker.sh index ddacdca359c..8e95d2467f0 100644 --- a/scripts/e2e/npm-onboard-channel-agent-docker.sh +++ b/scripts/e2e/npm-onboard-channel-agent-docker.sh @@ -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 diff --git a/scripts/e2e/npm-telegram-live-docker.sh b/scripts/e2e/npm-telegram-live-docker.sh index 58e662cb13f..62b263f1a03 100755 --- a/scripts/e2e/npm-telegram-live-docker.sh +++ b/scripts/e2e/npm-telegram-live-docker.sh @@ -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 \ diff --git a/scripts/e2e/npm-telegram-live-runner.ts b/scripts/e2e/npm-telegram-live-runner.ts index 4c0e02dce4b..51ca2930bf7 100644 --- a/scripts/e2e/npm-telegram-live-runner.ts +++ b/scripts/e2e/npm-telegram-live-runner.ts @@ -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(); diff --git a/scripts/e2e/openai-image-auth-docker.sh b/scripts/e2e/openai-image-auth-docker.sh index b8566e3c091..26479598225 100644 --- a/scripts/e2e/openai-image-auth-docker.sh +++ b/scripts/e2e/openai-image-auth-docker.sh @@ -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")" diff --git a/scripts/e2e/openwebui-docker.sh b/scripts/e2e/openwebui-docker.sh index 33a4e5a0a56..6c440f37426 100755 --- a/scripts/e2e/openwebui-docker.sh +++ b/scripts/e2e/openwebui-docker.sh @@ -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 diff --git a/scripts/e2e/pi-bundle-mcp-tools-docker-client.ts b/scripts/e2e/pi-bundle-mcp-tools-docker-client.ts index dd908764406..8a845b3438f 100644 --- a/scripts/e2e/pi-bundle-mcp-tools-docker-client.ts +++ b/scripts/e2e/pi-bundle-mcp-tools-docker-client.ts @@ -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); diff --git a/scripts/e2e/pi-bundle-mcp-tools-docker.sh b/scripts/e2e/pi-bundle-mcp-tools-docker.sh index e17294cb619..8eced1626ea 100755 --- a/scripts/e2e/pi-bundle-mcp-tools-docker.sh +++ b/scripts/e2e/pi-bundle-mcp-tools-docker.sh @@ -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 diff --git a/scripts/e2e/plugin-update-unchanged-docker.sh b/scripts/e2e/plugin-update-unchanged-docker.sh index ab9f3308c1c..3121a1340c9 100755 --- a/scripts/e2e/plugin-update-unchanged-docker.sh +++ b/scripts/e2e/plugin-update-unchanged-docker.sh @@ -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' diff --git a/scripts/e2e/session-runtime-context-docker-client.ts b/scripts/e2e/session-runtime-context-docker-client.ts index a8cd145363e..753c6e36c77 100644 --- a/scripts/e2e/session-runtime-context-docker-client.ts +++ b/scripts/e2e/session-runtime-context-docker-client.ts @@ -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; diff --git a/scripts/e2e/session-runtime-context-docker.sh b/scripts/e2e/session-runtime-context-docker.sh index a057c14b175..205c9ce058e 100644 --- a/scripts/e2e/session-runtime-context-docker.sh +++ b/scripts/e2e/session-runtime-context-docker.sh @@ -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 diff --git a/scripts/e2e/update-channel-switch-docker.sh b/scripts/e2e/update-channel-switch-docker.sh index 203c211db4e..c9bca848fb8 100755 --- a/scripts/e2e/update-channel-switch-docker.sh +++ b/scripts/e2e/update-channel-switch-docker.sh @@ -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" diff --git a/scripts/lib/docker-e2e-image.sh b/scripts/lib/docker-e2e-image.sh index c4cf8383b43..7e1fe576ee6 100644 --- a/scripts/lib/docker-e2e-image.sh +++ b/scripts/lib/docker-e2e-image.sh @@ -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[@]}" } diff --git a/scripts/lib/docker-e2e-package.sh b/scripts/lib/docker-e2e-package.sh new file mode 100644 index 00000000000..418905caf52 --- /dev/null +++ b/scripts/lib/docker-e2e-package.sh @@ -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") +} diff --git a/scripts/test-docker-all.mjs b/scripts/test-docker-all.mjs index 4d43b59c51f..b3d769022ee 100644 --- a/scripts/test-docker-all.mjs +++ b/scripts/test-docker-all.mjs @@ -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,