diff --git a/scripts/lib/docker-e2e-image.sh b/scripts/lib/docker-e2e-image.sh index 47ae63144ca6..c9021c216584 100644 --- a/scripts/lib/docker-e2e-image.sh +++ b/scripts/lib/docker-e2e-image.sh @@ -67,20 +67,39 @@ docker_e2e_build_or_reuse() { echo "Building Docker image: $image_name" local build_args=() + local package_tgz="" + local package_context="" + local package_pack_dir="" 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")" + if [ -z "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}" ]; then + package_pack_dir="$(dirname "$package_tgz")" + fi + local context_status=0 + package_context="$(docker_e2e_prepare_package_context "$package_tgz")" || context_status="$?" + if [ "$context_status" -ne 0 ]; then + if [ -n "$package_pack_dir" ]; then + rm -rf "$package_pack_dir" + fi + return "$context_status" + fi # 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[@]}" + local build_status=0 + docker_build_run "$label-build" "${build_args[@]}" || build_status="$?" + if [ -n "$package_context" ]; then + rm -rf "$package_context" + fi + if [ -n "$package_pack_dir" ]; then + rm -rf "$package_pack_dir" + fi + return "$build_status" } docker_e2e_test_state_shell_b64() { diff --git a/scripts/lib/docker-e2e-package.sh b/scripts/lib/docker-e2e-package.sh index a37c5fb0d552..a326f1f1a6f2 100644 --- a/scripts/lib/docker-e2e-package.sh +++ b/scripts/lib/docker-e2e-package.sh @@ -31,13 +31,19 @@ docker_e2e_prepare_package_tgz() { local pack_dir pack_dir="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-docker-e2e-pack.XXXXXX")" + local pack_status=0 package_tgz="$( node "$ROOT_DIR/scripts/package-openclaw-for-docker.mjs" \ --output-dir "$pack_dir" \ --output-name openclaw-current.tgz - )" + )" || pack_status="$?" + if [ "$pack_status" -ne 0 ]; then + rm -rf "$pack_dir" + return "$pack_status" + fi if [ -z "$package_tgz" ]; then echo "missing packed OpenClaw tarball" >&2 + rm -rf "$pack_dir" return 1 fi docker_e2e_abs_path "$package_tgz" @@ -49,7 +55,12 @@ docker_e2e_prepare_package_context() { 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" + local copy_status=0 + cp "$package_tgz" "$context_dir/openclaw-current.tgz" || copy_status="$?" + if [ "$copy_status" -ne 0 ]; then + rm -rf "$context_dir" + return "$copy_status" + fi printf '%s\n' "$context_dir" } diff --git a/test/scripts/docker-build-helper.test.ts b/test/scripts/docker-build-helper.test.ts index 0263bb28d652..1a29135e4208 100644 --- a/test/scripts/docker-build-helper.test.ts +++ b/test/scripts/docker-build-helper.test.ts @@ -62,6 +62,10 @@ const CENTRALIZED_BUILD_SCRIPTS = [ "scripts/test-live-build-docker.sh", ] as const; +function shellQuote(value: string): string { + return `'${value.replace(/'/gu, `'\\''`)}'`; +} + describe("docker build helper", () => { it("forces BuildKit for centralized Docker builds", () => { const helper = readFileSync(HELPER_PATH, "utf8"); @@ -109,6 +113,148 @@ describe("docker build helper", () => { ); }); + it("removes functional Docker build package inputs after the build", () => { + const workDir = mkdtempSync(join(tmpdir(), "openclaw-docker-build-cleanup-")); + + try { + const rootDir = process.cwd(); + const script = ` +set -euo pipefail +ROOT_DIR=${shellQuote(rootDir)} +TMPDIR=${shellQuote(workDir)} +export ROOT_DIR TMPDIR + +node() { + local script="$1" + shift + if [[ "$script" != "$ROOT_DIR/scripts/package-openclaw-for-docker.mjs" ]]; then + command node "$script" "$@" + return + fi + + local output_dir="" + local output_name="" + while [[ $# -gt 0 ]]; do + case "$1" in + --output-dir) + output_dir="$2" + shift 2 + ;; + --output-name) + output_name="$2" + shift 2 + ;; + *) + shift + ;; + esac + done + + mkdir -p "$output_dir" + printf fixture >"$output_dir/$output_name" + printf "%s\\n" "$output_dir/$output_name" +} +export -f node + +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" + +docker_build_run() { + local build_context="" + local arg + for arg in "$@"; do + case "$arg" in + openclaw_package=*) + build_context="\${arg#openclaw_package=}" + ;; + esac + done + + test -n "$build_context" + test -f "$build_context/openclaw-current.tgz" + printf "%s\\n" "$build_context" >"$TMPDIR/build-context-seen" +} + +docker_e2e_build_or_reuse \\ + openclaw-test-image \\ + cleanup-proof \\ + "$ROOT_DIR/scripts/e2e/Dockerfile" \\ + "$ROOT_DIR" \\ + functional + +test -f "$TMPDIR/build-context-seen" +leftovers="$(find "$TMPDIR" -maxdepth 1 \\( \\ + -name 'openclaw-docker-e2e-pack.*' \\ + -o -name 'openclaw-docker-e2e-package-context.*' \\ +\\) -print)" +if [[ -n "$leftovers" ]]; then + printf 'leftover functional build inputs:\\n%s\\n' "$leftovers" >&2 + exit 1 +fi +`; + + execFileSync("bash", ["-lc", script], { encoding: "utf8" }); + } finally { + rmSync(workDir, { recursive: true, force: true }); + } + }); + + it("keeps caller-provided functional Docker build packages", () => { + const workDir = mkdtempSync(join(tmpdir(), "openclaw-docker-build-external-package-")); + + try { + const rootDir = process.cwd(); + const script = ` +set -euo pipefail +ROOT_DIR=${shellQuote(rootDir)} +TMPDIR=${shellQuote(workDir)} +export ROOT_DIR TMPDIR + +external_dir="$TMPDIR/external-package" +mkdir -p "$external_dir" +printf fixture >"$external_dir/openclaw-current.tgz" +OPENCLAW_CURRENT_PACKAGE_TGZ="$external_dir/openclaw-current.tgz" +export OPENCLAW_CURRENT_PACKAGE_TGZ + +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" + +docker_build_run() { + local build_context="" + local arg + for arg in "$@"; do + case "$arg" in + openclaw_package=*) + build_context="\${arg#openclaw_package=}" + ;; + esac + done + + test -n "$build_context" + test -f "$build_context/openclaw-current.tgz" + printf "%s\\n" "$build_context" >"$TMPDIR/build-context-seen" +} + +docker_e2e_build_or_reuse \\ + openclaw-test-image \\ + external-package-proof \\ + "$ROOT_DIR/scripts/e2e/Dockerfile" \\ + "$ROOT_DIR" \\ + functional + +test -f "$TMPDIR/build-context-seen" +test -f "$OPENCLAW_CURRENT_PACKAGE_TGZ" +leftovers="$(find "$TMPDIR" -maxdepth 1 -name 'openclaw-docker-e2e-package-context.*' -print)" +if [[ -n "$leftovers" ]]; then + printf 'leftover functional build context:\\n%s\\n' "$leftovers" >&2 + exit 1 +fi +`; + + 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");