name: Manual Releases

on:
  workflow_dispatch:
    inputs:
      bump_mode:
        description: Release bump policy
        required: true
        default: auto
        type: choice
        options:
          - auto
          - major (x..)
          - minor (.x.)
          - patch (..x)
      version_override:
        description: Optional explicit release version (for example 0.1.011)
        required: false
        default: ""
        type: string
      macos_support_tier:
        description: macOS release support tier
        required: true
        default: experimental
        type: choice
        options:
          - experimental
          - first-class

concurrency:
  group: openq4-manual-releases
  cancel-in-progress: false

permissions:
  contents: read

jobs:
  metadata:
    name: Release Metadata
    runs-on: ubuntu-latest
    outputs:
      current_version: ${{ steps.version.outputs.current_version }}
      latest_release_tag: ${{ steps.version.outputs.latest_release_tag }}
      latest_release_version: ${{ steps.version.outputs.latest_release_version }}
      version: ${{ steps.version.outputs.version }}
      version_tag: ${{ steps.version.outputs.version_tag }}
      release_tag: ${{ steps.version.outputs.release_tag }}
      release_name: ${{ steps.version.outputs.release_name }}
      release_scale: ${{ steps.version.outputs.release_scale }}
      release_reason: ${{ steps.version.outputs.release_reason }}
      bump_mode: ${{ steps.version.outputs.bump_mode }}
      release_matrix: ${{ steps.release_matrix.outputs.release_matrix }}
      macos_signed_release_enabled: ${{ steps.release_matrix.outputs.macos_signed_release_enabled }}
      macos_support_tier: ${{ steps.release_matrix.outputs.macos_support_tier }}
      macos_release_reason: ${{ steps.release_matrix.outputs.macos_release_reason }}

    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Fetch tags
        shell: bash
        run: |
          set -euo pipefail
          git fetch --force --tags origin

      - name: Setup Python
        uses: actions/setup-python@v6
        with:
          python-version: "3.x"

      - name: Compute release version
        id: version
        env:
          INPUT_BUMP_MODE: ${{ inputs.bump_mode }}
          INPUT_VERSION_OVERRIDE: ${{ inputs.version_override }}
        shell: bash
        run: |
          set -euo pipefail

          args=(
            --source-root .
            --bump-mode "${INPUT_BUMP_MODE}"
          )

          if [ -n "${INPUT_VERSION_OVERRIDE}" ]; then
            args+=(--version-override "${INPUT_VERSION_OVERRIDE}")
          fi

          python tools/build/openq4_release_version.py "${args[@]}" >> "$GITHUB_OUTPUT"

      - name: Validate Discord webhook secret
        env:
          DISCORD_RELEASE_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
        shell: bash
        run: |
          set -euo pipefail
          if [ -z "${DISCORD_RELEASE_WEBHOOK}" ]; then
            echo "DISCORD_RELEASE_WEBHOOK must be configured as a repository secret before publishing an openQ4 release."
            exit 1
          fi

      - name: Build release matrix
        id: release_matrix
        env:
          MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64: ${{ secrets.MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64 }}
          MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD }}
          MACOS_DEVELOPER_ID_APPLICATION_IDENTITY: ${{ secrets.MACOS_DEVELOPER_ID_APPLICATION_IDENTITY }}
          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 }}
          OPENQ4_MACOS_SUPPORT_TIER: ${{ inputs.macos_support_tier }}
        shell: bash
        run: |
          set -euo pipefail

          python - <<'PY' >> "$GITHUB_OUTPUT"
          import json
          import os
          import pathlib

          matrix = [
              {
                  "os": "windows-2025",
                  "platform": "windows",
                  "label": "Windows x64",
                  "binary_arch": "x64",
                  "platform_backend": "sdl3",
                  "macos_graphics_bridge": "opengl",
                  "macos_openal_provider": "apple_framework",
                  "package_suffix": "",
                  "archive_format": "zip",
                  "archive_ext": ".zip",
              },
              {
                  "os": "windows-11-arm",
                  "platform": "windows",
                  "label": "Windows ARM64",
                  "binary_arch": "arm64",
                  "platform_backend": "sdl3",
                  "macos_graphics_bridge": "opengl",
                  "macos_openal_provider": "apple_framework",
                  "package_suffix": "",
                  "archive_format": "zip",
                  "archive_ext": ".zip",
              },
              {
                  "os": "ubuntu-24.04",
                  "platform": "linux",
                  "label": "Linux x64",
                  "binary_arch": "x64",
                  "platform_backend": "sdl3",
                  "macos_graphics_bridge": "opengl",
                  "macos_openal_provider": "apple_framework",
                  "package_suffix": "",
                  "archive_format": "tar.xz",
                  "archive_ext": ".tar.xz",
              },
              {
                  "os": "ubuntu-24.04-arm",
                  "platform": "linux",
                  "label": "Linux ARM64",
                  "binary_arch": "arm64",
                  "platform_backend": "sdl3",
                  "macos_graphics_bridge": "opengl",
                  "macos_openal_provider": "apple_framework",
                  "package_suffix": "",
                  "archive_format": "tar.xz",
                  "archive_ext": ".tar.xz",
              },
          ]

          required_macos_secrets = (
              "MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64",
              "MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD",
              "MACOS_DEVELOPER_ID_APPLICATION_IDENTITY",
              "MACOS_NOTARY_APPLE_ID",
              "MACOS_NOTARY_TEAM_ID",
              "MACOS_NOTARY_APP_PASSWORD",
          )
          missing_macos_secrets = [
              secret_name
              for secret_name in required_macos_secrets
              if not os.environ.get(secret_name)
          ]
          macos_support_tier = os.environ.get("OPENQ4_MACOS_SUPPORT_TIER", "experimental").strip() or "experimental"
          if macos_support_tier not in {"experimental", "first-class"}:
              raise SystemExit(f"Unsupported macOS support tier: {macos_support_tier}")
          if macos_support_tier == "first-class" and missing_macos_secrets:
              raise SystemExit(
                  "First-class macOS releases require signed/notarized DMGs; "
                  "missing Apple signing/notary repository secrets: "
                  + ", ".join(missing_macos_secrets)
              )
          if macos_support_tier == "first-class":
              evidence_path = pathlib.Path("docs/dev/macos-signoff-evidence.md")
              evidence = evidence_path.read_text(encoding="utf-8")
              incomplete_evidence_markers = [
                  marker
                  for marker in (
                      "Evidence status: pending",
                      "- [ ] No completed macOS first-class support evidence is recorded yet.",
                  )
                  if marker in evidence
              ]
              if incomplete_evidence_markers:
                  raise SystemExit(
                      "First-class macOS releases require completed signoff evidence in "
                      f"{evidence_path}, but it still contains: "
                      + "; ".join(repr(marker) for marker in incomplete_evidence_markers)
                      + ". Validate the signoff archive and record it with "
                      "tools/macos/record_signoff_evidence.py --update-index, or rerun "
                      "with macos_support_tier=experimental."
                  )
          macos_signed_release_enabled = not missing_macos_secrets
          macos_release_mode = "signed" if macos_signed_release_enabled else "unsigned"
          macos_archive_format = "dmg" if macos_signed_release_enabled else "tar.gz"
          macos_archive_ext = ".dmg" if macos_signed_release_enabled else ".tar.gz"
          macos_unsigned_suffix = "" if macos_signed_release_enabled else "-unsigned"
          macos_unsigned_label = "" if macos_signed_release_enabled else " (unsigned)"

          matrix.extend(
              [
                  {
                      "os": "macos-15",
                      "platform": "macos",
                      "label": f"macOS ARM64 OpenGL{macos_unsigned_label}",
                      "binary_arch": "arm64",
                      "platform_backend": "sdl3",
                      "macos_graphics_bridge": "opengl",
                      "macos_openal_provider": "apple_framework",
                      "macos_release_mode": macos_release_mode,
                      "package_suffix": f"-opengl{macos_unsigned_suffix}",
                      "archive_format": macos_archive_format,
                      "archive_ext": macos_archive_ext,
                  },
                  {
                      "os": "macos-15",
                      "platform": "macos",
                      "label": f"macOS ARM64 Metal{macos_unsigned_label}",
                      "binary_arch": "arm64",
                      "platform_backend": "sdl3",
                      "macos_graphics_bridge": "metal",
                      "macos_openal_provider": "apple_framework",
                      "macos_release_mode": macos_release_mode,
                      "package_suffix": f"-metal{macos_unsigned_suffix}",
                      "archive_format": macos_archive_format,
                      "archive_ext": macos_archive_ext,
                  },
              ]
          )

          if macos_signed_release_enabled:
              macos_release_reason = (
                  f"macOS {macos_support_tier} signed/notarized DMG release artifacts enabled."
              )
          else:
              macos_release_reason = (
                  "Experimental macOS unsigned/unnotarized tar.gz release artifacts enabled as fallback output "
                  "because Apple signing/notary repository secrets are not configured: "
                  + ", ".join(missing_macos_secrets)
              )

          print("macos_signed_release_enabled=" + ("true" if macos_signed_release_enabled else "false"))
          print(f"macos_support_tier={macos_support_tier}")
          print(f"macos_release_reason={macos_release_reason}")
          print("release_matrix=" + json.dumps({"include": matrix}, separators=(",", ":")))
          PY

  builds:
    name: ${{ matrix.label }}
    needs: metadata
    runs-on: ${{ matrix.os }}
    timeout-minutes: 180
    env:
      OPENQ4_GAMELIBS_REPO: ${{ github.workspace }}/openQ4-game
    strategy:
      fail-fast: false
      matrix: ${{ fromJSON(needs.metadata.outputs.release_matrix) }}

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Fetch openQ4-game
        shell: bash
        run: |
          set -euo pipefail
          git clone --depth 1 https://github.com/themuffinator/openQ4-game.git openQ4-game

      - name: Setup Python
        uses: actions/setup-python@v6
        with:
          python-version: "3.x"

      - name: Install Meson and Ninja
        run: |
          python -m pip install --upgrade pip
          python -m pip install meson ninja markdown

      - name: Show release metadata
        shell: bash
        run: |
          set -euo pipefail
          echo "Configured repo version: ${{ needs.metadata.outputs.current_version }}"
          echo "Latest release tag: ${{ needs.metadata.outputs.latest_release_tag }}"
          echo "Release version: ${{ needs.metadata.outputs.version }}"
          echo "Release tag: ${{ needs.metadata.outputs.release_tag }}"
          echo "Release scale: ${{ needs.metadata.outputs.release_scale }}"
          echo "Release reason: ${{ needs.metadata.outputs.release_reason }}"

      - name: Install Linux native dependencies
        if: matrix.platform == 'linux'
        shell: bash
        run: |
          set -euo pipefail
          for attempt in 1 2 3; do
            if sudo apt-get update && sudo apt-get install -y \
              binutils \
              libasound2-dev \
              libdbus-1-dev \
              libdecor-0-dev \
              libdrm-dev \
              libegl1-mesa-dev \
              libfribidi-dev \
              libgbm-dev \
              libgl1-mesa-dev \
              libibus-1.0-dev \
              libjack-dev \
              libopenal-dev \
              libpipewire-0.3-dev \
              libpulse-dev \
              libsndio-dev \
              libthai-dev \
              libudev-dev \
              libwayland-dev \
              libx11-dev \
              libxcursor-dev \
              libxext-dev \
              libxfixes-dev \
              libxi-dev \
              libxkbcommon-dev \
              libxrandr-dev \
              libxss-dev \
              libxtst-dev \
              libxxf86dga-dev \
              libxxf86vm-dev; then
              exit 0
            fi

            if [ "${attempt}" -eq 3 ]; then
              echo "Failed to install Linux native dependencies after ${attempt} attempts."
              exit 1
            fi

            echo "Linux dependency installation attempt ${attempt} failed; retrying in 10 seconds..."
            sleep 10
          done

      - name: Install macOS native dependencies
        if: matrix.platform == 'macos'
        shell: bash
        run: |
          set -euo pipefail
          echo "Using bundled Meson fallbacks for macOS third-party libraries."

      - name: Prepare OpenAL Soft (Windows)
        if: matrix.platform == 'windows'
        shell: pwsh
        run: |
          $ErrorActionPreference = "Stop"
          $outputRoot = Join-Path $env:GITHUB_WORKSPACE ".tmp/openal-soft-${{ matrix.binary_arch }}"
          $env:OPENQ4_VS_TARGET_ARCH = "${{ matrix.binary_arch }}"
          powershell -ExecutionPolicy Bypass -File tools/build/prepare_windows_openal.ps1 -Architecture "${{ matrix.binary_arch }}" -OutputRoot $outputRoot
          "OPENQ4_OPENAL_ROOT=$outputRoot" >> $env:GITHUB_ENV

      - name: Build and install (Windows)
        if: matrix.platform == 'windows'
        shell: pwsh
        run: |
          $ErrorActionPreference = "Stop"
          $env:OPENQ4_SKIP_GAMELIBS_SYNC = "1"
          $env:OPENQ4_VS_TARGET_ARCH = "${{ matrix.binary_arch }}"

          $setupArgs = @(
            "setup",
            "--wipe",
            "builddir",
            ".",
            "--backend",
            "ninja",
            "--buildtype=debugoptimized",
            "-Db_ndebug=true",
            "--wrap-mode=forcefallback",
            "-Dplatform_backend=${{ matrix.platform_backend }}",
            "-Dmacos_graphics_bridge=${{ matrix.macos_graphics_bridge }}",
            "-Dversion_track=stable",
            "-Dversion_base_override=${{ needs.metadata.outputs.version }}"
          )
          if (-not [string]::IsNullOrWhiteSpace($env:OPENQ4_OPENAL_ROOT)) {
            $setupArgs += "-Dopenal_root_override=$env:OPENQ4_OPENAL_ROOT"
          }

          powershell -ExecutionPolicy Bypass -File tools/build/meson_setup.ps1 @setupArgs
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

          powershell -ExecutionPolicy Bypass -File tools/build/meson_setup.ps1 compile -C builddir
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

          powershell -ExecutionPolicy Bypass -File tools/build/meson_setup.ps1 install -C builddir --no-rebuild --skip-subprojects
          if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

      - name: Build and install (Linux/macOS)
        if: matrix.platform != 'windows'
        shell: bash
        run: |
          set -euo pipefail
          export OPENQ4_SKIP_GAMELIBS_SYNC=1

          if [ "${{ matrix.platform }}" = "macos" ]; then
            # Keep Meson subprojects (SDL3, GLEW, stb_vorbis) on the same macOS 11.0
            # floor as the main project's -mmacosx-version-min=11.0 flags.
            export MACOSX_DEPLOYMENT_TARGET=11.0
          fi

          bash tools/build/meson_setup.sh setup --wipe builddir . --backend ninja --buildtype=debugoptimized -Db_ndebug=true --wrap-mode=forcefallback -Dplatform_backend=${{ matrix.platform_backend }} -Dmacos_graphics_bridge=${{ matrix.macos_graphics_bridge }} -Dmacos_openal_provider=${{ matrix.macos_openal_provider }} -Dversion_track=stable -Dversion_base_override=${{ needs.metadata.outputs.version }}
          bash tools/build/meson_setup.sh compile -C builddir
          bash tools/build/meson_setup.sh install -C builddir --no-rebuild --skip-subprojects

      - name: Validate staged payload
        shell: bash
        run: |
          set -euo pipefail

          if [ ! -d ".install/baseoq4" ]; then
            echo "Staged baseoq4 payload directory is missing."
            exit 1
          fi

          has_payload_files="$(find ".install/baseoq4" -type f -print -quit 2>/dev/null || true)"
          if [ -z "${has_payload_files}" ]; then
            echo "Staged baseoq4 payload directory has no files."
            exit 1
          fi

          if [ "${{ matrix.platform }}" = "linux" ]; then
            if [ ! -f ".install/share/applications/openq4.desktop" ]; then
              echo "Missing Linux desktop entry in staged payload."
              exit 1
            fi
            if [ ! -f ".install/share/icons/hicolor/256x256/apps/openq4.png" ]; then
              echo "Missing Linux 256x256 icon in staged payload."
              exit 1
            fi
          fi

          if [ "${{ matrix.platform }}" = "macos" ]; then
            required_macos_payload=(
              ".install/openQ4-client_${{ matrix.binary_arch }}"
              ".install/openQ4-ded_${{ matrix.binary_arch }}"
              ".install/baseoq4/game-sp_${{ matrix.binary_arch }}.dylib"
              ".install/baseoq4/game-mp_${{ matrix.binary_arch }}.dylib"
              ".install/collect_macos_support_info.sh"
              ".install/openQ4.icns"
              ".install/assets/splash/quake4_rt_bitmap_4001.bmp"
            )

            for payload in "${required_macos_payload[@]}"; do
              if [ ! -f "${payload}" ]; then
                echo "Missing macOS staged payload file: ${payload}"
                exit 1
              fi
            done

            for binary in ".install/openQ4-client_${{ matrix.binary_arch }}" ".install/openQ4-ded_${{ matrix.binary_arch }}" ".install/collect_macos_support_info.sh"; do
              if [ ! -x "${binary}" ]; then
                echo "Missing executable bit on macOS staged binary: ${binary}"
                exit 1
              fi
            done

            shopt -s nullglob
            bad_macos_modules=(
              .install/baseoq4/game-sp_*.dll
              .install/baseoq4/game-sp_*.so
              .install/baseoq4/game-mp_*.dll
              .install/baseoq4/game-mp_*.so
            )
            shopt -u nullglob
            if [ "${#bad_macos_modules[@]}" -ne 0 ]; then
              echo "macOS staged payload contains non-dylib game modules:"
              printf '  - %s\n' "${bad_macos_modules[@]}"
              exit 1
            fi
            expected_macos_root_binaries=(
              ".install/openQ4-client_${{ matrix.binary_arch }}"
              ".install/openQ4-ded_${{ matrix.binary_arch }}"
            )
            for staged_binary in .install/openQ4-client_* .install/openQ4-ded_*; do
              [ -e "${staged_binary}" ] || continue
              matched=0
              for expected_binary in "${expected_macos_root_binaries[@]}"; do
                if [ "${staged_binary}" = "${expected_binary}" ]; then
                  matched=1
                  break
                fi
              done
              if [ "${matched}" -eq 0 ]; then
                echo "macOS staged payload contains stale or mismatched root engine binary: ${staged_binary}"
                exit 1
              fi
            done
            expected_macos_game_modules=(
              ".install/baseoq4/game-sp_${{ matrix.binary_arch }}.dylib"
              ".install/baseoq4/game-mp_${{ matrix.binary_arch }}.dylib"
            )
            for staged_module in .install/baseoq4/game-sp_*.dylib .install/baseoq4/game-mp_*.dylib; do
              [ -e "${staged_module}" ] || continue
              matched=0
              for expected_module in "${expected_macos_game_modules[@]}"; do
                if [ "${staged_module}" = "${expected_module}" ]; then
                  matched=1
                  break
                fi
              done
              if [ "${matched}" -eq 0 ]; then
                echo "macOS staged payload contains stale or mismatched game module: ${staged_module}"
                exit 1
              fi
            done
          fi

      - name: Prepare Linux debug symbols
        if: matrix.platform == 'linux'
        id: linux_symbols
        shell: bash
        run: |
          set -euo pipefail

          symbols_stem="openq4-${{ needs.metadata.outputs.version_tag }}-linux-${{ matrix.binary_arch }}-debugsymbols"
          symbols_dir="${RUNNER_TEMP}/${symbols_stem}"
          symbols_archive="${RUNNER_TEMP}/${symbols_stem}.tar.xz"
          rm -rf "${symbols_dir}" "${symbols_archive}"
          mkdir -p "${symbols_dir}/baseoq4"

          binaries=(
            ".install/openQ4-client_${{ matrix.binary_arch }}"
            ".install/openQ4-ded_${{ matrix.binary_arch }}"
            ".install/baseoq4/game-sp_${{ matrix.binary_arch }}.so"
            ".install/baseoq4/game-mp_${{ matrix.binary_arch }}.so"
          )

          for binary in "${binaries[@]}"; do
            if [ ! -f "${binary}" ]; then
              echo "Required Linux binary for debug-symbol split is missing: ${binary}"
              exit 1
            fi

            relative_path="${binary#.install/}"
            debug_file="${symbols_dir}/${relative_path}.debug"
            mkdir -p "$(dirname "${debug_file}")"

            binary_abs="$(pwd -P)/${binary#./}"
            debug_basename="$(basename "${debug_file}")"

            objcopy --only-keep-debug "${binary_abs}" "${debug_file}"
            objcopy --strip-debug "${binary_abs}"
            (
              cd "$(dirname "${debug_file}")"
              objcopy --add-gnu-debuglink="${debug_basename}" "${binary_abs}"
            )
          done

          tar -C "${symbols_dir}" -cJf "${symbols_archive}" .
          echo "debug_symbols_path=${symbols_archive}" >> "$GITHUB_OUTPUT"

      - name: Import macOS Developer ID certificate
        if: matrix.platform == 'macos' && matrix.macos_release_mode == 'signed'
        id: macos_signing
        env:
          MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64: ${{ secrets.MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64 }}
          MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD }}
          MACOS_DEVELOPER_ID_APPLICATION_IDENTITY: ${{ secrets.MACOS_DEVELOPER_ID_APPLICATION_IDENTITY }}
          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 }}
        shell: bash
        run: |
          set -euo pipefail

          required_vars=(
            MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64
            MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD
            MACOS_DEVELOPER_ID_APPLICATION_IDENTITY
            MACOS_NOTARY_APPLE_ID
            MACOS_NOTARY_TEAM_ID
            MACOS_NOTARY_APP_PASSWORD
          )
          for var_name in "${required_vars[@]}"; do
            if [ -z "${!var_name}" ]; then
              echo "${var_name} must be configured before publishing macOS release packages."
              exit 1
            fi
          done

          keychain_path="${RUNNER_TEMP}/openq4-release-signing.keychain-db"
          certificate_path="${RUNNER_TEMP}/developer-id-application.p12"
          keychain_password="$(openssl rand -hex 24)"
          notary_profile="openq4-release-notary"

          printf '%s' "${MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64}" | base64 -D > "${certificate_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 "${certificate_path}" \
            -k "${keychain_path}" \
            -P "${MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD}" \
            -T /usr/bin/codesign
          # Keychain paths can contain spaces; keep them as array elements
          # instead of relying on word splitting. macOS runners ship bash 3.2,
          # so use a while-read loop instead of mapfile.
          existing_keychains=()
          while IFS= read -r keychain_line; do
            if [ -n "${keychain_line}" ]; then
              existing_keychains+=("${keychain_line}")
            fi
          done < <(security list-keychains -d user | sed -e 's/^[[:space:]]*"//' -e 's/"[[:space:]]*$//')
          security list-keychains -d user -s "${keychain_path}" "${existing_keychains[@]}"
          security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${keychain_password}" "${keychain_path}"

          xcrun notarytool store-credentials "${notary_profile}" \
            --apple-id "${MACOS_NOTARY_APPLE_ID}" \
            --team-id "${MACOS_NOTARY_TEAM_ID}" \
            --password "${MACOS_NOTARY_APP_PASSWORD}" \
            --keychain "${keychain_path}"

          security find-identity -v -p codesigning "${keychain_path}" | grep -F "${MACOS_DEVELOPER_ID_APPLICATION_IDENTITY}" >/dev/null
          echo "identity=${MACOS_DEVELOPER_ID_APPLICATION_IDENTITY}" >> "$GITHUB_OUTPUT"
          echo "keychain_path=${keychain_path}" >> "$GITHUB_OUTPUT"
          echo "notary_profile=${notary_profile}" >> "$GITHUB_OUTPUT"

      - name: Prepare package
        id: package
        shell: bash
        run: |
          set -euo pipefail

          package_args=(
            --platform "${{ matrix.platform }}"
            --arch "${{ matrix.binary_arch }}"
            --version "${{ needs.metadata.outputs.version }}"
            --version-tag "${{ needs.metadata.outputs.version_tag }}"
            --archive-format "${{ matrix.archive_format }}"
            --package-suffix="${{ matrix.package_suffix }}"
            --source-root "${GITHUB_WORKSPACE}"
            --output-dir "${RUNNER_TEMP}"
          )

          if [ "${{ matrix.platform }}" = "macos" ]; then
            if [ "${{ matrix.macos_release_mode }}" = "signed" ]; then
              package_args+=(
                --macos-signing-mode developer-id
                --macos-code-sign-identity "${{ steps.macos_signing.outputs.identity }}"
                --macos-notarize
                --macos-notary-keychain-profile "${{ steps.macos_signing.outputs.notary_profile }}"
                --macos-notary-keychain "${{ steps.macos_signing.outputs.keychain_path }}"
              )
            else
              package_args+=(--macos-signing-mode ad-hoc)
            fi
          fi

          python tools/build/package_release.py "${package_args[@]}"

          archive_path="${RUNNER_TEMP}/openq4-${{ needs.metadata.outputs.version_tag }}-${{ matrix.platform }}-${{ matrix.binary_arch }}${{ matrix.package_suffix }}${{ matrix.archive_ext }}"
          package_dir="${RUNNER_TEMP}/openq4-${{ needs.metadata.outputs.version_tag }}-${{ matrix.platform }}-${{ matrix.binary_arch }}${{ matrix.package_suffix }}"
          if [ ! -f "${archive_path}" ]; then
            echo "Expected package archive not found: ${archive_path}"
            exit 1
          fi
          case "$(basename "${archive_path}")" in
            *symbols*|*debugsymbols*)
              echo "Runtime package archive name must not look like a symbol/debug artifact: ${archive_path}"
              exit 1
              ;;
          esac
          if [ ! -d "${package_dir}" ]; then
            echo "Expected package directory not found: ${package_dir}"
            exit 1
          fi
          if [ "${{ matrix.platform }}" = "macos" ]; then
            symbols_archive_path="${RUNNER_TEMP}/openq4-${{ needs.metadata.outputs.version_tag }}-macos-${{ matrix.binary_arch }}${{ matrix.package_suffix }}-symbols.tar.xz"
            if [ ! -f "${symbols_archive_path}" ]; then
              echo "Expected macOS dSYM symbol archive not found: ${symbols_archive_path}"
              exit 1
            fi
            echo "symbols_archive_path=${symbols_archive_path}" >> "$GITHUB_OUTPUT"
          fi
          echo "archive_path=${archive_path}" >> "$GITHUB_OUTPUT"
          echo "package_dir=${package_dir}" >> "$GITHUB_OUTPUT"

      - name: Install Inno Setup
        if: matrix.platform == 'windows'
        shell: pwsh
        run: |
          $ErrorActionPreference = "Stop"
          if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
            throw "Chocolatey is not available on this Windows runner, so Inno Setup could not be installed."
          }
          choco install innosetup --no-progress -y

      - name: Build Windows installer
        if: matrix.platform == 'windows'
        id: windows_installer
        shell: pwsh
        run: |
          $ErrorActionPreference = "Stop"

          python tools/build/build_windows_installer.py `
            --package-dir "${{ steps.package.outputs.package_dir }}" `
            --version "${{ needs.metadata.outputs.version }}" `
            --version-tag "${{ needs.metadata.outputs.version_tag }}" `
            --arch "${{ matrix.binary_arch }}" `
            --source-root "${env:GITHUB_WORKSPACE}" `
            --output-dir "${env:RUNNER_TEMP}"

          $installerPath = Join-Path $env:RUNNER_TEMP "openq4-${{ needs.metadata.outputs.version_tag }}-windows-${{ matrix.binary_arch }}-setup.exe"
          if (-not (Test-Path -LiteralPath $installerPath)) {
            throw "Expected Windows installer not found: $installerPath"
          }

          "installer_path=$installerPath" >> $env:GITHUB_OUTPUT

      - name: Validate macOS app bundle
        if: matrix.platform == 'macos'
        shell: bash
        run: |
          set -euo pipefail
          app_root="${{ steps.package.outputs.package_dir }}/openQ4.app"
          app_plist="${app_root}/Contents/Info.plist"
          app_pkginfo="${app_root}/Contents/PkgInfo"
          app_exec="${app_root}/Contents/MacOS/openQ4"
          app_icon="${app_root}/Contents/Resources/openQ4.icns"
          app_version="${app_root}/Contents/Resources/VERSION.txt"
          symbol_manifest="${{ steps.package.outputs.package_dir }}/SYMBOLS.txt"
          support_collector="${{ steps.package.outputs.package_dir }}/collect_macos_support_info.sh"
          client_binary="${{ steps.package.outputs.package_dir }}/openQ4-client_${{ matrix.binary_arch }}"
          dedicated_binary="${{ steps.package.outputs.package_dir }}/openQ4-ded_${{ matrix.binary_arch }}"
          game_dir="${{ steps.package.outputs.package_dir }}/baseoq4"
          sp_module="${game_dir}/game-sp_${{ matrix.binary_arch }}.dylib"
          mp_module="${game_dir}/game-mp_${{ matrix.binary_arch }}.dylib"

          if [ ! -f "${app_plist}" ]; then
            echo "Missing macOS app Info.plist: ${app_plist}"
            exit 1
          fi
          if [ ! -f "${app_pkginfo}" ] || [ "$(cat "${app_pkginfo}")" != "APPL????" ]; then
            echo "Missing or invalid macOS app PkgInfo: ${app_pkginfo}"
            exit 1
          fi
          if [ ! -x "${app_exec}" ]; then
            echo "Missing or non-executable macOS app executable: ${app_exec}"
            exit 1
          fi
          if [ ! -f "${app_icon}" ]; then
            echo "Missing macOS app icon: ${app_icon}"
            exit 1
          fi
          if [ ! -f "${app_version}" ]; then
            echo "Missing macOS app version manifest: ${app_version}"
            exit 1
          fi
          if [ ! -f "${symbol_manifest}" ]; then
            echo "Missing macOS symbolication manifest: ${symbol_manifest}"
            exit 1
          fi
          if [ ! -x "${support_collector}" ]; then
            echo "Missing or non-executable macOS support collector: ${support_collector}"
            exit 1
          fi
          if LC_ALL=C grep -q $'\r' "${support_collector}"; then
            echo "macOS support collector contains CRLF or carriage returns: ${support_collector}"
            exit 1
          fi
          for support_token in \
            "OPENQ4_PACKAGE_ROOT" \
            "redact_text()" \
            "contains_control_chars()" \
            "Support package root must not contain control characters" \
            "Support output directory must not contain control characters" \
            ".XXXXXX.tar.gz.tmp" \
            "MAX_SUPPORT_ARCHIVE_BYTES" \
            "sanitize_text()" \
            "limit_stream_tail()" \
            "Support report output is limited to the final" \
            "write_bounded_report()" \
            "does not dump the environment" \
            "does not launch openQ4" \
            "does not copy retail q4base PK4 assets" \
            "truncated copy failed; source was not copied" \
            "COPYFILE_DISABLE=1 tar -czf" \
            "COPYFILE_DISABLE=1 tar -tzf" \
            "Support archive is empty or unreadable before publish" \
            "Support archive validation failed before publish" \
            "Support archive is too large before publish" \
            'ln "${ARCHIVE_TMP}" "${ARCHIVE_PATH}"'
          do
            if ! grep -Fq "${support_token}" "${support_collector}"; then
              echo "macOS support collector is missing marker '${support_token}': ${support_collector}"
              exit 1
            fi
          done
          for forbidden_support_token in \
            "printenv" \
            "env >" \
            "set >" \
            "openQ4-client_arm64 >" \
            "openQ4-client_arm64 2>" \
            "openQ4-client_x64 >" \
            "openQ4-client_x64 2>" \
            "openQ4-client_x86 >" \
            "openQ4-client_x86 2>" \
            "openQ4-ded_arm64 >" \
            "openQ4-ded_arm64 2>" \
            "openQ4-ded_x64 >" \
            "openQ4-ded_x64 2>" \
            "openQ4-ded_x86 >" \
            "openQ4-ded_x86 2>" \
            "xattr -l" \
            "xattr -p" \
            "xattr -w" \
            "|| cat" \
            'tail -c "${max_bytes}" < "${source_path}" 2>/dev/null || cat'
          do
            if grep -Fq "${forbidden_support_token}" "${support_collector}"; then
              echo "macOS support collector contains forbidden privacy/no-launch pattern '${forbidden_support_token}': ${support_collector}"
              exit 1
            fi
          done
          if ! grep -Fq "symbol_archive=openq4-${{ needs.metadata.outputs.version_tag }}-macos-${{ matrix.binary_arch }}${{ matrix.package_suffix }}-symbols.tar.xz" "${symbol_manifest}"; then
            echo "macOS symbolication manifest does not point at the matching dSYM archive: ${symbol_manifest}"
            cat "${symbol_manifest}"
            exit 1
          fi
          if find "${{ steps.package.outputs.package_dir }}" -name '*.dSYM' -print -quit | grep -q .; then
            echo "macOS runtime package directory must not contain .dSYM bundles."
            find "${{ steps.package.outputs.package_dir }}" -name '*.dSYM' -print
            exit 1
          fi
          if [ ! -x "${client_binary}" ]; then
            echo "Missing or non-executable macOS client binary: ${client_binary}"
            exit 1
          fi
          if [ ! -x "${dedicated_binary}" ]; then
            echo "Missing or non-executable macOS dedicated binary: ${dedicated_binary}"
            exit 1
          fi
          if [ ! -d "${game_dir}" ]; then
            echo "Missing macOS package game directory beside the app bundle: ${game_dir}"
            exit 1
          fi
          for module in "${sp_module}" "${mp_module}"; do
            if [ ! -x "${module}" ]; then
              echo "Missing or non-executable macOS package game module: ${module}"
              exit 1
            fi
          done
          if ! command -v otool >/dev/null 2>&1; then
            echo "otool is required for macOS runtime dependency validation."
            exit 1
          fi
          if ! command -v lipo >/dev/null 2>&1; then
            echo "lipo is required for macOS architecture validation."
            exit 1
          fi
          if ! command -v codesign >/dev/null 2>&1; then
            echo "codesign is required for macOS signature validation."
            exit 1
          fi
          macos_release_mode="${{ matrix.macos_release_mode }}"
          case "${{ matrix.binary_arch }}" in
            arm64) expected_lipo_arch="arm64" ;;
            x64) expected_lipo_arch="x86_64" ;;
            x86) expected_lipo_arch="i386" ;;
            *)
              echo "No lipo architecture mapping for ${{ matrix.binary_arch }}."
              exit 1
              ;;
          esac
          check_macos_binary_architecture() {
            binary="$1"
            if ! lipo -archs "${binary}" | tr ' ' '\n' | grep -qx "${expected_lipo_arch}"; then
              echo "macOS binary architecture mismatch: ${binary}; expected ${expected_lipo_arch}."
              lipo -archs "${binary}" || true
              exit 1
            fi
          }
          check_macos_codesignature() {
            target="$1"
            if ! codesign --verify --strict --verbose=2 "${target}" >/dev/null 2>&1; then
              echo "macOS code signature verification failed: ${target}"
              codesign --verify --strict --verbose=2 "${target}" || true
              exit 1
            fi
          }
          check_macos_developer_id_signature() {
            target="$1"
            details="$(codesign -dv --verbose=4 "${target}" 2>&1)"
            if ! grep -Fq "Authority=Developer ID Application:" <<< "${details}"; then
              echo "macOS target is not Developer ID Application signed: ${target}"
              echo "${details}"
              exit 1
            fi
            if grep -Fq "Signature=adhoc" <<< "${details}"; then
              echo "macOS target is still ad-hoc signed: ${target}"
              echo "${details}"
              exit 1
            fi
            if ! grep -Fq "Runtime Version=" <<< "${details}"; then
              echo "macOS target is missing Hardened Runtime metadata: ${target}"
              echo "${details}"
              exit 1
            fi
          }
          check_macos_ad_hoc_signature() {
            target="$1"
            details="$(codesign -dv --verbose=4 "${target}" 2>&1)"
            if ! grep -Fq "Signature=adhoc" <<< "${details}"; then
              echo "macOS unsigned release target should be ad-hoc signed: ${target}"
              echo "${details}"
              exit 1
            fi
          }
          check_macos_install_name() {
            binary="$1"
            expected="$2"
            actual="$(otool -D "${binary}" | sed -n '2p' | tr -d '[:space:]')"
            if [ "${actual}" != "${expected}" ]; then
              echo "macOS game module install name mismatch: ${binary}; expected ${expected}, found ${actual:-<empty>}."
              exit 1
            fi
          }
          check_macos_binary_dependencies() {
            binary="$1"
            deps=()
            while IFS= read -r dependency; do
              if [ -n "${dependency}" ]; then
                deps+=("${dependency}")
              fi
            done < <(otool -L "${binary}" | sed '1d' | awk '{print $1}')

            if [[ "${binary}" == *.dylib && "${#deps[@]}" -gt 0 ]]; then
              deps=("${deps[@]:1}")
            fi

            for dependency in "${deps[@]}"; do
              case "${dependency}" in
                /System/Library/*|/usr/lib/*)
                  ;;
                *)
                  echo "macOS binary has unbundled non-system dependency: ${binary} -> ${dependency}"
                  exit 1
                  ;;
              esac
            done
          }
          for binary in "${app_exec}" "${client_binary}" "${dedicated_binary}" "${sp_module}" "${mp_module}"; do
            check_macos_binary_architecture "${binary}"
            check_macos_binary_dependencies "${binary}"
            check_macos_codesignature "${binary}"
            if [ "${macos_release_mode}" = "signed" ]; then
              check_macos_developer_id_signature "${binary}"
            else
              check_macos_ad_hoc_signature "${binary}"
            fi
          done
          check_macos_codesignature "${app_root}"
          if [ "${macos_release_mode}" = "signed" ]; then
            check_macos_developer_id_signature "${app_root}"
            xcrun stapler validate "${app_root}"
            spctl --assess --type execute --verbose=4 "${app_root}"
          else
            check_macos_ad_hoc_signature "${app_root}"
            echo "macOS unsigned release archive is ad-hoc signed and not notarized."
          fi
          check_macos_install_name "${sp_module}" "@loader_path/game-sp_${{ matrix.binary_arch }}.dylib"
          check_macos_install_name "${mp_module}" "@loader_path/game-mp_${{ matrix.binary_arch }}.dylib"
          check_plist_value() {
            key="$1"
            expected="$2"
            actual="$(/usr/libexec/PlistBuddy -c "Print :${key}" "${app_plist}")"
            if [ "${actual}" != "${expected}" ]; then
              echo "macOS app Info.plist ${key} is '${actual}', expected '${expected}'."
              exit 1
            fi
          }
          check_plist_value CFBundleExecutable openQ4
          check_plist_value CFBundleDisplayName openQ4
          check_plist_value CFBundleIconFile openQ4.icns
          check_plist_value CFBundleIdentifier com.darkmatter.openq4
          check_plist_value CFBundleName openQ4
          check_plist_value CFBundlePackageType APPL
          check_plist_value CFBundleShortVersionString "${{ needs.metadata.outputs.version }}"
          check_plist_value CFBundleVersion "${{ needs.metadata.outputs.version }}"
          check_plist_value LSMinimumSystemVersion 11.0
          check_plist_value LSApplicationCategoryType public.app-category.games
          check_plist_value NSPrincipalClass NSApplication
          check_plist_value NSHighResolutionCapable true
          check_plist_value NSSupportsAutomaticGraphicsSwitching true
          if [ "${{ matrix.archive_format }}" = "dmg" ]; then
            dmg_path="${{ steps.package.outputs.archive_path }}"
            if ! command -v hdiutil >/dev/null 2>&1; then
              echo "hdiutil is required for macOS DMG validation."
              exit 1
            fi
            hdiutil verify "${dmg_path}"
            hdiutil imageinfo "${dmg_path}"
            xcrun stapler validate "${dmg_path}"
          fi

      - name: Upload artifact
        uses: actions/upload-artifact@v6
        with:
          name: openq4-release-${{ needs.metadata.outputs.version_tag }}-${{ matrix.platform }}-${{ matrix.binary_arch }}${{ matrix.package_suffix }}
          path: ${{ steps.package.outputs.archive_path }}
          if-no-files-found: error

      - name: Upload macOS dSYM symbols
        if: matrix.platform == 'macos'
        uses: actions/upload-artifact@v6
        with:
          name: openq4-release-${{ needs.metadata.outputs.version_tag }}-macos-${{ matrix.binary_arch }}${{ matrix.package_suffix }}-symbols
          path: ${{ steps.package.outputs.symbols_archive_path }}
          if-no-files-found: error

      - name: Upload Linux debug symbols
        if: matrix.platform == 'linux'
        uses: actions/upload-artifact@v6
        with:
          name: openq4-release-${{ needs.metadata.outputs.version_tag }}-linux-${{ matrix.binary_arch }}-debugsymbols
          path: ${{ steps.linux_symbols.outputs.debug_symbols_path }}
          if-no-files-found: error

      - name: Upload Windows installer
        if: matrix.platform == 'windows'
        uses: actions/upload-artifact@v6
        with:
          name: openq4-release-${{ needs.metadata.outputs.version_tag }}-windows-${{ matrix.binary_arch }}-installer
          path: ${{ steps.windows_installer.outputs.installer_path }}
          if-no-files-found: error

  release:
    name: Publish Release
    needs: [metadata, builds]
    if: ${{ needs.builds.result == 'success' }}
    runs-on: ubuntu-latest
    permissions:
      contents: write

    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Fetch tags
        shell: bash
        run: |
          set -euo pipefail
          git fetch --force --tags origin

      - name: Setup Python
        uses: actions/setup-python@v6
        with:
          python-version: "3.x"

      - name: Download packaged artifacts
        uses: actions/download-artifact@v5
        with:
          pattern: openq4-release-${{ needs.metadata.outputs.version_tag }}-*
          path: ${{ runner.temp }}/release-artifacts
          merge-multiple: true

      - name: Verify platform archives
        shell: bash
        run: |
          set -euo pipefail

          artifact_dir="${RUNNER_TEMP}/release-artifacts"
          if [ ! -d "${artifact_dir}" ]; then
            echo "Artifact directory is missing: ${artifact_dir}"
            exit 1
          fi

          ls -lah "${artifact_dir}"

          expected=(
            "openq4-${{ needs.metadata.outputs.version_tag }}-windows-x64.zip"
            "openq4-${{ needs.metadata.outputs.version_tag }}-windows-x64-setup.exe"
            "openq4-${{ needs.metadata.outputs.version_tag }}-windows-arm64.zip"
            "openq4-${{ needs.metadata.outputs.version_tag }}-windows-arm64-setup.exe"
            "openq4-${{ needs.metadata.outputs.version_tag }}-linux-x64.tar.xz"
            "openq4-${{ needs.metadata.outputs.version_tag }}-linux-x64-debugsymbols.tar.xz"
            "openq4-${{ needs.metadata.outputs.version_tag }}-linux-arm64.tar.xz"
            "openq4-${{ needs.metadata.outputs.version_tag }}-linux-arm64-debugsymbols.tar.xz"
          )

          echo "${{ needs.metadata.outputs.macos_release_reason }}"
          if [ "${{ needs.metadata.outputs.macos_signed_release_enabled }}" = "true" ]; then
            expected+=(
              "openq4-${{ needs.metadata.outputs.version_tag }}-macos-arm64-opengl.dmg"
              "openq4-${{ needs.metadata.outputs.version_tag }}-macos-arm64-opengl-symbols.tar.xz"
              "openq4-${{ needs.metadata.outputs.version_tag }}-macos-arm64-metal.dmg"
              "openq4-${{ needs.metadata.outputs.version_tag }}-macos-arm64-metal-symbols.tar.xz"
            )
          else
            expected+=(
              "openq4-${{ needs.metadata.outputs.version_tag }}-macos-arm64-opengl-unsigned.tar.gz"
              "openq4-${{ needs.metadata.outputs.version_tag }}-macos-arm64-opengl-unsigned-symbols.tar.xz"
              "openq4-${{ needs.metadata.outputs.version_tag }}-macos-arm64-metal-unsigned.tar.gz"
              "openq4-${{ needs.metadata.outputs.version_tag }}-macos-arm64-metal-unsigned-symbols.tar.xz"
            )
          fi

          missing=0
          for package_file in "${expected[@]}"; do
            if [ ! -f "${artifact_dir}/${package_file}" ]; then
              echo "Missing expected package archive: ${package_file}"
              missing=1
            fi
          done

          if [ "${missing}" -ne 0 ]; then
            exit 1
          fi

      - name: Generate changelog
        shell: bash
        run: |
          set -euo pipefail

          python tools/build/generate_release_changelog.py \
            --version "${{ needs.metadata.outputs.version }}" \
            --version-tag "${{ needs.metadata.outputs.version_tag }}" \
            --release-tag "${{ needs.metadata.outputs.release_tag }}" \
            --release-scale "${{ needs.metadata.outputs.release_scale }}" \
            --release-reason "${{ needs.metadata.outputs.release_reason }}" \
            --repo "${GITHUB_REPOSITORY}" \
            --source-root "${GITHUB_WORKSPACE}" \
            --run-id "${GITHUB_RUN_ID}" \
            --run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
            --output "${RUNNER_TEMP}/release-changelog.md"

      - name: Create or update release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        shell: bash
        run: |
          set -euo pipefail

          tag="${{ needs.metadata.outputs.release_tag }}"
          name="${{ needs.metadata.outputs.release_name }}"
          notes="${RUNNER_TEMP}/release-changelog.md"
          assets_dir="${RUNNER_TEMP}/release-artifacts"
          mapfile -t assets < <(find "${assets_dir}" -maxdepth 1 -type f | sort)

          if [ "${#assets[@]}" -eq 0 ]; then
            echo "No release assets were downloaded."
            exit 1
          fi

          if gh release view "${tag}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
            gh release edit "${tag}" \
              --repo "${GITHUB_REPOSITORY}" \
              --title "${name}" \
              --notes-file "${notes}"
            gh release upload "${tag}" "${assets[@]}" --repo "${GITHUB_REPOSITORY}" --clobber
          else
            gh release create "${tag}" "${assets[@]}" \
              --repo "${GITHUB_REPOSITORY}" \
              --title "${name}" \
              --notes-file "${notes}"
          fi

          release_id="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${tag}" --jq .id)"
          gh api \
            --method PATCH \
            "repos/${GITHUB_REPOSITORY}/releases/${release_id}" \
            -f prerelease=false \
            -f draft=false >/dev/null

  discord:
    name: Announce On Discord
    needs: [metadata, release]
    if: ${{ needs.release.result == 'success' }}
    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 22

      - name: Send release embed
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          RELEASE_TAG: ${{ needs.metadata.outputs.release_tag }}
          DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
          DISCORD_RELEASE_EMOJI: ${{ vars.DISCORD_RELEASE_EMOJI || '<:quake4:1425986174941397105>' }}
          DISCORD_RELEASE_MENTIONS: ${{ vars.DISCORD_RELEASE_MENTIONS || '<@&1425985498693898260> <@&1390287267276525628>' }}
          DISCORD_FEEDBACK_CHANNEL: ${{ vars.DISCORD_FEEDBACK_CHANNEL || '<#1509926146018513077>' }}
          DISCORD_RELEASE_AVATAR_URL: ${{ vars.DISCORD_RELEASE_AVATAR_URL || 'https://raw.githubusercontent.com/themuffinator/OpenQ4/main/assets/img/avatar.png' }}
        shell: bash
        run: |
          set -euo pipefail
          node .github/scripts/announce-release-discord.mjs
