Skip to content

Commit 5635d74

Browse files
authored
fix(release): contain installer artifact restores (#103608)
1 parent 2d54be3 commit 5635d74

2 files changed

Lines changed: 97 additions & 28 deletions

File tree

scripts/lib/docker-e2e-package.sh

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,14 @@ docker_e2e_abs_path() {
159159
docker_e2e_restore_package_dist_from_image() (
160160
local image="$1"
161161
local ai_backup_dir=""
162+
local ai_dist_dir=""
162163
local ai_dist_installed=0
164+
local ai_package_dir=""
163165
local backup_dir=""
164166
local container_id=""
165167
local dist_installed=0
166168
local requires_ai_dist=0
169+
local restore_root=""
167170
local restore_complete=0
168171
local temp_dir=""
169172

@@ -175,19 +178,20 @@ docker_e2e_restore_package_dist_from_image() (
175178
# so the package step cannot combine outputs from different builds.
176179
if [ "$restore_complete" != "1" ]; then
177180
if [ "$dist_installed" = "1" ]; then
178-
rm -rf "$ROOT_DIR/dist" >/dev/null 2>&1 || true
181+
rm -rf "$restore_root/dist" >/dev/null 2>&1 || true
179182
fi
180183
if [ -n "$backup_dir" ] && [ -d "$backup_dir" ]; then
181-
if [ ! -e "$ROOT_DIR/dist" ] && mv "$backup_dir" "$ROOT_DIR/dist" >/dev/null 2>&1; then
184+
if [ ! -e "$restore_root/dist" ] && \
185+
mv "$backup_dir" "$restore_root/dist" >/dev/null 2>&1; then
182186
backup_dir=""
183187
fi
184188
fi
185189
if [ "$ai_dist_installed" = "1" ]; then
186-
rm -rf "$ROOT_DIR/packages/ai/dist" >/dev/null 2>&1 || true
190+
rm -rf "$ai_dist_dir" >/dev/null 2>&1 || true
187191
fi
188192
if [ -n "$ai_backup_dir" ] && [ -d "$ai_backup_dir" ]; then
189-
if [ ! -e "$ROOT_DIR/packages/ai/dist" ] && \
190-
mv "$ai_backup_dir" "$ROOT_DIR/packages/ai/dist" >/dev/null 2>&1; then
193+
if [ ! -e "$ai_dist_dir" ] && \
194+
mv "$ai_backup_dir" "$ai_dist_dir" >/dev/null 2>&1; then
191195
ai_backup_dir=""
192196
fi
193197
fi
@@ -203,7 +207,29 @@ docker_e2e_restore_package_dist_from_image() (
203207
fi
204208
}
205209

206-
if [ -f "$ROOT_DIR/packages/ai/package.json" ]; then
210+
if ! restore_root="$(cd "$ROOT_DIR" && pwd -P)"; then
211+
echo "unable to resolve package restore root: $ROOT_DIR" >&2
212+
return 1
213+
fi
214+
# The trusted workflow owns this static checkout and runs no candidate process
215+
# concurrently. Resolve owner paths once and reuse them through every swap.
216+
if [ -L "$restore_root/packages" ] || [ -L "$restore_root/packages/ai" ]; then
217+
echo "refusing package artifact restore through a symlinked packages path" >&2
218+
return 1
219+
fi
220+
if [ -f "$restore_root/packages/ai/package.json" ]; then
221+
if ! ai_package_dir="$(cd "$restore_root/packages/ai" && pwd -P)"; then
222+
echo "unable to resolve bundled AI package path" >&2
223+
return 1
224+
fi
225+
case "$ai_package_dir/" in
226+
"$restore_root"/*) ;;
227+
*)
228+
echo "refusing bundled AI artifact restore outside the package root" >&2
229+
return 1
230+
;;
231+
esac
232+
ai_dist_dir="$ai_package_dir/dist"
207233
requires_ai_dist=1
208234
fi
209235

@@ -212,7 +238,7 @@ docker_e2e_restore_package_dist_from_image() (
212238
cleanup_restore_package_dist
213239
return 1
214240
fi
215-
if ! temp_dir="$(mktemp -d "$ROOT_DIR/.package-dist.XXXXXX")"; then
241+
if ! temp_dir="$(mktemp -d "$restore_root/.package-dist.XXXXXX")"; then
216242
cleanup_restore_package_dist
217243
return 1
218244
fi
@@ -227,41 +253,41 @@ docker_e2e_restore_package_dist_from_image() (
227253
cleanup_restore_package_dist
228254
return 1
229255
fi
230-
if [ -e "$ROOT_DIR/dist" ]; then
231-
if ! backup_dir="$(mktemp -d "$ROOT_DIR/.dist-backup.XXXXXX")"; then
256+
if [ -e "$restore_root/dist" ]; then
257+
if ! backup_dir="$(mktemp -d "$restore_root/.dist-backup.XXXXXX")"; then
232258
cleanup_restore_package_dist
233259
return 1
234260
fi
235261
if ! rmdir "$backup_dir"; then
236262
cleanup_restore_package_dist
237263
return 1
238264
fi
239-
if ! mv "$ROOT_DIR/dist" "$backup_dir"; then
265+
if ! mv "$restore_root/dist" "$backup_dir"; then
240266
cleanup_restore_package_dist
241267
return 1
242268
fi
243269
fi
244-
if ! mv "$temp_dir/dist" "$ROOT_DIR/dist"; then
270+
if ! mv "$temp_dir/dist" "$restore_root/dist"; then
245271
cleanup_restore_package_dist
246272
return 1
247273
fi
248274
dist_installed=1
249275
if [ "$requires_ai_dist" = "1" ]; then
250-
if [ -e "$ROOT_DIR/packages/ai/dist" ]; then
251-
if ! ai_backup_dir="$(mktemp -d "$ROOT_DIR/packages/ai/.dist-backup.XXXXXX")"; then
276+
if [ -e "$ai_dist_dir" ]; then
277+
if ! ai_backup_dir="$(mktemp -d "$ai_package_dir/.dist-backup.XXXXXX")"; then
252278
cleanup_restore_package_dist
253279
return 1
254280
fi
255281
if ! rmdir "$ai_backup_dir"; then
256282
cleanup_restore_package_dist
257283
return 1
258284
fi
259-
if ! mv "$ROOT_DIR/packages/ai/dist" "$ai_backup_dir"; then
285+
if ! mv "$ai_dist_dir" "$ai_backup_dir"; then
260286
cleanup_restore_package_dist
261287
return 1
262288
fi
263289
fi
264-
if ! mv "$temp_dir/ai-dist" "$ROOT_DIR/packages/ai/dist"; then
290+
if ! mv "$temp_dir/ai-dist" "$ai_dist_dir"; then
265291
cleanup_restore_package_dist
266292
return 1
267293
fi

test/scripts/test-install-sh-docker.test.ts

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
mkdtempSync,
88
readFileSync,
99
rmSync,
10+
symlinkSync,
1011
writeFileSync,
1112
} from "node:fs";
1213
import { tmpdir } from "node:os";
@@ -237,9 +238,14 @@ function extractEnsureLocalUpdateDistImportClosure(): string {
237238
return match[1];
238239
}
239240

240-
function runRestoreLocalDistFixture(options: { failAiSwap?: boolean } = {}) {
241+
type RestorePathEscape = "packages" | "ai";
242+
243+
function runRestoreLocalDistFixture(
244+
options: { failAiSwap?: boolean; symlinkEscape?: RestorePathEscape } = {},
245+
) {
241246
const fixtureRoot = tempDirs.make("openclaw-install-restore-root-");
242247
const imageRoot = tempDirs.make("openclaw-install-restore-image-");
248+
let externalSentinel = "";
243249
for (const [relativePath, contents] of [
244250
["dist/root.txt", "old-root"],
245251
["packages/ai/dist/ai.txt", "old-ai"],
@@ -258,6 +264,23 @@ function runRestoreLocalDistFixture(options: { failAiSwap?: boolean } = {}) {
258264
writeFileSync(target, contents);
259265
}
260266

267+
if (options.symlinkEscape) {
268+
const escapeRoot = tempDirs.make("openclaw-install-restore-escape-");
269+
const externalAiRoot =
270+
options.symlinkEscape === "packages" ? join(escapeRoot, "packages", "ai") : escapeRoot;
271+
externalSentinel = join(externalAiRoot, "dist", "ai.txt");
272+
mkdirSync(path.dirname(externalSentinel), { recursive: true });
273+
writeFileSync(join(externalAiRoot, "package.json"), "{}");
274+
writeFileSync(externalSentinel, "external-ai");
275+
if (options.symlinkEscape === "packages") {
276+
rmSync(join(fixtureRoot, "packages"), { force: true, recursive: true });
277+
symlinkSync(join(escapeRoot, "packages"), join(fixtureRoot, "packages"), "dir");
278+
} else {
279+
rmSync(join(fixtureRoot, "packages", "ai"), { force: true, recursive: true });
280+
symlinkSync(externalAiRoot, join(fixtureRoot, "packages", "ai"), "dir");
281+
}
282+
}
283+
261284
return spawnSync(
262285
"bash",
263286
[
@@ -269,6 +292,7 @@ REPO_ROOT="$FIXTURE_REPO"
269292
ROOT_DIR="$FIXTURE_ROOT"
270293
IMAGE_ROOT="$FIXTURE_IMAGE"
271294
docker_e2e_docker_cmd() {
295+
printf 'docker-call=%s\\n' "$1" >&2
272296
case "$1" in
273297
create)
274298
printf "fixture"
@@ -285,7 +309,7 @@ docker_e2e_docker_cmd() {
285309
esac
286310
}
287311
mv() {
288-
if [[ "$FAIL_AI_SWAP" == "1" && "$1" == */ai-dist && "$2" == "$ROOT_DIR/packages/ai/dist" ]]; then
312+
if [[ "$FAIL_AI_SWAP" == "1" && "$1" == */ai-dist && "$2" == */packages/ai/dist ]]; then
289313
return 1
290314
fi
291315
command mv "$@"
@@ -296,13 +320,17 @@ docker_e2e_restore_package_dist_from_image fixture-image || status=$?
296320
printf 'status=%s\\n' "$status"
297321
printf 'root=%s\\n' "$(cat "$ROOT_DIR/dist/root.txt")"
298322
printf 'ai=%s\\n' "$(cat "$ROOT_DIR/packages/ai/dist/ai.txt")"
323+
if [[ -n "$EXTERNAL_SENTINEL" ]]; then
324+
printf 'external=%s\\n' "$(cat "$EXTERNAL_SENTINEL")"
325+
fi
299326
`,
300327
],
301328
{
302329
encoding: "utf8",
303330
env: {
304331
...process.env,
305332
FAIL_AI_SWAP: options.failAiSwap ? "1" : "0",
333+
EXTERNAL_SENTINEL: externalSentinel,
306334
FIXTURE_IMAGE: imageRoot,
307335
FIXTURE_REPO: process.cwd(),
308336
FIXTURE_ROOT: fixtureRoot,
@@ -457,12 +485,12 @@ describe("test-install-sh-docker", () => {
457485
);
458486
expect(packageHelper).toContain('"${container_id}:/app/node_modules/@openclaw/ai/dist"');
459487
expect(packageHelper).toContain('"$temp_dir/ai-dist"');
460-
expect(packageHelper).toContain('mv "$temp_dir/ai-dist" "$ROOT_DIR/packages/ai/dist"');
488+
expect(packageHelper).toContain('mv "$temp_dir/ai-dist" "$ai_dist_dir"');
461489
expect(packageHelper).toContain("cleanup_restore_package_dist() {");
462-
expect(packageHelper).toContain('mv "$ROOT_DIR/dist" "$backup_dir"');
463-
expect(packageHelper).toContain('mv "$temp_dir/dist" "$ROOT_DIR/dist"');
464-
expect(packageHelper).toContain('rm -rf "$ROOT_DIR/dist" >/dev/null 2>&1 || true');
465-
expect(packageHelper).toContain('mv "$backup_dir" "$ROOT_DIR/dist"');
490+
expect(packageHelper).toContain('mv "$restore_root/dist" "$backup_dir"');
491+
expect(packageHelper).toContain('mv "$temp_dir/dist" "$restore_root/dist"');
492+
expect(packageHelper).toContain('rm -rf "$restore_root/dist" >/dev/null 2>&1 || true');
493+
expect(packageHelper).toContain('mv "$backup_dir" "$restore_root/dist"');
466494
expect(packageHelper).toContain('docker_e2e_docker_cmd rm -f "$container_id"');
467495
expect(script).not.toContain('container_id="$(docker create "$image")"');
468496
expect(script).not.toContain('docker cp "${container_id}:/app/dist" "$ROOT_DIR/dist"');
@@ -499,6 +527,21 @@ describe("test-install-sh-docker", () => {
499527
expect(result.stdout).toContain("ai=old-ai");
500528
});
501529

530+
it.each(["packages", "ai"] as const)(
531+
"rejects a symlinked %s path before restoring artifacts",
532+
(symlinkEscape) => {
533+
const result = runRestoreLocalDistFixture({ symlinkEscape });
534+
535+
expect(result.status, result.stderr).toBe(0);
536+
expect(result.stdout).toContain("status=1");
537+
expect(result.stdout).toContain("root=old-root");
538+
expect(result.stdout).toContain("ai=external-ai");
539+
expect(result.stdout).toContain("external=external-ai");
540+
expect(result.stderr).not.toContain("docker-call=");
541+
expect(result.stderr).toContain("refusing package artifact restore through a symlinked");
542+
},
543+
);
544+
502545
it("fails closed when exact image artifacts fail import closure", () => {
503546
const result = spawnSync(
504547
"bash",
@@ -1069,13 +1112,13 @@ describe("bun global install smoke", () => {
10691112
);
10701113
expect(packageHelper).toContain('"${container_id}:/app/node_modules/@openclaw/ai/dist"');
10711114
expect(packageHelper).toContain('"$temp_dir/ai-dist"');
1072-
expect(packageHelper).toContain('mv "$temp_dir/ai-dist" "$ROOT_DIR/packages/ai/dist"');
1115+
expect(packageHelper).toContain('mv "$temp_dir/ai-dist" "$ai_dist_dir"');
10731116
expect(packageHelper).toContain("cleanup_restore_package_dist() {");
1074-
expect(packageHelper).toContain('mv "$ROOT_DIR/dist" "$backup_dir"');
1075-
expect(packageHelper).toContain('mv "$temp_dir/dist" "$ROOT_DIR/dist"');
1076-
expect(packageHelper).toContain('mktemp -d "$ROOT_DIR/.package-dist.XXXXXX"');
1077-
expect(packageHelper).toContain('rm -rf "$ROOT_DIR/dist" >/dev/null 2>&1 || true');
1078-
expect(packageHelper).toContain('mv "$backup_dir" "$ROOT_DIR/dist"');
1117+
expect(packageHelper).toContain('mv "$restore_root/dist" "$backup_dir"');
1118+
expect(packageHelper).toContain('mv "$temp_dir/dist" "$restore_root/dist"');
1119+
expect(packageHelper).toContain('mktemp -d "$restore_root/.package-dist.XXXXXX"');
1120+
expect(packageHelper).toContain('rm -rf "$restore_root/dist" >/dev/null 2>&1 || true');
1121+
expect(packageHelper).toContain('mv "$backup_dir" "$restore_root/dist"');
10791122
expect(packageHelper).toContain('docker_e2e_docker_cmd rm -f "$container_id"');
10801123
expect(script).not.toContain('container_id="$(docker create "$image")"');
10811124
expect(script).not.toContain('docker cp "${container_id}:/app/dist" "$ROOT_DIR/dist"');

0 commit comments

Comments
 (0)