From dac72374944f35fb89e21dce0f3f2299f27d03c9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 29 Apr 2026 20:40:32 +0100 Subject: [PATCH] ci: shard release validation hotspots --- .github/workflows/install-smoke.yml | 10 +- .../openclaw-live-and-e2e-checks-reusable.yml | 82 ++++-- scripts/docker/install-sh-e2e/run.sh | 240 +++++++++++++----- scripts/test-install-sh-e2e-docker.sh | 1 + test/scripts/docker-build-helper.test.ts | 19 +- .../package-acceptance-workflow.test.ts | 9 + test/scripts/test-install-sh-docker.test.ts | 2 + 7 files changed, 270 insertions(+), 93 deletions(-) diff --git a/.github/workflows/install-smoke.yml b/.github/workflows/install-smoke.yml index 380b8e6be76..b3299ca2795 100644 --- a/.github/workflows/install-smoke.yml +++ b/.github/workflows/install-smoke.yml @@ -107,13 +107,15 @@ jobs: with: max-cache-size-mb: 800000 - # Blacksmith's builder owns the Docker layer cache; keep smoke builds off - # explicit gha cache directives so local tags still load cleanly. + # Keep the Blacksmith builder hot across release runs. The image still + # loads into the local Docker daemon for downstream smoke containers. - name: Build root Dockerfile smoke image uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2 with: context: . file: ./Dockerfile + cache-from: type=gha,scope=openclaw-dockerfile-smoke + cache-to: type=gha,scope=openclaw-dockerfile-smoke,mode=max build-args: | OPENCLAW_EXTENSIONS=matrix tags: | @@ -216,8 +218,6 @@ jobs: with: max-cache-size-mb: 800000 - # Blacksmith's builder owns the Docker layer cache; keep smoke builds off - # explicit gha cache directives so local tags still load cleanly. - name: Run QR package install smoke env: OPENCLAW_QR_SMOKE_FORCE_INSTALL: "1" @@ -230,6 +230,8 @@ jobs: with: context: . file: ./Dockerfile + cache-from: type=gha,scope=openclaw-dockerfile-smoke + cache-to: type=gha,scope=openclaw-dockerfile-smoke,mode=max build-args: | OPENCLAW_EXTENSIONS=matrix tags: | diff --git a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml index dd6335c5f84..7e438e9bea8 100644 --- a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml +++ b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml @@ -1747,9 +1747,31 @@ jobs: timeout_minutes: 90 profile_env_only: false profiles: full - - suite_id: native-live-src-gateway-profiles-opencode-go - label: Native live gateway profiles OpenCode Go deep - command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=opencode-go node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles + - suite_id: native-live-src-gateway-profiles-opencode-go-deepseek-glm + suite_group: native-live-src-gateway-profiles-opencode-go + label: Native live gateway profiles OpenCode Go DeepSeek/GLM + command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=opencode-go OPENCLAW_LIVE_GATEWAY_MODELS=opencode-go/deepseek-v4-flash,opencode-go/deepseek-v4-pro,opencode-go/glm-5,opencode-go/glm-5.1 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles + timeout_minutes: 90 + profile_env_only: false + profiles: full + - suite_id: native-live-src-gateway-profiles-opencode-go-kimi + suite_group: native-live-src-gateway-profiles-opencode-go + label: Native live gateway profiles OpenCode Go Kimi + command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=opencode-go OPENCLAW_LIVE_GATEWAY_MODELS=opencode-go/kimi-k2.5,opencode-go/kimi-k2.6 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles + timeout_minutes: 90 + profile_env_only: false + profiles: full + - suite_id: native-live-src-gateway-profiles-opencode-go-mimo + suite_group: native-live-src-gateway-profiles-opencode-go + label: Native live gateway profiles OpenCode Go MiMo + command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=opencode-go OPENCLAW_LIVE_GATEWAY_MODELS=opencode-go/mimo-v2-omni,opencode-go/mimo-v2-pro,opencode-go/mimo-v2.5,opencode-go/mimo-v2.5-pro node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles + timeout_minutes: 90 + profile_env_only: false + profiles: full + - suite_id: native-live-src-gateway-profiles-opencode-go-minimax-qwen + suite_group: native-live-src-gateway-profiles-opencode-go + label: Native live gateway profiles OpenCode Go MiniMax/Qwen + command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=opencode-go OPENCLAW_LIVE_GATEWAY_MODELS=opencode-go/minimax-m2.5,opencode-go/minimax-m2.7,opencode-go/qwen3.5-plus,opencode-go/qwen3.6-plus node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles timeout_minutes: 90 profile_env_only: false profiles: full @@ -1871,14 +1893,14 @@ jobs: OPENCLAW_VITEST_MAX_WORKERS: "2" steps: - name: Checkout selected ref - if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-src-gateway-profiles-opencode-go' && startsWith(matrix.suite_id, 'native-live-src-gateway-profiles-opencode-go-'))) uses: actions/checkout@v6 with: ref: ${{ needs.validate_selected_ref.outputs.selected_sha }} fetch-depth: 1 - name: Checkout trusted live shard harness - if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-src-gateway-profiles-opencode-go' && startsWith(matrix.suite_id, 'native-live-src-gateway-profiles-opencode-go-'))) uses: actions/checkout@v6 with: ref: ${{ github.sha }} @@ -1886,7 +1908,7 @@ jobs: path: .release-harness - name: Setup Node environment - if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-src-gateway-profiles-opencode-go' && startsWith(matrix.suite_id, 'native-live-src-gateway-profiles-opencode-go-'))) uses: ./.github/actions/setup-node-env with: node-version: ${{ env.NODE_VERSION }} @@ -1894,11 +1916,11 @@ jobs: install-bun: "true" - name: Hydrate live auth/profile inputs - if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-src-gateway-profiles-opencode-go' && startsWith(matrix.suite_id, 'native-live-src-gateway-profiles-opencode-go-'))) run: bash scripts/ci-hydrate-live-auth.sh - name: Configure suite-specific env - if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-src-gateway-profiles-opencode-go' && startsWith(matrix.suite_id, 'native-live-src-gateway-profiles-opencode-go-'))) shell: bash run: | set -euo pipefail @@ -1951,7 +1973,7 @@ jobs: esac - name: Run ${{ matrix.label }} - if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-src-gateway-profiles-opencode-go' && startsWith(matrix.suite_id, 'native-live-src-gateway-profiles-opencode-go-'))) env: OPENCLAW_LIVE_COMMAND: ${{ matrix.command }} OPENCLAW_LIVE_SUITE_ADVISORY: ${{ matrix.advisory }} @@ -2169,9 +2191,31 @@ jobs: timeout_minutes: 90 profile_env_only: false profiles: full - - suite_id: native-live-extensions-media-video - label: Native live media video plugins - command: node .release-harness/scripts/test-live-shard.mjs native-live-extensions-media-video + - suite_id: native-live-extensions-media-video-a + suite_group: native-live-extensions-media-video + label: Native live media video plugins A + command: OPENCLAW_LIVE_VIDEO_GENERATION_PROVIDERS=alibaba,byteplus,deepinfra,fal node .release-harness/scripts/test-live-shard.mjs native-live-extensions-media-video + timeout_minutes: 90 + profile_env_only: false + profiles: full + - suite_id: native-live-extensions-media-video-b + suite_group: native-live-extensions-media-video + label: Native live media video plugins B + command: OPENCLAW_LIVE_VIDEO_GENERATION_PROVIDERS=google,minimax node .release-harness/scripts/test-live-shard.mjs native-live-extensions-media-video + timeout_minutes: 90 + profile_env_only: false + profiles: full + - suite_id: native-live-extensions-media-video-c + suite_group: native-live-extensions-media-video + label: Native live media video plugins C + command: OPENCLAW_LIVE_VIDEO_GENERATION_PROVIDERS=openai,openrouter,xai node .release-harness/scripts/test-live-shard.mjs native-live-extensions-media-video + timeout_minutes: 90 + profile_env_only: false + profiles: full + - suite_id: native-live-extensions-media-video-d + suite_group: native-live-extensions-media-video + label: Native live media video plugins D + command: OPENCLAW_LIVE_VIDEO_GENERATION_PROVIDERS=qwen,runway,together,vydra node .release-harness/scripts/test-live-shard.mjs native-live-extensions-media-video timeout_minutes: 90 profile_env_only: false profiles: full @@ -2226,14 +2270,14 @@ jobs: OPENCLAW_VITEST_MAX_WORKERS: "2" steps: - name: Checkout selected ref - if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-extensions-media-video' && startsWith(matrix.suite_id, 'native-live-extensions-media-video-'))) uses: actions/checkout@v6 with: ref: ${{ needs.validate_selected_ref.outputs.selected_sha }} fetch-depth: 1 - name: Checkout trusted live shard harness - if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-extensions-media-video' && startsWith(matrix.suite_id, 'native-live-extensions-media-video-'))) uses: actions/checkout@v6 with: ref: ${{ github.sha }} @@ -2241,7 +2285,7 @@ jobs: path: .release-harness - name: Verify preinstalled live media dependencies - if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-extensions-media-video' && startsWith(matrix.suite_id, 'native-live-extensions-media-video-'))) shell: bash run: | set -euo pipefail @@ -2249,7 +2293,7 @@ jobs: ffprobe -version | head -1 - name: Setup Node environment - if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-extensions-media-video' && startsWith(matrix.suite_id, 'native-live-extensions-media-video-'))) uses: ./.github/actions/setup-node-env with: node-version: ${{ env.NODE_VERSION }} @@ -2257,11 +2301,11 @@ jobs: install-bun: "true" - name: Hydrate live auth/profile inputs - if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-extensions-media-video' && startsWith(matrix.suite_id, 'native-live-extensions-media-video-'))) run: bash scripts/ci-hydrate-live-auth.sh - name: Configure suite-specific env - if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-extensions-media-video' && startsWith(matrix.suite_id, 'native-live-extensions-media-video-'))) shell: bash run: | set -euo pipefail @@ -2270,5 +2314,5 @@ jobs: fi - name: Run ${{ matrix.label }} - if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-extensions-media-video' && startsWith(matrix.suite_id, 'native-live-extensions-media-video-'))) run: ${{ matrix.command }} diff --git a/scripts/docker/install-sh-e2e/run.sh b/scripts/docker/install-sh-e2e/run.sh index 1ed310f3ac1..5b4e10dfb7c 100755 --- a/scripts/docker/install-sh-e2e/run.sh +++ b/scripts/docker/install-sh-e2e/run.sh @@ -24,6 +24,40 @@ OPENAI_API_KEY="${OPENAI_API_KEY:-}" ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-}" ANTHROPIC_API_TOKEN="${ANTHROPIC_API_TOKEN:-}" AGENT_TURN_TIMEOUT_SECONDS="${OPENCLAW_INSTALL_E2E_AGENT_TURN_TIMEOUT_SECONDS:-600}" +AGENT_TURNS_PARALLEL="${OPENCLAW_INSTALL_E2E_AGENT_TURNS_PARALLEL:-1}" + +time_phase() { + local name="$1" + shift + local started_at + local finished_at + local status + started_at="$(date +%s)" + echo "==> Phase start: $name" + set +e + "$@" + status="$?" + set -e + finished_at="$(date +%s)" + if [[ "$status" -eq 0 ]]; then + echo "==> Phase passed: $name ($((finished_at - started_at))s)" + else + echo "==> Phase failed: $name ($((finished_at - started_at))s, status=$status)" >&2 + fi + return "$status" +} + +PHASE_MARK_STARTED_AT=0 + +phase_mark_start() { + PHASE_MARK_STARTED_AT="$(date +%s)" + echo "==> Phase start: $1" +} + +phase_mark_passed() { + local name="$1" + echo "==> Phase passed: $name ($(($(date +%s) - PHASE_MARK_STARTED_AT))s)" +} # This image runs as a non-root user, so seed a user-local npm prefix before we # preinstall an older global version to exercise the upgrade path. @@ -53,48 +87,58 @@ elif [[ "$MODELS_MODE" == "anthropic" && -z "$ANTHROPIC_API_TOKEN" && -z "$ANTHR exit 2 fi -echo "==> Resolve npm versions" -EXPECTED_VERSION="$(quiet_npm view "openclaw@${INSTALL_TAG}" version)" -if [[ -z "$EXPECTED_VERSION" || "$EXPECTED_VERSION" == "undefined" || "$EXPECTED_VERSION" == "null" ]]; then - echo "ERROR: unable to resolve openclaw@${INSTALL_TAG} version" >&2 - exit 2 -fi -if [[ -n "$E2E_PREVIOUS_VERSION" ]]; then - PREVIOUS_VERSION="$E2E_PREVIOUS_VERSION" -else - PREVIOUS_VERSION="$(VERSIONS_JSON="$(quiet_npm view openclaw versions --json)" node - <<'NODE' +resolve_npm_versions() { + EXPECTED_VERSION="$(quiet_npm view "openclaw@${INSTALL_TAG}" version)" + if [[ -z "$EXPECTED_VERSION" || "$EXPECTED_VERSION" == "undefined" || "$EXPECTED_VERSION" == "null" ]]; then + echo "ERROR: unable to resolve openclaw@${INSTALL_TAG} version" >&2 + return 2 + fi + if [[ -n "$E2E_PREVIOUS_VERSION" ]]; then + PREVIOUS_VERSION="$E2E_PREVIOUS_VERSION" + else + PREVIOUS_VERSION="$(VERSIONS_JSON="$(quiet_npm view openclaw versions --json)" node - <<'NODE' const versions = JSON.parse(process.env.VERSIONS_JSON || "[]"); if (!Array.isArray(versions) || versions.length === 0) process.exit(1); process.stdout.write(versions.length >= 2 ? versions[versions.length - 2] : versions[0]); NODE - )" -fi -echo "expected=$EXPECTED_VERSION previous=$PREVIOUS_VERSION" + )" + fi + echo "expected=$EXPECTED_VERSION previous=$PREVIOUS_VERSION" +} -if [[ "$SKIP_PREVIOUS" == "1" ]]; then - echo "==> Skip preinstall previous (OPENCLAW_INSTALL_E2E_SKIP_PREVIOUS=1)" -else - echo "==> Preinstall previous (forces installer upgrade path; avoids read() prompt)" - quiet_npm install -g "openclaw@${PREVIOUS_VERSION}" -fi +preinstall_previous_version() { + if [[ "$SKIP_PREVIOUS" == "1" ]]; then + echo "Skip preinstall previous (OPENCLAW_INSTALL_E2E_SKIP_PREVIOUS=1)" + else + echo "Preinstall previous (forces installer upgrade path; avoids read() prompt)" + quiet_npm install -g "openclaw@${PREVIOUS_VERSION}" + fi +} -echo "==> Run official installer one-liner" -if [[ "$INSTALL_TAG" == "beta" ]]; then - curl -fsSL "$INSTALL_URL" | OPENCLAW_BETA=1 bash -elif [[ "$INSTALL_TAG" != "latest" ]]; then - curl -fsSL "$INSTALL_URL" | OPENCLAW_VERSION="$INSTALL_TAG" bash -else - curl -fsSL "$INSTALL_URL" | bash -fi +run_official_installer() { + if [[ "$INSTALL_TAG" == "beta" ]]; then + curl -fsSL "$INSTALL_URL" | OPENCLAW_BETA=1 bash + elif [[ "$INSTALL_TAG" != "latest" ]]; then + curl -fsSL "$INSTALL_URL" | OPENCLAW_VERSION="$INSTALL_TAG" bash + else + curl -fsSL "$INSTALL_URL" | bash + fi +} -echo "==> Verify installed version" -INSTALLED_VERSION="$(openclaw --version 2>/dev/null | head -n 1 | tr -d '\r')" -INSTALLED_VERSION="$(extract_openclaw_semver "$INSTALLED_VERSION")" -echo "installed=$INSTALLED_VERSION expected=$EXPECTED_VERSION" -if [[ "$INSTALLED_VERSION" != "$EXPECTED_VERSION" ]]; then - echo "ERROR: expected openclaw@$EXPECTED_VERSION, got openclaw@$INSTALLED_VERSION" >&2 - exit 1 -fi +verify_installed_version() { + INSTALLED_VERSION="$(openclaw --version 2>/dev/null | head -n 1 | tr -d '\r')" + INSTALLED_VERSION="$(extract_openclaw_semver "$INSTALLED_VERSION")" + echo "installed=$INSTALLED_VERSION expected=$EXPECTED_VERSION" + if [[ "$INSTALLED_VERSION" != "$EXPECTED_VERSION" ]]; then + echo "ERROR: expected openclaw@$EXPECTED_VERSION, got openclaw@$INSTALLED_VERSION" >&2 + return 1 + fi +} + +time_phase "Resolve npm versions" resolve_npm_versions +time_phase "Preinstall previous" preinstall_previous_version +time_phase "Run official installer one-liner" run_official_installer +time_phase "Verify installed version" verify_installed_version set_image_model() { local profile="$1" @@ -249,6 +293,46 @@ fs.writeFileSync(path, `${JSON.stringify(parsed, null, 2)}\n`, "utf8"); NODE } +RUN_AGENT_TURN_BG_PID="" + +run_agent_turn_logged() { + local label="$1" + local profile="$2" + local session_id="$3" + local prompt="$4" + local out_json="$5" + local started_at + SESSION_JSONL="$(session_jsonl_path "$profile" "$session_id")" + started_at="$(date +%s)" + echo "==> Agent turn start: $label ($profile)" + run_agent_turn "$profile" "$session_id" "$prompt" "$out_json" + echo "==> Agent turn passed: $label ($profile, $(($(date +%s) - started_at))s)" +} + +run_agent_turn_bg() { + local label="$1" + local profile="$2" + local session_id="$3" + local prompt="$4" + local out_json="$5" + ( + set -euo pipefail + run_agent_turn_logged "$label" "$profile" "$session_id" "$prompt" "$out_json" + ) & + RUN_AGENT_TURN_BG_PID="$!" +} + +wait_agent_turn_batch() { + local failed=0 + local pid + for pid in "$@"; do + if ! wait "$pid"; then + failed=1 + fi + done + return "$failed" +} + dump_profile_debug() { local profile="$1" local turn_output="$2" @@ -450,7 +534,7 @@ run_profile() { local workspace="$3" local agent_model_provider="$4" # "openai"|"anthropic" - echo "==> Onboard ($profile)" + phase_mark_start "Onboard ($profile)" if [[ "$agent_model_provider" == "openai" ]]; then openclaw --profile "$profile" onboard \ --non-interactive \ @@ -501,8 +585,9 @@ run_profile() { --workspace "$workspace" \ --skip-health fi + phase_mark_passed "Onboard ($profile)" - echo "==> Verify workspace identity files ($profile)" + phase_mark_start "Verify workspace identity files ($profile)" test -f "$workspace/AGENTS.md" test -f "$workspace/IDENTITY.md" test -f "$workspace/USER.md" @@ -512,8 +597,9 @@ run_profile() { # first-run identity ritual. Drop BOOTSTRAP.md so provider prompts stay focused # on the fixture task and do not spend turns following onboarding copy. rm -f "$workspace/BOOTSTRAP.md" + phase_mark_passed "Verify workspace identity files ($profile)" - echo "==> Configure models ($profile)" + phase_mark_start "Configure models ($profile)" local agent_model local image_model if [[ "$agent_model_provider" == "openai" ]]; then @@ -534,8 +620,9 @@ run_profile() { fi echo "model=$agent_model" echo "imageModel=$image_model" + phase_mark_passed "Configure models ($profile)" - echo "==> Prepare tool fixtures ($profile)" + phase_mark_start "Prepare tool fixtures ($profile)" PROOF_TXT="$workspace/proof.txt" PROOF_COPY="$workspace/copy.txt" HOSTNAME_TXT="$workspace/hostname.txt" @@ -548,8 +635,9 @@ run_profile() { echo -n "$PROOF_VALUE" >"$PROOF_TXT" write_png_lr_rg "$IMAGE_PNG" EXPECTED_HOSTNAME="$(hostname | tr -d '\r\n')" + phase_mark_passed "Prepare tool fixtures ($profile)" - echo "==> Start gateway ($profile)" + phase_mark_start "Start gateway ($profile)" GATEWAY_LOG="$workspace/gateway.log" openclaw --profile "$profile" gateway --port "$port" --bind loopback >"$GATEWAY_LOG" 2>&1 & GATEWAY_PID="$!" @@ -560,6 +648,7 @@ run_profile() { fi } trap cleanup_profile EXIT + phase_mark_passed "Start gateway ($profile)" TURN1_JSON="/tmp/agent-${profile}-1.json" TURN2_JSON="/tmp/agent-${profile}-2.json" @@ -569,7 +658,7 @@ run_profile() { TURN4_JSON="/tmp/agent-${profile}-4.json" HEALTH_JSON="/tmp/health-${profile}.json" - echo "==> Wait for health ($profile)" + phase_mark_start "Wait for health ($profile)" for _ in $(seq 1 240); do if openclaw --profile "$profile" health --timeout 5000 --json >/dev/null 2>&1; then break @@ -581,14 +670,46 @@ run_profile() { dump_profile_debug "$profile" "$HEALTH_JSON" >&2 || true return 1 fi + phase_mark_passed "Wait for health ($profile)" - echo "==> Agent turns ($profile)" + phase_mark_start "Agent turns ($profile)" TURN1_SESSION_ID="${SESSION_ID_PREFIX}-read-proof" - SESSION_JSONL="$(session_jsonl_path "$profile" "$TURN1_SESSION_ID")" - run_agent_turn "$profile" "$TURN1_SESSION_ID" \ - "Use the read tool (not exec) to read ${PROOF_TXT}. Reply with the exact contents only (no extra whitespace)." \ - "$TURN1_JSON" + local prompt1 + prompt1="Use the read tool (not exec) to read ${PROOF_TXT}. Reply with the exact contents only (no extra whitespace)." + local prompt2 + prompt2=$'Use the write tool (not exec) to write exactly this string into '"${PROOF_COPY}"$':\n'"${PROOF_VALUE}"$'\nReply with exactly: WROTE' + local prompt3 + prompt3="Use the exec tool to run this command: hostname. Reply with the exact stdout only (trim trailing newline)." + local prompt3b + prompt3b=$'Use the write tool to write exactly this string into '"${HOSTNAME_TXT}"$':\n'"${EXPECTED_HOSTNAME}"$'\nReply with exactly: WROTE' + local prompt4 + prompt4="Use the image tool on ${IMAGE_PNG}. Determine which color is on the left half and which is on the right half. Then use the write tool to write exactly: LEFT=RED RIGHT=GREEN into ${IMAGE_TXT}. Reply with exactly: LEFT=RED RIGHT=GREEN" + TURN2_SESSION_ID="${SESSION_ID_PREFIX}-write-copy" + TURN3_SESSION_ID="${SESSION_ID_PREFIX}-exec-hostname" + TURN3B_SESSION_ID="${SESSION_ID_PREFIX}-write-hostname" + TURN4_SESSION_ID="${SESSION_ID_PREFIX}-image-write" + if [[ "$AGENT_TURNS_PARALLEL" == "1" ]]; then + local turn_pids=() + run_agent_turn_bg "read proof" "$profile" "$TURN1_SESSION_ID" "$prompt1" "$TURN1_JSON" + turn_pids+=("$RUN_AGENT_TURN_BG_PID") + run_agent_turn_bg "write proof copy" "$profile" "$TURN2_SESSION_ID" "$prompt2" "$TURN2_JSON" + turn_pids+=("$RUN_AGENT_TURN_BG_PID") + run_agent_turn_bg "exec hostname" "$profile" "$TURN3_SESSION_ID" "$prompt3" "$TURN3_JSON" + turn_pids+=("$RUN_AGENT_TURN_BG_PID") + run_agent_turn_bg "write hostname" "$profile" "$TURN3B_SESSION_ID" "$prompt3b" "$TURN3B_JSON" + turn_pids+=("$RUN_AGENT_TURN_BG_PID") + run_agent_turn_bg "image write" "$profile" "$TURN4_SESSION_ID" "$prompt4" "$TURN4_JSON" + turn_pids+=("$RUN_AGENT_TURN_BG_PID") + wait_agent_turn_batch "${turn_pids[@]}" + else + run_agent_turn_logged "read proof" "$profile" "$TURN1_SESSION_ID" "$prompt1" "$TURN1_JSON" + run_agent_turn_logged "write proof copy" "$profile" "$TURN2_SESSION_ID" "$prompt2" "$TURN2_JSON" + run_agent_turn_logged "exec hostname" "$profile" "$TURN3_SESSION_ID" "$prompt3" "$TURN3_JSON" + run_agent_turn_logged "write hostname" "$profile" "$TURN3B_SESSION_ID" "$prompt3b" "$TURN3B_JSON" + run_agent_turn_logged "image write" "$profile" "$TURN4_SESSION_ID" "$prompt4" "$TURN4_JSON" + fi + assert_agent_json_has_text "$TURN1_JSON" assert_agent_json_ok "$TURN1_JSON" "$agent_model_provider" local reply1 @@ -598,11 +719,6 @@ run_profile() { exit 1 fi - local prompt2 - prompt2=$'Use the write tool (not exec) to write exactly this string into '"${PROOF_COPY}"$':\n'"${reply1}"$'\nReply with exactly: WROTE' - TURN2_SESSION_ID="${SESSION_ID_PREFIX}-write-copy" - SESSION_JSONL="$(session_jsonl_path "$profile" "$TURN2_SESSION_ID")" - run_agent_turn "$profile" "$TURN2_SESSION_ID" "$prompt2" "$TURN2_JSON" assert_agent_json_has_text "$TURN2_JSON" assert_agent_json_ok "$TURN2_JSON" "$agent_model_provider" local copy_value @@ -612,8 +728,7 @@ run_profile() { exit 1 fi TURN2B_SESSION_ID="${SESSION_ID_PREFIX}-read-copy" - SESSION_JSONL="$(session_jsonl_path "$profile" "$TURN2B_SESSION_ID")" - run_agent_turn "$profile" "$TURN2B_SESSION_ID" \ + run_agent_turn_logged "read proof copy" "$profile" "$TURN2B_SESSION_ID" \ "Use the read tool (not exec) to read ${PROOF_COPY}. Reply with the exact contents only (no extra whitespace)." \ "$TURN2B_JSON" assert_agent_json_has_text "$TURN2B_JSON" @@ -625,11 +740,6 @@ run_profile() { exit 1 fi - TURN3_SESSION_ID="${SESSION_ID_PREFIX}-exec-hostname" - SESSION_JSONL="$(session_jsonl_path "$profile" "$TURN3_SESSION_ID")" - run_agent_turn "$profile" "$TURN3_SESSION_ID" \ - "Use the exec tool to run this command: hostname. Reply with the exact stdout only (trim trailing newline)." \ - "$TURN3_JSON" assert_agent_json_has_text "$TURN3_JSON" assert_agent_json_ok "$TURN3_JSON" "$agent_model_provider" local reply3 @@ -638,11 +748,6 @@ run_profile() { echo "ERROR: agent did not run hostname correctly ($profile): $reply3" >&2 exit 1 fi - local prompt3b - prompt3b=$'Use the write tool to write exactly this string into '"${HOSTNAME_TXT}"$':\n'"${reply3}"$'\nReply with exactly: WROTE' - TURN3B_SESSION_ID="${SESSION_ID_PREFIX}-write-hostname" - SESSION_JSONL="$(session_jsonl_path "$profile" "$TURN3B_SESSION_ID")" - run_agent_turn "$profile" "$TURN3B_SESSION_ID" "$prompt3b" "$TURN3B_JSON" assert_agent_json_has_text "$TURN3B_JSON" assert_agent_json_ok "$TURN3B_JSON" "$agent_model_provider" if [[ "$(cat "$HOSTNAME_TXT" 2>/dev/null | tr -d '\r\n' || true)" != "$EXPECTED_HOSTNAME" ]]; then @@ -650,11 +755,6 @@ run_profile() { exit 1 fi - TURN4_SESSION_ID="${SESSION_ID_PREFIX}-image-write" - SESSION_JSONL="$(session_jsonl_path "$profile" "$TURN4_SESSION_ID")" - run_agent_turn "$profile" "$TURN4_SESSION_ID" \ - "Use the image tool on ${IMAGE_PNG}. Determine which color is on the left half and which is on the right half. Then use the write tool to write exactly: LEFT=RED RIGHT=GREEN into ${IMAGE_TXT}. Reply with exactly: LEFT=RED RIGHT=GREEN" \ - "$TURN4_JSON" assert_agent_json_has_text "$TURN4_JSON" assert_agent_json_ok "$TURN4_JSON" "$agent_model_provider" if [[ "$(cat "$IMAGE_TXT" 2>/dev/null | tr -d '\r\n' || true)" != "LEFT=RED RIGHT=GREEN" ]]; then @@ -667,8 +767,9 @@ run_profile() { echo "ERROR: agent reply did not contain expected marker ($profile): $reply4" >&2 exit 1 fi + phase_mark_passed "Agent turns ($profile)" - echo "==> Verify tool usage via session transcript ($profile)" + phase_mark_start "Verify tool usage via session transcript ($profile)" # Give the gateway a moment to flush transcripts. sleep 1 assert_session_used_tools "$(session_jsonl_path "$profile" "$TURN1_SESSION_ID")" read @@ -677,6 +778,7 @@ run_profile() { assert_session_used_tools "$(session_jsonl_path "$profile" "$TURN3_SESSION_ID")" exec assert_session_used_tools "$(session_jsonl_path "$profile" "$TURN3B_SESSION_ID")" write assert_session_used_tools "$(session_jsonl_path "$profile" "$TURN4_SESSION_ID")" image write + phase_mark_passed "Verify tool usage via session transcript ($profile)" cleanup_profile trap - EXIT diff --git a/scripts/test-install-sh-e2e-docker.sh b/scripts/test-install-sh-e2e-docker.sh index e7686211384..d69adca6aef 100755 --- a/scripts/test-install-sh-e2e-docker.sh +++ b/scripts/test-install-sh-e2e-docker.sh @@ -25,6 +25,7 @@ docker run --rm \ -e OPENCLAW_INSTALL_E2E_PREVIOUS="${OPENCLAW_INSTALL_E2E_PREVIOUS:-}" \ -e OPENCLAW_INSTALL_E2E_SKIP_PREVIOUS="${OPENCLAW_INSTALL_E2E_SKIP_PREVIOUS:-0}" \ -e OPENCLAW_INSTALL_E2E_AGENT_TURN_TIMEOUT_SECONDS="${OPENCLAW_INSTALL_E2E_AGENT_TURN_TIMEOUT_SECONDS:-600}" \ + -e OPENCLAW_INSTALL_E2E_AGENT_TURNS_PARALLEL="${OPENCLAW_INSTALL_E2E_AGENT_TURNS_PARALLEL:-1}" \ -e OPENCLAW_NO_ONBOARD=1 \ -e OPENAI_API_KEY \ -e ANTHROPIC_API_KEY \ diff --git a/test/scripts/docker-build-helper.test.ts b/test/scripts/docker-build-helper.test.ts index 998740e1003..6c87bd9e3a2 100644 --- a/test/scripts/docker-build-helper.test.ts +++ b/test/scripts/docker-build-helper.test.ts @@ -114,6 +114,23 @@ describe("docker build helper", () => { ); }); + it("times and parallelizes release installer E2E agent turns after gateway startup", () => { + const runner = readFileSync(INSTALL_E2E_RUNNER_PATH, "utf8"); + const wrapper = readFileSync("scripts/test-install-sh-e2e-docker.sh", "utf8"); + + expect(runner).toContain( + 'AGENT_TURNS_PARALLEL="${OPENCLAW_INSTALL_E2E_AGENT_TURNS_PARALLEL:-1}"', + ); + expect(runner).toContain("time_phase"); + expect(runner).toContain("phase_mark_start"); + expect(runner).toContain("run_agent_turn_bg"); + expect(runner).toContain("wait_agent_turn_batch"); + expect(runner).toContain('run_agent_turn_bg "read proof"'); + expect(runner).toContain('run_agent_turn_bg "image write"'); + expect(runner).toContain('run_agent_turn_logged "read proof copy"'); + expect(wrapper).toContain("OPENCLAW_INSTALL_E2E_AGENT_TURNS_PARALLEL"); + }); + it("keeps package acceptance plugin coverage offline-capable", () => { const scenarios = readFileSync(DOCKER_E2E_SCENARIOS_PATH, "utf8"); @@ -214,7 +231,7 @@ describe("docker build helper", () => { expect(runner).toContain('rm -f "$workspace/BOOTSTRAP.md"'); expect(runner.indexOf('rm -f "$workspace/BOOTSTRAP.md"')).toBeLessThan( - runner.indexOf('echo "==> Agent turns ($profile)"'), + runner.indexOf('phase_mark_start "Agent turns ($profile)"'), ); }); diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index 8b69a763683..639673649a4 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -177,6 +177,15 @@ describe("package artifact reuse", () => { expect(workflow).toContain("suite_id: native-live-extensions-media-music-google"); expect(workflow).toContain("suite_id: native-live-extensions-media-music-minimax"); expect(workflow).toContain("suite_id: native-live-extensions-media-video"); + expect(workflow).toContain("suite_group: native-live-extensions-media-video"); + expect(workflow).toContain("OPENCLAW_LIVE_VIDEO_GENERATION_PROVIDERS=google,minimax"); + expect(workflow).toContain("OPENCLAW_LIVE_VIDEO_GENERATION_PROVIDERS=openai,openrouter,xai"); + expect(workflow).toContain("suite_group: native-live-src-gateway-profiles-opencode-go"); + expect(workflow).toContain("opencode-go/mimo-v2-omni"); + expect(workflow).toContain( + "inputs.live_suite_filter == 'native-live-src-gateway-profiles-opencode-go'", + ); + expect(workflow).toContain("inputs.live_suite_filter == 'native-live-extensions-media-video'"); expect(workflow).not.toContain("needs_ffmpeg: true"); expect(retryHelper).toContain("OPENCLAW_LIVE_COMMAND_ATTEMPTS:-2"); expect(retryHelper).toContain("ECONNRESET"); diff --git a/test/scripts/test-install-sh-docker.test.ts b/test/scripts/test-install-sh-docker.test.ts index 3ca0a78cd7e..4477d9f91df 100644 --- a/test/scripts/test-install-sh-docker.test.ts +++ b/test/scripts/test-install-sh-docker.test.ts @@ -201,6 +201,8 @@ describe("bun global install smoke", () => { expect(workflow).toContain("install-smoke-fast:"); expect(workflow).toContain("run_fast_install_smoke"); expect(workflow).toContain("run_full_install_smoke"); + expect(workflow).toContain("cache-from: type=gha,scope=openclaw-dockerfile-smoke"); + expect(workflow).toContain("cache-to: type=gha,scope=openclaw-dockerfile-smoke,mode=max"); expect(workflow).toContain('OPENCLAW_INSTALL_SMOKE_SKIP_NPM_GLOBAL: "1"'); expect(releaseChecks).toContain("install_smoke_release_checks:"); expect(releaseChecks).toContain("uses: ./.github/workflows/install-smoke.yml");