openclaw/scripts/lib/docker-e2e-package.sh
Peter Steinberger 3cb68020b6
refactor(scripts): deslop shared tooling
## What Problem This Solves

Repository scripts retain private copies of shared tooling helpers and an unused translation subprocess runner, increasing maintenance work across sibling scripts.

## User Impact

Internal tooling cleanup: existing flags, diagnostics, generated bytes, JSON formats, exit codes, and cleanup policies stay the same. The build wrapper additionally detects forbidden dynamic-import warnings when compiler output splits the warning marker across chunks.

## Why This Change Was Made

- Remove the retired i18n process runner. Its final production caller moved to the shared formatter in #95534; retain real CLI/privacy/provider tests and run their subprocesses with the existing dependency.
- Share Docker resource, signal-trap, platform, workspace-staging, and plugin-selection helpers while retaining each caller's environment precedence and lifecycle decisions.
- Reuse existing E2E fixture JSON/assertion and package-path helpers; retain distinct write modes, recovery ordering, and mounted/frozen harness contracts.
- Share macOS guest desktop-user/home resolution through its existing owner while preserving per-caller timeout policy.
- Route release/mobile flags through the existing version flag specifications, preserving split-only values, duplicate/mode rules, help timing, and error text. Reuse existing retry sleep and comparator owners and remove an unreachable iOS output branch.
- Reuse existing guard entrypoint, failure-trailer, diagnostic-line, and metadata-normalization owners.

Measured reduction: **732 net production lines**, with test changes counted separately. PR tooling and its protected import closure, CI planners/shards, baselines, and generated artifacts are unchanged.

## Fixes Found Along the Way

The tsdown scanner checked each raw output chunk for its warning marker. It now checks the existing combined-line buffer. The regression exercises every split inside the marker and fails on the original source for the intended missing-warning assertion.

## Evidence

- Blacksmith Testbox `tbx_01m3tp4sabwd0807kj9jkqqmbr`: frozen dependency install and candidate source-byte verification; all nine changed test files and 59 selected sibling files passed, including real CLI, package fixture, recovery, Docker harness, and Parallels transport contracts.
- Generated channel metadata is byte-identical; differential metadata normalization cases passed.
- SDK surface check passed. Independent isolated Codex review completed with no P0–P2 findings.
- Initial changed checks caught an invalid direct source import in a proposed snapshot-distance consolidation; that independent change was withdrawn. No boundary exception was added. The final changed checks pass, including script/test lint and Docker shell/scheduler checks; madge reports 0 cycles and the runtime import check reports 0 cycles.
- Per-file single-worker wall time for changed suites: translation 9.92s; Docker helper 38.88s; live Docker auth 1.71s; mobile ref 2.36s; mobile release 6.23s; release preparation 2.42s; version 2.04s; installer 15.01s; tsdown 2.70s. The existing Docker helper suite executes shell/process cleanup and container-command boundary fixtures; new assertions reuse those fixtures. New parser/scanner cases use no sleeps or polling.

Hosted CI will be verified against this PR's exact pushed head. No live deployment or release was performed.
2026-10-01 05:14:07 +00:00

501 lines
16 KiB
Bash

#!/usr/bin/env bash
#
# Shared package helpers for Docker E2E scripts.
# Builds or resolves one OpenClaw npm tarball and exposes mount/build-context
# helpers so Docker lanes test the package artifact instead of repo sources.
DOCKER_E2E_PACKAGE_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="${ROOT_DIR:-$(cd "$DOCKER_E2E_PACKAGE_LIB_DIR/../.." && pwd)}"
if ! declare -F run_logged >/dev/null 2>&1; then
source "$DOCKER_E2E_PACKAGE_LIB_DIR/docker-e2e-logs.sh"
fi
if ! declare -F docker_e2e_docker_cmd >/dev/null 2>&1 || \
! declare -F docker_e2e_docker_run_cmd >/dev/null 2>&1; then
source "$DOCKER_E2E_PACKAGE_LIB_DIR/docker-e2e-container.sh"
fi
docker_e2e_abs_path() {
local file="$1"
(cd "$(dirname "$file")" && printf '%s/%s\n' "$(pwd)" "$(basename "$file")")
}
docker_e2e_restore_package_dist_from_image() (
local image="$1"
local ai_backup_dir=""
local ai_dist_dir=""
local ai_dist_installed=0
local ai_package_dir=""
local backup_dir=""
local container_id=""
local dist_installed=0
local requires_ai_dist=0
local restore_root=""
local restore_complete=0
local temp_dir=""
cleanup_restore_package_dist() {
if [ -n "$container_id" ]; then
docker_e2e_docker_cmd rm -f "$container_id" >/dev/null 2>&1 || true
fi
# Root and AI artifacts come from one image. Restore both on partial failure
# so the package step cannot combine outputs from different builds.
if [ "$restore_complete" != "1" ]; then
if [ "$dist_installed" = "1" ]; then
rm -rf "$restore_root/dist" >/dev/null 2>&1 || true
fi
if [ -n "$backup_dir" ] && [ -d "$backup_dir" ]; then
if [ ! -e "$restore_root/dist" ] && \
mv "$backup_dir" "$restore_root/dist" >/dev/null 2>&1; then
backup_dir=""
fi
fi
if [ "$ai_dist_installed" = "1" ]; then
rm -rf "$ai_dist_dir" >/dev/null 2>&1 || true
fi
if [ -n "$ai_backup_dir" ] && [ -d "$ai_backup_dir" ]; then
if [ ! -e "$ai_dist_dir" ] && \
mv "$ai_backup_dir" "$ai_dist_dir" >/dev/null 2>&1; then
ai_backup_dir=""
fi
fi
fi
if [ -n "$temp_dir" ]; then
rm -rf "$temp_dir"
fi
if [ "$restore_complete" = "1" ] && [ -n "$backup_dir" ]; then
rm -rf "$backup_dir"
fi
if [ "$restore_complete" = "1" ] && [ -n "$ai_backup_dir" ]; then
rm -rf "$ai_backup_dir"
fi
}
if ! restore_root="$(cd "$ROOT_DIR" && pwd -P)"; then
echo "unable to resolve package restore root: $ROOT_DIR" >&2
return 1
fi
# The trusted workflow owns this static checkout and runs no candidate process
# concurrently. Resolve owner paths once and reuse them through every swap.
if [ -L "$restore_root/packages" ] || [ -L "$restore_root/packages/ai" ]; then
echo "refusing package artifact restore through a symlinked packages path" >&2
return 1
fi
if [ -f "$restore_root/packages/ai/package.json" ]; then
if ! ai_package_dir="$(cd "$restore_root/packages/ai" && pwd -P)"; then
echo "unable to resolve bundled AI package path" >&2
return 1
fi
case "$ai_package_dir/" in
"$restore_root"/*) ;;
*)
echo "refusing bundled AI artifact restore outside the package root" >&2
return 1
;;
esac
ai_dist_dir="$ai_package_dir/dist"
requires_ai_dist=1
fi
echo "==> Reuse package build artifacts from Docker image: $image"
if ! container_id="$(docker_e2e_docker_cmd create "$image")"; then
cleanup_restore_package_dist
return 1
fi
if ! temp_dir="$(mktemp -d "$restore_root/.package-dist.XXXXXX")"; then
cleanup_restore_package_dist
return 1
fi
if ! docker_e2e_docker_cmd cp "${container_id}:/app/dist" "$temp_dir/dist"; then
cleanup_restore_package_dist
return 1
fi
if [ "$requires_ai_dist" = "1" ] && \
! docker_e2e_docker_cmd cp \
"${container_id}:/app/node_modules/@openclaw/ai/dist" \
"$temp_dir/ai-dist"; then
cleanup_restore_package_dist
return 1
fi
if [ -e "$restore_root/dist" ]; then
if ! backup_dir="$(mktemp -d "$restore_root/.dist-backup.XXXXXX")"; then
cleanup_restore_package_dist
return 1
fi
if ! rmdir "$backup_dir"; then
cleanup_restore_package_dist
return 1
fi
if ! mv "$restore_root/dist" "$backup_dir"; then
cleanup_restore_package_dist
return 1
fi
fi
if ! mv "$temp_dir/dist" "$restore_root/dist"; then
cleanup_restore_package_dist
return 1
fi
dist_installed=1
if [ "$requires_ai_dist" = "1" ]; then
if [ -e "$ai_dist_dir" ]; then
if ! ai_backup_dir="$(mktemp -d "$ai_package_dir/.dist-backup.XXXXXX")"; then
cleanup_restore_package_dist
return 1
fi
if ! rmdir "$ai_backup_dir"; then
cleanup_restore_package_dist
return 1
fi
if ! mv "$ai_dist_dir" "$ai_backup_dir"; then
cleanup_restore_package_dist
return 1
fi
fi
if ! mv "$temp_dir/ai-dist" "$ai_dist_dir"; then
cleanup_restore_package_dist
return 1
fi
ai_dist_installed=1
fi
restore_complete=1
cleanup_restore_package_dist
)
docker_e2e_prepare_package_tgz() {
local label="$1"
local package_tgz="${2:-${OPENCLAW_CURRENT_PACKAGE_TGZ:-}}"
if [ -n "$package_tgz" ]; then
if [ ! -f "$package_tgz" ]; then
echo "OpenClaw package tarball does not exist: $package_tgz" >&2
return 1
fi
docker_e2e_abs_path "$package_tgz"
return 0
fi
local pack_dir
pack_dir="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-docker-e2e-pack.XXXXXX")"
local pack_status=0
# ROOT_DIR can be a frozen candidate; resolve tooling beside this helper.
package_tgz="$(
node "$DOCKER_E2E_PACKAGE_LIB_DIR/../package-openclaw-for-docker.mjs" \
--source-dir "${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$ROOT_DIR}" \
--allow-unreleased-changelog \
--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
touch "$pack_dir/.openclaw-docker-e2e-generated-package"
docker_e2e_abs_path "$package_tgz"
}
docker_e2e_prepare_package_context() {
local package_tgz="$1"
local context_dir
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.
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
# The root package keeps its published dependency declarations. Carry the
# verified candidate registry into BuildKit so unpublished core packages resolve.
if ! node --input-type=module - \
"$DOCKER_E2E_PACKAGE_LIB_DIR/../prepublish-plugin-registry-artifact.mjs" \
"$context_dir" <<'NODE'
import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
const [, , artifactScript, contextDir] = process.argv;
const registryDir = process.env.OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR;
const target = path.join(contextDir, "prepublish-plugin-registry");
fs.mkdirSync(target);
let identity = {};
if (registryDir) {
const { PREPUBLISH_PLUGIN_REGISTRY_MANIFEST, validatePrepublishPluginRegistryArtifact } =
await import(pathToFileURL(artifactScript).href);
const bytes = fs.readFileSync(path.join(registryDir, PREPUBLISH_PLUGIN_REGISTRY_MANIFEST));
const manifest = JSON.parse(bytes);
identity = {
sourceSha: process.env.OPENCLAW_DOCKER_E2E_SELECTED_SHA || manifest.sourceSha,
candidateVersion: process.env.OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_CANDIDATE_VERSION || manifest.candidateVersion,
manifestSha256: process.env.OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_MANIFEST_SHA256 || createHash("sha256").update(bytes).digest("hex"),
};
validatePrepublishPluginRegistryArtifact({
artifactDir: registryDir,
expectedSourceSha: identity.sourceSha,
expectedCandidateVersion: identity.candidateVersion,
expectedManifestSha256: identity.manifestSha256,
requiredPackages: [],
});
fs.cpSync(registryDir, target, { recursive: true });
}
fs.writeFileSync(path.join(contextDir, "registry-identity.json"), `${JSON.stringify(identity)}\n`);
NODE
then
rm -rf "$context_dir"
return 1
fi
printf '%s\n' "$context_dir"
}
docker_e2e_package_mount_args() {
local package_tgz="$1"
local target="${2:-/tmp/openclaw-current.tgz}"
DOCKER_E2E_PACKAGE_ARGS=(-v "$package_tgz:$target:ro" -e "OPENCLAW_CURRENT_PACKAGE_TGZ=$target")
if [ -n "${OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR:-}" ]; then
source "$DOCKER_E2E_PACKAGE_LIB_DIR/../e2e/lib/prepublish-plugin-registry.sh"
openclaw_prepublish_plugin_registry_configure_docker_args "$OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR"
DOCKER_E2E_PACKAGE_ARGS+=(
"${OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DOCKER_ARGS[@]}"
)
fi
if [ -n "${OPENCLAW_E2E_NPM_INSTALL_TIMEOUT:-}" ]; then
DOCKER_E2E_PACKAGE_ARGS+=(-e "OPENCLAW_E2E_NPM_INSTALL_TIMEOUT=$OPENCLAW_E2E_NPM_INSTALL_TIMEOUT")
fi
if [ -n "${OPENCLAW_E2E_COMMAND_TIMEOUT:-}" ]; then
DOCKER_E2E_PACKAGE_ARGS+=(-e "OPENCLAW_E2E_COMMAND_TIMEOUT=$OPENCLAW_E2E_COMMAND_TIMEOUT")
fi
}
docker_e2e_cleanup_package_tgz() {
local package_tgz="${1:-}"
[ -n "$package_tgz" ] || return 0
[ "$(basename "$package_tgz")" = "openclaw-current.tgz" ] || return 0
local pack_dir
pack_dir="$(dirname "$package_tgz")"
if [ -f "$pack_dir/.openclaw-docker-e2e-generated-package" ]; then
rm -rf "$pack_dir"
fi
}
docker_e2e_cleanup_package_run() {
docker_e2e_cleanup_package_tgz "${1:-}"
if [ -n "${2:-}" ]; then
rm -f "$2"
fi
}
docker_e2e_cleanup_package_mount_args() {
local expect_volume_path=0
local arg
for arg in "${DOCKER_E2E_PACKAGE_ARGS[@]:-}"; do
if [ "$expect_volume_path" = "1" ]; then
docker_e2e_cleanup_package_tgz "${arg%%:*}"
expect_volume_path=0
continue
fi
if [ "$arg" = "-v" ]; then
expect_volume_path=1
fi
done
}
docker_e2e_cleanup_container_cidfile() {
local cidfile="${1:-}"
[ -n "$cidfile" ] || return 0
if [ -f "$cidfile" ]; then
local container_id
container_id="$(head -n 1 "$cidfile" 2>/dev/null || true)"
if [ -n "$container_id" ]; then
docker_e2e_docker_cmd rm -f "$container_id" >/dev/null 2>&1 || true
fi
rm -f "$cidfile"
fi
}
docker_e2e_print_failed_container_state() {
local cidfile="${1:-}"
[ -f "$cidfile" ] || return 0
local container_id
container_id="$(head -n 1 "$cidfile" 2>/dev/null || true)"
[ -n "$container_id" ] || return 0
local inspect_output=""
local inspect_status=0
inspect_output="$(
docker_e2e_docker_cmd inspect --format 'ExitCode={{.State.ExitCode}}
OOMKilled={{.State.OOMKilled}}
Init={{.HostConfig.Init}}
Error={{printf "%.4096s" .State.Error}}' "$container_id" 2>&1
)" || inspect_status="$?"
if [ "$inspect_status" -ne 0 ]; then
printf 'Docker container state unavailable (inspect exit %s): %.4096s\n' \
"$inspect_status" "$inspect_output" >&2
return 0
fi
echo "Docker container state:" >&2
printf '%.4608s\n' "$inspect_output" >&2
}
docker_e2e_harness_mount_args() {
local harness_root="${DOCKER_E2E_HARNESS_ROOT_DIR:-$ROOT_DIR}"
local windows_helpers="${DOCKER_E2E_WINDOWS_HELPERS_PATH:-$harness_root/scripts/windows-cmd-helpers.mjs}"
DOCKER_E2E_HARNESS_ARGS=(
-v "$harness_root/scripts/e2e:/app/scripts/e2e:ro"
-v "$harness_root/scripts/docker/verify-fs-safe-native.mjs:/app/scripts/docker/verify-fs-safe-native.mjs:ro"
-v "$harness_root/scripts/lib:/app/scripts/lib:ro"
-v "$harness_root/packages/gateway-client/src:/app/packages/gateway-client/src:ro"
-v "$harness_root/packages/normalization-core/package.json:/app/packages/normalization-core/package.json:ro"
-v "$harness_root/packages/normalization-core/src:/app/packages/normalization-core/src:ro"
-v "$harness_root/tsconfig.json:/app/tsconfig.json:ro"
-v "$harness_root/test/e2e/qa-lab:/app/test/e2e/qa-lab:ro"
-v "$harness_root/test/helpers:/app/test/helpers:ro"
-v "$harness_root/scripts/prepublish-plugin-registry-artifact.mjs:/app/scripts/prepublish-plugin-registry-artifact.mjs:ro"
-v "$windows_helpers:/app/scripts/windows-cmd-helpers.mjs:ro"
)
}
docker_e2e_run_with_harness() {
docker_e2e_harness_mount_args
local run_status=0
local cid_dir
local cidfile
local docker_run_pid=""
local harness_stdin_fd=""
local cleanup_done=0
local previous_int_trap
local previous_term_trap
local previous_hup_trap
cid_dir="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-docker-e2e-container.XXXXXX")"
cidfile="$cid_dir/container.cid"
previous_int_trap="$(trap -p INT || true)"
previous_term_trap="$(trap -p TERM || true)"
previous_hup_trap="$(trap -p HUP || true)"
docker_e2e_harness_descendant_pids() {
local parent_pid="$1"
local child_pid
for child_pid in $(pgrep -P "$parent_pid" 2>/dev/null || true); do
docker_e2e_harness_descendant_pids "$child_pid"
printf '%s\n' "$child_pid"
done
}
terminate_harness_docker_run() {
[ -n "$docker_run_pid" ] || return 0
kill -0 "$docker_run_pid" 2>/dev/null || return 0
local descendant_pids
descendant_pids="$(docker_e2e_harness_descendant_pids "$docker_run_pid")"
if [ -n "$descendant_pids" ]; then
kill -TERM $descendant_pids 2>/dev/null || true
fi
kill -TERM "$docker_run_pid" 2>/dev/null || true
local grace_seconds="${OPENCLAW_DOCKER_E2E_CONTAINER_TERM_GRACE_SECONDS:-10}"
if ! [[ "$grace_seconds" =~ ^[0-9]+$ ]] || [ "$grace_seconds" -lt 1 ]; then
grace_seconds="10"
else
grace_seconds="$((10#$grace_seconds))"
fi
local wait_attempt
for wait_attempt in $(seq 1 "$((grace_seconds * 10))"); do
if ! kill -0 "$docker_run_pid" 2>/dev/null; then
return 0
fi
/bin/sleep 0.1
done
descendant_pids="$(docker_e2e_harness_descendant_pids "$docker_run_pid")"
if [ -n "$descendant_pids" ]; then
kill -KILL $descendant_pids 2>/dev/null || true
fi
kill -KILL "$docker_run_pid" 2>/dev/null || true
}
cleanup_harness_run() {
local cleanup_status="${1:-$?}"
local exit_after_cleanup="${2:-0}"
if [ "$cleanup_done" = "1" ]; then
if [ "$exit_after_cleanup" = "1" ]; then
exit "$cleanup_status"
fi
return "$cleanup_status"
fi
cleanup_done=1
trap - INT TERM HUP
terminate_harness_docker_run
wait "$docker_run_pid" 2>/dev/null || true
docker_e2e_cleanup_container_cidfile "$cidfile"
rmdir "$cid_dir" 2>/dev/null || true
docker_e2e_cleanup_package_mount_args
if [ -n "$harness_stdin_fd" ]; then
eval "exec ${harness_stdin_fd}<&-"
fi
docker_e2e_restore_signal_traps "$previous_int_trap" "$previous_term_trap" "$previous_hup_trap"
if [ "$exit_after_cleanup" = "1" ]; then
exit "$cleanup_status"
fi
return "$cleanup_status"
}
trap 'cleanup_harness_run 130 1' INT
trap 'cleanup_harness_run 143 1' TERM
trap 'cleanup_harness_run 129 1' HUP
local candidate_fd
for candidate_fd in 19 18 17 16 15 14 13 12 11 10; do
if ! eval "true <&${candidate_fd}" 2>/dev/null; then
harness_stdin_fd="$candidate_fd"
break
fi
done
if [ -z "$harness_stdin_fd" ]; then
echo "no free file descriptor available for Docker harness stdin" >&2
cleanup_harness_run 1
return 1
fi
eval "exec ${harness_stdin_fd}<&0"
docker_e2e_docker_run_cmd run --cidfile "$cidfile" "${DOCKER_E2E_HARNESS_ARGS[@]}" "$@" <&$harness_stdin_fd &
docker_run_pid="$!"
local had_errexit=0
case "$-" in
*e*)
had_errexit=1
;;
esac
set +e
wait "$docker_run_pid"
run_status="$?"
if [ "$had_errexit" = "1" ]; then
set -e
fi
if [ "$run_status" -ne 0 ]; then
docker_e2e_print_failed_container_state "$cidfile"
fi
cleanup_harness_run 0
return "$run_status"
}
docker_e2e_run_detached_with_harness() {
docker_e2e_harness_mount_args
docker_e2e_docker_cmd run -d "${DOCKER_E2E_HARNESS_ARGS[@]}" "$@"
}
docker_e2e_run_logged_with_harness() {
local label="$1"
shift
run_logged "$label" docker_e2e_run_with_harness "$@"
}
docker_e2e_run_logged_print_with_harness() {
local label="$1"
shift
local heartbeat_seconds
heartbeat_seconds="$(docker_e2e_read_positive_int_env OPENCLAW_DOCKER_E2E_LOG_HEARTBEAT_SECONDS 30)" || return $?
run_logged_print_heartbeat \
"$label" \
"$heartbeat_seconds" \
docker_e2e_run_with_harness \
"$@"
}