Skip to content

Commit 42a8679

Browse files
authored
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
1 parent 32de666 commit 42a8679

6 files changed

Lines changed: 276 additions & 104 deletions

File tree

extensions/qqbot/src/engine/approval/index.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ describe("buildExecApprovalText", () => {
3232
commandPreview: `${safePrefix}🎉 trailing text`,
3333
},
3434
});
35-
const codeFence = text.split("```\n")[1]?.split("\n```")[0] ?? "";
36-
expect(codeFence).toBe(safePrefix);
35+
const expectedCommandBlock = ["```", safePrefix, "```"].join("\n");
36+
expect(text).toContain(expectedCommandBlock);
3737
});
3838
});

extensions/slack/src/monitor.tool-result.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ describe("monitorSlackProvider tool results", () => {
351351
(client.auth as { test: ReturnType<typeof vi.fn> }).test.mockResolvedValue({
352352
user_id: "bot-user",
353353
team_id: "T1",
354-
api_app_id: "A1",
354+
app_id: "A1",
355355
});
356356

357357
await runSlackMessageOnce(

scripts/e2e/bun-global-install-smoke.sh

Lines changed: 109 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ DIST_IMAGE="${OPENCLAW_BUN_GLOBAL_SMOKE_DIST_IMAGE:-}"
2424
PACKAGE_TGZ="${OPENCLAW_BUN_GLOBAL_SMOKE_PACKAGE_TGZ:-}"
2525
COMMAND_TIMEOUT_MS="$(read_positive_int_env OPENCLAW_BUN_GLOBAL_SMOKE_TIMEOUT_MS 180000)"
2626
DOCKER_COMMAND_TIMEOUT="${DOCKER_COMMAND_TIMEOUT:-${OPENCLAW_BUN_GLOBAL_SMOKE_DOCKER_COMMAND_TIMEOUT:-600s}}"
27+
AI_PACKAGE_TGZ=""
2728
SMOKE_DIR=""
2829
PACK_DIR=""
2930

@@ -36,6 +37,39 @@ cleanup() {
3637
fi
3738
}
3839

40+
prepare_ai_candidate() {
41+
local ai_manifest
42+
local ai_package_dir
43+
local ai_tarballs
44+
local root_manifest
45+
46+
if [ -z "$PACK_DIR" ]; then
47+
PACK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-bun-pack.XXXXXX")"
48+
fi
49+
echo "==> Extract bundled candidate @openclaw/ai package"
50+
ai_package_dir="$PACK_DIR/ai-candidate"
51+
mkdir -p "$ai_package_dir"
52+
tar -xzf "$PACKAGE_TGZ" \
53+
-C "$ai_package_dir" \
54+
--strip-components=4 \
55+
package/node_modules/@openclaw/ai
56+
root_manifest="$PACK_DIR/openclaw-package.json"
57+
ai_manifest="$ai_package_dir/package.json"
58+
tar -xOf "$PACKAGE_TGZ" package/package.json >"$root_manifest"
59+
node scripts/e2e/lib/bun-global-install/assertions.mjs \
60+
assert-release-versions \
61+
"$root_manifest" \
62+
"$ai_manifest" \
63+
>/dev/null
64+
npm pack --ignore-scripts --silent --pack-destination "$PACK_DIR" "$ai_package_dir" >/dev/null
65+
ai_tarballs=("$PACK_DIR"/openclaw-ai-*.tgz)
66+
if [ "${#ai_tarballs[@]}" -ne 1 ] || [ ! -f "${ai_tarballs[0]}" ]; then
67+
echo "expected one packed @openclaw/ai candidate in $PACK_DIR" >&2
68+
exit 1
69+
fi
70+
AI_PACKAGE_TGZ="${ai_tarballs[0]}"
71+
}
72+
3973
trap cleanup EXIT
4074

4175
run_with_timeout() {
@@ -44,52 +78,50 @@ run_with_timeout() {
4478
node scripts/e2e/lib/bun-global-install/assertions.mjs run-with-timeout "$timeout_ms" "$@"
4579
}
4680

47-
resolve_pack_tarball_path() {
48-
local pack_json_file="$1"
49-
local pack_dir="$2"
50-
node -e '
51-
const fs = require("node:fs");
52-
const path = require("node:path");
53-
const raw = fs.readFileSync(process.argv[1], "utf8") || "[]";
54-
const parsed = JSON.parse(raw);
55-
const last = Array.isArray(parsed) ? parsed.at(-1) : null;
56-
const filename = typeof last?.filename === "string" ? last.filename.trim() : "";
57-
if (
58-
!filename.endsWith(".tgz") ||
59-
filename.includes("\0") ||
60-
filename !== path.basename(filename) ||
61-
filename !== path.win32.basename(filename)
62-
) {
63-
console.error(`ERROR: npm pack reported unsafe tarball filename ${JSON.stringify(filename)}`);
64-
process.exit(1);
65-
}
66-
process.stdout.write(path.resolve(process.argv[2], filename));
67-
' "$pack_json_file" "$pack_dir"
68-
}
69-
7081
restore_dist_from_image() {
7182
local image="$1"
83+
local ai_backup_dir=""
84+
local ai_dist_installed=0
7285
local backup_dir=""
7386
local container_id=""
74-
local swapped=0
87+
local dist_installed=0
88+
local restore_complete=0
7589
local temp_dir=""
7690

7791
cleanup_restore_dist() {
7892
if [ -n "$container_id" ]; then
7993
docker_e2e_docker_cmd rm -f "$container_id" >/dev/null 2>&1 || true
8094
fi
81-
if [ "$swapped" != "1" ] && [ -n "$backup_dir" ] && [ -d "$backup_dir" ]; then
82-
rm -rf "$ROOT_DIR/dist" >/dev/null 2>&1 || true
83-
if [ ! -e "$ROOT_DIR/dist" ] && mv "$backup_dir" "$ROOT_DIR/dist" >/dev/null 2>&1; then
84-
backup_dir=""
95+
# Both build trees come from one image. A partial swap must restore both or
96+
# the following package step could mix artifacts from different builds.
97+
if [ "$restore_complete" != "1" ]; then
98+
if [ "$dist_installed" = "1" ]; then
99+
rm -rf "$ROOT_DIR/dist" >/dev/null 2>&1 || true
100+
fi
101+
if [ -n "$backup_dir" ] && [ -d "$backup_dir" ]; then
102+
if [ ! -e "$ROOT_DIR/dist" ] && mv "$backup_dir" "$ROOT_DIR/dist" >/dev/null 2>&1; then
103+
backup_dir=""
104+
fi
105+
fi
106+
if [ "$ai_dist_installed" = "1" ]; then
107+
rm -rf "$ROOT_DIR/packages/ai/dist" >/dev/null 2>&1 || true
108+
fi
109+
if [ -n "$ai_backup_dir" ] && [ -d "$ai_backup_dir" ]; then
110+
if [ ! -e "$ROOT_DIR/packages/ai/dist" ] && \
111+
mv "$ai_backup_dir" "$ROOT_DIR/packages/ai/dist" >/dev/null 2>&1; then
112+
ai_backup_dir=""
113+
fi
85114
fi
86115
fi
87116
if [ -n "$temp_dir" ]; then
88117
rm -rf "$temp_dir"
89118
fi
90-
if [ "$swapped" = "1" ] && [ -n "$backup_dir" ]; then
119+
if [ "$restore_complete" = "1" ] && [ -n "$backup_dir" ]; then
91120
rm -rf "$backup_dir"
92121
fi
122+
if [ "$restore_complete" = "1" ] && [ -n "$ai_backup_dir" ]; then
123+
rm -rf "$ai_backup_dir"
124+
fi
93125
}
94126

95127
echo "==> Reuse dist/ from Docker image: $image"
@@ -105,6 +137,12 @@ restore_dist_from_image() {
105137
cleanup_restore_dist
106138
return 1
107139
fi
140+
if ! docker_e2e_docker_cmd cp \
141+
"${container_id}:/app/node_modules/@openclaw/ai/dist" \
142+
"$temp_dir/ai-dist"; then
143+
cleanup_restore_dist
144+
return 1
145+
fi
108146
if [ -e "$ROOT_DIR/dist" ]; then
109147
if ! backup_dir="$(mktemp -d "$ROOT_DIR/.dist-backup.XXXXXX")"; then
110148
cleanup_restore_dist
@@ -123,7 +161,27 @@ restore_dist_from_image() {
123161
cleanup_restore_dist
124162
return 1
125163
fi
126-
swapped=1
164+
dist_installed=1
165+
if [ -e "$ROOT_DIR/packages/ai/dist" ]; then
166+
if ! ai_backup_dir="$(mktemp -d "$ROOT_DIR/packages/ai/.dist-backup.XXXXXX")"; then
167+
cleanup_restore_dist
168+
return 1
169+
fi
170+
if ! rmdir "$ai_backup_dir"; then
171+
cleanup_restore_dist
172+
return 1
173+
fi
174+
if ! mv "$ROOT_DIR/packages/ai/dist" "$ai_backup_dir"; then
175+
cleanup_restore_dist
176+
return 1
177+
fi
178+
fi
179+
if ! mv "$temp_dir/ai-dist" "$ROOT_DIR/packages/ai/dist"; then
180+
cleanup_restore_dist
181+
return 1
182+
fi
183+
ai_dist_installed=1
184+
restore_complete=1
127185
cleanup_restore_dist
128186
}
129187

@@ -151,16 +209,15 @@ resolve_package_tgz() {
151209
exit 1
152210
fi
153211

154-
echo "==> Write package inventory"
155-
node --import tsx scripts/write-package-dist-inventory.ts
156-
157-
local pack_json_file
158212
PACK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-bun-pack.XXXXXX")"
159-
pack_json_file="$PACK_DIR/pack.json"
160213

161214
echo "==> Pack OpenClaw tarball"
162-
npm pack --ignore-scripts --json --pack-destination "$PACK_DIR" >"$pack_json_file"
163-
PACKAGE_TGZ="$(resolve_pack_tarball_path "$pack_json_file" "$PACK_DIR")"
215+
PACKAGE_TGZ="$(
216+
node scripts/package-openclaw-for-docker.mjs \
217+
--skip-build \
218+
--output-dir "$PACK_DIR" \
219+
--output-name openclaw-current.tgz
220+
)"
164221
if [ -z "$PACKAGE_TGZ" ] || [ ! -f "$PACKAGE_TGZ" ]; then
165222
echo "missing packed OpenClaw tarball" >&2
166223
exit 1
@@ -176,6 +233,7 @@ main() {
176233
fi
177234

178235
resolve_package_tgz
236+
prepare_ai_candidate
179237

180238
local bun_path
181239
local openclaw_bin
@@ -188,8 +246,21 @@ main() {
188246
export OPENCLAW_NO_ONBOARD=1
189247
export OPENCLAW_DISABLE_UPDATE_CHECK=1
190248
export NO_COLOR=1
191-
mkdir -p "$HOME" "$BUN_INSTALL/bin" "$XDG_CACHE_HOME"
249+
mkdir -p "$HOME" "$BUN_INSTALL/bin" "$BUN_INSTALL/install/global" "$XDG_CACHE_HOME"
192250
export PATH="$BUN_INSTALL/bin:$(dirname "$(command -v node)"):$PATH"
251+
# Release publishes @openclaw/ai first. Bun 1.3.14 ignores bundled deps in
252+
# local tarballs, so resolve that one package from the exact candidate bytes.
253+
node --input-type=module - \
254+
"$BUN_INSTALL/install/global/package.json" \
255+
"$AI_PACKAGE_TGZ" <<'NODE'
256+
import fs from "node:fs";
257+
258+
const [, , packageJsonPath, aiPackageTarball] = process.argv;
259+
fs.writeFileSync(
260+
packageJsonPath,
261+
`${JSON.stringify({ private: true, overrides: { "@openclaw/ai": `file:${aiPackageTarball}` } })}\n`,
262+
);
263+
NODE
193264

194265
echo "==> Bun version"
195266
"$bun_path" --version

scripts/e2e/lib/bun-global-install/assertions.mjs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
// Assertions for Bun global install E2E validation.
22
import { spawn } from "node:child_process";
3+
import fs from "node:fs";
34

45
const DEFAULT_TIMEOUT_KILL_GRACE_MS = 30_000;
56
const PARENT_TERMINATION_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
67

78
const usage = () => {
8-
console.error("Usage: assertions.mjs <run-with-timeout|assert-image-providers> [...]");
9+
console.error(
10+
"Usage: assertions.mjs <run-with-timeout|assert-image-providers|assert-release-versions> [...]",
11+
);
912
process.exit(2);
1013
};
1114

@@ -204,4 +207,28 @@ if (mode === "assert-image-providers") {
204207
process.exit(0);
205208
}
206209

210+
if (mode === "assert-release-versions") {
211+
const [rootManifestPath, aiManifestPath] = args;
212+
if (!rootManifestPath || !aiManifestPath) {
213+
usage();
214+
}
215+
const rootManifest = JSON.parse(fs.readFileSync(rootManifestPath, "utf8"));
216+
const aiManifest = JSON.parse(fs.readFileSync(aiManifestPath, "utf8"));
217+
const rootVersion = rootManifest.version;
218+
const aiVersion = aiManifest.version;
219+
const rootAiVersion = rootManifest.dependencies?.["@openclaw/ai"];
220+
if (
221+
typeof rootVersion !== "string" ||
222+
typeof aiVersion !== "string" ||
223+
rootVersion !== aiVersion ||
224+
rootAiVersion !== aiVersion
225+
) {
226+
throw new Error(
227+
`candidate version mismatch: openclaw=${String(rootVersion)}, dependency=${String(rootAiVersion)}, @openclaw/ai=${String(aiVersion)}`,
228+
);
229+
}
230+
process.stdout.write(aiVersion);
231+
process.exit(0);
232+
}
233+
207234
usage();

scripts/plugin-clawhub-publish.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ fi
200200

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

0 commit comments

Comments
 (0)