diff --git a/package.json b/package.json index 40ea7ac93048..f63ea7030f84 100644 --- a/package.json +++ b/package.json @@ -1782,6 +1782,7 @@ "test:docker:live-models:gemini": "OPENCLAW_LIVE_PROVIDERS=google-gemini-cli OPENCLAW_LIVE_MODELS=google-gemini-cli/gemini-3.1-pro-preview bash scripts/test-live-models-docker.sh", "test:docker:live:all": "node scripts/run-with-env.mjs OPENCLAW_DOCKER_ALL_LIVE_MODE=only -- node scripts/test-docker-all.mjs", "test:docker:local:all": "node scripts/run-with-env.mjs OPENCLAW_DOCKER_ALL_LIVE_MODE=skip -- node scripts/test-docker-all.mjs", + "test:docker:cli-installer-distribution": "bash scripts/e2e/cli-installer-distribution-docker.sh", "test:docker:mcp-channels": "bash scripts/e2e/mcp-channels-docker.sh", "test:docker:mcp-code-mode-gateway": "bash scripts/e2e/mcp-code-mode-gateway-docker.sh", "test:docker:multi-node-update": "bash scripts/e2e/multi-node-update-docker.sh", diff --git a/qa/scenarios/cli/installer-distribution.yaml b/qa/scenarios/cli/installer-distribution.yaml new file mode 100644 index 000000000000..ee95e4993ddf --- /dev/null +++ b/qa/scenarios/cli/installer-distribution.yaml @@ -0,0 +1,33 @@ +title: CLI installer distribution + +scenario: + id: cli-installer-distribution + surface: cli + category: cli.cli-setup + coverage: + primary: + - cli.local-prefix-install + - cli.source-checkout-install + secondary: + - cli.installer-scripts + objective: Verify the hosted installer installs an exact package candidate and the CLI installer builds an exact source checkout into a dedicated Node and OpenClaw prefix. + successCriteria: + - "`install.sh` validates the supported Node runtime, installs the exact package candidate, honors no-onboard mode, and exposes a PATH-resolved CLI." + - "`install-cli.sh` provisions prefix-owned Node and an OpenClaw wrapper without using the system prefix." + - The source checkout is pinned to the exact tested commit, builds successfully, and the wrapper targets its built entrypoint. + - The installed source CLI serves help and version and reports git install kind through update status. + docsRefs: + - docs/install/index.md + - docs/install/updating.md + codeRefs: + - scripts/install.sh + - scripts/install-cli.sh + - scripts/e2e/cli-installer-distribution-docker.sh + execution: + kind: script + path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts + summary: Runs exact-candidate hosted-package and dedicated-prefix source-checkout installer proofs in clean Docker containers. + timeoutMs: 1800000 + args: + - --lane + - cli-installer-distribution diff --git a/qa/scenarios/cli/launcher-runtime.yaml b/qa/scenarios/cli/launcher-runtime.yaml new file mode 100644 index 000000000000..47e91e25f44f --- /dev/null +++ b/qa/scenarios/cli/launcher-runtime.yaml @@ -0,0 +1,26 @@ +title: CLI launcher runtime and recovery + +scenario: + id: cli-launcher-runtime + surface: cli + category: cli.cli-setup + coverage: + primary: + - cli.cli-entrypoint + - cli.supported-node-runtime + objective: Verify the actual OpenClaw launcher accepts supported runtimes, rejects unsupported runtimes, serves help and version, and reports source-build recovery guidance. + successCriteria: + - The copied launcher executes under every supported Node minimum and future supported majors. + - Below-floor and unsupported Node versions exit before loading the CLI with the documented runtime requirement. + - "`--help` and `--version` execute through the launcher fast path." + - A source checkout without built output reports the supported recovery command. + docsRefs: + - docs/install/index.md + - docs/help/troubleshooting.md + codeRefs: + - openclaw.mjs + - test/openclaw-launcher.e2e.test.ts + execution: + kind: vitest + path: test/openclaw-launcher.e2e.test.ts + summary: Executes copied launcher processes across supported and rejected runtime versions, CLI fast paths, and missing-build recovery. diff --git a/qa/scenarios/runtime/docker-package-install.yaml b/qa/scenarios/runtime/docker-package-install.yaml index b63bc65ee30c..ff0e783a7f03 100644 --- a/qa/scenarios/runtime/docker-package-install.yaml +++ b/qa/scenarios/runtime/docker-package-install.yaml @@ -5,16 +5,19 @@ scenario: surface: containers category: containers.image-release-and-validation coverage: + primary: + - cli.package-manager-installs secondary: - containers.docker-e2e-package-artifact-generation - - cli.package-manager-installs - objective: Build the real OpenClaw package artifact, install and execute it in the scheduler-selected bare container image, and record exact package, image, and container identities. + - cli.cli-entrypoint + objective: Build the real OpenClaw package artifact, install it globally with npm, pnpm, and Bun in clean containers, execute each manager-created PATH entrypoint, and record exact identities. successCriteria: - The canonical Docker package helper builds and validates the real npm tarball. - The scheduler prepares the package and bare target image through the shared planner and catalog. - - A clean target container installs the tarball with npm and executes the installed OpenClaw CLI. - - The installed package name and version match the tarball metadata. - - Evidence records the package digest and version, bare image ID, target container ID, installed package root, and executed CLI version. + - Clean target containers install the same tarball globally with npm, pnpm, and Bun. + - Each package manager creates a PATH-resolved OpenClaw executable that serves help and the candidate version. + - Every installed package version matches the tarball metadata. + - Evidence records the package digest and version, bare image ID, manager container IDs, package roots, executable paths, and CLI versions. docsRefs: - docs/install/updating.md - docs/help/testing.md @@ -27,7 +30,7 @@ scenario: execution: kind: script path: test/e2e/qa-lab/runtime/docker-artifact-proof.ts - summary: Builds the real package artifact, installs and executes it in a clean target container, and records artifact identities. + summary: Builds one real package artifact, installs and executes it through npm, pnpm, and Bun PATH entrypoints in clean containers, and records artifact identities. timeoutMs: 1800000 args: - --artifact-base diff --git a/qa/scenarios/runtime/docker-update-channel-switch.yaml b/qa/scenarios/runtime/docker-update-channel-switch.yaml index 84b16667bb95..1a131aef917c 100644 --- a/qa/scenarios/runtime/docker-update-channel-switch.yaml +++ b/qa/scenarios/runtime/docker-update-channel-switch.yaml @@ -7,14 +7,15 @@ scenario: coverage: primary: - cli.install-kind-switching - - cli.update-status-and-rpc - secondary: - cli.update-channels - objective: Verify package-to-git and git-to-package update switching plus machine-readable update status in Docker. + - cli.update-status-and-rpc + objective: Verify stable, beta, and dev channel selection, package-to-git and git-to-package switching, and machine-readable update status in Docker. successCriteria: + - The package install resolves the exact candidate through a hermetic beta dist-tag fixture, persists beta, and remains a package install. - The package install updates to a package-derived git dev fixture. - The git install updates back to the package tarball. - - "`openclaw update status --json` reports the expected git and package status after each switch." + - The beta, dev, and stable selections persist in canonical config. + - "`openclaw update status --json` reports the expected package, git, and package status after each switch." docsRefs: - docs/cli/update.md - docs/install/updating.md @@ -25,7 +26,7 @@ scenario: execution: kind: script path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts - summary: Runs the existing update-channel-switch Docker E2E lane and records QA Lab evidence for install-kind switching and update status coverage. + summary: Runs stable, beta, and dev update selections through the existing Docker E2E lane and records install-kind, persisted-channel, and update-status evidence. args: - --lane - update-channel-switch diff --git a/scripts/e2e/bun-global-install-smoke.sh b/scripts/e2e/bun-global-install-smoke.sh index cb5ad1385631..490a3e13b9ad 100755 --- a/scripts/e2e/bun-global-install-smoke.sh +++ b/scripts/e2e/bun-global-install-smoke.sh @@ -177,12 +177,35 @@ NODE fi echo "==> OpenClaw version through Bun global install" - run_with_timeout "$COMMAND_TIMEOUT_MS" "$openclaw_bin" --version + local openclaw_version + openclaw_version="$(run_with_timeout "$COMMAND_TIMEOUT_MS" "$openclaw_bin" --version)" + printf "%s\n" "$openclaw_version" + + echo "==> OpenClaw help through Bun global install" + run_with_timeout "$COMMAND_TIMEOUT_MS" "$openclaw_bin" --help >/dev/null echo "==> OpenClaw image providers through Bun global install" local providers_json providers_json="$(run_with_timeout "$COMMAND_TIMEOUT_MS" "$openclaw_bin" infer image providers --json)" OPENCLAW_IMAGE_PROVIDERS_JSON="$providers_json" node scripts/e2e/lib/bun-global-install/assertions.mjs assert-image-providers + + if [ -n "${OPENCLAW_BUN_GLOBAL_SMOKE_PROOF_PATH:-}" ]; then + node --input-type=module - \ + "$OPENCLAW_BUN_GLOBAL_SMOKE_PROOF_PATH" \ + "$bun_path" \ + "$openclaw_bin" \ + "$openclaw_version" <<'NODE' +import fs from "node:fs"; +import path from "node:path"; + +const [, , proofPath, bunPath, openclawPath, openclawVersion] = process.argv; +fs.mkdirSync(path.dirname(proofPath), { recursive: true }); +fs.writeFileSync( + proofPath, + `${JSON.stringify({ bunPath, openclawPath, openclawVersion }, null, 2)}\n`, +); +NODE + fi } main "$@" diff --git a/scripts/e2e/cli-installer-distribution-docker.sh b/scripts/e2e/cli-installer-distribution-docker.sh new file mode 100755 index 000000000000..43872afe3ad6 --- /dev/null +++ b/scripts/e2e/cli-installer-distribution-docker.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash +# Proves hosted npm installation plus dedicated-prefix source installation. +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" + +IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-cli-installer-distribution:local")" +PACKAGE_TGZ="$( + docker_e2e_prepare_package_tgz cli-installer-distribution "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}" +)" +HOSTED_PROOF_CONTAINER="openclaw-hosted-installer-proof-$$" +SOURCE_PROOF_CONTAINER="openclaw-source-installer-proof-$$" +SOURCE_BUNDLE="$(mktemp "${TMPDIR:-/tmp}/openclaw-source.XXXXXX.bundle")" +SOURCE_PROOF_SCRIPT="$(mktemp "${TMPDIR:-/tmp}/openclaw-source-proof.XXXXXX.sh")" +SOURCE_SHA="$(git -C "$ROOT_DIR" rev-parse HEAD)" +SOURCE_MEMORY="${OPENCLAW_CLI_INSTALLER_SOURCE_MEMORY:-16g}" + +cleanup() { + docker_e2e_docker_cmd rm -f \ + "$HOSTED_PROOF_CONTAINER" \ + "$SOURCE_PROOF_CONTAINER" >/dev/null 2>&1 || true + docker_e2e_cleanup_package_tgz "$PACKAGE_TGZ" + rm -f "$SOURCE_BUNDLE" "$SOURCE_PROOF_SCRIPT" +} +trap cleanup EXIT + +git -C "$ROOT_DIR" bundle create "$SOURCE_BUNDLE" HEAD +cat >"$SOURCE_PROOF_SCRIPT" <<'SOURCE_PROOF' +#!/usr/bin/env bash +set -euo pipefail + +test -r "$0" +test -x "$0" +command -v curl >/dev/null +git clone -q /tmp/openclaw-source.bundle /tmp/openclaw-source +git -C /tmp/openclaw-source checkout -q --detach "$OPENCLAW_SOURCE_SHA" +bash /tmp/openclaw-source/scripts/install-cli.sh \ + --install-method git \ + --git-dir /tmp/openclaw-source \ + --version "$OPENCLAW_SOURCE_SHA" \ + --no-git-update \ + --prefix /tmp/openclaw-prefix \ + --node-version 24.15.0 \ + --no-onboard + +prefix_node=/tmp/openclaw-prefix/tools/node/bin/node +prefix_cli=/tmp/openclaw-prefix/bin/openclaw +test -x "$prefix_node" +test -x "$prefix_cli" +grep -Fq "exec \"$prefix_node\"" "$prefix_cli" +grep -Fq "/tmp/openclaw-source/dist/entry.js" "$prefix_cli" +export PATH="/tmp/openclaw-prefix/bin:$PATH" +test "$(command -v openclaw)" = "$prefix_cli" +test "$(git -C /tmp/openclaw-source rev-parse HEAD)" = "$OPENCLAW_SOURCE_SHA" +openclaw_version="$(openclaw --version)" +openclaw --help >/tmp/openclaw-help +test -s /tmp/openclaw-help +status_json="$(openclaw update status --json)" +STATUS_JSON="$status_json" node -e " + const status = JSON.parse(process.env.STATUS_JSON); + if (status.update?.installKind !== \"git\") { + throw new Error(\`expected git install kind, got \${status.update?.installKind}\`); + } +" +printf "prefixNode=%s@%s\n" "$prefix_node" "$("$prefix_node" --version)" +printf "prefixOpenClaw=%s@%s\n" "$prefix_cli" "$openclaw_version" +printf "sourceHead=%s installKind=git\n" "$OPENCLAW_SOURCE_SHA" +printf "sourceOnboard=disabled\n" +touch /tmp/openclaw-proof-ready +exec sleep infinity +SOURCE_PROOF +chmod 0555 "$SOURCE_PROOF_SCRIPT" + +docker_e2e_build_or_reuse \ + "$IMAGE_NAME" \ + cli-installer-distribution \ + "$ROOT_DIR/scripts/e2e/Dockerfile" \ + "$ROOT_DIR" \ + bare + +echo "==> Hosted install.sh exact-candidate proof" +docker_e2e_docker_run_cmd run -d \ + --name "$HOSTED_PROOF_CONTAINER" \ + -e HOME=/tmp/openclaw-hosted-home \ + -e OPENCLAW_NO_ONBOARD=1 \ + -e OPENCLAW_NO_PROMPT=1 \ + -v "$PACKAGE_TGZ:/tmp/openclaw-current.tgz:ro" \ + -v "$ROOT_DIR/scripts/install.sh:/tmp/install.sh:ro" \ + "$IMAGE_NAME" \ + bash -lc ' + set -euo pipefail + mkdir -p "$HOME" + bash /tmp/install.sh \ + --install-method npm \ + --version file:/tmp/openclaw-current.tgz \ + --no-onboard \ + --no-prompt + source "$HOME/.bashrc" + hash -r + openclaw_path="$(command -v openclaw)" + test -n "$openclaw_path" + node_path="$(command -v node)" + node_version="$(node --version)" + openclaw_version="$(openclaw --version)" + openclaw --help >/tmp/openclaw-help + test -s /tmp/openclaw-help + printf "hostedNode=%s@%s\n" "$node_path" "$node_version" + printf "hostedOpenClaw=%s@%s\n" "$openclaw_path" "$openclaw_version" + printf "hostedOnboard=disabled\n" + touch /tmp/openclaw-proof-ready + exec sleep infinity + ' >/dev/null + +# A full source install builds every workspace package; the shared 8g cap OOMs before wrapper creation. +echo "==> install-cli.sh dedicated-prefix source-checkout proof" +docker_e2e_docker_run_cmd run -d \ + --name "$SOURCE_PROOF_CONTAINER" \ + --memory "$SOURCE_MEMORY" \ + -e HOME=/tmp/openclaw-source-home \ + -e OPENCLAW_NO_ONBOARD=1 \ + -e OPENCLAW_NO_PROMPT=1 \ + -e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ + -e "OPENCLAW_SOURCE_SHA=$SOURCE_SHA" \ + --user root \ + -v "$SOURCE_BUNDLE:/tmp/openclaw-source.bundle:ro" \ + -v "$SOURCE_PROOF_SCRIPT:/tmp/source-proof.sh:ro" \ + "$IMAGE_NAME" \ + bash -lc ' + set -euo pipefail + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl + rm -rf /var/lib/apt/lists/* + command -v curl >/dev/null + install -d -o appuser -g appuser "$HOME" + exec runuser -u appuser -- env \ + HOME="$HOME" \ + OPENCLAW_NO_ONBOARD="$OPENCLAW_NO_ONBOARD" \ + OPENCLAW_NO_PROMPT="$OPENCLAW_NO_PROMPT" \ + COREPACK_ENABLE_DOWNLOAD_PROMPT="$COREPACK_ENABLE_DOWNLOAD_PROMPT" \ + OPENCLAW_SOURCE_SHA="$OPENCLAW_SOURCE_SHA" \ + bash /tmp/source-proof.sh + ' >/dev/null + +wait_for_proof() { + local container_name="$1" + for _ in $(seq 1 1200); do + if docker exec "$container_name" test -f /tmp/openclaw-proof-ready; then + docker logs "$container_name" + return 0 + fi + if [ "$(docker inspect --format '{{.State.Running}}' "$container_name")" != "true" ]; then + docker logs "$container_name" >&2 + return 1 + fi + sleep 1 + done + docker logs "$container_name" >&2 + return 1 +} + +wait_for_proof "$HOSTED_PROOF_CONTAINER" +wait_for_proof "$SOURCE_PROOF_CONTAINER" +echo "CLI installer distribution proof passed." diff --git a/scripts/e2e/docker-package-install.sh b/scripts/e2e/docker-package-install.sh index 16fe9c025eb7..ed1e3e289bd1 100755 --- a/scripts/e2e/docker-package-install.sh +++ b/scripts/e2e/docker-package-install.sh @@ -7,62 +7,157 @@ source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-docker-e2e-bare:local")" PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz docker-package-install "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")" IDENTITY_PATH="${OPENCLAW_DOCKER_ARTIFACT_IDENTITY_PATH:-$ROOT_DIR/.artifacts/docker-tests/docker-package-install-identities.json}" -CONTAINER_NAME="openclaw-package-proof-$$" +NPM_PROOF_CONTAINER="openclaw-package-npm-proof-$$" +PNPM_PROOF_CONTAINER="openclaw-package-pnpm-proof-$$" +BUN_PROOF_CONTAINER="openclaw-package-bun-proof-$$" DOCKER_RUN_TIMEOUT="${OPENCLAW_DOCKER_PACKAGE_INSTALL_RUN_TIMEOUT:-120s}" +BUN_HARNESS_DIR="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-bun-harness.XXXXXX")" cleanup() { - docker_e2e_docker_cmd rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true + docker_e2e_docker_cmd rm -f \ + "$NPM_PROOF_CONTAINER" \ + "$PNPM_PROOF_CONTAINER" \ + "$BUN_PROOF_CONTAINER" >/dev/null 2>&1 || true docker_e2e_cleanup_package_tgz "$PACKAGE_TGZ" + rm -rf "$BUN_HARNESS_DIR" } trap cleanup EXIT docker_e2e_build_or_reuse "$IMAGE_NAME" docker-package-install "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" bare -echo "Installing the real OpenClaw package artifact in the target container..." +for harness_path in \ + scripts/e2e/bun-global-install-smoke.sh \ + scripts/e2e/lib/bun-global-install/assertions.mjs \ + scripts/lib/docker-e2e-container.sh \ + scripts/lib/docker-e2e-logs.sh \ + scripts/lib/docker-e2e-package.sh \ + scripts/lib/docker-e2e-resource-diagnostics.sh; do + mkdir -p "$BUN_HARNESS_DIR/$(dirname "$harness_path")" + cp "$ROOT_DIR/$harness_path" "$BUN_HARNESS_DIR/$harness_path" +done +chmod -R a+rX "$BUN_HARNESS_DIR" + +echo "Installing the real OpenClaw package artifact with npm..." DOCKER_COMMAND_TIMEOUT="$DOCKER_RUN_TIMEOUT" docker_e2e_docker_run_cmd run -d \ - --name "$CONTAINER_NAME" \ + --name "$NPM_PROOF_CONTAINER" \ -v "$PACKAGE_TGZ:/tmp/openclaw-current.tgz:ro" \ "$IMAGE_NAME" \ bash -lc ' set -euo pipefail npm install -g --prefix /tmp/openclaw-proof /tmp/openclaw-current.tgz --no-fund --no-audit + export PATH="/tmp/openclaw-proof/bin:$PATH" package_root=/tmp/openclaw-proof/lib/node_modules/openclaw - "$package_root/openclaw.mjs" --version > /tmp/openclaw-version - "$package_root/openclaw.mjs" --help > /tmp/openclaw-help + test "$(command -v openclaw)" = "/tmp/openclaw-proof/bin/openclaw" + openclaw --version > /tmp/openclaw-version + openclaw --help > /tmp/openclaw-help test -s /tmp/openclaw-help touch /tmp/openclaw-proof-ready exec sleep infinity ' >/dev/null -for _ in $(seq 1 240); do - if docker exec "$CONTAINER_NAME" test -f /tmp/openclaw-proof-ready; then - break - fi - if [ "$(docker inspect --format '{{.State.Running}}' "$CONTAINER_NAME")" != "true" ]; then - docker logs "$CONTAINER_NAME" >&2 +echo "Installing the real OpenClaw package artifact with pnpm..." +DOCKER_COMMAND_TIMEOUT="$DOCKER_RUN_TIMEOUT" docker_e2e_docker_run_cmd run -d \ + --name "$PNPM_PROOF_CONTAINER" \ + -v "$PACKAGE_TGZ:/tmp/openclaw-current.tgz:ro" \ + "$IMAGE_NAME" \ + bash -lc ' + set -euo pipefail + export PNPM_HOME=/tmp/pnpm-home + export PATH="$PNPM_HOME:$PATH" + corepack prepare pnpm@11.15.1 --activate + pnpm config set global-bin-dir "$PNPM_HOME" + pnpm config set global-dir /tmp/pnpm-global + pnpm add --global --allow-build=openclaw /tmp/openclaw-current.tgz + test "$(command -v openclaw)" = "$PNPM_HOME/openclaw" + package_root="$(pnpm root --global)/openclaw" + printf "%s\n" "$package_root" > /tmp/openclaw-package-root + openclaw --version > /tmp/openclaw-version + openclaw --help > /tmp/openclaw-help + test -s /tmp/openclaw-help + touch /tmp/openclaw-proof-ready + exec sleep infinity + ' >/dev/null + +echo "Installing the real OpenClaw package artifact with Bun..." +DOCKER_COMMAND_TIMEOUT="$DOCKER_RUN_TIMEOUT" docker_e2e_docker_run_cmd run -d \ + --name "$BUN_PROOF_CONTAINER" \ + -v "$PACKAGE_TGZ:/tmp/openclaw-current.tgz:ro" \ + -v "$BUN_HARNESS_DIR:/repo:ro" \ + "$IMAGE_NAME" \ + bash -lc ' + set -euo pipefail + npm install -g --prefix /tmp/bun-runtime bun@1.3.14 --no-fund --no-audit + cd /repo + BUN_BIN=/tmp/bun-runtime/bin/bun \ + OPENCLAW_BUN_GLOBAL_SMOKE_HOST_BUILD=0 \ + OPENCLAW_BUN_GLOBAL_SMOKE_PACKAGE_TGZ=/tmp/openclaw-current.tgz \ + OPENCLAW_BUN_GLOBAL_SMOKE_PROOF_PATH=/tmp/openclaw-bun-proof.json \ + bash scripts/e2e/bun-global-install-smoke.sh + touch /tmp/openclaw-proof-ready + exec sleep infinity + ' >/dev/null + +wait_for_proof() { + local container_name="$1" + for _ in $(seq 1 240); do + if docker exec "$container_name" test -f /tmp/openclaw-proof-ready; then + return 0 + fi + if [ "$(docker inspect --format '{{.State.Running}}' "$container_name")" != "true" ]; then + docker logs "$container_name" >&2 + return 1 + fi + sleep 1 + done + docker logs "$container_name" >&2 + return 1 +} + +for container_name in "$NPM_PROOF_CONTAINER" "$PNPM_PROOF_CONTAINER" "$BUN_PROOF_CONTAINER"; do + wait_for_proof "$container_name" +done + +NPM_PACKAGE_ROOT="/tmp/openclaw-proof/lib/node_modules/openclaw" +NPM_INSTALLED_VERSION="$(docker exec "$NPM_PROOF_CONTAINER" cat /tmp/openclaw-version | tr -d '\r\n')" +PNPM_PACKAGE_ROOT="$(docker exec "$PNPM_PROOF_CONTAINER" cat /tmp/openclaw-package-root | tr -d '\r\n')" +PNPM_INSTALLED_VERSION="$(docker exec "$PNPM_PROOF_CONTAINER" cat /tmp/openclaw-version | tr -d '\r\n')" +BUN_OPENCLAW_PATH="$( + docker exec "$BUN_PROOF_CONTAINER" \ + node -p 'JSON.parse(require("node:fs").readFileSync("/tmp/openclaw-bun-proof.json", "utf8")).openclawPath' +)" +BUN_INSTALLED_VERSION="$( + docker exec "$BUN_PROOF_CONTAINER" \ + node -p 'JSON.parse(require("node:fs").readFileSync("/tmp/openclaw-bun-proof.json", "utf8")).openclawVersion' +)" +PACKAGE_VERSION="$(docker exec "$NPM_PROOF_CONTAINER" node -p "require('$NPM_PACKAGE_ROOT/package.json').version")" +for installed_version in "$NPM_INSTALLED_VERSION" "$PNPM_INSTALLED_VERSION" "$BUN_INSTALLED_VERSION"; do + if [[ "$installed_version" != *"$PACKAGE_VERSION"* ]]; then + echo "installed CLI output $installed_version does not contain package version $PACKAGE_VERSION" >&2 exit 1 fi - sleep 1 done -docker exec "$CONTAINER_NAME" test -f /tmp/openclaw-proof-ready - -INSTALLED_VERSION="$(docker exec "$CONTAINER_NAME" cat /tmp/openclaw-version | tr -d '\r\n')" -PACKAGE_ROOT="/tmp/openclaw-proof/lib/node_modules/openclaw" -PACKAGE_VERSION="$(docker exec "$CONTAINER_NAME" node -p "require('$PACKAGE_ROOT/package.json').version")" -if [[ "$INSTALLED_VERSION" != *"$PACKAGE_VERSION"* ]]; then - echo "installed CLI output $INSTALLED_VERSION does not contain package version $PACKAGE_VERSION" >&2 - exit 1 -fi node --import tsx "$ROOT_DIR/scripts/e2e/lib/docker-artifact-proof/write-identities.ts" \ --scenario docker-package-install \ --output "$IDENTITY_PATH" \ --image "$IMAGE_NAME" \ --package "$PACKAGE_TGZ" \ - --container "target=$CONTAINER_NAME" \ - --detail "target:installedPackageRoot=$PACKAGE_ROOT" \ - --detail "target:installedPackageVersion=$PACKAGE_VERSION" \ - --detail "target:openclawVersion=$INSTALLED_VERSION" \ - --detail "target:helpCommand=passed" + --container "npm=$NPM_PROOF_CONTAINER" \ + --container "pnpm=$PNPM_PROOF_CONTAINER" \ + --container "bun=$BUN_PROOF_CONTAINER" \ + --detail "npm:installedPackageRoot=$NPM_PACKAGE_ROOT" \ + --detail "npm:installedPackageVersion=$PACKAGE_VERSION" \ + --detail "npm:openclawVersion=$NPM_INSTALLED_VERSION" \ + --detail "npm:openclawPath=/tmp/openclaw-proof/bin/openclaw" \ + --detail "npm:helpCommand=passed" \ + --detail "pnpm:installedPackageRoot=$PNPM_PACKAGE_ROOT" \ + --detail "pnpm:installedPackageVersion=$PACKAGE_VERSION" \ + --detail "pnpm:openclawVersion=$PNPM_INSTALLED_VERSION" \ + --detail "pnpm:openclawPath=/tmp/pnpm-home/openclaw" \ + --detail "pnpm:helpCommand=passed" \ + --detail "bun:installedPackageVersion=$PACKAGE_VERSION" \ + --detail "bun:openclawVersion=$BUN_INSTALLED_VERSION" \ + --detail "bun:openclawPath=$BUN_OPENCLAW_PATH" \ + --detail "bun:helpCommand=passed" -echo "Package artifact container proof passed." +echo "npm, pnpm, and Bun package artifact proofs passed." diff --git a/scripts/e2e/lib/plugins/npm-registry-server.mjs b/scripts/e2e/lib/plugins/npm-registry-server.mjs index e10c3f8ce353..dff4b8f17ba3 100644 --- a/scripts/e2e/lib/plugins/npm-registry-server.mjs +++ b/scripts/e2e/lib/plugins/npm-registry-server.mjs @@ -30,6 +30,21 @@ function normalizeUpstreamRegistry(raw) { } const upstreamRegistry = normalizeUpstreamRegistry(process.env.OPENCLAW_NPM_REGISTRY_UPSTREAM); +const distTagOverrides = new Map( + (process.env.OPENCLAW_NPM_REGISTRY_DIST_TAGS ?? "") + .split(",") + .map((entry) => entry.trim()) + .filter(Boolean) + .map((entry) => { + const separator = entry.indexOf("="); + if (separator <= 0 || separator === entry.length - 1) { + throw new Error( + "OPENCLAW_NPM_REGISTRY_DIST_TAGS must contain comma-separated tag=version entries", + ); + } + return [entry.slice(0, separator).trim(), entry.slice(separator + 1).trim()]; + }), +); // Match other E2E package-download budgets while keeping public-registry hops // inside the install deadline and decoded bodies inside a fixed memory budget. const UPSTREAM_REQUEST_TIMEOUT_MS = 120_000; @@ -88,7 +103,10 @@ for (let index = 0; index < packageArgs.length; index += 3) { const metadataFor = (entry, baseUrl) => ({ name: entry.packageName, - "dist-tags": { latest: entry.latestVersion }, + "dist-tags": { + latest: entry.latestVersion, + ...Object.fromEntries(distTagOverrides), + }, versions: Object.fromEntries( [...entry.versions.entries()].map(([version, versionEntry]) => [ version, diff --git a/scripts/e2e/lib/update-channel-switch/assertions.mjs b/scripts/e2e/lib/update-channel-switch/assertions.mjs index b022e3a4142e..65882ed225bc 100644 --- a/scripts/e2e/lib/update-channel-switch/assertions.mjs +++ b/scripts/e2e/lib/update-channel-switch/assertions.mjs @@ -8,7 +8,7 @@ const controlUiHtml = "