name: Release (Stable)

on:
  push:
    tags:
      - 'stable-v*'

env:
  # Bump this when Tauri/Rust target artifacts capture stale absolute paths.
  RUST_TARGET_CACHE_VERSION: v2026-04-14-tolaria

concurrency:
  group: release-stable-${{ github.ref }}
  cancel-in-progress: true

jobs:
  # ─────────────────────────────────────────────────────────────
  # Phase 1: Compute the stable version string once
  # ─────────────────────────────────────────────────────────────
  version:
    name: Compute stable version
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.ver.outputs.version }}
      display_version: ${{ steps.ver.outputs.display_version }}
      tag: ${{ steps.ver.outputs.tag }}
    steps:
      - id: ver
        shell: bash
        run: |
          python3 <<'PY' > version.env
          import os
          import re
          from datetime import date

          tag = os.environ["GITHUB_REF_NAME"]
          version = tag.removeprefix("stable-v")
          match = re.fullmatch(r"(\d{4})\.(\d{1,2})\.(\d{1,2})", version)
          if not match:
              raise SystemExit(f"Stable tags must use stable-vYYYY.M.D, got {tag}")

          date(*map(int, match.groups()))
          print(f"version={version}")
          print(f"display_version={version}")
          print(f"tag={tag}")
          PY

          cat version.env >> "$GITHUB_OUTPUT"
          DISPLAY_VERSION=$(grep '^display_version=' version.env | cut -d= -f2-)
          echo "### Stable version: \`$DISPLAY_VERSION\`" >> "$GITHUB_STEP_SUMMARY"

  # ─────────────────────────────────────────────────────────────
  # Phase 2: Build release bundles in parallel
  # ─────────────────────────────────────────────────────────────
  build:
    name: Build (${{ matrix.arch }})
    needs: version
    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@v4
        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@v2
        with:
          bun-version: latest

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@stable
        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="${{ needs.version.outputs.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: ${{ needs.version.outputs.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 ${{ matrix.target }}

      - name: Upload .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)
    needs: version
    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 \
            librsvg2-dev \
            curl \
            wget \
            patchelf \
            build-essential \
            file

      - name: Setup pnpm
        uses: pnpm/action-setup@v4
        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@stable
        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="${{ needs.version.outputs.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:
          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: ${{ needs.version.outputs.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,appimage

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

      - 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/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)
    needs: version
    runs-on: windows-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup pnpm
        uses: pnpm/action-setup@v4
        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@stable
        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: 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 = "${{ needs.version.outputs.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
        shell: bash
        env:
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_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

      - name: Build Tauri app (Windows bundles)
        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: ${{ needs.version.outputs.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-pc-windows-msvc --bundles nsis

      - 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")" != *"${{ needs.version.outputs.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

  # ─────────────────────────────────────────────────────────────
  # Phase 3: Publish GitHub Release
  # ─────────────────────────────────────────────────────────────
  release:
    name: GitHub Release (stable)
    needs: [version, build, build-linux, build-windows]
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Download all artifacts
        uses: actions/download-artifact@v4

      - name: Normalize macOS release artifact names
        run: |
          normalize_macos_artifacts() {
            local arch="$1"
            local normalized_updater="$2"
            local normalized_dmg="$3"
            local updater_dir="updater-${arch}"
            local updater_file
            updater_file=$(find "$updater_dir" -maxdepth 1 -name "*.app.tar.gz" -print -quit)
            if [ -z "$updater_file" ]; then
              echo "::error::Missing macOS updater artifact in ${updater_dir}" >&2
              return 1
            fi

            local sig_file="${updater_file}.sig"
            if [ ! -f "$sig_file" ]; then
              echo "::error::Missing macOS updater signature for ${updater_file}" >&2
              return 1
            fi

            local normalized_sig="${normalized_updater}.sig"
            if [ "$updater_file" != "$normalized_updater" ]; then
              mv "$updater_file" "$normalized_updater"
            fi
            if [ "$sig_file" != "$normalized_sig" ]; then
              mv "$sig_file" "$normalized_sig"
            fi

            local dmg_dir="dmg-${arch}"
            local dmg_file
            dmg_file=$(find "$dmg_dir" -maxdepth 1 -name "*.dmg" -print -quit)
            if [ -z "$dmg_file" ]; then
              echo "::error::Missing macOS DMG artifact in ${dmg_dir}" >&2
              return 1
            fi

            if [ "$dmg_file" != "$normalized_dmg" ]; then
              mv "$dmg_file" "$normalized_dmg"
            fi
          }

          normalize_macos_artifacts aarch64 \
            "updater-aarch64/Tolaria_${{ needs.version.outputs.version }}_macOS_Silicon.app.tar.gz" \
            "dmg-aarch64/Tolaria_${{ needs.version.outputs.version }}_macOS_Silicon.dmg"
          normalize_macos_artifacts x86_64 \
            "updater-x86_64/Tolaria_${{ needs.version.outputs.version }}_macOS_Intel.app.tar.gz" \
            "dmg-x86_64/Tolaria_${{ needs.version.outputs.version }}_macOS_Intel.dmg"

      - name: Generate release notes
        run: |
          PREV_TAG=$(git tag --list 'stable-v*' --sort=-version:refname | grep -vx "${{ needs.version.outputs.tag }}" | head -n 1 || echo "")
          if [ -z "$PREV_TAG" ]; then
            NOTES=$(git log --oneline --no-merges -20)
          else
            NOTES=$(git log --oneline --no-merges "${PREV_TAG}..${{ needs.version.outputs.tag }}")
          fi
          {
            echo "## What's Changed"
            echo ""
            echo "$NOTES" | while IFS= read -r line; do echo "- $line"; done
            echo ""
            echo "---"
            echo "**Stable release — manually promoted from \`main\`**"
            echo ""
            echo "**Includes macOS (Apple Silicon and Intel), Windows x64, and Linux x64 bundles**"
            echo ""
            echo "*Built from \`$(git rev-parse --short ${{ needs.version.outputs.tag }})\` on $(date -u +%Y-%m-%d)*"
          } > release_notes.md

      - name: Build stable-latest.json
        run: |
          VERSION="${{ needs.version.outputs.version }}"
          TAG="${{ needs.version.outputs.tag }}"
          REPO="${GITHUB_REPOSITORY}"
          REPO_NAME="${REPO#*/}"
          PAGES_URL="https://refactoringhq.github.io/${REPO_NAME}/"

          find_required() {
            local patterns=("$@")
            for pattern in "${patterns[@]}"; do
              set -- $pattern
              if [ -e "$1" ]; then
                printf '%s\n' "$1"
                return 0
              fi
            done
            echo "::error::Missing required artifact matching one of: ${patterns[*]}" >&2
            return 1
          }

          ARM_SIG_FILE=$(find_required "updater-aarch64/*.app.tar.gz.sig")
          ARM_UPDATER_FILE="${ARM_SIG_FILE%.sig}"
          ARM_SIG=$(cat "$ARM_SIG_FILE")
          ARM_TARBALL=$(basename "$ARM_UPDATER_FILE")
          ARM_DMG=$(basename "$(find_required "dmg-aarch64/*.dmg")")

          INTEL_SIG_FILE=$(find_required "updater-x86_64/*.app.tar.gz.sig")
          INTEL_UPDATER_FILE="${INTEL_SIG_FILE%.sig}"
          INTEL_SIG=$(cat "$INTEL_SIG_FILE")
          INTEL_TARBALL=$(basename "$INTEL_UPDATER_FILE")
          INTEL_DMG=$(basename "$(find_required "dmg-x86_64/*.dmg")")

          LINUX_SIG_FILE=$(find_required "linux-x86_64-bundles/*/*.AppImage.sig" "linux-x86_64-bundles/*/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*/*.deb.sig" "linux-x86_64-bundles/*.AppImage.sig" "linux-x86_64-bundles/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*.deb.sig")
          LINUX_UPDATER_FILE="${LINUX_SIG_FILE%.sig}"
          LINUX_SIG=$(cat "$LINUX_SIG_FILE")
          LINUX_UPDATER=$(basename "$LINUX_UPDATER_FILE")
          LINUX_DOWNLOAD=$(basename "$(find_required "linux-x86_64-bundles/*/*.AppImage" "linux-x86_64-bundles/*/*.deb" "linux-x86_64-bundles/*/*.AppImage.tar.gz" "linux-x86_64-bundles/*.AppImage" "linux-x86_64-bundles/*.deb" "linux-x86_64-bundles/*.AppImage.tar.gz")")

          WINDOWS_SIG_FILE=$(find_required "windows-x86_64-bundles/*/*-setup.exe.sig" "windows-x86_64-bundles/*/*.msi.sig" "windows-x86_64-bundles/*/*.nsis.zip.sig" "windows-x86_64-bundles/*/*.msi.zip.sig" "windows-x86_64-bundles/*-setup.exe.sig" "windows-x86_64-bundles/*.msi.sig" "windows-x86_64-bundles/*.nsis.zip.sig" "windows-x86_64-bundles/*.msi.zip.sig")
          WINDOWS_UPDATER_FILE="${WINDOWS_SIG_FILE%.sig}"
          WINDOWS_SIG=$(cat "$WINDOWS_SIG_FILE")
          WINDOWS_UPDATER=$(basename "$WINDOWS_UPDATER_FILE")
          WINDOWS_DOWNLOAD=$(basename "$(find_required "windows-x86_64-bundles/*/*-setup.exe" "windows-x86_64-bundles/*/*.msi" "windows-x86_64-bundles/*/*.nsis.zip" "windows-x86_64-bundles/*/*.msi.zip" "windows-x86_64-bundles/*-setup.exe" "windows-x86_64-bundles/*.msi" "windows-x86_64-bundles/*.nsis.zip" "windows-x86_64-bundles/*.msi.zip")")

          cat > stable-latest.json << EOF
          {
            "version": "${VERSION}",
            "notes": "Stable release. See ${PAGES_URL} for full release notes.",
            "pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
            "platforms": {
              "darwin-aarch64": {
                "signature": "${ARM_SIG}",
                "url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_TARBALL}",
                "dmg_url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_DMG}"
              },
              "darwin-x86_64": {
                "signature": "${INTEL_SIG}",
                "url": "https://github.com/${REPO}/releases/download/${TAG}/${INTEL_TARBALL}",
                "dmg_url": "https://github.com/${REPO}/releases/download/${TAG}/${INTEL_DMG}"
              },
              "linux-x86_64": {
                "signature": "${LINUX_SIG}",
                "url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_UPDATER}",
                "download_url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_DOWNLOAD}"
              },
              "windows-x86_64": {
                "signature": "${WINDOWS_SIG}",
                "url": "https://github.com/${REPO}/releases/download/${TAG}/${WINDOWS_UPDATER}",
                "download_url": "https://github.com/${REPO}/releases/download/${TAG}/${WINDOWS_DOWNLOAD}"
              }
            }
          }
          EOF
          echo "stable-latest.json:"; cat stable-latest.json

      - name: Publish GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ needs.version.outputs.tag }}
          name: Tolaria ${{ needs.version.outputs.display_version }}
          body_path: release_notes.md
          draft: false
          prerelease: false
          files: |
            dmg-aarch64/*.dmg
            updater-aarch64/*.app.tar.gz
            updater-aarch64/*.app.tar.gz.sig
            dmg-x86_64/*.dmg
            updater-x86_64/*.app.tar.gz
            updater-x86_64/*.app.tar.gz.sig
            linux-x86_64-bundles/*.deb
            linux-x86_64-bundles/*.deb.sig
            linux-x86_64-bundles/*.AppImage
            linux-x86_64-bundles/*.AppImage.sig
            linux-x86_64-bundles/*.AppImage.tar.gz
            linux-x86_64-bundles/*.AppImage.tar.gz.sig
            linux-x86_64-bundles/*/*.deb
            linux-x86_64-bundles/*/*.deb.sig
            linux-x86_64-bundles/*/*.AppImage
            linux-x86_64-bundles/*/*.AppImage.sig
            linux-x86_64-bundles/*/*.AppImage.tar.gz
            linux-x86_64-bundles/*/*.AppImage.tar.gz.sig
            windows-x86_64-bundles/*.exe
            windows-x86_64-bundles/*.exe.sig
            windows-x86_64-bundles/*.msi
            windows-x86_64-bundles/*.msi.sig
            windows-x86_64-bundles/*.zip
            windows-x86_64-bundles/*.zip.sig
            windows-x86_64-bundles/*/*.exe
            windows-x86_64-bundles/*/*.exe.sig
            windows-x86_64-bundles/*/*.msi
            windows-x86_64-bundles/*/*.msi.sig
            windows-x86_64-bundles/*/*.zip
            windows-x86_64-bundles/*/*.zip.sig
            stable-latest.json

  # ─────────────────────────────────────────────────────────────
  # Phase 4: Update GitHub Pages with docs, release history, and download assets
  # ─────────────────────────────────────────────────────────────
  pages:
    name: Update docs and release pages
    needs: [version, release]
    runs-on: ubuntu-latest
    permissions:
      contents: write
    concurrency:
      group: github-pages
      cancel-in-progress: false
    steps:
      - uses: actions/checkout@v4

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

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

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest

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

      - name: Build docs and release pages
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          VITEPRESS_BASE="/${GITHUB_REPOSITORY#*/}/" pnpm docs:build
          mkdir -p _site/alpha _site/stable
          cp -R site/.vitepress/dist/. _site/
          gh api -H "Accept: application/vnd.github.html+json" repos/${{ github.repository }}/releases --paginate > _site/releases.json
          PAGES_URL="https://refactoringhq.github.io/${GITHUB_REPOSITORY#*/}"

          curl -fsSL "${PAGES_URL}/alpha/latest.json" -o _site/alpha/latest.json || echo '{}' > _site/alpha/latest.json
          gh release download --repo ${{ github.repository }} "${{ needs.version.outputs.tag }}" --pattern "stable-latest.json" --output _site/stable/latest.json || echo '{}' > _site/stable/latest.json
          bun scripts/build-release-download-page.ts --latest-json _site/stable/latest.json --releases-json _site/releases.json --output-file _site/stable/download/index.html
          bun scripts/build-release-history-page.ts --releases-json _site/releases.json --output-file _site/releases/index.html
          mkdir -p _site/download
          cp _site/stable/download/index.html _site/download/index.html

          cp _site/alpha/latest.json _site/latest.json
          cp _site/alpha/latest.json _site/latest-canary.json

      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v4
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./_site
          commit_message: "Update release history for ${{ needs.version.outputs.tag }}"
