openclaw/scripts/e2e/update-first-hop-compat-docker.sh
Dallin Romney 935563c38d
improve(ci): run first-hop post-convergence proof once (#163631)
* perf(ci): deduplicate post-convergence first-hop proof

* test(ci): cover canonical first-hop lane admission
2026-10-02 10:12:15 -07:00

177 lines
8.3 KiB
Bash
Executable file

#!/usr/bin/env bash
# Bash 5.3+ can deadlock writing heredoc pipes on macOS before the reader starts.
if [[ ${OSTYPE:-} == darwin* && $BASH != /bin/bash ]] && ((BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 3))); then
exec /bin/bash "$0" "$@"
fi
# Proves the selected first-hop method and a fresh updater without legacy chunks.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
if [ "${OPENCLAW_QA_ALLOW_UPDATE_FIRST_HOP:-0}" != "1" ]; then
echo "blocked destructive package self-update; set OPENCLAW_QA_ALLOW_UPDATE_FIRST_HOP=1 to run" >&2
exit 2
fi
IMAGE_NAME="$(
docker_e2e_resolve_image \
"openclaw-update-first-hop-compat-e2e" \
OPENCLAW_UPDATE_FIRST_HOP_E2E_IMAGE
)"
SKIP_BUILD="${OPENCLAW_UPDATE_FIRST_HOP_E2E_SKIP_BUILD:-0}"
# Run 36506342273 (hosted 4-vCPU): 1558s before the final candidate hop
# + projected 560s hop + ~5s assertions ~= 2125s; x ~1.5 => 3200s per source.
DOCKER_RUN_TIMEOUT="${OPENCLAW_UPDATE_FIRST_HOP_DOCKER_RUN_TIMEOUT:-3200s}"
# Space- or comma-separated recorded release versions; empty runs every recorded source.
SOURCE_VERSION_FILTER="${OPENCLAW_UPDATE_FIRST_HOP_SOURCE_VERSIONS:-}"
SCENARIO="${OPENCLAW_UPDATE_FIRST_HOP_SCENARIO:-all}"
ARTIFACT_DIR="${OPENCLAW_UPDATE_FIRST_HOP_ARTIFACT_DIR:-$ROOT_DIR/.artifacts/update-first-hop-compat${SOURCE_VERSION_FILTER:+-${SOURCE_VERSION_FILTER//[ ,]/-}}}"
SOURCE_PACKAGE="${OPENCLAW_UPDATE_FIRST_HOP_SOURCE_PACKAGE_TGZ:-}"
FIXTURE_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-update-first-hop.XXXXXX")"
PACKAGE_TGZ=""
FIXTURE_HELPER="$ROOT_DIR/scripts/e2e/lib/update-first-hop-package-fixtures.mjs"
cleanup() {
local exit_status="$?"
trap - EXIT
docker_e2e_cleanup_package_tgz "${PACKAGE_TGZ:-}"
rm -rf "$FIXTURE_ROOT"
exit "$exit_status"
}
trap cleanup EXIT
mkdir -p "$ARTIFACT_DIR" "$FIXTURE_ROOT/source" "$FIXTURE_ROOT/packages"
chmod -R a+rwX "$ARTIFACT_DIR" || true
if [ -n "$SOURCE_PACKAGE" ] && [ ! -f "$SOURCE_PACKAGE" ]; then
echo "source package tarball does not exist: $SOURCE_PACKAGE" >&2
exit 2
fi
if [ -n "$SOURCE_PACKAGE" ] && [ -n "$SOURCE_VERSION_FILTER" ]; then
echo "an explicit source tarball cannot be combined with OPENCLAW_UPDATE_FIRST_HOP_SOURCE_VERSIONS" >&2
exit 2
fi
case "$SCENARIO" in
all | source | missing-load-path) ;;
*)
echo "invalid first-hop scenario: $SCENARIO" >&2
exit 2
;;
esac
if [ "$SCENARIO" = "missing-load-path" ] && { [ -n "$SOURCE_PACKAGE" ] || [ -n "$SOURCE_VERSION_FILTER" ]; }; then
echo "the missing-load-path scenario does not accept a historical source" >&2
exit 2
fi
PACKAGE_TGZ="$(
docker_e2e_prepare_package_tgz \
update-first-hop-compat \
"${OPENCLAW_UPDATE_FIRST_HOP_CANDIDATE_PACKAGE_TGZ:-}"
)"
# Published updaters can intentionally skip same-version payloads. Relabel only
# fixture metadata so every source performs a real hop with the candidate's bridges.
FIRST_HOP_TGZ="$FIXTURE_ROOT/first-hop.tgz"
node "$FIXTURE_HELPER" first-hop-tarball "$PACKAGE_TGZ" "$FIRST_HOP_TGZ" 0 \
>"$ARTIFACT_DIR/first-hop-fixture.json"
node "$FIXTURE_HELPER" future-tarball "$FIRST_HOP_TGZ" "$FIXTURE_ROOT/future.tgz" 1 \
>"$ARTIFACT_DIR/second-hop-fixture.json"
ADMISSION_PROTOCOL="$(tar -xOf "$PACKAGE_TGZ" package/package.json | node -pe 'JSON.parse(require("node:fs").readFileSync(0, "utf8")).openclaw?.updateAdmissionProtocol ?? ""')"
if [ "$ADMISSION_PROTOCOL" = "1" ] && [ "$SCENARIO" != "missing-load-path" ]; then
node "$FIXTURE_HELPER" unsupported-admission-tarball "$FIXTURE_ROOT/future.tgz" \
"$FIXTURE_ROOT/unsupported-admission.tgz" 2 >"$ARTIFACT_DIR/unsupported-admission-fixture.json"
fi
docker_e2e_package_mount_args "$FIRST_HOP_TGZ" /tmp/openclaw-update-first-hop-candidate.tgz
if [ "$ADMISSION_PROTOCOL" = "1" ] && [ "$SCENARIO" != "missing-load-path" ]; then
DOCKER_E2E_PACKAGE_ARGS+=(-v "$FIXTURE_ROOT/unsupported-admission.tgz:/tmp/openclaw-update-first-hop-unsupported-admission.tgz:ro")
fi
mkdir -p "$FIXTURE_ROOT/packages/original"
tar -xzf "$PACKAGE_TGZ" -C "$FIXTURE_ROOT/packages/original"
docker_e2e_build_or_reuse \
"$IMAGE_NAME" \
update-first-hop-compat \
"$ROOT_DIR/scripts/e2e/Dockerfile" \
"$ROOT_DIR" \
bare \
"$SKIP_BUILD"
SOURCE_VERSIONS=("")
if [ "$SCENARIO" != "missing-load-path" ] && [ -z "$SOURCE_PACKAGE" ]; then
SOURCE_VERSIONS=()
node "$FIXTURE_HELPER" sources "$FIXTURE_ROOT/packages/original/package" \
"$SOURCE_VERSION_FILTER" >"$FIXTURE_ROOT/source-versions.txt"
while IFS= read -r version; do
SOURCE_VERSIONS+=("$version")
done <"$FIXTURE_ROOT/source-versions.txt"
fi
for version in "${SOURCE_VERSIONS[@]}"; do
lane_artifact_dir="$ARTIFACT_DIR"
source_package="${SOURCE_PACKAGE:-$FIRST_HOP_TGZ}"
expected_missing_chunk=""
negative_tgz="$FIRST_HOP_TGZ"
if [ "$SCENARIO" != "missing-load-path" ]; then
if [ -n "$version" ]; then
lane_artifact_dir="$ARTIFACT_DIR/$version"
mkdir -p "$lane_artifact_dir"
cp "$ARTIFACT_DIR/second-hop-fixture.json" "$lane_artifact_dir/second-hop-fixture.json"
npm pack "openclaw@$version" --ignore-scripts --json --min-release-age=0 \
--pack-destination "$FIXTURE_ROOT/source" >"$lane_artifact_dir/source-pack.json"
source_package="$FIXTURE_ROOT/source/$(
node "$FIXTURE_HELPER" pack-filename "$lane_artifact_dir/source-pack.json"
)"
fi
node "$FIXTURE_HELPER" source "$FIXTURE_ROOT/packages/original/package" \
"$source_package" "$version" >"$lane_artifact_dir/source.json"
expected_missing_chunk="$(node -e '
const source = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8"));
process.stdout.write(source.expectedMissingChunk ?? "");
' "$lane_artifact_dir/source.json")"
negative_tgz="$FIXTURE_ROOT/negative-${version:-explicit}.tgz"
node "$FIXTURE_HELPER" negative-tarball "$FIRST_HOP_TGZ" "$negative_tgz" \
"$expected_missing_chunk" >"$lane_artifact_dir/negative-fixture.json"
{
printf 'source=%s\n' "$source_package"
printf 'original_candidate=%s\n' "$PACKAGE_TGZ"
printf 'candidate=%s\n' "$FIRST_HOP_TGZ"
printf 'expected_missing_chunk=%s\n' "$expected_missing_chunk"
shasum -a 256 "$source_package" "$PACKAGE_TGZ" "$FIRST_HOP_TGZ" "$negative_tgz" "$FIXTURE_ROOT/future.tgz"
if [ "$ADMISSION_PROTOCOL" = "1" ]; then
shasum -a 256 "$FIXTURE_ROOT/unsupported-admission.tgz"
fi
printf '\nsource_build_info=' && tar -xOf "$source_package" package/dist/build-info.json
printf '\noriginal_candidate_build_info=' && tar -xOf "$PACKAGE_TGZ" package/dist/build-info.json
printf '\ncandidate_build_info=' && tar -xOf "$FIRST_HOP_TGZ" package/dist/build-info.json
printf '\nfuture_build_info=' && tar -xOf "$FIXTURE_ROOT/future.tgz" package/dist/build-info.json
} >"$lane_artifact_dir/inputs.txt"
fi
chmod a+rwx "$lane_artifact_dir"
echo "Running packaged updater first-hop compatibility Docker E2E (${version:-explicit source})..."
docker_e2e_run_with_harness \
-e OPENCLAW_QA_ALLOW_UPDATE_FIRST_HOP=1 \
-e OPENCLAW_UPDATE_FIRST_HOP_ARTIFACT_DIR=/tmp/openclaw-update-first-hop-artifacts \
-e OPENCLAW_UPDATE_FIRST_HOP_EXPECTED_MISSING_CHUNK="$expected_missing_chunk" \
-e OPENCLAW_UPDATE_FIRST_HOP_ADMISSION_PROTOCOL="$ADMISSION_PROTOCOL" \
-e OPENCLAW_UPDATE_FIRST_HOP_SCENARIO="$SCENARIO" \
-v "$lane_artifact_dir:/tmp/openclaw-update-first-hop-artifacts" \
-v "$(docker_e2e_abs_path "$source_package"):/tmp/openclaw-update-first-hop-source.tgz:ro" \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
-v "$PACKAGE_TGZ:/tmp/openclaw-update-first-hop-original.tgz:ro" \
-v "$negative_tgz:/tmp/openclaw-update-first-hop-negative.tgz:ro" \
-v "$FIXTURE_ROOT/future.tgz:/tmp/openclaw-update-first-hop-future.tgz:ro" \
"$IMAGE_NAME" \
timeout --kill-after=30s "$DOCKER_RUN_TIMEOUT" \
bash scripts/e2e/lib/upgrade-survivor/update-first-hop-compat.sh
done
if [ "$SCENARIO" != "missing-load-path" ] && [ -z "$SOURCE_PACKAGE" ]; then
node -e '
const fs = require("node:fs"), path = require("node:path");
const [root, ...versions] = process.argv.slice(1);
const sources = versions.map(version => JSON.parse(fs.readFileSync(path.join(root, version, "summary.json"), "utf8")));
fs.writeFileSync(path.join(root, "summary.json"), `${JSON.stringify({ sources }, null, 2)}\n`);
' "$ARTIFACT_DIR" "${SOURCE_VERSIONS[@]}"
fi