diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index f645b45ca293..ffaa86ced9cf 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -18,9 +18,6 @@ concurrency: permissions: contents: write -env: - CREATE_DMG_VERSION: 8.0.0 - jobs: decide: runs-on: ubuntu-latest @@ -126,7 +123,7 @@ jobs: - name: Install build deps run: | - npm install --global "create-dmg@${CREATE_DMG_VERSION}" + brew list create-dmg >/dev/null 2>&1 || brew install create-dmg - name: Download pre-built GhosttyKit.xcframework run: | @@ -312,8 +309,6 @@ jobs: local dmg_release="$2" local dmg_immutable="$3" local zip_submit="${dmg_release%.dmg}-notary.zip" - local dmg_tmp_dir - local created_dmg ditto -c -k --sequesterRsrc --keepParent "$app_path" "$zip_submit" APP_SUBMIT_JSON="$(xcrun notarytool submit "$zip_submit" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --wait --output-format json)" @@ -328,19 +323,7 @@ jobs: xcrun stapler validate "$app_path" spctl -a -vv --type execute "$app_path" rm -f "$zip_submit" - - dmg_tmp_dir="$(mktemp -d)" - create-dmg \ - --identity="$APPLE_SIGNING_IDENTITY" \ - "$app_path" \ - "$dmg_tmp_dir" - created_dmg="$(find "$dmg_tmp_dir" -maxdepth 1 -name '*.dmg' | head -n 1)" - if [ -z "$created_dmg" ]; then - echo "Failed to locate created DMG for $app_path" >&2 - exit 1 - fi - mv "$created_dmg" "$dmg_release" - rm -rf "$dmg_tmp_dir" + CMUX_CREATE_DMG_REQUIRE_STYLED=1 ./scripts/create_release_dmg.sh "$app_path" "$dmg_release" "$APPLE_SIGNING_IDENTITY" DMG_SUBMIT_JSON="$(xcrun notarytool submit "$dmg_release" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --wait --output-format json)" DMG_SUBMIT_ID="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<<"$DMG_SUBMIT_JSON")" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ec935c6322f9..cd6e54202cd1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,9 +9,6 @@ on: permissions: contents: write -env: - CREATE_DMG_VERSION: 8.0.0 - jobs: build-sign-notarize: runs-on: depot-macos-latest @@ -21,7 +18,24 @@ jobs: with: submodules: recursive + - name: Determine release mode + id: release_mode + run: | + set -euo pipefail + if [[ "${GITHUB_EVENT_NAME}" == "push" && "${GITHUB_REF:-}" == refs/tags/* ]]; then + echo "publish_release=true" >> "$GITHUB_OUTPUT" + echo "release_tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + echo "artifact_name=release-${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + else + REF_SLUG="$(printf '%s' "${GITHUB_REF_NAME}" | tr '/[:space:]' '-' | tr -cd '[:alnum:]-_.')" + SHORT_SHA="${GITHUB_SHA::7}" + echo "publish_release=false" >> "$GITHUB_OUTPUT" + echo "release_tag=verify-${SHORT_SHA}" >> "$GITHUB_OUTPUT" + echo "artifact_name=release-verification-${REF_SLUG}-${SHORT_SHA}" >> "$GITHUB_OUTPUT" + fi + - name: Guard immutable release assets + if: steps.release_mode.outputs.publish_release == 'true' id: guard_release_assets uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: @@ -99,13 +113,23 @@ jobs: - name: Install build deps if: steps.guard_release_assets.outputs.skip_all != 'true' run: | - npm install --global "create-dmg@${CREATE_DMG_VERSION}" + brew list create-dmg >/dev/null 2>&1 || brew install create-dmg - name: Download pre-built GhosttyKit.xcframework if: steps.guard_release_assets.outputs.skip_all != 'true' run: | ./scripts/download-prebuilt-ghosttykit.sh + - name: Verify GhosttyKit architectures + if: steps.guard_release_assets.outputs.skip_all != 'true' + run: | + set -euo pipefail + GHOSTTYKIT_BINARY="GhosttyKit.xcframework/macos-arm64_x86_64/libghostty.a" + test -f "$GHOSTTYKIT_BINARY" + GHOSTTYKIT_ARCHS="$(lipo -archs "$GHOSTTYKIT_BINARY")" + echo "GhosttyKit architectures: $GHOSTTYKIT_ARCHS" + [[ "$GHOSTTYKIT_ARCHS" == *arm64* && "$GHOSTTYKIT_ARCHS" == *x86_64* ]] + - name: Cache Swift packages if: steps.guard_release_assets.outputs.skip_all != 'true' uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 @@ -131,9 +155,37 @@ jobs: if: steps.guard_release_assets.outputs.skip_all != 'true' run: | xcodebuild -scheme cmux -configuration Release -derivedDataPath build \ + -destination 'generic/platform=macOS' \ -clonedSourcePackagesDirPath .spm-cache \ + ARCHS="arm64 x86_64" \ + ONLY_ACTIVE_ARCH=NO \ CODE_SIGNING_ALLOWED=NO build + - name: Verify release binary architectures + if: steps.guard_release_assets.outputs.skip_all != 'true' + run: | + set -euo pipefail + APP_BINARY="build/Build/Products/Release/cmux.app/Contents/MacOS/cmux" + CLI_BINARY="build/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux" + APP_ARCHS="$(lipo -archs "$APP_BINARY")" + CLI_ARCHS="$(lipo -archs "$CLI_BINARY")" + if [[ "${{ steps.release_mode.outputs.publish_release }}" == "true" ]]; then + RELEASE_MODE="publish" + else + RELEASE_MODE="verify" + fi + echo "App binary architectures: $APP_ARCHS" + echo "CLI binary architectures: $CLI_ARCHS" + [[ "$APP_ARCHS" == *arm64* && "$APP_ARCHS" == *x86_64* ]] + [[ "$CLI_ARCHS" == *arm64* && "$CLI_ARCHS" == *x86_64* ]] + { + echo "Release mode: $RELEASE_MODE" + echo "Ref: ${GITHUB_REF_NAME}" + echo "Commit: ${GITHUB_SHA}" + echo "App binary architectures: $APP_ARCHS" + echo "CLI binary architectures: $CLI_ARCHS" + } > release-verification.txt + - name: Inject Sparkle keys into Info.plist if: steps.guard_release_assets.outputs.skip_all != 'true' run: | @@ -218,12 +270,7 @@ jobs: xcrun stapler validate "$APP_PATH" spctl -a -vv --type execute "$APP_PATH" rm -f "$ZIP_SUBMIT" - # create-dmg generates a styled drag-to-install DMG - create-dmg \ - --identity="$APPLE_SIGNING_IDENTITY" \ - "$APP_PATH" \ - ./ - mv ./cmux*.dmg "$DMG_RELEASE" + CMUX_CREATE_DMG_REQUIRE_STYLED=1 ./scripts/create_release_dmg.sh "$APP_PATH" "$DMG_RELEASE" "$APPLE_SIGNING_IDENTITY" DMG_SUBMIT_JSON="$(xcrun notarytool submit "$DMG_RELEASE" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --wait --output-format json)" DMG_SUBMIT_ID="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<<"$DMG_SUBMIT_JSON")" DMG_STATUS="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])' <<<"$DMG_SUBMIT_JSON")" @@ -258,10 +305,10 @@ jobs: echo "Missing SPARKLE_PRIVATE_KEY secret" >&2 exit 1 fi - ./scripts/sparkle_generate_appcast.sh cmux-macos.dmg "$GITHUB_REF_NAME" appcast.xml + ./scripts/sparkle_generate_appcast.sh cmux-macos.dmg "${{ steps.release_mode.outputs.release_tag }}" appcast.xml - name: Upload release asset - if: steps.guard_release_assets.outputs.skip_upload != 'true' + if: steps.release_mode.outputs.publish_release == 'true' && steps.guard_release_assets.outputs.skip_upload != 'true' uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2 with: files: | @@ -270,6 +317,16 @@ jobs: generate_release_notes: true overwrite_files: false + - name: Upload verification artifacts + if: steps.release_mode.outputs.publish_release != 'true' && steps.guard_release_assets.outputs.skip_all != 'true' + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: ${{ steps.release_mode.outputs.artifact_name }} + path: |- + cmux-macos.dmg + appcast.xml + release-verification.txt + - name: Cleanup keychain if: always() run: | diff --git a/scripts/build-sign-upload.sh b/scripts/build-sign-upload.sh index 08d1f84cfd8f..da95fe41b0e4 100755 --- a/scripts/build-sign-upload.sh +++ b/scripts/build-sign-upload.sh @@ -53,7 +53,7 @@ APP_PATH="build/Build/Products/Release/cmux.app" # --- Pre-flight --- source ~/.secrets/cmuxterm.env export SPARKLE_PRIVATE_KEY -for tool in zig xcodebuild create-dmg xcrun codesign ditto gh; do +for tool in zig xcodebuild xcrun codesign ditto gh; do command -v "$tool" >/dev/null || { echo "MISSING: $tool" >&2; exit 1; } done echo "Pre-flight checks passed" @@ -68,12 +68,31 @@ else echo "GhosttyKit.xcframework exists, skipping build" fi +GHOSTTYKIT_BINARY="GhosttyKit.xcframework/macos-arm64_x86_64/libghostty.a" +test -f "$GHOSTTYKIT_BINARY" +GHOSTTYKIT_ARCHS="$(lipo -archs "$GHOSTTYKIT_BINARY")" +echo "GhosttyKit architectures: $GHOSTTYKIT_ARCHS" +[[ "$GHOSTTYKIT_ARCHS" == *arm64* && "$GHOSTTYKIT_ARCHS" == *x86_64* ]] + # --- Build app (Release, unsigned) --- echo "Building app..." rm -rf build/ -xcodebuild -scheme cmux -configuration Release -derivedDataPath build CODE_SIGNING_ALLOWED=NO build 2>&1 | tail -5 +xcodebuild -scheme cmux -configuration Release -derivedDataPath build \ + -destination 'generic/platform=macOS' \ + ARCHS="arm64 x86_64" \ + ONLY_ACTIVE_ARCH=NO \ + CODE_SIGNING_ALLOWED=NO build 2>&1 | tail -5 echo "Build succeeded" +APP_BINARY="$APP_PATH/Contents/MacOS/cmux" +CLI_BINARY="$APP_PATH/Contents/Resources/bin/cmux" +APP_ARCHS="$(lipo -archs "$APP_BINARY")" +CLI_ARCHS="$(lipo -archs "$CLI_BINARY")" +echo "App binary architectures: $APP_ARCHS" +echo "CLI binary architectures: $CLI_ARCHS" +[[ "$APP_ARCHS" == *arm64* && "$APP_ARCHS" == *x86_64* ]] +[[ "$CLI_ARCHS" == *arm64* && "$CLI_ARCHS" == *x86_64* ]] + # --- Inject Sparkle keys --- echo "Injecting Sparkle keys..." SPARKLE_PUBLIC_KEY_DERIVED=$(swift scripts/derive_sparkle_public_key.swift "$SPARKLE_PRIVATE_KEY") @@ -107,7 +126,7 @@ echo "App notarized" # --- Create and notarize DMG --- echo "Creating DMG..." rm -f cmux-macos.dmg -create-dmg --codesign "$SIGN_HASH" cmux-macos.dmg "$APP_PATH" +CMUX_CREATE_DMG_REQUIRE_STYLED=1 ./scripts/create_release_dmg.sh "$APP_PATH" "cmux-macos.dmg" "$SIGN_HASH" echo "Notarizing DMG..." xcrun notarytool submit cmux-macos.dmg \ --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --wait diff --git a/scripts/create_release_dmg.sh b/scripts/create_release_dmg.sh new file mode 100755 index 000000000000..4bbe9fee9834 --- /dev/null +++ b/scripts/create_release_dmg.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then + echo "Usage: $0 [signing-identity]" >&2 + exit 1 +fi + +APP_PATH="$1" +DMG_OUTPUT="$2" +SIGNING_IDENTITY="${3:-}" +MODERN_CREATE_DMG_VERSION="${CMUX_CREATE_DMG_MODERN_VERSION:-8.0.0}" +REQUIRE_STYLED="${CMUX_CREATE_DMG_REQUIRE_STYLED:-0}" + +if [ ! -d "$APP_PATH" ]; then + echo "App not found: $APP_PATH" >&2 + exit 1 +fi + +OUTPUT_DIR="$(dirname "$DMG_OUTPUT")" +mkdir -p "$OUTPUT_DIR" + +cleanup_paths=() +cleanup() { + local path + for path in "${cleanup_paths[@]-}"; do + [ -n "$path" ] && rm -rf "$path" + done +} +trap cleanup EXIT + +detect_create_dmg_mode() { + local bin_name version_output major help_output + bin_name="$1" + + version_output="$("$bin_name" --version 2>/dev/null || true)" + major="$(printf '%s\n' "$version_output" | sed -n 's/.* \([0-9][0-9]*\)\..*/\1/p' | head -n1)" + if [ -n "$major" ]; then + if [ "$major" -lt 2 ]; then + echo "legacy" + else + echo "modern" + fi + return + fi + + help_output="$("$bin_name" --help 2>&1 || true)" + if printf '%s\n' "$help_output" | grep -Fq " "; then + echo "legacy" + else + echo "modern" + fi +} + +find_brew_legacy_create_dmg() { + local brew_prefix candidate + + if ! command -v brew >/dev/null 2>&1; then + return 1 + fi + + brew_prefix="$(brew --prefix create-dmg 2>/dev/null || true)" + if [ -z "$brew_prefix" ]; then + return 1 + fi + + candidate="$brew_prefix/bin/create-dmg" + if [ ! -x "$candidate" ]; then + return 1 + fi + + if [ "$(detect_create_dmg_mode "$candidate")" != "legacy" ]; then + return 1 + fi + + printf '%s\n' "$candidate" +} + +find_path_create_dmg() { + command -v create-dmg 2>/dev/null || true +} + +create_dmg_legacy() { + local legacy_bin staging_dir app_name volume_name cmd + legacy_bin="$1" + staging_dir="$(mktemp -d)" + cleanup_paths+=("$staging_dir") + + cp -R "$APP_PATH" "$staging_dir/" + app_name="$(basename "$APP_PATH")" + volume_name="$(basename "$DMG_OUTPUT" .dmg)" + + cmd=( + "$legacy_bin" + --volname "$volume_name" + --window-size 660 400 + --icon-size 128 + --icon "$app_name" 180 170 + --hide-extension "$app_name" + --app-drop-link 480 170 + ) + if [ -n "$SIGNING_IDENTITY" ]; then + cmd+=(--codesign "$SIGNING_IDENTITY") + fi + cmd+=("$DMG_OUTPUT" "$staging_dir") + + "${cmd[@]}" +} + +create_dmg_modern() { + local temp_output_dir generated_dmg + local -a modern_bin cmd + + modern_bin=("$@") + temp_output_dir="$(mktemp -d)" + cleanup_paths+=("$temp_output_dir") + + cmd=("${modern_bin[@]}" --overwrite) + if [ -n "$SIGNING_IDENTITY" ]; then + cmd+=(--identity="$SIGNING_IDENTITY") + else + cmd+=(--no-code-sign) + fi + cmd+=("$APP_PATH" "$temp_output_dir") + + "${cmd[@]}" + + generated_dmg="$(find "$temp_output_dir" -maxdepth 1 -type f -name '*.dmg' | head -n1)" + if [ -z "$generated_dmg" ]; then + echo "create-dmg did not produce a DMG file in $temp_output_dir" >&2 + exit 1 + fi + + rm -f "$DMG_OUTPUT" + mv "$generated_dmg" "$DMG_OUTPUT" +} + +legacy_bin="$(find_brew_legacy_create_dmg || true)" +if [ -z "$legacy_bin" ]; then + path_bin="$(find_path_create_dmg)" + if [ -n "$path_bin" ] && [ "$(detect_create_dmg_mode "$path_bin")" = "legacy" ]; then + legacy_bin="$path_bin" + fi +fi + +if [ -n "$legacy_bin" ]; then + create_dmg_legacy "$legacy_bin" + exit 0 +fi + +if [ "$REQUIRE_STYLED" = "1" ]; then + echo "Styled DMG creation requires the legacy create-dmg CLI with layout flags." >&2 + echo "Install the Homebrew create-dmg formula or provide a legacy create-dmg on PATH." >&2 + exit 1 +fi + +path_bin="$(find_path_create_dmg)" +if [ -n "$path_bin" ] && [ "$(detect_create_dmg_mode "$path_bin")" = "modern" ]; then + create_dmg_modern "$path_bin" + exit 0 +fi + +if command -v npx >/dev/null 2>&1; then + create_dmg_modern npx --yes "create-dmg@${MODERN_CREATE_DMG_VERSION}" + exit 0 +fi + +if [ -z "$path_bin" ]; then + echo "create-dmg is required but not found in PATH" >&2 + exit 1 +fi + +create_dmg_modern "$path_bin" diff --git a/tests/test_ci_create_dmg_pinned.sh b/tests/test_ci_create_dmg_pinned.sh index 1199f699a322..6300717e65ca 100755 --- a/tests/test_ci_create_dmg_pinned.sh +++ b/tests/test_ci_create_dmg_pinned.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash -# Regression test for https://github.com/manaflow-ai/cmux/issues/387. -# Ensures release workflows pin create-dmg to an explicit version. +# Regression test for the signed DMG packaging toolchain. +# Ensures release workflows provision the styled Homebrew create-dmg formula +# and do not silently switch to the generic npm CLI. set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" @@ -11,15 +12,15 @@ WORKFLOWS=( ) for workflow in "${WORKFLOWS[@]}"; do - if ! grep -Eq 'npm install --global .*create-dmg@' "$workflow"; then - echo "FAIL: $workflow must install create-dmg with an explicit version" + if ! grep -Eq 'brew list create-dmg >/dev/null 2>&1 \|\| brew install create-dmg' "$workflow"; then + echo "FAIL: $workflow must provision the Homebrew create-dmg formula" exit 1 fi - if grep -Eq 'npm install --global[[:space:]]+create-dmg([[:space:]]|$)' "$workflow"; then - echo "FAIL: $workflow still has unpinned create-dmg install" + if grep -Eq 'npm install --global .*create-dmg' "$workflow"; then + echo "FAIL: $workflow still installs the npm create-dmg CLI" exit 1 fi done -echo "PASS: create-dmg install is pinned in release workflows" +echo "PASS: signed workflows provision the styled create-dmg formula" diff --git a/tests/test_create_release_dmg.sh b/tests/test_create_release_dmg.sh new file mode 100644 index 000000000000..104b477d0109 --- /dev/null +++ b/tests/test_create_release_dmg.sh @@ -0,0 +1,236 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +SCRIPT="$ROOT_DIR/scripts/create_release_dmg.sh" + +if [ ! -x "$SCRIPT" ]; then + echo "FAIL: missing executable script $SCRIPT" + exit 1 +fi + +TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TMPDIR"' EXIT + +BIN_WITH_BREW="$TMPDIR/bin-with-brew" +BIN_WITH_MODERN="$TMPDIR/bin-with-modern" +BIN_WITH_LEGACY_PATH="$TMPDIR/bin-with-legacy-path" +LEGACY_PREFIX="$TMPDIR/legacy-prefix" +PATH_BASE="/usr/bin:/bin:/usr/sbin:/sbin" +mkdir -p "$BIN_WITH_BREW" "$BIN_WITH_MODERN" "$BIN_WITH_LEGACY_PATH" "$LEGACY_PREFIX/bin" + +cat > "$BIN_WITH_MODERN/create-dmg" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +if [ "${1:-}" = "--version" ]; then + echo "create-dmg 8.0.0" + exit 0 +fi + +if [ "${1:-}" = "--help" ]; then + echo "Usage: create-dmg [destination]" + exit 0 +fi + +printf '%s\n' "$*" >> "$FAKE_MODERN_LOG" + +if printf '%s\n' "$*" | grep -Fq -- "--overwrite"; then + dest="${!#}" + mkdir -p "$dest" + : > "$dest/generated.dmg" + exit 0 +fi + +echo "fake modern create-dmg did not receive a supported invocation" >&2 +exit 1 +EOF + +cat > "$LEGACY_PREFIX/bin/create-dmg" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +if [ "${1:-}" = "--version" ]; then + echo "create-dmg 1.2.3" + exit 0 +fi + +if [ "${1:-}" = "--help" ]; then + echo "Usage: create-dmg [options] " + exit 0 +fi + +printf '%s\n' "$*" >> "$FAKE_LEGACY_LOG" + +for arg in "$@"; do + if [[ "$arg" == *.dmg ]]; then + mkdir -p "$(dirname "$arg")" + : > "$arg" + exit 0 + fi +done + +echo "fake legacy create-dmg did not receive an output .dmg argument" >&2 +exit 1 +EOF + +cat > "$BIN_WITH_MODERN/npx" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +printf '%s\n' "$*" >> "$FAKE_NPX_LOG" + +if [ "${1:-}" != "--yes" ]; then + echo "expected --yes as first arg" >&2 + exit 1 +fi + +if [[ "${2:-}" != create-dmg@* ]]; then + echo "expected create-dmg@ as second arg" >&2 + exit 1 +fi + +dest="${!#}" +mkdir -p "$dest" +: > "$dest/generated-via-npx.dmg" +EOF + +cat > "$BIN_WITH_BREW/brew" <&2 +exit 1 +EOF + +cp "$BIN_WITH_MODERN/create-dmg" "$BIN_WITH_BREW/create-dmg" +cp "$BIN_WITH_MODERN/npx" "$BIN_WITH_BREW/npx" +cp "$LEGACY_PREFIX/bin/create-dmg" "$BIN_WITH_LEGACY_PATH/create-dmg" + +chmod +x \ + "$BIN_WITH_MODERN/create-dmg" \ + "$BIN_WITH_MODERN/npx" \ + "$BIN_WITH_BREW/brew" \ + "$BIN_WITH_BREW/create-dmg" \ + "$BIN_WITH_BREW/npx" \ + "$LEGACY_PREFIX/bin/create-dmg" \ + "$BIN_WITH_LEGACY_PATH/create-dmg" + +APP_DIR="$TMPDIR/cmux.app" +mkdir -p "$APP_DIR/Contents" + +run_script() { + local output_path legacy_log modern_log npx_log + output_path="$1" + legacy_log="$2" + modern_log="$3" + npx_log="$4" + shift 4 + + FAKE_LEGACY_LOG="$legacy_log" \ + FAKE_MODERN_LOG="$modern_log" \ + FAKE_NPX_LOG="$npx_log" \ + "$@" "$SCRIPT" "$APP_DIR" "$output_path" "SIGNING-ID" +} + +case_prefers_brew_legacy_for_styled() { + local output_path legacy_log modern_log npx_log + output_path="$TMPDIR/brew-legacy/cmux-macos.dmg" + legacy_log="$TMPDIR/brew-legacy.log" + modern_log="$TMPDIR/brew-modern.log" + npx_log="$TMPDIR/brew-npx.log" + : > "$legacy_log" + : > "$modern_log" + : > "$npx_log" + + run_script "$output_path" "$legacy_log" "$modern_log" "$npx_log" \ + env PATH="$BIN_WITH_BREW:$PATH_BASE" CMUX_CREATE_DMG_REQUIRE_STYLED=1 + + [ -f "$output_path" ] || { echo "FAIL: brew legacy case did not produce DMG"; exit 1; } + grep -F -- "--app-drop-link 480 170" "$legacy_log" >/dev/null || { + echo "FAIL: brew legacy case did not use legacy DMG layout" + exit 1 + } + if [ -s "$modern_log" ] || [ -s "$npx_log" ]; then + echo "FAIL: brew legacy case should not invoke modern tooling" + exit 1 + fi +} + +case_path_legacy_works_without_brew() { + local output_path legacy_log modern_log npx_log + output_path="$TMPDIR/path-legacy/cmux-macos.dmg" + legacy_log="$TMPDIR/path-legacy.log" + modern_log="$TMPDIR/path-modern.log" + npx_log="$TMPDIR/path-npx.log" + : > "$legacy_log" + : > "$modern_log" + : > "$npx_log" + + run_script "$output_path" "$legacy_log" "$modern_log" "$npx_log" \ + env PATH="$BIN_WITH_LEGACY_PATH:$PATH_BASE" CMUX_CREATE_DMG_REQUIRE_STYLED=1 + + [ -f "$output_path" ] || { echo "FAIL: path legacy case did not produce DMG"; exit 1; } + grep -F -- "--app-drop-link 480 170" "$legacy_log" >/dev/null || { + echo "FAIL: path legacy case did not use legacy DMG layout" + exit 1 + } + if [ -s "$modern_log" ] || [ -s "$npx_log" ]; then + echo "FAIL: path legacy case should not invoke modern tooling" + exit 1 + fi +} + +case_modern_fallback_uses_modern_cli() { + local output_path legacy_log modern_log npx_log + output_path="$TMPDIR/modern/cmux-macos.dmg" + legacy_log="$TMPDIR/modern-legacy.log" + modern_log="$TMPDIR/modern-modern.log" + npx_log="$TMPDIR/modern-npx.log" + : > "$legacy_log" + : > "$modern_log" + : > "$npx_log" + + run_script "$output_path" "$legacy_log" "$modern_log" "$npx_log" \ + env PATH="$BIN_WITH_MODERN:$PATH_BASE" + + [ -f "$output_path" ] || { echo "FAIL: modern fallback case did not produce DMG"; exit 1; } + grep -F -- "--overwrite" "$modern_log" >/dev/null || { + echo "FAIL: modern fallback case did not invoke the modern CLI" + exit 1 + } + if [ -s "$legacy_log" ] || [ -s "$npx_log" ]; then + echo "FAIL: modern fallback case should not invoke legacy or npx tooling" + exit 1 + fi +} + +case_require_styled_without_legacy_fails() { + local output_path legacy_log modern_log npx_log + output_path="$TMPDIR/require-styled-fail/cmux-macos.dmg" + legacy_log="$TMPDIR/require-styled-legacy.log" + modern_log="$TMPDIR/require-styled-modern.log" + npx_log="$TMPDIR/require-styled-npx.log" + : > "$legacy_log" + : > "$modern_log" + : > "$npx_log" + + if run_script "$output_path" "$legacy_log" "$modern_log" "$npx_log" \ + env PATH="$BIN_WITH_MODERN:$PATH_BASE" CMUX_CREATE_DMG_REQUIRE_STYLED=1 \ + >/dev/null 2>&1; then + echo "FAIL: require styled without legacy tooling should fail" + exit 1 + fi +} + +case_prefers_brew_legacy_for_styled +case_path_legacy_works_without_brew +case_modern_fallback_uses_modern_cli +case_require_styled_without_legacy_fails + +echo "PASS: create_release_dmg script preserves styled DMGs and isolates modern fallback"