From b28de9a7d9a2a330b0c425ee8fa3e157ae1d83e8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Apr 2026 22:14:33 +0100 Subject: [PATCH] ci: centralize docker build wrapper --- scripts/docker/setup.sh | 3 +- scripts/e2e/browser-cdp-snapshot-docker.sh | 2 +- scripts/e2e/qr-import-docker.sh | 15 +++----- scripts/lib/docker-build.sh | 40 ++++++++++++++++++++++ scripts/lib/docker-e2e-image.sh | 9 ++--- scripts/sandbox-browser-setup.sh | 5 ++- scripts/sandbox-common-setup.sh | 22 ++++-------- scripts/sandbox-setup.sh | 5 ++- scripts/test-cleanup-docker.sh | 4 +-- scripts/test-install-sh-docker.sh | 5 +-- scripts/test-install-sh-e2e-docker.sh | 3 +- scripts/test-live-build-docker.sh | 4 +-- test/scripts/docker-build-helper.test.ts | 38 ++++++++++++++++++++ 13 files changed, 115 insertions(+), 40 deletions(-) create mode 100644 scripts/lib/docker-build.sh create mode 100644 test/scripts/docker-build-helper.test.ts diff --git a/scripts/docker/setup.sh b/scripts/docker/setup.sh index 96be3aedfbe..d47d7526efd 100755 --- a/scripts/docker/setup.sh +++ b/scripts/docker/setup.sh @@ -2,6 +2,7 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +source "$ROOT_DIR/scripts/lib/docker-build.sh" COMPOSE_FILE="$ROOT_DIR/docker-compose.yml" EXTRA_COMPOSE_FILE="$ROOT_DIR/docker-compose.extra.yml" IMAGE_NAME="${OPENCLAW_IMAGE:-openclaw:local}" @@ -27,7 +28,7 @@ require_cmd() { run_docker_build() { # Dockerfile uses BuildKit-only syntax (RUN --mount=type=cache). Force # BuildKit so hosts defaulting to the legacy builder do not fail. - DOCKER_BUILDKIT=1 docker build "$@" + docker_build_exec "$@" } is_truthy_value() { diff --git a/scripts/e2e/browser-cdp-snapshot-docker.sh b/scripts/e2e/browser-cdp-snapshot-docker.sh index daa32d470bf..543f9cebff8 100755 --- a/scripts/e2e/browser-cdp-snapshot-docker.sh +++ b/scripts/e2e/browser-cdp-snapshot-docker.sh @@ -39,7 +39,7 @@ RUN apt-get update \\ USER appuser EOF echo "Building Docker image: $IMAGE_NAME" - run_logged browser-cdp-snapshot-build docker build -t "$IMAGE_NAME" -f "$build_dir/Dockerfile" "$build_dir" + docker_build_run browser-cdp-snapshot-build -t "$IMAGE_NAME" -f "$build_dir/Dockerfile" "$build_dir" fi echo "Starting browser CDP snapshot container..." diff --git a/scripts/e2e/qr-import-docker.sh b/scripts/e2e/qr-import-docker.sh index 3aa3e00148c..6f9f99bdfc0 100755 --- a/scripts/e2e/qr-import-docker.sh +++ b/scripts/e2e/qr-import-docker.sh @@ -2,7 +2,7 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh" +source "$ROOT_DIR/scripts/lib/docker-build.sh" IMAGE_NAME="${OPENCLAW_QR_SMOKE_IMAGE:-openclaw-qr-smoke}" DOCKER_BUILD_ARGS=() @@ -15,16 +15,11 @@ if [[ "${OPENCLAW_QR_SMOKE_FORCE_INSTALL:-0}" == "1" ]]; then fi echo "Building Docker image..." -DOCKER_BUILD_CMD=(docker build) -if ((${#DOCKER_BUILD_ARGS[@]} > 0)); then - DOCKER_BUILD_CMD+=("${DOCKER_BUILD_ARGS[@]}") -fi -DOCKER_BUILD_CMD+=( - -t "$IMAGE_NAME" - -f "$ROOT_DIR/scripts/e2e/Dockerfile.qr-import" +docker_build_run qr-import-build \ + "${DOCKER_BUILD_ARGS[@]}" \ + -t "$IMAGE_NAME" \ + -f "$ROOT_DIR/scripts/e2e/Dockerfile.qr-import" \ "$ROOT_DIR" -) -run_logged qr-import-build "${DOCKER_BUILD_CMD[@]}" echo "Running qrcode-tui import smoke..." run_logged qr-import-run docker run --rm -t "$IMAGE_NAME" node -e "import('@vincentkoc/qrcode-tui').then(async (m)=>{process.stdout.write(await m.renderTerminal('qr-smoke',{small:true}))})" diff --git a/scripts/lib/docker-build.sh b/scripts/lib/docker-build.sh new file mode 100644 index 00000000000..e62f41b6321 --- /dev/null +++ b/scripts/lib/docker-build.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +DOCKER_BUILD_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +if ! declare -F run_logged >/dev/null 2>&1; then + source "$DOCKER_BUILD_LIB_DIR/docker-e2e-logs.sh" +fi + +docker_build_exec() { + local build_cmd=(docker build) + if [ "${OPENCLAW_DOCKER_BUILD_USE_BUILDX:-0}" = "1" ]; then + build_cmd=(docker buildx build --load) + if [ -n "${OPENCLAW_DOCKER_BUILD_CACHE_FROM:-}" ]; then + build_cmd+=(--cache-from "${OPENCLAW_DOCKER_BUILD_CACHE_FROM}") + fi + if [ -n "${OPENCLAW_DOCKER_BUILD_CACHE_TO:-}" ]; then + build_cmd+=(--cache-to "${OPENCLAW_DOCKER_BUILD_CACHE_TO}") + fi + fi + + env DOCKER_BUILDKIT=1 "${build_cmd[@]}" "$@" +} + +docker_build_run() { + local label="$1" + shift + + local build_cmd=(docker build) + if [ "${OPENCLAW_DOCKER_BUILD_USE_BUILDX:-0}" = "1" ]; then + build_cmd=(docker buildx build --load) + if [ -n "${OPENCLAW_DOCKER_BUILD_CACHE_FROM:-}" ]; then + build_cmd+=(--cache-from "${OPENCLAW_DOCKER_BUILD_CACHE_FROM}") + fi + if [ -n "${OPENCLAW_DOCKER_BUILD_CACHE_TO:-}" ]; then + build_cmd+=(--cache-to "${OPENCLAW_DOCKER_BUILD_CACHE_TO}") + fi + fi + + run_logged "$label" env DOCKER_BUILDKIT=1 "${build_cmd[@]}" "$@" +} diff --git a/scripts/lib/docker-e2e-image.sh b/scripts/lib/docker-e2e-image.sh index 32b94893c48..c4cf8383b43 100644 --- a/scripts/lib/docker-e2e-image.sh +++ b/scripts/lib/docker-e2e-image.sh @@ -4,6 +4,7 @@ DOCKER_E2E_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="${ROOT_DIR:-$(cd "$DOCKER_E2E_LIB_DIR/../.." && pwd)}" source "$DOCKER_E2E_LIB_DIR/docker-e2e-logs.sh" +source "$DOCKER_E2E_LIB_DIR/docker-build.sh" docker_e2e_resolve_image() { local default_image="$1" @@ -48,10 +49,10 @@ docker_e2e_build_or_reuse() { fi echo "Building Docker image: $image_name" - local build_cmd=(docker build) + local build_args=() if [ -n "$target" ]; then - build_cmd+=(--target "$target") + build_args+=(--target "$target") fi - build_cmd+=(-t "$image_name" -f "$dockerfile" "$context") - run_logged "$label-build" "${build_cmd[@]}" + build_args+=(-t "$image_name" -f "$dockerfile" "$context") + docker_build_run "$label-build" "${build_args[@]}" } diff --git a/scripts/sandbox-browser-setup.sh b/scripts/sandbox-browser-setup.sh index 74b4605eb0d..bec750cf9e8 100755 --- a/scripts/sandbox-browser-setup.sh +++ b/scripts/sandbox-browser-setup.sh @@ -1,7 +1,10 @@ #!/usr/bin/env bash set -euo pipefail +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "$ROOT_DIR/scripts/lib/docker-build.sh" + IMAGE_NAME="openclaw-sandbox-browser:bookworm-slim" -docker build -t "${IMAGE_NAME}" -f Dockerfile.sandbox-browser . +docker_build_exec -t "${IMAGE_NAME}" -f "$ROOT_DIR/Dockerfile.sandbox-browser" "$ROOT_DIR" echo "Built ${IMAGE_NAME}" diff --git a/scripts/sandbox-common-setup.sh b/scripts/sandbox-common-setup.sh index 258ed19bcae..4d1dff1d983 100755 --- a/scripts/sandbox-common-setup.sh +++ b/scripts/sandbox-common-setup.sh @@ -1,6 +1,9 @@ #!/usr/bin/env bash set -euo pipefail +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "$ROOT_DIR/scripts/lib/docker-build.sh" + BASE_IMAGE="${BASE_IMAGE:-openclaw-sandbox:bookworm-slim}" TARGET_IMAGE="${TARGET_IMAGE:-openclaw-sandbox-common:bookworm-slim}" PACKAGES="${PACKAGES:-curl wget jq coreutils grep nodejs npm python3 git ca-certificates golang-go rustc cargo unzip pkg-config libasound2-dev build-essential file}" @@ -17,25 +20,14 @@ OPENCLAW_DOCKER_BUILD_CACHE_TO="${OPENCLAW_DOCKER_BUILD_CACHE_TO:-}" if ! docker image inspect "${BASE_IMAGE}" >/dev/null 2>&1; then echo "Base image missing: ${BASE_IMAGE}" echo "Building base image via scripts/sandbox-setup.sh..." - scripts/sandbox-setup.sh + "$ROOT_DIR/scripts/sandbox-setup.sh" fi echo "Building ${TARGET_IMAGE} with: ${PACKAGES}" -build_cmd=(docker build) -if [ "${OPENCLAW_DOCKER_BUILD_USE_BUILDX}" = "1" ]; then - build_cmd=(docker buildx build --load) - if [ -n "${OPENCLAW_DOCKER_BUILD_CACHE_FROM}" ]; then - build_cmd+=(--cache-from "${OPENCLAW_DOCKER_BUILD_CACHE_FROM}") - fi - if [ -n "${OPENCLAW_DOCKER_BUILD_CACHE_TO}" ]; then - build_cmd+=(--cache-to "${OPENCLAW_DOCKER_BUILD_CACHE_TO}") - fi -fi - -"${build_cmd[@]}" \ +docker_build_exec \ -t "${TARGET_IMAGE}" \ - -f Dockerfile.sandbox-common \ + -f "$ROOT_DIR/Dockerfile.sandbox-common" \ --build-arg BASE_IMAGE="${BASE_IMAGE}" \ --build-arg PACKAGES="${PACKAGES}" \ --build-arg INSTALL_PNPM="${INSTALL_PNPM}" \ @@ -44,7 +36,7 @@ fi --build-arg INSTALL_BREW="${INSTALL_BREW}" \ --build-arg BREW_INSTALL_DIR="${BREW_INSTALL_DIR}" \ --build-arg FINAL_USER="${FINAL_USER}" \ - . + "$ROOT_DIR" cat < Build image: $IMAGE_NAME" -run_logged cleanup-build docker build \ +docker_build_run cleanup-build \ -t "$IMAGE_NAME" \ -f "$ROOT_DIR/scripts/docker/cleanup-smoke/Dockerfile" \ "$ROOT_DIR" diff --git a/scripts/test-install-sh-docker.sh b/scripts/test-install-sh-docker.sh index 71fe31cf11b..73e70f4fd8a 100755 --- a/scripts/test-install-sh-docker.sh +++ b/scripts/test-install-sh-docker.sh @@ -4,6 +4,7 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # shellcheck source=./docker/install-sh-common/version-parse.sh source "$ROOT_DIR/scripts/docker/install-sh-common/version-parse.sh" +source "$ROOT_DIR/scripts/lib/docker-build.sh" resolve_default_smoke_platform() { local host_os @@ -358,7 +359,7 @@ if [[ "$SKIP_SMOKE_IMAGE_BUILD" == "1" ]]; then echo "==> Reuse prebuilt smoke image: $SMOKE_IMAGE" else echo "==> Build smoke image (upgrade, root, ${SMOKE_PLATFORM}): $SMOKE_IMAGE" - docker build \ + docker_build_run install-smoke-build \ --platform "$SMOKE_PLATFORM" \ -t "$SMOKE_IMAGE" \ -f "$ROOT_DIR/scripts/docker/install-sh-smoke/Dockerfile" \ @@ -441,7 +442,7 @@ else echo "==> Reuse prebuilt non-root image: $NONROOT_IMAGE" else echo "==> Build non-root image (${NONROOT_PLATFORM}): $NONROOT_IMAGE" - docker build \ + docker_build_run install-nonroot-build \ --platform "$NONROOT_PLATFORM" \ -t "$NONROOT_IMAGE" \ -f "$ROOT_DIR/scripts/docker/install-sh-nonroot/Dockerfile" \ diff --git a/scripts/test-install-sh-e2e-docker.sh b/scripts/test-install-sh-e2e-docker.sh index 217e5ba6ee4..e7686211384 100755 --- a/scripts/test-install-sh-e2e-docker.sh +++ b/scripts/test-install-sh-e2e-docker.sh @@ -2,6 +2,7 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "$ROOT_DIR/scripts/lib/docker-build.sh" IMAGE_NAME="${OPENCLAW_INSTALL_E2E_IMAGE:-openclaw-install-e2e:local}" INSTALL_URL="${OPENCLAW_INSTALL_URL:-https://openclaw.bot/install.sh}" @@ -11,7 +12,7 @@ ANTHROPIC_API_TOKEN="${ANTHROPIC_API_TOKEN:-}" OPENCLAW_E2E_MODELS="${OPENCLAW_E2E_MODELS:-}" echo "==> Build image: $IMAGE_NAME" -docker build \ +docker_build_run install-e2e-build \ -t "$IMAGE_NAME" \ -f "$ROOT_DIR/scripts/docker/install-sh-e2e/Dockerfile" \ "$ROOT_DIR/scripts/docker" diff --git a/scripts/test-live-build-docker.sh b/scripts/test-live-build-docker.sh index 2ca474c597e..947b462fd17 100755 --- a/scripts/test-live-build-docker.sh +++ b/scripts/test-live-build-docker.sh @@ -2,7 +2,7 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh" +source "$ROOT_DIR/scripts/lib/docker-build.sh" IMAGE_NAME="${OPENCLAW_IMAGE:-openclaw:local}" LIVE_IMAGE_NAME="${OPENCLAW_LIVE_IMAGE:-${IMAGE_NAME}-live}" DOCKER_BUILD_EXTENSIONS="${OPENCLAW_DOCKER_BUILD_EXTENSIONS:-${OPENCLAW_EXTENSIONS:-}}" @@ -27,4 +27,4 @@ fi echo "==> Build live-test image: $LIVE_IMAGE_NAME (target=build)" echo "==> Bundled plugin deps: ${DOCKER_BUILD_EXTENSIONS}" -run_logged live-build docker build "${DOCKER_BUILD_ARGS[@]}" --target build -t "$LIVE_IMAGE_NAME" -f "$ROOT_DIR/Dockerfile" "$ROOT_DIR" +docker_build_run live-build "${DOCKER_BUILD_ARGS[@]}" --target build -t "$LIVE_IMAGE_NAME" -f "$ROOT_DIR/Dockerfile" "$ROOT_DIR" diff --git a/test/scripts/docker-build-helper.test.ts b/test/scripts/docker-build-helper.test.ts new file mode 100644 index 00000000000..82e7096fb81 --- /dev/null +++ b/test/scripts/docker-build-helper.test.ts @@ -0,0 +1,38 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const HELPER_PATH = "scripts/lib/docker-build.sh"; +const CENTRALIZED_BUILD_SCRIPTS = [ + "scripts/docker/setup.sh", + "scripts/e2e/browser-cdp-snapshot-docker.sh", + "scripts/e2e/qr-import-docker.sh", + "scripts/lib/docker-e2e-image.sh", + "scripts/sandbox-browser-setup.sh", + "scripts/sandbox-common-setup.sh", + "scripts/sandbox-setup.sh", + "scripts/test-cleanup-docker.sh", + "scripts/test-install-sh-docker.sh", + "scripts/test-install-sh-e2e-docker.sh", + "scripts/test-live-build-docker.sh", +] as const; + +describe("docker build helper", () => { + it("forces BuildKit for centralized Docker builds", () => { + const helper = readFileSync(HELPER_PATH, "utf8"); + + expect(helper).toContain("DOCKER_BUILDKIT=1"); + expect(helper).toContain("docker_build_exec()"); + expect(helper).toContain("docker_build_run()"); + expect(helper).toContain("docker buildx build --load"); + }); + + it("keeps shell-script Docker builds behind the helper", () => { + for (const path of CENTRALIZED_BUILD_SCRIPTS) { + const script = readFileSync(path, "utf8"); + + expect(script, path).toMatch(/docker-build\.sh|docker-e2e-image\.sh/); + expect(script, path).not.toMatch(/\bdocker build\b/); + expect(script, path).not.toMatch(/run_logged\s+\S+\s+docker\s+build/); + } + }); +});