diff --git a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml index 245204e482f..33cc3fb9c6b 100644 --- a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml +++ b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml @@ -144,6 +144,7 @@ on: permissions: contents: read + packages: write pull-requests: read env: @@ -349,8 +350,8 @@ jobs: run: ${{ matrix.command }} validate_docker_e2e: - needs: validate_selected_ref - if: inputs.include_release_path_suites || inputs.include_openwebui + needs: [validate_selected_ref, prepare_docker_e2e_image] + if: inputs.include_release_path_suites runs-on: blacksmith-32vcpu-ubuntu-2404 timeout-minutes: ${{ matrix.timeout_minutes }} strategy: @@ -362,55 +363,66 @@ jobs: command: pnpm test:docker:onboard timeout_minutes: 60 release_path: true - openwebui_only: false + - suite_id: docker-npm-onboard-channel-agent + label: Npm Onboard Channel Agent Docker E2E + command: pnpm test:docker:npm-onboard-channel-agent + timeout_minutes: 90 + release_path: true - suite_id: docker-gateway-network label: Gateway Network Docker E2E command: pnpm test:docker:gateway-network timeout_minutes: 60 release_path: true - openwebui_only: false - suite_id: docker-mcp-channels label: MCP Channels Docker E2E command: pnpm test:docker:mcp-channels timeout_minutes: 60 release_path: true - openwebui_only: false + - suite_id: docker-pi-bundle-mcp-tools + label: Pi Bundle MCP Tools Docker E2E + command: pnpm test:docker:pi-bundle-mcp-tools + timeout_minutes: 60 + release_path: true + - suite_id: docker-cron-mcp-cleanup + label: Cron MCP Cleanup Docker E2E + command: pnpm test:docker:cron-mcp-cleanup + timeout_minutes: 60 + release_path: true - suite_id: docker-plugins label: Plugins Docker E2E command: pnpm test:docker:plugins timeout_minutes: 75 release_path: true - openwebui_only: false + - suite_id: docker-plugin-update + label: Plugin Update Docker E2E + command: pnpm test:docker:plugin-update + timeout_minutes: 60 + release_path: true + - suite_id: docker-config-reload + label: Config Reload Docker E2E + command: pnpm test:docker:config-reload + timeout_minutes: 60 + release_path: true - suite_id: docker-bundled-channel-deps label: Bundled Channel Runtime Deps Docker E2E command: pnpm test:docker:bundled-channel-deps timeout_minutes: 75 release_path: true - openwebui_only: false - suite_id: docker-doctor-switch label: Doctor Install Switch Docker E2E command: pnpm test:docker:doctor-switch timeout_minutes: 60 release_path: true - openwebui_only: false - suite_id: docker-qr label: QR Import Docker E2E command: pnpm test:docker:qr timeout_minutes: 60 release_path: true - openwebui_only: false - suite_id: docker-install-e2e label: Installer Docker E2E command: pnpm test:install:e2e timeout_minutes: 120 release_path: true - openwebui_only: false - - suite_id: docker-openwebui - label: Open WebUI Docker E2E - command: pnpm test:docker:openwebui - timeout_minutes: 75 - release_path: false - openwebui_only: true env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }} @@ -455,6 +467,8 @@ jobs: OPENCLAW_CLAUDE_SETTINGS_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_JSON }} OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }} OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }} + OPENCLAW_DOCKER_E2E_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.image }} + OPENCLAW_SKIP_DOCKER_BUILD: "1" steps: - name: Checkout selected ref uses: actions/checkout@v6 @@ -462,6 +476,13 @@ jobs: ref: ${{ needs.validate_selected_ref.outputs.selected_sha }} fetch-depth: 1 + - name: Log in to GHCR for shared Docker E2E image + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + - name: Setup Node environment uses: ./.github/actions/setup-node-env with: @@ -497,20 +518,106 @@ jobs: exit 1 fi ;; - docker-openwebui) - [[ -n "${OPENAI_API_KEY:-}" ]] || { - echo "OPENAI_API_KEY is required for the Open WebUI Docker smoke." >&2 - exit 1 - } - ;; esac - name: Run ${{ matrix.label }} - if: | - (inputs.include_release_path_suites && matrix.release_path) || - (inputs.include_openwebui && matrix.openwebui_only) run: ${{ matrix.command }} + validate_docker_openwebui: + needs: [validate_selected_ref, prepare_docker_e2e_image] + if: inputs.include_openwebui + runs-on: blacksmith-32vcpu-ubuntu-2404 + timeout-minutes: 75 + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }} + OPENCLAW_DOCKER_E2E_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.image }} + OPENCLAW_SKIP_DOCKER_BUILD: "1" + steps: + - name: Checkout selected ref + uses: actions/checkout@v6 + with: + ref: ${{ needs.validate_selected_ref.outputs.selected_sha }} + fetch-depth: 1 + + - name: Log in to GHCR for shared Docker E2E image + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Setup Node environment + uses: ./.github/actions/setup-node-env + with: + node-version: ${{ env.NODE_VERSION }} + pnpm-version: ${{ env.PNPM_VERSION }} + install-bun: "true" + + - name: Validate Open WebUI credentials + shell: bash + run: | + set -euo pipefail + [[ -n "${OPENAI_API_KEY:-}" ]] || { + echo "OPENAI_API_KEY is required for the Open WebUI Docker smoke." >&2 + exit 1 + } + + - name: Run Open WebUI Docker E2E + run: pnpm test:docker:openwebui + + prepare_docker_e2e_image: + needs: validate_selected_ref + if: inputs.include_release_path_suites || inputs.include_openwebui + runs-on: blacksmith-32vcpu-ubuntu-2404 + timeout-minutes: 90 + permissions: + contents: read + packages: write + outputs: + image: ${{ steps.image.outputs.image }} + env: + DOCKER_BUILD_SUMMARY: "false" + DOCKER_BUILD_RECORD_UPLOAD: "false" + steps: + - name: Checkout selected ref + uses: actions/checkout@v6 + with: + ref: ${{ needs.validate_selected_ref.outputs.selected_sha }} + fetch-depth: 1 + + - name: Resolve shared Docker E2E image tag + id: image + shell: bash + env: + SELECTED_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }} + run: | + set -euo pipefail + repository="${GITHUB_REPOSITORY,,}" + image="ghcr.io/${repository}-docker-e2e:${SELECTED_SHA}" + echo "image=$image" >> "$GITHUB_OUTPUT" + echo "Shared Docker E2E image: \`$image\`" >> "$GITHUB_STEP_SUMMARY" + + - name: Log in to GHCR + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Build and push shared Docker E2E image + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + with: + context: . + file: ./scripts/e2e/Dockerfile + target: build + platforms: linux/amd64 + cache-from: type=gha,scope=docker-e2e + cache-to: type=gha,mode=max,scope=docker-e2e + tags: ${{ steps.image.outputs.image }} + provenance: false + push: true + validate_live_provider_suites: needs: validate_selected_ref if: inputs.include_live_suites diff --git a/.github/workflows/openclaw-release-checks.yml b/.github/workflows/openclaw-release-checks.yml index c780b9875e2..99b4900c5ca 100644 --- a/.github/workflows/openclaw-release-checks.yml +++ b/.github/workflows/openclaw-release-checks.yml @@ -144,6 +144,7 @@ jobs: needs: [resolve_target] permissions: contents: read + packages: write pull-requests: read uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml with: diff --git a/.github/workflows/openclaw-scheduled-live-checks.yml b/.github/workflows/openclaw-scheduled-live-checks.yml index 027a67b1929..d58980a51f9 100644 --- a/.github/workflows/openclaw-scheduled-live-checks.yml +++ b/.github/workflows/openclaw-scheduled-live-checks.yml @@ -7,6 +7,7 @@ on: permissions: contents: read + packages: write pull-requests: read concurrency: @@ -20,6 +21,7 @@ jobs: live_and_openwebui_checks: permissions: contents: read + packages: write pull-requests: read uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml with: diff --git a/docs/ci.md b/docs/ci.md index 950754b00fd..95b631f4c5e 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. 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. +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` prebuilds one shared `scripts/e2e/Dockerfile` built-app image and reuses it across the E2E container smoke runners; the reusable live/E2E workflow mirrors that pattern by building and pushing one SHA-tagged GHCR Docker E2E image before the Docker matrix, then running the matrix with `OPENCLAW_SKIP_DOCKER_BUILD=1`. QR and installer Docker tests keep their own install-focused Dockerfiles. 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 dde69cc80eb..ea4a164d76a 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -908,7 +908,7 @@ These Docker runners split into two buckets: `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. 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. +- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:npm-onboard-channel-agent`, `test:docker:gateway-network`, `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: @@ -919,12 +919,14 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or - Gateway + dev agent: `pnpm test:docker:live-gateway` (script: `scripts/test-live-gateway-models-docker.sh`) - Open WebUI live smoke: `pnpm test:docker:openwebui` (script: `scripts/e2e/openwebui-docker.sh`) - Onboarding wizard (TTY, full scaffolding): `pnpm test:docker:onboard` (script: `scripts/e2e/onboard-docker.sh`) +- Npm tarball onboarding/channel/agent smoke: `pnpm test:docker:npm-onboard-channel-agent` installs the packed OpenClaw tarball globally in Docker, configures OpenAI via env-ref onboarding plus Telegram by default, verifies enabling the plugin installs its runtime deps on demand, runs doctor, and runs one mocked OpenAI agent turn. Reuse a prebuilt tarball with `OPENCLAW_NPM_ONBOARD_PACKAGE_TGZ=/path/to/openclaw-*.tgz`, skip the host rebuild with `OPENCLAW_NPM_ONBOARD_HOST_BUILD=0`, or switch channel with `OPENCLAW_NPM_ONBOARD_CHANNEL=discord`. - Gateway networking (two containers, WS auth + health): `pnpm test:docker:gateway-network` (script: `scripts/e2e/gateway-network-docker.sh`) - MCP channel bridge (seeded Gateway + stdio bridge + raw Claude notification-frame smoke): `pnpm test:docker:mcp-channels` (script: `scripts/e2e/mcp-channels-docker.sh`) - Pi bundle MCP tools (real stdio MCP server + embedded Pi profile allow/deny smoke): `pnpm test:docker:pi-bundle-mcp-tools` (script: `scripts/e2e/pi-bundle-mcp-tools-docker.sh`) - Cron/subagent MCP cleanup (real Gateway + stdio MCP child teardown after isolated cron and one-shot subagent runs): `pnpm test:docker:cron-mcp-cleanup` (script: `scripts/e2e/cron-mcp-cleanup-docker.sh`) - Plugins (install smoke + `/plugin` alias + Claude-bundle restart semantics): `pnpm test:docker:plugins` (script: `scripts/e2e/plugins-docker.sh`) -- Npm tarball onboarding/channel/agent smoke: `pnpm test:docker:npm-onboard-channel-agent` installs the packed OpenClaw tarball globally in Docker, configures OpenAI via env-ref onboarding plus Telegram by default, verifies enabling the plugin installs its runtime deps on demand, runs doctor, and runs one mocked OpenAI agent turn. Reuse a prebuilt tarball with `OPENCLAW_NPM_ONBOARD_PACKAGE_TGZ=/path/to/openclaw-*.tgz`, skip the host rebuild with `OPENCLAW_NPM_ONBOARD_HOST_BUILD=0`, or switch channel with `OPENCLAW_NPM_ONBOARD_CHANNEL=discord`. +- Plugin update unchanged smoke: `pnpm test:docker:plugin-update` (script: `scripts/e2e/plugin-update-unchanged-docker.sh`) +- Config reload metadata smoke: `pnpm test:docker:config-reload` (script: `scripts/e2e/config-reload-source-docker.sh`) - Bundled plugin runtime deps: `pnpm test:docker:bundled-channel-deps` builds a small Docker runner image by default, builds and packs OpenClaw once on the host, then mounts that tarball into each Linux install scenario. Reuse the image with `OPENCLAW_SKIP_DOCKER_BUILD=1`, skip the host rebuild after a fresh local build with `OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD=0`, or point at an existing tarball with `OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ=/path/to/openclaw-*.tgz`. - 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`. @@ -936,7 +938,7 @@ 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. +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. 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 diff --git a/package.json b/package.json index 881e801e77e..b52e1c7c553 100644 --- a/package.json +++ b/package.json @@ -1420,6 +1420,7 @@ "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:config-reload": "bash scripts/e2e/config-reload-source-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", @@ -1448,6 +1449,7 @@ "test:docker:onboard": "bash scripts/e2e/onboard-docker.sh", "test:docker:openwebui": "bash scripts/e2e/openwebui-docker.sh", "test:docker:pi-bundle-mcp-tools": "bash scripts/e2e/pi-bundle-mcp-tools-docker.sh", + "test:docker:plugin-update": "bash scripts/e2e/plugin-update-unchanged-docker.sh", "test:docker:plugins": "bash scripts/e2e/plugins-docker.sh", "test:docker:qr": "bash scripts/e2e/qr-import-docker.sh", "test:e2e": "node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts", diff --git a/scripts/lib/docker-e2e-image.sh b/scripts/lib/docker-e2e-image.sh index e42d67ac39d..32b94893c48 100644 --- a/scripts/lib/docker-e2e-image.sh +++ b/scripts/lib/docker-e2e-image.sh @@ -37,9 +37,12 @@ docker_e2e_build_or_reuse() { 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 + echo "Docker image not found locally; pulling: $image_name" + if ! docker pull "$image_name"; then + echo "Docker image not found: $image_name" >&2 + echo "Build it first or unset OPENCLAW_SKIP_DOCKER_BUILD." >&2 + return 1 + fi fi return 0 fi diff --git a/scripts/test-docker-all.sh b/scripts/test-docker-all.sh index 6fce6d451a9..9dfd2c016cb 100644 --- a/scripts/test-docker-all.sh +++ b/scripts/test-docker-all.sh @@ -21,5 +21,7 @@ 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:plugin-update +OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:config-reload OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:bundled-channel-deps pnpm test:docker:cleanup