diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5663695a..b05227d5 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -15,12 +15,12 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- name: Setup .NET
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9
+ uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: "10.0.x"
@@ -77,13 +77,335 @@ jobs:
cp publish/linux-arm64/WheelWizard artifacts/WheelWizard_arm64_Linux
chmod +x artifacts/WheelWizard_Linux artifacts/WheelWizard_arm64_Linux
+ - name: Upload Linux + Windows artifacts
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ with:
+ name: artifacts-linux-windows
+ path: artifacts/
+ retention-days: 1
+
+ # ──────────────────────────────────────────────────────────────
+ # macOS builds (native on macOS runner)
+ # ──────────────────────────────────────────────────────────────
+ macos-arm64:
+ name: Build macOS (arm64)
+ runs-on: macos-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
+ with:
+ persist-credentials: false
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
+ with:
+ dotnet-version: "10.0.x"
+
+ - name: Build .app bundle for macOS arm64
+ run: |
+ chmod +x macos/release-macos.sh
+ ./macos/release-macos.sh
+ env:
+ BUILD_ARCH: arm64
+
+ - name: Import signing certificate
+ env:
+ MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
+ MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
+ run: |
+ # Write the certificate from the secret
+ echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12
+
+ # Create a temporary keychain
+ security create-keychain -p temp build.keychain
+ security default-keychain -s build.keychain
+ security unlock-keychain -p temp build.keychain
+
+ # Import the certificate
+ security import certificate.p12 -k build.keychain \
+ -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
+
+ # Allow codesign to access the keychain
+ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k temp build.keychain
+
+ # Verify the import
+ echo "[INFO] Available signing identities:"
+ security find-identity -v -p basic
+ rm certificate.p12
+
+ - name: Sign the .app bundle with Developer ID
+ run: |
+ ENTITLEMENTS="${{ github.workspace }}/macos/certs/WheelWizardEntitlements.entitlements"
+ APP_PATH="release/WheelWizard.app"
+ SIGNING_IDENTITY="Developer ID Application: Gabriel Magaña (Z4D7MUNZ97)"
+
+ # List available identities for debugging
+ echo "[INFO] Checking signing identities..."
+ security find-identity -v -p codesigning
+
+ # Sign all executables inside the bundle first
+ find "$APP_PATH/Contents/MacOS" -type f | while read fname; do
+ echo "[INFO] Signing $fname"
+ codesign --force --timestamp --options=runtime \
+ --entitlements "$ENTITLEMENTS" \
+ --sign "$SIGNING_IDENTITY" "$fname"
+ done
+
+ # Sign the .app bundle itself
+ echo "[INFO] Signing .app bundle"
+ codesign --force --timestamp --options=runtime \
+ --entitlements "$ENTITLEMENTS" \
+ --sign "$SIGNING_IDENTITY" "$APP_PATH"
+
+ - name: Create DMG
+ id: create-dmg
+ uses: L-Super/create-dmg-actions@8c59360de7ec731bfa03ce3bd2ad5ae650169864 # v1
+ with:
+ dmg_name: WheelWizard-macOSarm64
+ src_dir: release/WheelWizard.app
+ background: macos/backgr.png
+ window_size: 650 376
+ icon_size: 128
+ icon_position: 200 150
+ app_drop_link: 450 150
+
+ - name: Notarize and staple the .app
+ env:
+ MACOS_NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }}
+ MACOS_NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }}
+ MACOS_NOTARY_APP_PASSWORD: ${{ secrets.MACOS_NOTARY_APP_PASSWORD }}
+ run: |
+ # Notarize the .app bundle
+ ditto -c -k --sequesterRsrc --keepParent \
+ release/WheelWizard.app \
+ WheelWizard-app-notarize.zip
+ xcrun notarytool submit WheelWizard-app-notarize.zip \
+ --apple-id "$MACOS_NOTARY_APPLE_ID" \
+ --team-id "$MACOS_NOTARY_TEAM_ID" \
+ --password "$MACOS_NOTARY_APP_PASSWORD" \
+ --wait
+ rm WheelWizard-app-notarize.zip
+ xcrun stapler staple release/WheelWizard.app
+
+ - name: Notarize the DMG
+ env:
+ MACOS_NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }}
+ MACOS_NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }}
+ MACOS_NOTARY_APP_PASSWORD: ${{ secrets.MACOS_NOTARY_APP_PASSWORD }}
+ run: |
+ xcrun notarytool submit WheelWizard-macOSarm64.dmg \
+ --apple-id "$MACOS_NOTARY_APPLE_ID" \
+ --team-id "$MACOS_NOTARY_TEAM_ID" \
+ --password "$MACOS_NOTARY_APP_PASSWORD" \
+ --wait
+ xcrun stapler staple WheelWizard-macOSarm64.dmg
+
+ - name: Upload macOS DMG
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ with:
+ name: artifacts-macos-arm64
+ path: WheelWizard-macOSarm64.dmg
+ retention-days: 1
+
+ macos-x64:
+ name: Build macOS (x64)
+ # Cross-compile x64 on an arm64 runner since macos-13 (Intel) is deprecated.
+ # .NET handles cross-compilation via the runtime identifier (osx-x64).
+ runs-on: macos-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
+ with:
+ persist-credentials: false
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
+ with:
+ dotnet-version: "10.0.x"
+
+ - name: Build .app bundle for macOS x64
+ run: |
+ chmod +x macos/release-macos.sh
+ ./macos/release-macos.sh
+ env:
+ BUILD_ARCH: x64
+
+ - name: Import signing certificate
+ env:
+ MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
+ MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
+ run: |
+ echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12
+ security create-keychain -p temp build.keychain
+ security default-keychain -s build.keychain
+ security unlock-keychain -p temp build.keychain
+ security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
+ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k temp build.keychain
+ rm certificate.p12
+
+ - name: Sign the .app bundle with Developer ID
+ run: |
+ ENTITLEMENTS="${{ github.workspace }}/macos/certs/WheelWizardEntitlements.entitlements"
+ APP_PATH="release/WheelWizard.app"
+ SIGNING_IDENTITY="Developer ID Application: Gabriel Magaña (Z4D7MUNZ97)"
+
+ # Sign all executables inside the bundle first
+ find "$APP_PATH/Contents/MacOS" -type f | while read fname; do
+ echo "[INFO] Signing $fname"
+ codesign --force --timestamp --options=runtime \
+ --entitlements "$ENTITLEMENTS" \
+ --sign "$SIGNING_IDENTITY" "$fname"
+ done
+
+ # Sign the .app bundle itself
+ echo "[INFO] Signing .app bundle"
+ codesign --force --timestamp --options=runtime \
+ --entitlements "$ENTITLEMENTS" \
+ --sign "$SIGNING_IDENTITY" "$APP_PATH"
+
+ - name: Create DMG
+ id: create-dmg
+ uses: L-Super/create-dmg-actions@8c59360de7ec731bfa03ce3bd2ad5ae650169864 # v1
+ with:
+ dmg_name: WheelWizard-macOSintel
+ src_dir: release/WheelWizard.app
+ background: macos/backgr.png
+ window_size: 650 376
+ icon_size: 128
+ icon_position: 200 150
+ app_drop_link: 450 150
+
+ - name: Notarize and staple the .app
+ env:
+ MACOS_NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }}
+ MACOS_NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }}
+ MACOS_NOTARY_APP_PASSWORD: ${{ secrets.MACOS_NOTARY_APP_PASSWORD }}
+ run: |
+ # Notarize the .app bundle
+ ditto -c -k --sequesterRsrc --keepParent \
+ release/WheelWizard.app \
+ WheelWizard-app-notarize.zip
+ xcrun notarytool submit WheelWizard-app-notarize.zip \
+ --apple-id "$MACOS_NOTARY_APPLE_ID" \
+ --team-id "$MACOS_NOTARY_TEAM_ID" \
+ --password "$MACOS_NOTARY_APP_PASSWORD" \
+ --wait
+ rm WheelWizard-app-notarize.zip
+ xcrun stapler staple release/WheelWizard.app
+
+ - name: Notarize the DMG
+ env:
+ MACOS_NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }}
+ MACOS_NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }}
+ MACOS_NOTARY_APP_PASSWORD: ${{ secrets.MACOS_NOTARY_APP_PASSWORD }}
+ run: |
+ xcrun notarytool submit WheelWizard-macOSintel.dmg \
+ --apple-id "$MACOS_NOTARY_APPLE_ID" \
+ --team-id "$MACOS_NOTARY_TEAM_ID" \
+ --password "$MACOS_NOTARY_APP_PASSWORD" \
+ --wait
+ xcrun stapler staple WheelWizard-macOSintel.dmg
+
+ - name: Upload macOS DMG
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ with:
+ name: artifacts-macos-x64
+ path: WheelWizard-macOSintel.dmg
+ retention-days: 1
+
+ # ──────────────────────────────────────────────────────────────
+ # Create GitHub release with all assets
+ # ──────────────────────────────────────────────────────────────
+ create-release:
+ name: Create GitHub Release
+ needs: [release, macos-arm64, macos-x64]
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Download all artifacts
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
+
+ - name: Prepare release assets
+ run: |
+ mkdir -p final-artifacts
+
+ # Linux & Windows
+ cp artifacts-linux-windows/WheelWizardWindows.exe final-artifacts/
+ cp artifacts-linux-windows/WheelWizard_Linux final-artifacts/
+ cp artifacts-linux-windows/WheelWizard_arm64_Linux final-artifacts/
+
+ # macOS DMGs
+ cp artifacts-macos-arm64/WheelWizard-macOSarm64.dmg final-artifacts/
+ cp artifacts-macos-x64/WheelWizard-macOSintel.dmg final-artifacts/
+
+ chmod +x final-artifacts/WheelWizard_Linux final-artifacts/WheelWizard_arm64_Linux
+ ls -la final-artifacts/
+
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
- run: >
- gh release create "${{ github.ref_name }}"
- artifacts/WheelWizardWindows.exe
- artifacts/WheelWizard_Linux
- artifacts/WheelWizard_arm64_Linux
- --title "${{ github.ref_name }}"
- --generate-notes
+ run: |
+ REPO="${{ github.repository }}"
+ TAG_NAME="${GITHUB_REF_NAME}"
+ ASSETS=(final-artifacts/*)
+ gh release create "$TAG_NAME" "${ASSETS[@]}" \
+ --repo "$REPO" \
+ --title "$TAG_NAME" \
+ --generate-notes
+
+
+ # ──────────────────────────────────────────────────────────────
+ # Update Homebrew tap with new version and SHA256
+ # (Disabled for now — will be re-enabled later)
+ # ──────────────────────────────────────────────────────────────
+ # update-tap:
+ # name: Update Homebrew Tap
+ # needs: [create-release, macos-arm64, macos-x64]
+ # runs-on: ubuntu-latest
+ #
+ # steps:
+ # - name: Download SHA256 artifacts
+ # uses: actions/download-artifact@v4
+ # with:
+ # pattern: sha256-*
+ # merge-multiple: true
+ #
+ # - name: Read SHA256 values
+ # id: shas
+ # run: |
+ # echo "arm64=$(cat sha256-arm64.txt)" >> "$GITHUB_OUTPUT"
+ # echo "intel=$(cat sha256-intel.txt)" >> "$GITHUB_OUTPUT"
+ #
+ # - name: Checkout homebrew-tap repository
+ # uses: actions/checkout@v4
+ # with:
+ # repository: TeamWheelWizard/homebrew-tap
+ # token: ${{ secrets.TAP_TOKEN }}
+ # path: homebrew-tap
+ #
+ # - name: Update cask with real SHA256 values
+ # run: |
+ # VERSION="${GITHUB_REF_NAME#v}"
+ # CASK_FILE="homebrew-tap/Casks/wheelwizard.rb"
+ #
+ # # Replace version
+ # sed -i "s/version \".*\"/version \"$VERSION\"/" "$CASK_FILE"
+ #
+ # # Replace SHA256 placeholders with real values
+ # sed -i "s/REPLACE_ME_ARM64/${{ steps.shas.outputs.arm64 }}/" "$CASK_FILE"
+ # sed -i "s/REPLACE_ME_INTEL/${{ steps.shas.outputs.intel }}/" "$CASK_FILE"
+ #
+ # echo "=== Updated $CASK_FILE ==="
+ # cat "$CASK_FILE"
+ #
+ # - name: Commit and push to homebrew-tap
+ # run: |
+ # cd homebrew-tap
+ # git config user.name "github-actions[bot]"
+ # git config user.email "github-actions[bot]@users.noreply.github.com"
+ # git add Casks/wheelwizard.rb
+ # git commit -m "chore: update wheelwizard to v${GITHUB_REF_NAME#v}"
+ # git push
diff --git a/.gitignore b/.gitignore
index 1c87ff33..635b27bb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -458,6 +458,9 @@ Network Trash Folder
Temporary Items
.apdisk
+# Build output directories
+/release/
+
# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
# Windows thumbnail cache files
Thumbs.db
diff --git a/build-mac.sh b/build-mac.sh
deleted file mode 100755
index 56aec8c6..00000000
--- a/build-mac.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env bash
-# https://avaloniaui.net/blog/the-definitive-guide-to-building-and-deploying-avalonia-applications-for-macos
-
-PROJECT_NAME="$1"
-OUTPUT_DIR="./WheelWizard/bin/Release/compiled"
-
-build_for_arch() {
- local arch=$1
- echo "Building for $arch..."
- dotnet publish -r osx-$arch -c Release /p:PublishSingleFile=true \
- /p:IncludeAllContentForSelfExtract=true /p:IncludeNativeLibrariesForSelfExtract=true \
- /p:EnableCompressionInSingleFile=true /p:PublishReadyToRun=true \
- -p:UseAppHost=true --self-contained true -o "$OUTPUT_DIR/osx-$arch"
-}
-
-build_for_arch "x64"
-build_for_arch "arm64"
-
-echo "Creating universal binary..."
-mkdir -p "$OUTPUT_DIR/Universal"
-lipo -create \
- "$OUTPUT_DIR/osx-x64/WheelWizard" \
- "$OUTPUT_DIR/osx-arm64/WheelWizard" \
- -output "$OUTPUT_DIR/Universal/WheelWizard"
-
-echo "Universal binary created"
-
diff --git a/macos/MacAppTemplate/Contents/CodeResources b/macos/MacAppTemplate/Contents/CodeResources
new file mode 100644
index 00000000..6e9f2841
Binary files /dev/null and b/macos/MacAppTemplate/Contents/CodeResources differ
diff --git a/macos/MacAppTemplate/Contents/Info.plist b/macos/MacAppTemplate/Contents/Info.plist
new file mode 100644
index 00000000..75cd7963
--- /dev/null
+++ b/macos/MacAppTemplate/Contents/Info.plist
@@ -0,0 +1,22 @@
+
+
+
+
+ CFBundleName
+ WheelWizard
+ CFBundleIdentifier
+ ga.gabema.WheelWizard
+ CFBundleShortVersionString
+ 2.5.1
+ CFBundleVersion
+ 2.5.1
+ CFBundlePackageType
+ APPL
+ CFBundleSignature
+ ????
+ CFBundleExecutable
+ WheelWizard
+ CFBundleIconFile
+ WheelWizard.icns
+
+
diff --git a/macos/MacAppTemplate/Contents/Resources/WheelWizard.icns b/macos/MacAppTemplate/Contents/Resources/WheelWizard.icns
new file mode 100644
index 00000000..31fbafb2
Binary files /dev/null and b/macos/MacAppTemplate/Contents/Resources/WheelWizard.icns differ
diff --git a/macos/backgr.png b/macos/backgr.png
new file mode 100644
index 00000000..bbdf116f
Binary files /dev/null and b/macos/backgr.png differ
diff --git a/macos/certs/WheelWizardEntitlements.entitlements b/macos/certs/WheelWizardEntitlements.entitlements
new file mode 100644
index 00000000..0546d84b
--- /dev/null
+++ b/macos/certs/WheelWizardEntitlements.entitlements
@@ -0,0 +1,24 @@
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+ com.apple.security.cs.disable-library-validation
+
+ com.apple.security.automation.apple-events
+
+
+ com.apple.security.files.user-selected.read-write
+
+ com.apple.security.files.downloads.read-write
+
+
+ com.apple.security.temporary-exception.files.absolute-path.read-write
+
+ /Users/
+
+
+
\ No newline at end of file
diff --git a/macos/release-macos.sh b/macos/release-macos.sh
new file mode 100755
index 00000000..6bc4b049
--- /dev/null
+++ b/macos/release-macos.sh
@@ -0,0 +1,137 @@
+#!/usr/bin/env bash
+# =============================================================================
+# WheelWizard macOS Build Script
+# =============================================================================
+# Builds WheelWizard for macOS and creates a .app bundle.
+# Designed to run on macOS CI runners (GitHub Actions).
+#
+# Environment variables:
+# BUILD_ARCH - "arm64" or "x64" (default: auto-detect)
+# SKIP_BUILD - Set to "true" to skip dotnet build
+# OUTPUT_DIR - Output directory (default: ./release)
+#
+# No codesigning, no notarization, no DMG creation.
+# DMG is created by the GitHub Actions workflow using create-dmg action.
+# =============================================================================
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+MACOS_DIR="$SCRIPT_DIR"
+WW_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+MAC_DIRS="$MACOS_DIR/MacAppTemplate"
+DEFAULT_OUTPUT="$WW_DIR/release"
+
+# ---- Detect host architecture ----
+HOST_ARCH="$(uname -m)"
+case "$HOST_ARCH" in
+ arm64|aarch64) HOST_BUILD_ARCH="arm64" ;;
+ x86_64|amd64) HOST_BUILD_ARCH="x64" ;;
+ *) HOST_BUILD_ARCH="x64" ;;
+esac
+
+# Use BUILD_ARCH if set, otherwise default to host architecture
+BUILD_ARCH="${BUILD_ARCH:-$HOST_BUILD_ARCH}"
+RID="osx-$BUILD_ARCH"
+OUTPUT_DIR="${OUTPUT_DIR:-$DEFAULT_OUTPUT}"
+
+echo "[INFO] Host architecture: $HOST_BUILD_ARCH"
+echo "[INFO] Building for RID: $RID (arch: $BUILD_ARCH)"
+echo "[INFO] Output: $OUTPUT_DIR"
+
+# If cross-compiling (e.g., building x64 on arm64 host), set the appropriate architecture flag
+if [ "$BUILD_ARCH" != "$HOST_BUILD_ARCH" ]; then
+ echo "[INFO] Cross-compiling: $HOST_BUILD_ARCH -> $BUILD_ARCH"
+ case "$BUILD_ARCH" in
+ x64) ARCH_FLAG="-arch x86_64" ;;
+ arm64) ARCH_FLAG="-arch arm64" ;;
+ esac
+else
+ ARCH_FLAG=""
+fi
+
+mkdir -p "$OUTPUT_DIR"
+
+# =============================================================================
+# STEP 1: Build
+# =============================================================================
+if [ "${SKIP_BUILD:-}" != "true" ]; then
+ echo "[INFO] Building WheelWizard for $RID..."
+ cd "$WW_DIR"
+ dotnet publish -r "$RID" -c Release-macOS \
+ /p:PublishSingleFile=true \
+ /p:IncludeAllContentForSelfExtract=true \
+ /p:IncludeNativeLibrariesForSelfExtract=true \
+ /p:EnableCompressionInSingleFile=true \
+ /p:PublishReadyToRun=true \
+ -p:UseAppHost=true \
+ --self-contained true \
+ -o "$OUTPUT_DIR/compiled/$RID"
+ cd "$SCRIPT_DIR"
+else
+ echo "[INFO] Skipping build (SKIP_BUILD=true)"
+fi
+
+EXE_DIR="$OUTPUT_DIR/compiled/$RID"
+if [ ! -f "$EXE_DIR/WheelWizard" ]; then
+ echo "[ERROR] Built binary not found at $EXE_DIR/WheelWizard"
+ echo " Make sure the build step succeeded or set SKIP_BUILD=true if pre-built."
+ exit 1
+fi
+
+# =============================================================================
+# STEP 2: Create .app bundle
+# =============================================================================
+APP_BUNDLE="$OUTPUT_DIR/WheelWizard.app"
+echo "[INFO] Creating .app bundle at $APP_BUNDLE"
+
+# Clean any previous bundle
+rm -rf "$APP_BUNDLE"
+
+# Copy the .app template
+if [ -d "$MAC_DIRS" ]; then
+ cp -R "$MAC_DIRS" "$APP_BUNDLE"
+else
+ echo "[ERROR] Template directory not found: $MAC_DIRS"
+ echo " The MacAppTemplate directory is required for the .app bundle structure."
+ exit 1
+fi
+
+# Place the binary
+mkdir -p "$APP_BUNDLE/Contents/MacOS"
+cp "$EXE_DIR/WheelWizard" "$APP_BUNDLE/Contents/MacOS/WheelWizard"
+
+# Copy the icon if present
+if [ -f "$MAC_DIRS/Contents/Resources/WheelWizard.icns" ]; then
+ mkdir -p "$APP_BUNDLE/Contents/Resources"
+ cp "$MAC_DIRS/Contents/Resources/WheelWizard.icns" "$APP_BUNDLE/Contents/Resources/WheelWizard.icns"
+fi
+
+# Set the bundle version from the release tag (e.g. "v2.5.1" -> "2.5.1"),
+# falling back to the version declared in the project file.
+if [ -n "${GITHUB_REF_NAME:-}" ]; then
+ BUNDLE_VERSION="${GITHUB_REF_NAME#v}"
+else
+ BUNDLE_VERSION="$(sed -n 's/.*\(.*\)<\/Version>.*/\1/p' "$WW_DIR/WheelWizard/WheelWizard.csproj" | head -n1)"
+fi
+if [ -z "$BUNDLE_VERSION" ]; then
+ echo "[WARN] Could not determine version; leaving Info.plist version unchanged."
+else
+ echo "[INFO] Setting bundle version to $BUNDLE_VERSION"
+ /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUNDLE_VERSION" "$APP_BUNDLE/Contents/Info.plist"
+ /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $BUNDLE_VERSION" "$APP_BUNDLE/Contents/Info.plist"
+fi
+
+echo "[INFO] .app bundle created successfully"
+
+# =============================================================================
+# STEP 3: Cleanup compiled artifacts
+# =============================================================================
+echo "[INFO] Cleaning up intermediate build artifacts..."
+rm -rf "$OUTPUT_DIR/compiled"
+
+echo ""
+echo "============================================"
+echo " ✅ Build complete!"
+echo " .app bundle: $APP_BUNDLE"
+echo "============================================"