diff --git a/scripts/create-dmg.sh b/scripts/create-dmg.sh index db8d819a27e3..7393f3a4c382 100755 --- a/scripts/create-dmg.sh +++ b/scripts/create-dmg.sh @@ -91,6 +91,23 @@ cleanup_dmg() { } trap cleanup_dmg EXIT +detach_dmg() { + local attempt + for attempt in {1..15}; do + if hdiutil detach "$MOUNT_POINT" -quiet 2>/dev/null; then + MOUNTED=0 + return + fi + if (( attempt >= 3 )) && hdiutil detach "$MOUNT_POINT" -force 2>/dev/null; then + MOUNTED=0 + return + fi + # Finder can retain the just-closed volume briefly on macOS runners. + sleep 2 + done + return 1 +} + mkdir -p "$DMG_SOURCE" "$MOUNT_POINT" cp -R "$APP_PATH" "$DMG_SOURCE/" ln -s /Applications "$DMG_SOURCE/Applications" @@ -161,20 +178,7 @@ end tell EOF fi -for i in {1..5}; do - if hdiutil detach "$MOUNT_POINT" -quiet 2>/dev/null; then - MOUNTED=0 - break - fi - if [[ "$i" == "3" ]]; then - if hdiutil detach "$MOUNT_POINT" -force 2>/dev/null; then - MOUNTED=0 - break - fi - fi - sleep 2 -done -if [[ "$MOUNTED" == "1" ]]; then +if ! detach_dmg; then echo "Error: Failed to detach DMG mount: $MOUNT_POINT" >&2 exit 1 fi diff --git a/test/scripts/create-dmg.test.ts b/test/scripts/create-dmg.test.ts index 8bbabc31288d..d35e6d5aba8f 100644 --- a/test/scripts/create-dmg.test.ts +++ b/test/scripts/create-dmg.test.ts @@ -88,6 +88,16 @@ case "$command_name" in if [[ "\${HDIUTIL_DETACH_FAIL:-0}" == "1" ]]; then exit 9 fi + detach_attempts_file="\${HDIUTIL_LOG}.detach-attempts" + detach_attempts=0 + if [[ -f "$detach_attempts_file" ]]; then + detach_attempts="$(cat "$detach_attempts_file")" + fi + detach_attempts=$((detach_attempts + 1)) + printf '%s' "$detach_attempts" > "$detach_attempts_file" + if (( detach_attempts <= \${HDIUTIL_DETACH_FAIL_COUNT:-0} )); then + exit 9 + fi ;; resize) if [[ "\${1:-}" == "-limits" ]]; then @@ -300,6 +310,28 @@ describe.runIf(process.platform === "darwin")("create-dmg ownership boundaries", rmSync(path.dirname(mountPoint as string), { recursive: true, force: true }); }); + it("retries a delayed DMG detach before finalizing the artifact", () => { + const app = makeValidApp(); + const outputDir = mkdtempSync(path.join(tmpdir(), "openclaw-create-dmg-output-")); + tempDirs.push(outputDir); + const output = path.join(outputDir, "OpenClaw.dmg"); + const tools = makeFakeDmgTools(); + + const result = runScript([app, output], { + ...tools.env, + HDIUTIL_DETACH_FAIL_COUNT: "6", + }); + + expect(result.status).toBe(0); + expect(readFileSync(output, "utf8")).toBe("converted"); + const log = readFileSync(tools.hdiutilLog, "utf8"); + expect(log.match(/^detach /gm)).toHaveLength(7); + expect(log).toContain("detach "); + expect(log).toContain("-force"); + expect(log).toContain("resize"); + expect(log).toContain("convert "); + }); + it("styles the private mount without closing unrelated Finder windows", () => { const app = makeValidApp(); const outputDir = mkdtempSync(path.join(tmpdir(), "openclaw-create-dmg-output-"));