name: Release build artifacts

on:
  workflow_call:
    inputs:
      version:
        required: true
        type: string
      macos_bundles:
        required: false
        type: string
        default: ""
      upload_macos_dmg:
        required: true
        type: boolean

env:
  # Bump this when Tauri/Rust target artifacts capture stale absolute paths.
  RUST_TARGET_CACHE_VERSION: v2026-04-14-tolaria
  # The production Vite bundle can exceed Node's default ~2GB heap on
  # macOS arm64 runners while Tauri runs beforeBuildCommand.
  NODE_OPTIONS: --max-old-space-size=4096

jobs:
  build:
    name: Build (${{ matrix.arch }})
    runs-on: macos-15
    strategy:
      fail-fast: true
      matrix:
        include:
          - arch: aarch64
            target: aarch64-apple-darwin
          - arch: x86_64
            target: x86_64-apple-darwin
    steps:
      - uses: actions/checkout@v4

      - name: Setup pnpm
        uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa
        with:
          version: 10

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: "pnpm"

      - name: Setup Bun (required for bundle-qmd.sh)
        uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
        with:
          bun-version: latest

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8
        with:
          targets: ${{ matrix.target }}

      - name: Cache Rust dependencies
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            src-tauri/target
          key: ${{ runner.os }}-release-cargo-${{ matrix.target }}-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-release-cargo-${{ matrix.target }}-${{ env.RUST_TARGET_CACHE_VERSION }}-

      - name: Install frontend dependencies
        run: pnpm install --frozen-lockfile

      - name: Clear cached bundle artifacts
        run: |
          rm -rf src-tauri/target/${{ matrix.target }}/release/bundle

      - name: Set version
        run: |
          VERSION="${{ inputs.version }}"
          jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json
          sed -i '' "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml

      - name: Import Apple Developer certificate into keychain
        env:
          APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
        run: |
          CERT_PATH="$RUNNER_TEMP/apple_cert.p12"
          KEYCHAIN_PATH="$RUNNER_TEMP/laputa-signing.keychain-db"
          KEYCHAIN_PASSWORD="$(uuidgen)"
          echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH"
          security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
          security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
          security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
          security list-keychain -d user -s "$KEYCHAIN_PATH"
          security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
          echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"

      - name: Validate telemetry env
        env:
          VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
          SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
          VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
          VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
        run: |
          python3 <<'PY'
          import os
          import re
          import sys
          from urllib.parse import urlparse

          DISALLOWED_PLACEHOLDERS = {
              "",
              "-",
              "_",
              "false",
              "true",
              "null",
              "undefined",
              "none",
              "disabled",
          }

          def normalize(name: str) -> str:
              value = os.getenv(name, "").strip()
              if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
                  value = value[1:-1].strip()
              return value

          def normalize_http_like(value: str) -> str:
              if "://" in value:
                  return value
              return f"https://{value}"

          def normalize_hostname(hostname: str) -> str:
              normalized = hostname.strip().rstrip('.').lower()
              if normalized.startswith('[') and normalized.endswith(']'):
                  normalized = normalized[1:-1]
              return normalized

          def is_ip_address(hostname: str) -> bool:
              if re.fullmatch(r"(?:\d{1,3}\.){3}\d{1,3}", hostname):
                  return all(0 <= int(part) <= 255 for part in hostname.split('.'))
              return ':' in hostname and re.fullmatch(r"[\da-f:]+", hostname, re.IGNORECASE) is not None

          def is_allowed_hostname(hostname: str) -> bool:
              normalized = normalize_hostname(hostname)
              if not normalized or normalized in DISALLOWED_PLACEHOLDERS:
                  return False
              if normalized == 'localhost':
                  return True
              return '.' in normalized or is_ip_address(normalized)

          def is_http_url(value: str) -> bool:
              parsed = urlparse(normalize_http_like(value))
              return parsed.scheme in {"http", "https"} and is_allowed_hostname(parsed.hostname or "")

          values = {
              name: normalize(name)
              for name in (
                  "VITE_SENTRY_DSN",
                  "SENTRY_DSN",
                  "VITE_POSTHOG_KEY",
                  "VITE_POSTHOG_HOST",
              )
          }
          errors = []

          for name in ("VITE_SENTRY_DSN", "SENTRY_DSN", "VITE_POSTHOG_HOST"):
              value = values[name]
              if value.lower() in DISALLOWED_PLACEHOLDERS:
                  errors.append(f"{name} must be set to a real value, not a placeholder")
              elif not is_http_url(value):
                  errors.append(f"{name} must be a valid http(s) URL with a non-placeholder host")

          if values["VITE_POSTHOG_KEY"].lower() in DISALLOWED_PLACEHOLDERS:
              errors.append("VITE_POSTHOG_KEY must be set to a real project API key, not a placeholder")

          if errors:
              print("Telemetry env validation failed:", file=sys.stderr)
              for error in errors:
                  print(f"- {error}", file=sys.stderr)
              raise SystemExit(1)

          print("Telemetry env validation passed.")
          PY

      - name: Build Tauri app (with signing + notarization)
        env:
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
          APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
          APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
          VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
          VITE_SENTRY_RELEASE: ${{ inputs.version }}
          SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
          VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
          VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
        run: |
          MACOS_BUNDLES="${{ inputs.macos_bundles }}"
          if [ -n "$MACOS_BUNDLES" ]; then
            pnpm tauri build --target ${{ matrix.target }} --bundles "$MACOS_BUNDLES"
          else
            pnpm tauri build --target ${{ matrix.target }}
          fi

      - name: Upload .dmg
        if: ${{ inputs.upload_macos_dmg }}
        uses: actions/upload-artifact@v4
        with:
          name: dmg-${{ matrix.arch }}
          path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
          retention-days: 1

      - name: Upload updater artifacts (.tar.gz + .sig)
        uses: actions/upload-artifact@v4
        with:
          name: updater-${{ matrix.arch }}
          path: |
            src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz
            src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz.sig
          retention-days: 1

  build-linux:
    name: Build (linux-x86_64)
    runs-on: ubuntu-22.04

    steps:
      - uses: actions/checkout@v4

      - name: Install Tauri Linux system dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            libwebkit2gtk-4.1-dev \
            libsoup-3.0-dev \
            libxdo-dev \
            libssl-dev \
            libayatana-appindicator3-dev \
            fcitx5-frontend-gtk3 \
            libfuse2 \
            librsvg2-dev \
            curl \
            wget \
            patchelf \
            build-essential \
            file \
            cpio \
            rpm

      - name: Setup pnpm
        uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa
        with:
          version: 10

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: "pnpm"

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8
        with:
          targets: x86_64-unknown-linux-gnu

      - name: Cache Rust dependencies
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            src-tauri/target
          key: ${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}-

      - name: Install frontend dependencies
        run: pnpm install --frozen-lockfile

      - name: Clear cached bundle artifacts
        run: |
          rm -rf src-tauri/target/x86_64-unknown-linux-gnu/release/bundle

      - name: Set version
        run: |
          VERSION="${{ inputs.version }}"
          jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json
          sed -i "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml

      - name: Build Tauri app (Linux bundles)
        env:
          APPIMAGE_EXTRACT_AND_RUN: 1
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
          VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
          VITE_SENTRY_RELEASE: ${{ inputs.version }}
          SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
          VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
          VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
        run: |
          pnpm tauri build --target x86_64-unknown-linux-gnu --bundles deb,rpm,appimage

      - name: Validate Linux bundles
        run: |
          shopt -s nullglob
          appimages=(
            src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
          )
          installers=(
            "${appimages[@]}"
            src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
            src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
          )
          signatures=(
            src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig
            src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig
            src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig
          )
          if [ ${#appimages[@]} -eq 0 ]; then
            echo "::error::Linux build produced no AppImage bundle."
            exit 1
          fi
          if [ ${#installers[@]} -eq 0 ]; then
            echo "::error::Linux build produced no AppImage, deb or rpm bundle."
            exit 1
          fi
          if [ ${#signatures[@]} -eq 0 ]; then
            echo "::error::Linux build produced no updater signature (.sig) artifact."
            exit 1
          fi

          validate_desktop_categories() {
            local package_path="$1"
            local extract_dir="$2"

            rm -rf "$extract_dir"
            mkdir -p "$extract_dir"

            case "$package_path" in
              *.deb)
                dpkg-deb -x "$package_path" "$extract_dir"
                ;;
              *.rpm)
                (
                  cd "$extract_dir"
                  rpm2cpio "$GITHUB_WORKSPACE/$package_path" | cpio -id --quiet
                )
                ;;
              *)
                echo "::error::Unsupported package format for desktop entry validation: $package_path"
                exit 1
                ;;
            esac

            mapfile -t desktop_files < <(find "$extract_dir/usr/share/applications" -type f -name "*.desktop" 2>/dev/null)
            if [ ${#desktop_files[@]} -eq 0 ]; then
              echo "::error::$package_path did not include a desktop entry under /usr/share/applications."
              exit 1
            fi

            for desktop_file in "${desktop_files[@]}"; do
              local categories
              categories=$(grep "^Categories=" "$desktop_file" | cut -d= -f2- || true)
              if [ -z "$categories" ]; then
                echo "::error::$package_path has an empty Categories field in $(basename "$desktop_file")."
                exit 1
              fi
              if [[ "$categories" != *";" ]]; then
                echo "::error::$package_path has a Categories field that is not semicolon-terminated: $categories"
                exit 1
              fi
              case ";$categories" in
                *";Office;"*|*";Utility;"*)
                  ;;
                *)
                  echo "::error::$package_path has an unexpected launcher category: $categories"
                  exit 1
                  ;;
              esac
            done
          }

          for deb in src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb; do
            validate_desktop_categories "$deb" "$RUNNER_TEMP/tolaria-deb-desktop"
          done

          for rpm_package in src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm; do
            validate_desktop_categories "$rpm_package" "$RUNNER_TEMP/tolaria-rpm-desktop"
          done

      - name: Upload Linux bundles
        uses: actions/upload-artifact@v4
        with:
          name: linux-x86_64-bundles
          path: |
            src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
            src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig
            src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
            src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
            src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig
            src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz
            src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig
          if-no-files-found: error
          retention-days: 1

  build-windows:
    name: Build (windows-x86_64)
    runs-on: windows-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup pnpm
        uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa
        with:
          version: 10

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: "pnpm"

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8
        with:
          targets: x86_64-pc-windows-msvc

      - name: Cache Rust dependencies
        uses: actions/cache@v4
        with:
          path: |
            ~\.cargo\registry
            ~\.cargo\git
            src-tauri\target
          key: ${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-

      - name: Cache Tauri Windows tools
        uses: actions/cache@v4
        with:
          path: ~\AppData\Local\tauri
          key: ${{ runner.os }}-tauri-tools-nsis-3.11-nsis-tauri-utils-0.5.3

      - name: Prefetch Tauri NSIS toolchain
        shell: pwsh
        run: ./.github/scripts/prefetch-tauri-nsis.ps1

      - name: Install frontend dependencies
        run: pnpm install --frozen-lockfile

      - name: Clear cached Windows bundle artifacts
        shell: pwsh
        run: |
          Remove-Item -Recurse -Force -ErrorAction SilentlyContinue "src-tauri/target/x86_64-pc-windows-msvc/release/bundle"

      - name: Set version
        shell: pwsh
        run: |
          $version = "${{ inputs.version }}"
          $tauri = Get-Content "src-tauri/tauri.conf.json" | ConvertFrom-Json
          $tauri.version = $version
          $tauri | ConvertTo-Json -Depth 100 | Set-Content "src-tauri/tauri.conf.json"
          (Get-Content "src-tauri/Cargo.toml") -replace '^version = ".*"$', "version = `"$version`"" | Set-Content "src-tauri/Cargo.toml"

      - name: Validate Windows release env
        id: windows-signing
        shell: bash
        env:
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
          WINDOWS_CODE_SIGNING_CERTIFICATE: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE }}
          WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD }}
          WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
          WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
        run: |
          for name in TAURI_SIGNING_PRIVATE_KEY TAURI_KEY_PASSWORD; do
            if [ -z "${!name}" ]; then
              echo "::error::$name is required to build signed Windows updater artifacts."
              exit 1
            fi
          done

          has_certificate=false
          has_password=false
          if [ -n "$WINDOWS_CODE_SIGNING_CERTIFICATE" ] || [ -n "$WINDOWS_CERTIFICATE" ]; then
            has_certificate=true
          fi
          if [ -n "$WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD" ] || [ -n "$WINDOWS_CERTIFICATE_PASSWORD" ]; then
            has_password=true
          fi

          if [ "$has_certificate" != "$has_password" ]; then
            echo "::error::Windows Authenticode signing is partially configured. Set both certificate and password secrets, or remove both to build with Tauri updater signatures only."
            exit 1
          fi

          if [ "$has_certificate" = "true" ]; then
            echo "authenticode_available=true" >> "$GITHUB_OUTPUT"
          else
            echo "::warning::Windows Authenticode certificate secrets are not configured. Building Windows artifacts without Authenticode signatures; Tauri updater signatures are still required."
            echo "authenticode_available=false" >> "$GITHUB_OUTPUT"
          fi

      - name: Prepare Windows Authenticode signing
        if: ${{ steps.windows-signing.outputs.authenticode_available == 'true' }}
        shell: pwsh
        env:
          WINDOWS_CODE_SIGNING_CERTIFICATE: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE }}
          WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD }}
          WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT }}
          WINDOWS_CODE_SIGNING_TIMESTAMP_URL: ${{ secrets.WINDOWS_CODE_SIGNING_TIMESTAMP_URL }}
          WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
          WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
          WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
          WINDOWS_TIMESTAMP_URL: ${{ secrets.WINDOWS_TIMESTAMP_URL }}
        run: ./.github/scripts/configure-windows-authenticode.ps1

      - name: Build Tauri app (Windows bundles)
        shell: pwsh
        env:
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
          VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
          VITE_SENTRY_RELEASE: ${{ inputs.version }}
          SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
          VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
          VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
        run: |
          if ("${{ steps.windows-signing.outputs.authenticode_available }}" -eq "true") {
            pnpm tauri build --target x86_64-pc-windows-msvc --bundles nsis --config src-tauri/tauri.windows-signing.conf.json
          } else {
            pnpm tauri build --target x86_64-pc-windows-msvc --bundles nsis
          }

      - name: Validate Windows Authenticode signatures
        if: ${{ steps.windows-signing.outputs.authenticode_available == 'true' }}
        shell: pwsh
        run: |
          $expectedThumbprint = $env:WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT
          if ([string]::IsNullOrWhiteSpace($expectedThumbprint)) {
            throw "WINDOWS_CODE_SIGNING_CERTIFICATE_THUMBPRINT was not exported by the signing setup step."
          }
          $expectedThumbprint = ($expectedThumbprint -replace "\s", "").ToUpperInvariant()
          $paths = @()
          $paths += Get-ChildItem -Path "src-tauri/target/x86_64-pc-windows-msvc/release" -Filter "*.exe" -File -ErrorAction SilentlyContinue
          $paths += Get-ChildItem -Path "src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis" -Filter "*.exe" -File -ErrorAction SilentlyContinue
          $paths += Get-ChildItem -Path "src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi" -Filter "*.msi" -File -ErrorAction SilentlyContinue
          $paths = @($paths | Sort-Object FullName -Unique)
          if ($paths.Count -eq 0) {
            throw "No Windows executable or installer artifacts found to verify."
          }
          foreach ($path in $paths) {
            $signature = Get-AuthenticodeSignature -FilePath $path.FullName
            if ($signature.Status -ne "Valid") {
              throw "Invalid Authenticode signature for $($path.FullName): $($signature.Status)"
            }
            if ($null -eq $signature.SignerCertificate) {
              throw "Missing signer certificate for $($path.FullName)."
            }
            $actualThumbprint = ($signature.SignerCertificate.Thumbprint -replace "\s", "").ToUpperInvariant()
            if ($actualThumbprint -ne $expectedThumbprint) {
              throw "Unexpected signer thumbprint for $($path.FullName): $actualThumbprint"
            }
            Write-Host "Authenticode signature OK: $($path.FullName)"
          }

      - name: Validate Windows bundles
        shell: bash
        run: |
          shopt -s nullglob
          installers=(
            src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe
            src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
          )
          signatures=(
            src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe.sig
            src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.nsis.zip.sig
            src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig
            src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.zip.sig
          )
          if [ ${#installers[@]} -eq 0 ]; then
            echo "::error::Windows build produced no installable NSIS or MSI bundle."
            exit 1
          fi
          for installer in "${installers[@]}"; do
            if [[ "$(basename "$installer")" != *"${{ inputs.version }}"* ]]; then
              echo "::error::Windows build produced an installer for a different version: $(basename "$installer")"
              exit 1
            fi
          done
          if [ ${#signatures[@]} -eq 0 ]; then
            echo "::error::Windows build produced no updater signature (.sig) artifact."
            exit 1
          fi

      - name: Upload Windows bundles
        uses: actions/upload-artifact@v4
        with:
          name: windows-x86_64-bundles
          path: |
            src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
            src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe.sig
            src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip
            src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip.sig
            src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
            src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig
            src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip
            src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip.sig
          if-no-files-found: error
          retention-days: 1
