@@ -24,6 +24,7 @@ DIST_IMAGE="${OPENCLAW_BUN_GLOBAL_SMOKE_DIST_IMAGE:-}"
2424PACKAGE_TGZ=" ${OPENCLAW_BUN_GLOBAL_SMOKE_PACKAGE_TGZ:- } "
2525COMMAND_TIMEOUT_MS=" $( read_positive_int_env OPENCLAW_BUN_GLOBAL_SMOKE_TIMEOUT_MS 180000) "
2626DOCKER_COMMAND_TIMEOUT=" ${DOCKER_COMMAND_TIMEOUT:- ${OPENCLAW_BUN_GLOBAL_SMOKE_DOCKER_COMMAND_TIMEOUT:- 600s} } "
27+ AI_PACKAGE_TGZ=" "
2728SMOKE_DIR=" "
2829PACK_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+
3973trap cleanup EXIT
4074
4175run_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-
7081restore_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
0 commit comments