fix: restore release validation on main (#102516)

* test(qqbot): assert approval fence exactly

* fix(release): wait for ClawHub publish logging

* fix(ci): repair Bun global install smoke

* test(slack): fix app identity fixture
This commit is contained in:
Peter Steinberger 2026-07-09 09:39:08 +01:00 committed by GitHub
parent 32de666163
commit 42a8679c4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 278 additions and 106 deletions

View file

@ -24,6 +24,7 @@ DIST_IMAGE="${OPENCLAW_BUN_GLOBAL_SMOKE_DIST_IMAGE:-}"
PACKAGE_TGZ="${OPENCLAW_BUN_GLOBAL_SMOKE_PACKAGE_TGZ:-}"
COMMAND_TIMEOUT_MS="$(read_positive_int_env OPENCLAW_BUN_GLOBAL_SMOKE_TIMEOUT_MS 180000)"
DOCKER_COMMAND_TIMEOUT="${DOCKER_COMMAND_TIMEOUT:-${OPENCLAW_BUN_GLOBAL_SMOKE_DOCKER_COMMAND_TIMEOUT:-600s}}"
AI_PACKAGE_TGZ=""
SMOKE_DIR=""
PACK_DIR=""
@ -36,6 +37,39 @@ cleanup() {
fi
}
prepare_ai_candidate() {
local ai_manifest
local ai_package_dir
local ai_tarballs
local root_manifest
if [ -z "$PACK_DIR" ]; then
PACK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-bun-pack.XXXXXX")"
fi
echo "==> Extract bundled candidate @openclaw/ai package"
ai_package_dir="$PACK_DIR/ai-candidate"
mkdir -p "$ai_package_dir"
tar -xzf "$PACKAGE_TGZ" \
-C "$ai_package_dir" \
--strip-components=4 \
package/node_modules/@openclaw/ai
root_manifest="$PACK_DIR/openclaw-package.json"
ai_manifest="$ai_package_dir/package.json"
tar -xOf "$PACKAGE_TGZ" package/package.json >"$root_manifest"
node scripts/e2e/lib/bun-global-install/assertions.mjs \
assert-release-versions \
"$root_manifest" \
"$ai_manifest" \
>/dev/null
npm pack --ignore-scripts --silent --pack-destination "$PACK_DIR" "$ai_package_dir" >/dev/null
ai_tarballs=("$PACK_DIR"/openclaw-ai-*.tgz)
if [ "${#ai_tarballs[@]}" -ne 1 ] || [ ! -f "${ai_tarballs[0]}" ]; then
echo "expected one packed @openclaw/ai candidate in $PACK_DIR" >&2
exit 1
fi
AI_PACKAGE_TGZ="${ai_tarballs[0]}"
}
trap cleanup EXIT
run_with_timeout() {
@ -44,52 +78,50 @@ run_with_timeout() {
node scripts/e2e/lib/bun-global-install/assertions.mjs run-with-timeout "$timeout_ms" "$@"
}
resolve_pack_tarball_path() {
local pack_json_file="$1"
local pack_dir="$2"
node -e '
const fs = require("node:fs");
const path = require("node:path");
const raw = fs.readFileSync(process.argv[1], "utf8") || "[]";
const parsed = JSON.parse(raw);
const last = Array.isArray(parsed) ? parsed.at(-1) : null;
const filename = typeof last?.filename === "string" ? last.filename.trim() : "";
if (
!filename.endsWith(".tgz") ||
filename.includes("\0") ||
filename !== path.basename(filename) ||
filename !== path.win32.basename(filename)
) {
console.error(`ERROR: npm pack reported unsafe tarball filename ${JSON.stringify(filename)}`);
process.exit(1);
}
process.stdout.write(path.resolve(process.argv[2], filename));
' "$pack_json_file" "$pack_dir"
}
restore_dist_from_image() {
local image="$1"
local ai_backup_dir=""
local ai_dist_installed=0
local backup_dir=""
local container_id=""
local swapped=0
local dist_installed=0
local restore_complete=0
local temp_dir=""
cleanup_restore_dist() {
if [ -n "$container_id" ]; then
docker_e2e_docker_cmd rm -f "$container_id" >/dev/null 2>&1 || true
fi
if [ "$swapped" != "1" ] && [ -n "$backup_dir" ] && [ -d "$backup_dir" ]; then
rm -rf "$ROOT_DIR/dist" >/dev/null 2>&1 || true
if [ ! -e "$ROOT_DIR/dist" ] && mv "$backup_dir" "$ROOT_DIR/dist" >/dev/null 2>&1; then
backup_dir=""
# Both build trees come from one image. A partial swap must restore both or
# the following package step could mix artifacts from different builds.
if [ "$restore_complete" != "1" ]; then
if [ "$dist_installed" = "1" ]; then
rm -rf "$ROOT_DIR/dist" >/dev/null 2>&1 || true
fi
if [ -n "$backup_dir" ] && [ -d "$backup_dir" ]; then
if [ ! -e "$ROOT_DIR/dist" ] && mv "$backup_dir" "$ROOT_DIR/dist" >/dev/null 2>&1; then
backup_dir=""
fi
fi
if [ "$ai_dist_installed" = "1" ]; then
rm -rf "$ROOT_DIR/packages/ai/dist" >/dev/null 2>&1 || true
fi
if [ -n "$ai_backup_dir" ] && [ -d "$ai_backup_dir" ]; then
if [ ! -e "$ROOT_DIR/packages/ai/dist" ] && \
mv "$ai_backup_dir" "$ROOT_DIR/packages/ai/dist" >/dev/null 2>&1; then
ai_backup_dir=""
fi
fi
fi
if [ -n "$temp_dir" ]; then
rm -rf "$temp_dir"
fi
if [ "$swapped" = "1" ] && [ -n "$backup_dir" ]; then
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
}
echo "==> Reuse dist/ from Docker image: $image"
@ -105,6 +137,12 @@ restore_dist_from_image() {
cleanup_restore_dist
return 1
fi
if ! docker_e2e_docker_cmd cp \
"${container_id}:/app/node_modules/@openclaw/ai/dist" \
"$temp_dir/ai-dist"; then
cleanup_restore_dist
return 1
fi
if [ -e "$ROOT_DIR/dist" ]; then
if ! backup_dir="$(mktemp -d "$ROOT_DIR/.dist-backup.XXXXXX")"; then
cleanup_restore_dist
@ -123,7 +161,27 @@ restore_dist_from_image() {
cleanup_restore_dist
return 1
fi
swapped=1
dist_installed=1
if [ -e "$ROOT_DIR/packages/ai/dist" ]; then
if ! ai_backup_dir="$(mktemp -d "$ROOT_DIR/packages/ai/.dist-backup.XXXXXX")"; then
cleanup_restore_dist
return 1
fi
if ! rmdir "$ai_backup_dir"; then
cleanup_restore_dist
return 1
fi
if ! mv "$ROOT_DIR/packages/ai/dist" "$ai_backup_dir"; then
cleanup_restore_dist
return 1
fi
fi
if ! mv "$temp_dir/ai-dist" "$ROOT_DIR/packages/ai/dist"; then
cleanup_restore_dist
return 1
fi
ai_dist_installed=1
restore_complete=1
cleanup_restore_dist
}
@ -151,16 +209,15 @@ resolve_package_tgz() {
exit 1
fi
echo "==> Write package inventory"
node --import tsx scripts/write-package-dist-inventory.ts
local pack_json_file
PACK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-bun-pack.XXXXXX")"
pack_json_file="$PACK_DIR/pack.json"
echo "==> Pack OpenClaw tarball"
npm pack --ignore-scripts --json --pack-destination "$PACK_DIR" >"$pack_json_file"
PACKAGE_TGZ="$(resolve_pack_tarball_path "$pack_json_file" "$PACK_DIR")"
PACKAGE_TGZ="$(
node scripts/package-openclaw-for-docker.mjs \
--skip-build \
--output-dir "$PACK_DIR" \
--output-name openclaw-current.tgz
)"
if [ -z "$PACKAGE_TGZ" ] || [ ! -f "$PACKAGE_TGZ" ]; then
echo "missing packed OpenClaw tarball" >&2
exit 1
@ -176,6 +233,7 @@ main() {
fi
resolve_package_tgz
prepare_ai_candidate
local bun_path
local openclaw_bin
@ -188,8 +246,21 @@ main() {
export OPENCLAW_NO_ONBOARD=1
export OPENCLAW_DISABLE_UPDATE_CHECK=1
export NO_COLOR=1
mkdir -p "$HOME" "$BUN_INSTALL/bin" "$XDG_CACHE_HOME"
mkdir -p "$HOME" "$BUN_INSTALL/bin" "$BUN_INSTALL/install/global" "$XDG_CACHE_HOME"
export PATH="$BUN_INSTALL/bin:$(dirname "$(command -v node)"):$PATH"
# Release publishes @openclaw/ai first. Bun 1.3.14 ignores bundled deps in
# local tarballs, so resolve that one package from the exact candidate bytes.
node --input-type=module - \
"$BUN_INSTALL/install/global/package.json" \
"$AI_PACKAGE_TGZ" <<'NODE'
import fs from "node:fs";
const [, , packageJsonPath, aiPackageTarball] = process.argv;
fs.writeFileSync(
packageJsonPath,
`${JSON.stringify({ private: true, overrides: { "@openclaw/ai": `file:${aiPackageTarball}` } })}\n`,
);
NODE
echo "==> Bun version"
"$bun_path" --version

View file

@ -1,11 +1,14 @@
// Assertions for Bun global install E2E validation.
import { spawn } from "node:child_process";
import fs from "node:fs";
const DEFAULT_TIMEOUT_KILL_GRACE_MS = 30_000;
const PARENT_TERMINATION_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
const usage = () => {
console.error("Usage: assertions.mjs <run-with-timeout|assert-image-providers> [...]");
console.error(
"Usage: assertions.mjs <run-with-timeout|assert-image-providers|assert-release-versions> [...]",
);
process.exit(2);
};
@ -204,4 +207,28 @@ if (mode === "assert-image-providers") {
process.exit(0);
}
if (mode === "assert-release-versions") {
const [rootManifestPath, aiManifestPath] = args;
if (!rootManifestPath || !aiManifestPath) {
usage();
}
const rootManifest = JSON.parse(fs.readFileSync(rootManifestPath, "utf8"));
const aiManifest = JSON.parse(fs.readFileSync(aiManifestPath, "utf8"));
const rootVersion = rootManifest.version;
const aiVersion = aiManifest.version;
const rootAiVersion = rootManifest.dependencies?.["@openclaw/ai"];
if (
typeof rootVersion !== "string" ||
typeof aiVersion !== "string" ||
rootVersion !== aiVersion ||
rootAiVersion !== aiVersion
) {
throw new Error(
`candidate version mismatch: openclaw=${String(rootVersion)}, dependency=${String(rootAiVersion)}, @openclaw/ai=${String(aiVersion)}`,
);
}
process.stdout.write(aiVersion);
process.exit(0);
}
usage();

View file

@ -200,7 +200,7 @@ fi
publish_log="${pack_dir}/publish.log"
for attempt in $(seq 1 "${OPENCLAW_CLAWHUB_PUBLISH_ATTEMPTS:-8}"); do
if CLAWHUB_WORKDIR="${clawhub_workdir}" "${publish_cmd[@]}" > >(tee "${publish_log}") 2>&1; then
if CLAWHUB_WORKDIR="${clawhub_workdir}" "${publish_cmd[@]}" 2>&1 | tee "${publish_log}"; then
exit 0
fi
if ! grep -Eqi "rate limit|too many requests|\\b429\\b" "${publish_log}"; then