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

@ -32,7 +32,7 @@ describe("buildExecApprovalText", () => {
commandPreview: `${safePrefix}🎉 trailing text`,
},
});
const codeFence = text.split("```\n")[1]?.split("\n```")[0] ?? "";
expect(codeFence).toBe(safePrefix);
const expectedCommandBlock = ["```", safePrefix, "```"].join("\n");
expect(text).toContain(expectedCommandBlock);
});
});

View file

@ -351,7 +351,7 @@ describe("monitorSlackProvider tool results", () => {
(client.auth as { test: ReturnType<typeof vi.fn> }).test.mockResolvedValue({
user_id: "bot-user",
team_id: "T1",
api_app_id: "A1",
app_id: "A1",
});
await runSlackMessageOnce(

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

View file

@ -1,6 +1,14 @@
// Test Install Sh Docker tests cover test install sh docker script behavior.
import { spawn, spawnSync } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path, { join } from "node:path";
import { runInNewContext } from "node:vm";
@ -216,40 +224,6 @@ read_pack_tarball_filename "$pack_json_file"`,
);
}
function extractResolvePackTarballPath(): string {
const script = readFileSync(BUN_GLOBAL_SMOKE_PATH, "utf8");
const match = script.match(/(resolve_pack_tarball_path\(\) \{[\s\S]*?\n\})\n\nrestore_dist/u);
if (!match) {
throw new Error("resolve_pack_tarball_path helper was not found");
}
return match[1];
}
function runResolvePackTarballPath(filename: string) {
return spawnSync(
"bash",
[
"--noprofile",
"--norc",
"-c",
`${extractResolvePackTarballPath()}
pack_dir="$(mktemp -d)"
pack_json_file="$pack_dir/pack.json"
trap 'rm -rf "$pack_dir"' EXIT
printf '%s' "$PACK_JSON" >"$pack_json_file"
resolve_pack_tarball_path "$pack_json_file" "$pack_dir"`,
],
{
encoding: "utf8",
env: {
HOME: "/tmp",
PACK_JSON: JSON.stringify([{ filename }]),
PATH: process.env.PATH ?? "",
},
},
);
}
describe("test-install-sh-docker", () => {
it("defaults ARM hosts to native arm64 while keeping x64 CI on amd64", () => {
expect(runDefaultSmokePlatform({ CI: "true" }, "aarch64")).toBe("linux/arm64");
@ -902,7 +876,10 @@ describe("bun global install smoke", () => {
const script = readFileSync(BUN_GLOBAL_SMOKE_PATH, "utf8");
const assertions = readFileSync(BUN_GLOBAL_ASSERTIONS_PATH, "utf8");
expect(script).toContain("npm pack --ignore-scripts --json --pack-destination");
expect(script).toContain("node scripts/package-openclaw-for-docker.mjs");
expect(script).toContain("--skip-build");
expect(script).toContain("--output-name openclaw-current.tgz");
expect(script).not.toContain("npm pack --ignore-scripts --json --pack-destination");
expect(script).toContain('"$bun_path" install -g "$PACKAGE_TGZ" --no-progress');
expect(script).toContain("infer image providers --json");
expect(script).toContain("assert-image-providers");
@ -919,6 +896,9 @@ describe("bun global install smoke", () => {
expect(script).toContain(
'docker_e2e_docker_cmd cp "${container_id}:/app/dist" "$temp_dir/dist"',
);
expect(script).toContain('"${container_id}:/app/node_modules/@openclaw/ai/dist"');
expect(script).toContain('"$temp_dir/ai-dist"');
expect(script).toContain('mv "$temp_dir/ai-dist" "$ROOT_DIR/packages/ai/dist"');
expect(script).toContain("cleanup_restore_dist() {");
expect(script).toContain('mv "$ROOT_DIR/dist" "$backup_dir"');
expect(script).toContain('mv "$temp_dir/dist" "$ROOT_DIR/dist"');
@ -947,40 +927,134 @@ describe("bun global install smoke", () => {
expect(result.stderr).not.toContain("Bun is required");
});
it("keeps npm pack tarball paths inside the Bun smoke pack directory", () => {
it("uses the canonical package builder for bundled workspace dependencies", () => {
const script = readFileSync(BUN_GLOBAL_SMOKE_PATH, "utf8");
expect(script).toContain("resolve_pack_tarball_path()");
expect(script).toContain(
'PACKAGE_TGZ="$(resolve_pack_tarball_path "$pack_json_file" "$PACK_DIR")"',
expect(script).toContain('PACK_DIR="$(mktemp -d');
expect(script).toContain("node scripts/package-openclaw-for-docker.mjs");
expect(script).toContain('--output-dir "$PACK_DIR"');
expect(script).toContain("--output-name openclaw-current.tgz");
});
it("resolves the matching candidate AI package without changing the public registry", () => {
const script = readFileSync(BUN_GLOBAL_SMOKE_PATH, "utf8");
expect(script).toContain("assert-release-versions");
expect(script).toContain('"$BUN_INSTALL/install/global/package.json"');
expect(script).toContain("package/node_modules/@openclaw/ai");
expect(script).toContain("--strip-components=4");
expect(script).toContain('npm pack --ignore-scripts --silent --pack-destination "$PACK_DIR"');
expect(script).toContain('overrides: { "@openclaw/ai": `file:${aiPackageTarball}` }');
expect(script).not.toContain("--registry");
expect(script).not.toContain("@openclaw:registry");
});
it("requires root and AI candidate versions to match", () => {
const tempDir = tempDirs.make("openclaw-bun-candidate-versions-");
const rootManifestPath = join(tempDir, "openclaw.json");
const aiManifestPath = join(tempDir, "ai.json");
writeFileSync(
rootManifestPath,
JSON.stringify({
name: "openclaw",
version: "2026.6.17",
dependencies: { "@openclaw/ai": "2026.6.17" },
}),
);
writeFileSync(aiManifestPath, JSON.stringify({ name: "@openclaw/ai", version: "2026.6.17" }));
const matching = spawnSync(
process.execPath,
[BUN_GLOBAL_ASSERTIONS_PATH, "assert-release-versions", rootManifestPath, aiManifestPath],
{ encoding: "utf8" },
);
expect(matching).toMatchObject({ status: 0, stdout: "2026.6.17" });
writeFileSync(aiManifestPath, JSON.stringify({ name: "@openclaw/ai", version: "2026.6.18" }));
const mismatched = spawnSync(
process.execPath,
[BUN_GLOBAL_ASSERTIONS_PATH, "assert-release-versions", rootManifestPath, aiManifestPath],
{ encoding: "utf8" },
);
expect(mismatched.status).not.toBe(0);
expect(mismatched.stderr).toContain(
"candidate version mismatch: openclaw=2026.6.17, dependency=2026.6.17, @openclaw/ai=2026.6.18",
);
expect(script).toContain("filename !== path.basename(filename)");
expect(script).toContain("filename !== path.win32.basename(filename)");
expect(script).toContain("npm pack reported unsafe tarball filename");
});
it("rejects path-like npm pack tarball filenames in Bun smoke metadata", () => {
const safeResult = runResolvePackTarballPath("openclaw-2026.6.17.tgz");
it.runIf(process.platform !== "win32")(
"uses bundled AI bytes when a prebuilt tarball is provided",
() => {
const tempDir = tempDirs.make("openclaw-bun-prebuilt-");
const packageDir = join(tempDir, "fixture", "package");
const aiDir = join(packageDir, "node_modules", "@openclaw", "ai");
const packageTgz = join(tempDir, "openclaw-prebuilt.tgz");
const bunPath = join(tempDir, "bun");
mkdirSync(aiDir, { recursive: true });
writeFileSync(
join(packageDir, "package.json"),
JSON.stringify({
name: "openclaw",
version: "2026.6.17",
dependencies: { "@openclaw/ai": "2026.6.17" },
bundleDependencies: ["@openclaw/ai"],
}),
);
writeFileSync(
join(aiDir, "package.json"),
JSON.stringify({ name: "@openclaw/ai", version: "2026.6.17" }),
);
const packed = spawnSync(
"tar",
["-czf", packageTgz, "-C", join(tempDir, "fixture"), "package"],
{
encoding: "utf8",
},
);
expect(packed.status, packed.stderr).toBe(0);
writeFileSync(
bunPath,
`#!/usr/bin/env bash
set -euo pipefail
if [ "\${1:-}" = "--version" ]; then
echo "1.3.14"
exit 0
fi
override="$(node -e 'const p=require(process.argv[1]);process.stdout.write(p.overrides["@openclaw/ai"])' "$BUN_INSTALL/install/global/package.json")"
case "\${override#file:}" in
*.tgz) ;;
*) exit 1 ;;
esac
test -f "\${override#file:}"
mkdir -p "$BUN_INSTALL/bin"
cat >"$BUN_INSTALL/bin/openclaw" <<'OPENCLAW'
#!/usr/bin/env bash
if [ "\${1:-}" = "--version" ]; then
echo "OpenClaw 2026.6.17"
else
printf '[{"id":"google"},{"id":"openai"},{"id":"xai"}]\n'
fi
OPENCLAW
chmod +x "$BUN_INSTALL/bin/openclaw"
`,
);
chmodSync(bunPath, 0o755);
expect(safeResult.status).toBe(0);
expect(safeResult.stdout).toMatch(/\/openclaw-2026\.6\.17\.tgz$/u);
const result = spawnSync("bash", [BUN_GLOBAL_SMOKE_PATH], {
encoding: "utf8",
env: {
...process.env,
BUN_BIN: bunPath,
OPENCLAW_BUN_GLOBAL_SMOKE_HOST_BUILD: "0",
OPENCLAW_BUN_GLOBAL_SMOKE_PACKAGE_TGZ: packageTgz,
OPENCLAW_BUN_GLOBAL_SMOKE_TIMEOUT_MS: "10000",
},
});
const unsafeFilenames = [
"../openclaw.tgz",
"nested/openclaw.tgz",
"nested\\openclaw.tgz",
"/tmp/openclaw.tgz",
"C:\\temp\\openclaw.tgz",
"openclaw.tar.gz",
];
for (const filename of unsafeFilenames) {
const result = runResolvePackTarballPath(filename);
expect(result.status, filename).not.toBe(0);
expect(result.stderr, filename).toContain("npm pack reported unsafe tarball filename");
}
});
expect(result.status, result.stderr).toBe(0);
expect(result.stdout).toContain("bun-global-install-smoke: image providers OK (3 providers)");
},
);
it.runIf(process.platform !== "win32" && existsSync("/usr/bin/time"))(
"preserves Bun global timeout kill grace after the leader exits",