diff --git a/CHANGELOG.md b/CHANGELOG.md index 44ce83b9c4a3..edfde0152b6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ Docs: https://docs.openclaw.ai - Release/CI/E2E: fail early when Crabbox sparse-sync full checkouts do not have enough local disk, with guidance for moving the sync root. - Release/CI/E2E: reset shared Crabbox pnpm hydrate state before installs so stale `/var/tmp` stores cannot leave `pnpm install` spinning after completion. - Release/CI/E2E: print heartbeat progress during centralized Docker builds while keeping successful build logs quiet. +- Release/CI/E2E: avoid heartbeat-tail delays in Docker E2E log wrappers while reporting captured log bytes during long runs. - Build: render independent CLI startup metadata help snapshots concurrently to cut cold build-all metadata time. - Plugins: stop timed-out package-boundary prep steps by process group so descendant TypeScript/helper processes do not survive local check cleanup. - Control UI: serve static assets asynchronously after safe-open checks so large UI files do not block Gateway request handling. diff --git a/scripts/lib/docker-build.sh b/scripts/lib/docker-build.sh index 84817bbd9210..6d47569b90c5 100644 --- a/scripts/lib/docker-build.sh +++ b/scripts/lib/docker-build.sh @@ -76,7 +76,7 @@ docker_build_timeout_required() { docker_build_heartbeat_seconds() { local configured="${OPENCLAW_DOCKER_BUILD_HEARTBEAT_SECONDS:-30}" if [[ "$configured" =~ ^[0-9]+$ ]] && [ "$configured" -ge 1 ]; then - echo "$configured" + echo "$((10#$configured))" return fi echo 30 diff --git a/scripts/lib/docker-e2e-logs.sh b/scripts/lib/docker-e2e-logs.sh index bf5d67cb1d97..3d5f839b0b21 100644 --- a/scripts/lib/docker-e2e-logs.sh +++ b/scripts/lib/docker-e2e-logs.sh @@ -35,19 +35,29 @@ run_logged_print_heartbeat() { local label="$1" local interval_seconds="$2" shift 2 + if ! [[ "$interval_seconds" =~ ^[0-9]+$ ]] || [ "$interval_seconds" -lt 1 ]; then + interval_seconds="30" + else + interval_seconds="$((10#$interval_seconds))" + fi local log_file log_file="$(docker_e2e_run_log "$label")" "$@" >"$log_file" 2>&1 & local command_pid=$! - local started_at - started_at="$(date +%s)" + local started_at="$SECONDS" + local next_heartbeat=$interval_seconds local status=0 while kill -0 "$command_pid" 2>/dev/null; do - sleep "$interval_seconds" - if kill -0 "$command_pid" 2>/dev/null; then - local now - now="$(date +%s)" - echo "still running $label ($((now - started_at))s elapsed)" + /bin/sleep 1 + local elapsed_seconds=$((SECONDS - started_at)) + if [ "$elapsed_seconds" -ge "$next_heartbeat" ] && kill -0 "$command_pid" 2>/dev/null; then + local log_bytes="0" + if [ -f "$log_file" ]; then + log_bytes="$(wc -c <"$log_file" 2>/dev/null || echo 0)" + log_bytes="${log_bytes//[[:space:]]/}" + fi + echo "still running $label (${elapsed_seconds}s elapsed, ${log_bytes} log bytes captured)" + next_heartbeat=$((elapsed_seconds + interval_seconds)) fi done set +e diff --git a/test/scripts/docker-build-helper.test.ts b/test/scripts/docker-build-helper.test.ts index 2886941c5eb1..028416eb1daa 100644 --- a/test/scripts/docker-build-helper.test.ts +++ b/test/scripts/docker-build-helper.test.ts @@ -339,6 +339,22 @@ output="$(docker_build_run e2e-build -t demo-image .)" } }); + it("normalizes zero-padded centralized Docker build heartbeat intervals", () => { + const rootDir = process.cwd(); + const script = ` +set -euo pipefail +ROOT_DIR=${shellQuote(rootDir)} +export ROOT_DIR +export OPENCLAW_DOCKER_BUILD_HEARTBEAT_SECONDS=08 + +source "$ROOT_DIR/scripts/lib/docker-build.sh" + +[[ "$(docker_build_heartbeat_seconds)" = "8" ]] +`; + + execFileSync("bash", ["-lc", script], { encoding: "utf8" }); + }); + it("fails centralized Docker builds fast when timeout is unavailable", () => { const workDir = mkdtempSync(join(tmpdir(), "openclaw-docker-build-timeout-required-")); @@ -1555,6 +1571,82 @@ test -f "$TMPDIR/docker-cmd-seen" expect(runner).not.toContain("docker_e2e_run_logged_with_harness plugins-run"); }); + it("prints heartbeat progress for long successful Docker E2E log captures", () => { + const workDir = mkdtempSync(join(tmpdir(), "openclaw-docker-e2e-log-heartbeat-")); + + try { + const rootDir = process.cwd(); + const script = ` +set -euo pipefail +ROOT_DIR=${shellQuote(rootDir)} +TMPDIR=${shellQuote(workDir)} +export ROOT_DIR TMPDIR + +source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh" + +output="$(run_logged_print_heartbeat plugins-run 1 bash -c 'printf "captured container log\\\\n"; /bin/sleep 2')" +[[ "$output" = *"still running plugins-run ("* ]] +[[ "$output" = *"log bytes captured"* ]] +[[ "$output" = *"captured container log"* ]] +`; + + execFileSync("bash", ["-lc", script], { encoding: "utf8" }); + } finally { + rmSync(workDir, { recursive: true, force: true }); + } + }); + + it("does not delay fast successful Docker E2E log captures until the next heartbeat", () => { + const workDir = mkdtempSync(join(tmpdir(), "openclaw-docker-e2e-log-fast-heartbeat-")); + + try { + const rootDir = process.cwd(); + const script = ` +set -euo pipefail +ROOT_DIR=${shellQuote(rootDir)} +TMPDIR=${shellQuote(workDir)} +export ROOT_DIR TMPDIR + +source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh" + +output="$(run_logged_print_heartbeat plugins-run 30 bash -c 'printf "quick container log\\\\n"')" +[[ "$output" = "quick container log" ]] +`; + const startedAt = Date.now(); + + execFileSync("bash", ["-lc", script], { encoding: "utf8" }); + + expect(Date.now() - startedAt).toBeLessThan(5_000); + } finally { + rmSync(workDir, { recursive: true, force: true }); + } + }); + + it("normalizes zero-padded Docker E2E log heartbeat intervals", () => { + const workDir = mkdtempSync(join(tmpdir(), "openclaw-docker-e2e-log-zero-heartbeat-")); + + try { + const rootDir = process.cwd(); + const script = ` +set -euo pipefail +ROOT_DIR=${shellQuote(rootDir)} +TMPDIR=${shellQuote(workDir)} +export ROOT_DIR TMPDIR + +source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh" + +output="$(run_logged_print_heartbeat plugins-run 08 bash -c 'printf "captured container log\\\\n"; /bin/sleep 9')" +[[ "$output" = *"still running plugins-run (8s elapsed,"* ]] +[[ "$output" = *"log bytes captured"* ]] +[[ "$output" = *"captured container log"* ]] +`; + + execFileSync("bash", ["-lc", script], { encoding: "utf8" }); + } finally { + rmSync(workDir, { recursive: true, force: true }); + } + }); + it("includes procps in the shared Docker E2E image for process watchdogs", () => { const dockerfile = readFileSync("scripts/e2e/Dockerfile", "utf8");