name: Release

# Builds the macOS, Windows, and Linux desktop apps, an Android APK, an iOS
# IPA, a Nintendo Switch SD-ready zip (experimental), Xbox UWP, and the Anbernic
# RG34XXSP (Stock OS 64-bit MOD / PortMaster) port, then publishes them as a
# GitHub Release.
#
# Versioning:
#   - First ever release is 0.1.0.
#   - Every push to main auto-increments the patch: 0.1.0 -> 0.1.1 -> ... -> 0.1.99,
#     then rolls over to 0.2.0 and keeps going.
#   - To force a specific version, either:
#       * run this workflow manually (Actions tab) and type it into "version", or
#       * put "[release X.Y.Z]" anywhere in the commit message.
#
# Branch model: day-to-day work merges to `dev`. Releases stay on `main` only
# so promoting `dev` -> `main` is the ship gate that cuts a build.

on:
  push:
    branches: [main]
    # CI/workflow and docs-only changes don't ship anything to users, so they
    # don't earn a release. A push touching these *and* real source still
    # releases; only pushes confined entirely to these paths are skipped.
    paths-ignore:
      - '.github/**'
      - '**.md'
      - 'mobile/ios/app-repo.json'
  workflow_dispatch:
    inputs:
      version:
        description: "Exact version to release (e.g. 0.2.0). Leave blank to auto-increment."
        required: false
        default: ""

permissions:
  contents: write
  issues: read
  pull-requests: read

concurrency:
  group: release
  cancel-in-progress: false

jobs:
  version:
    name: determine release version
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.ver.outputs.version }}
      tag: ${{ steps.ver.outputs.tag }}
    steps:
      - uses: actions/checkout@v7
        with:
          fetch-depth: 0
          fetch-tags: true
      - name: Determine version
        id: ver
        env:
          DISPATCH_VERSION: ${{ github.event.inputs.version }}
          GH_TOKEN: ${{ github.token }}
        run: |
          set -euo pipefail
          semver_re='^[0-9]+\.[0-9]+\.[0-9]+$'

          # 1) Explicit override from a manual run.
          override=""
          if [ -n "${DISPATCH_VERSION:-}" ]; then
            override="$DISPATCH_VERSION"
          else
            # 2) Override from the commit message: [release X.Y.Z]
            msg="$(git log -1 --pretty=%B || true)"
            tag_ver="$(printf '%s' "$msg" | sed -n -E 's/.*\[release[[:space:]]+([0-9]+\.[0-9]+\.[0-9]+)\].*/\1/p' | head -1)"
            if [ -n "$tag_ver" ]; then
              override="$tag_ver"
            fi
          fi

          if [ -n "$override" ]; then
            if ! printf '%s' "$override" | grep -Eq "$semver_re"; then
              echo "::error::Invalid version override '$override' (expected X.Y.Z)"
              exit 1
            fi
            version="$override"
            echo "Using override version: $version"
          else
            # 3) Auto-increment from the highest existing vX.Y.Z tag.
            latest="$(git tag -l 'v*' \
              | sed -E 's/^v//' \
              | grep -E "$semver_re" \
              | sort -t. -k1,1n -k2,2n -k3,3n \
              | tail -1 || true)"
            if [ -z "$latest" ]; then
              version="0.1.0"
              echo "No existing release tag; starting at $version"
            else
              major="${latest%%.*}"
              rest="${latest#*.}"
              minor="${rest%%.*}"
              patch="${rest##*.}"
              patch=$((patch + 1))
              if [ "$patch" -gt 99 ]; then
                minor=$((minor + 1))
                patch=0
              fi
              version="${major}.${minor}.${patch}"
              echo "Latest was $latest; next is $version"
            fi
          fi

          tag="v${version}"
          if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then
            echo "::error::Tag $tag already exists. Pick a different version."
            exit 1
          fi
          if gh release view "$tag" >/dev/null 2>&1; then
            echo "::error::Release $tag already exists. Pick a different version."
            exit 1
          fi
          echo "version=$version" >> "$GITHUB_OUTPUT"
          echo "tag=$tag" >> "$GITHUB_OUTPUT"

  love-payload:
    name: build release game.love
    needs: version
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - name: Build shared payload
        run: |
          scripts/pack_love.sh \
            --output dist/payload/game.love \
            --listing dist/payload/love-listing.txt \
            --version "${{ needs.version.outputs.version }}"
      - name: Upload shared payload
        uses: actions/upload-artifact@v7
        with:
          name: gen1recomp-release-love
          path: dist/payload/game.love
          if-no-files-found: error
          retention-days: 1

  linux-arm64:
    name: build Linux arm64 AppImage
    needs: [version, love-payload]
    # GitHub's free arm64 runner for public repos. It has to be arm64: the
    # AppImage compiles LÖVE natively inside a Debian bullseye arm64
    # container, and the qemu-emulated alternative takes hours.
    runs-on: ubuntu-24.04-arm
    steps:
      - uses: actions/checkout@v7
      - name: Download shared payload
        uses: actions/download-artifact@v8
        with:
          name: gen1recomp-release-love
          path: .bazinga/work
      - name: Build Linux arm64 AppImage
        run: |
          set -euo pipefail
          scripts/build_linux_arm64.sh \
            --version "${{ needs.version.outputs.version }}" \
            --game-love .bazinga/work/game.love
      - name: Upload Linux arm64 release
        uses: actions/upload-artifact@v7
        with:
          name: gen1recomp-linux-arm64-release
          path: |
            dist/linux-arm64/gen1recomp-${{ needs.version.outputs.version }}-linux-arm64.AppImage
            dist/linux-arm64/gen1recomp-${{ needs.version.outputs.version }}-linux-arm64.AppImage.sha256
          if-no-files-found: error
          retention-days: 1

  xbox-uwp:
    name: build Xbox UWP release
    needs: [version, love-payload]
    runs-on: windows-2022
    steps:
      - uses: actions/checkout@v7
      - name: Download shared payload
        uses: actions/download-artifact@v8
        with:
          name: gen1recomp-release-love
          path: .bazinga/work
      - name: Prepare signing certificate
        shell: pwsh
        env:
          CERTIFICATE_BASE64: ${{ secrets.XBOX_UWP_SIGNING_CERTIFICATE }}
          CERTIFICATE_PASSWORD: ${{ secrets.XBOX_UWP_SIGNING_PASSWORD }}
          CANONICAL_REPOSITORY: ${{ github.repository == 'bryanthaboi/gen1recomp' }}
        run: |
          if ($env:CANONICAL_REPOSITORY -eq 'true' -and
              [string]::IsNullOrWhiteSpace($env:CERTIFICATE_BASE64)) {
              throw 'XBOX_UWP_SIGNING_CERTIFICATE is not configured.'
          }
          if ([string]::IsNullOrWhiteSpace($env:CERTIFICATE_BASE64)) {
              "UWP_PUBLISHER=CN=Gen1Recomp" | Out-File $env:GITHUB_ENV -Append
              exit 0
          }
          $pfx = Join-Path $env:RUNNER_TEMP 'gen1recomp-uwp.pfx'
          [IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:CERTIFICATE_BASE64))
          $flags = [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::EphemeralKeySet
          $cert = [Security.Cryptography.X509Certificates.X509Certificate2]::new(
              $pfx, $env:CERTIFICATE_PASSWORD, $flags)
          $cer = Join-Path $env:RUNNER_TEMP 'gen1recomp-uwp.cer'
          [IO.File]::WriteAllBytes(
              $cer,
              $cert.Export([Security.Cryptography.X509Certificates.X509ContentType]::Cert))
          Import-Certificate -FilePath $cer -CertStoreLocation Cert:\LocalMachine\TrustedPeople | Out-Null
          "UWP_PFX=$pfx" | Out-File $env:GITHUB_ENV -Append
          "UWP_CERT_THUMBPRINT=$($cert.Thumbprint)" | Out-File $env:GITHUB_ENV -Append
          "UWP_PUBLISHER=$($cert.Subject)" | Out-File $env:GITHUB_ENV -Append
      - name: Build Xbox UWP package
        shell: bash
        run: |
          bash scripts/build_xbox_uwp.sh \
            --release \
            --version "${{ needs.version.outputs.version }}" \
            --publisher "$UWP_PUBLISHER" \
            --game-love .bazinga/work/game.love
      - name: Sign and stage Xbox UWP release
        shell: pwsh
        env:
          CERTIFICATE_PASSWORD: ${{ secrets.XBOX_UWP_SIGNING_PASSWORD }}
        run: |
          if (-not $env:UWP_PFX) {
              exit 0
          }
          scripts/xbox-uwp/stage_release.ps1 `
            -Version '${{ needs.version.outputs.version }}' `
            -Configuration Release `
            -BuildInfo .bazinga/work/xbox-uwp-build-info.json `
            -CertificatePath $env:UWP_PFX `
            -CertificatePassword $env:CERTIFICATE_PASSWORD
      - name: Upload Xbox UWP release
        uses: actions/upload-artifact@v7
        with:
          name: gen1recomp-xbox-uwp-release
          path: |
            dist/xbox-uwp/gen1recomp-${{ needs.version.outputs.version }}-xbox-uwp.zip
            dist/xbox-uwp/gen1recomp-${{ needs.version.outputs.version }}-xbox-uwp.zip.sha256
          if-no-files-found: error
          retention-days: 1
      - name: Remove signing certificate
        if: always()
        shell: pwsh
        run: |
          if ($env:UWP_CERT_THUMBPRINT) {
              Remove-Item "Cert:\LocalMachine\TrustedPeople\$env:UWP_CERT_THUMBPRINT" -ErrorAction SilentlyContinue
          }
          if ($env:UWP_PFX) {
              Remove-Item $env:UWP_PFX -Force -ErrorAction SilentlyContinue
          }

  release:
    needs: [version, xbox-uwp, linux-arm64]
    runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}

    steps:
      # The self-hosted runner lives under the machine owner's home
      # directory; mask it first so absolute paths in every later step's
      # output show up as *** in the public workflow logs.
      - name: Mask runner paths
        run: echo "::add-mask::$HOME"

      - name: Checkout
        uses: actions/checkout@v7
        with:
          fetch-depth: 0
          fetch-tags: true

      - name: Import signing certificate into a temporary keychain
        if: github.repository == 'bryanthaboi/gen1recomp'
        run: |
          set -euo pipefail
          KEYCHAIN_PATH="$RUNNER_TEMP/pokemon-signing.keychain-db"
          ci_dir="${POKEMON_CI_DIR:-$HOME/.config/pokemon-ci}"
          p12="$ci_dir/signing.p12"
          passfile="$ci_dir/signing.pass"
          if [ ! -f "$p12" ] || [ ! -f "$passfile" ]; then
            echo "::error::Signing material not found in $ci_dir. Run scripts/ci-setup-signing.sh on the runner."
            exit 1
          fi
          p12pw="$(cat "$passfile")"

          kcpw="$(openssl rand -base64 24)"
          echo "::add-mask::$kcpw"

          # Fresh, dedicated keychain — no dependence on the login keychain/session.
          security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true
          security create-keychain -p "$kcpw" "$KEYCHAIN_PATH"
          security set-keychain-settings "$KEYCHAIN_PATH"          # disable auto-lock
          security unlock-keychain -p "$kcpw" "$KEYCHAIN_PATH"

          security import "$p12" -P "$p12pw" -k "$KEYCHAIN_PATH" \
            -T /usr/bin/codesign -T /usr/bin/security

          # Let codesign use the key non-interactively.
          security set-key-partition-list -S apple-tool:,apple:,codesign: \
            -s -k "$kcpw" "$KEYCHAIN_PATH" >/dev/null

          # Make the keychain visible to find-identity/codesign (prepend to search list).
          existing="$(security list-keychains -d user | sed -e 's/^[[:space:]]*//' -e 's/"//g')"
          security list-keychains -d user -s "$KEYCHAIN_PATH" $existing

          echo "Identities available to codesign:"
          security find-identity -v -p codesigning "$KEYCHAIN_PATH"

      - name: Build macOS + Windows + Linux
        run: |
          set -euo pipefail
          # Sign in-build (identity auto-detected from the temp keychain);
          # notarize separately below so it uses secret credentials, not a
          # login-keychain profile. "all" also builds the Linux AppImage,
          # which needs no signing/notarization.
          scripts/build.sh all --version "${{ needs.version.outputs.version }}" --no-notarize

      - name: Build Android
        run: |
          set -euo pipefail
          scripts/build_android.sh --version "${{ needs.version.outputs.version }}"

      - name: Install xcbeautify
        run: |
          set -euo pipefail
          brew list xcbeautify >/dev/null 2>&1 || brew install xcbeautify

      - name: Build iOS
        env:
          CANONICAL_REPOSITORY: ${{ github.repository == 'bryanthaboi/gen1recomp' }}
        run: |
          set -euo pipefail
          if [ "$CANONICAL_REPOSITORY" = true ]; then
            scripts/build_ios.sh --fetch --device --release \
              --version "${{ needs.version.outputs.version }}"
          else
            scripts/build_ios.sh --fetch --release \
              --version "${{ needs.version.outputs.version }}"
          fi

      - name: Build Switch
        run: |
          set -euo pipefail
          # Hard-fail gate: Switch ships with every release (never soft-fail).
          # PR CI is path-gated (ubuntu selftest + canonical fused); release
          # always builds Switch regardless of which files changed.
          # Needs native switch-tools (nacptool/elf2nro) and/or Docker on the
          # Mac self-hosted runner; see docs/switch-build.md.
          scripts/build_switch.sh --fetch --fused \
            --version "${{ needs.version.outputs.version }}"

      - name: Build Anbernic RG34XXSP port
        run: |
          set -euo pipefail
          # Self-contained aarch64 PortMaster-style pack; pulls the LÖVE 11.5
          # runtime from PortMaster-GUI, so it needs no signing/notarization.
          ./build-rg34xxsp.sh --version "${{ needs.version.outputs.version }}"

      - name: Notarize & staple macOS app
        if: github.repository == 'bryanthaboi/gen1recomp'
        run: |
          set -euo pipefail
          ci_dir="${POKEMON_CI_DIR:-$HOME/.config/pokemon-ci}"
          if [ ! -f "$ci_dir/notary.env" ]; then
            echo "::error::Missing $ci_dir/notary.env. Run scripts/ci-setup-signing.sh on the runner."
            exit 1
          fi
          set -a; . "$ci_dir/notary.env"; set +a
          echo "::add-mask::$APPLE_APP_PASSWORD"

          app=".bazinga/work/gen1recomp.app"
          zip="dist/mac/gen1recomp-macos.zip"
          [ -d "$app" ] || { echo "::error::signed app not found at $app"; exit 1; }
          if [ -z "${APPLE_ID:-}" ] || [ -z "${APPLE_APP_PASSWORD:-}" ] || [ -z "${APPLE_TEAM_ID:-}" ]; then
            echo "::error::notary.env is missing APPLE_ID / APPLE_APP_PASSWORD / APPLE_TEAM_ID."
            exit 1
          fi

          echo "Submitting to Apple notary service (can take a few minutes)..."
          xcrun notarytool submit "$zip" \
            --apple-id "$APPLE_ID" \
            --team-id "$APPLE_TEAM_ID" \
            --password "$APPLE_APP_PASSWORD" \
            --wait

          echo "Stapling ticket to the app..."
          xcrun stapler staple "$app"

          # Re-zip the now-stapled app (same format build.sh uses).
          rm -f "$zip"
          ditto -c -k --sequesterRsrc --keepParent "$app" "$zip"
          echo "Notarized + stapled ✓"

      - name: Download Xbox UWP release
        if: github.repository == 'bryanthaboi/gen1recomp'
        uses: actions/download-artifact@v8
        with:
          name: gen1recomp-xbox-uwp-release
          path: dist/xbox-uwp

      - name: Download Linux arm64 release
        if: github.repository == 'bryanthaboi/gen1recomp'
        uses: actions/download-artifact@v8
        with:
          name: gen1recomp-linux-arm64-release
          path: dist/linux-arm64

      - name: Stage release assets
        if: github.repository == 'bryanthaboi/gen1recomp'
        id: assets
        run: |
          set -euo pipefail
          v="${{ needs.version.outputs.version }}"
          outdir="dist/release"
          rm -rf "$outdir"
          mkdir -p "$outdir"
          cp "dist/mac/gen1recomp-macos.zip" "$outdir/gen1recomp-${v}-macos.zip"
          cp "dist/win/gen1recomp-win64.zip" "$outdir/gen1recomp-${v}-windows.zip"
          cp "dist/linux/gen1recomp-linux.zip" "$outdir/gen1recomp-${v}-linux.zip"

          # arm64 desktop Linux (Raspberry Pi, Armbian, arm64 VMs). Built on
          # its own runner because LÖVE publishes no aarch64 binary and the
          # AppImage has to be compiled natively; ships as a runnable
          # AppImage rather than a zip so `chmod +x && ./it` just works.
          arm64_appimage="dist/linux-arm64/gen1recomp-${v}-linux-arm64.AppImage"
          [ -f "$arm64_appimage" ] || { echo "::error::$arm64_appimage not found (expected from the linux-arm64 job)"; exit 1; }
          cp "$arm64_appimage" "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
          chmod +x "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
          apk="$(find dist/android/debug -name '*.apk' | head -1)"
          [ -n "$apk" ] || { echo "::error::no Android APK found under dist/android/debug"; exit 1; }
          cp "$apk" "$outdir/gen1recomp-${v}-android.apk"

          ipa="dist/ios/gen1recomp.ipa"
          [ -f "$ipa" ] || { echo "::error::$ipa not found (expected from scripts/build_ios.sh --device)"; exit 1; }
          cp "$ipa" "$outdir/gen1recomp-${v}-ios.ipa"

          swzip="dist/switch/gen1recomp-${v}-switch.zip"
          [ -f "$swzip" ] || { echo "::error::$swzip not found (expected from scripts/build_switch.sh --fused → pack_sd_zip.sh)"; exit 1; }
          cp "$swzip" "$outdir/gen1recomp-${v}-switch.zip"
          # Local fused .nro stays under dist/switch/ for PR CI / debug; release
          # publishes the SD-ready zip only.

          uwp="dist/xbox-uwp/gen1recomp-${v}-xbox-uwp.zip"
          [ -f "$uwp" ] || { echo "::error::$uwp not found (expected from the Xbox UWP job)"; exit 1; }
          cp "$uwp" "$outdir/gen1recomp-${v}-xbox-uwp.zip"

          # Anbernic handheld port (suffix names the CFW it targets, so a
          # future RG35XX/other-CFW pack can ship alongside it).
          rg34="dist/rg34xxsp/gen1recomp-rg34xxsp-stockos64-mod.zip"
          [ -f "$rg34" ] || { echo "::error::$rg34 not found (expected from ./build-rg34xxsp.sh)"; exit 1; }
          cp "$rg34" "$outdir/gen1recomp-${v}-rg34xxsp-stockos64-mod.zip"

          # Platform-independent update payload, built alongside the desktop
          # apps above (same game.love that gets fused into each of them).
          love_file=".bazinga/work/game.love"
          [ -f "$love_file" ] || { echo "::error::$love_file not found (expected from scripts/build.sh)"; exit 1; }
          cp "$love_file" "$outdir/gen1recomp-${v}.love"

          ls -lh "$outdir"

          # Checksums for every staged release asset (sums file itself is
          # written after this and named outside the gen1recomp-* glob, so it
          # never lists itself).
          (cd "$outdir" && shasum -a 256 gen1recomp-* > sha256sums.txt)
          cat "$outdir/sha256sums.txt"

      - name: Publish GitHub Release
        if: github.repository == 'bryanthaboi/gen1recomp'
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          set -euo pipefail
          v="${{ needs.version.outputs.version }}"
          tag="${{ needs.version.outputs.tag }}"

          # Issues this release closes. Three sources, deduped by number:
          #   1. GitHub's own "closing issues" links on every PR whose
          #      commits are in the range (works for squash, rebase, and
          #      merge commits alike) -- including PRs that were merged
          #      into a branch this release PR is itself merging in.
          #   2. CLOSES/fixes/resolves text in those PRs' titles + bodies
          #      that GitHub didn't turn into a closing link (a PR into a
          #      non-default branch never gets one).
          #   3. The same text in raw commit messages, so a direct push
          #      with "CLOSES #N #M" still lands in the notes.
          # Scans every commit since the previous tag so a skipped release
          # run doesn't drop issues on the floor.
          prev_tag="$(git tag -l 'v*' --sort=-v:refname | grep -v "^${tag}$" | head -1 || true)"
          range="${prev_tag:+${prev_tag}..}$GITHUB_SHA"

          # every #N on a line that carries a closing keyword (handles the
          # multi-issue "CLOSES #1 #2 #3" form the tracker uses)
          scan_closes() {
            grep -iE '\b(close[sd]?|fix(es|ed)?|resolve[sd]?)\b' \
              | grep -oE '#[0-9]+' | tr -d '#' || true
          }

          nums=""
          nums+=" $(git log --pretty=%B "$range" | scan_closes | tr '\n' ' ')"

          prs="$(git log --pretty=%H "$range" \
                   | xargs -I{} gh api "repos/$GITHUB_REPOSITORY/commits/{}/pulls" \
                         --jq '.[].number' 2>/dev/null \
                   | sort -un || true)"
          for pr in $prs; do
            nums+=" $(gh api graphql \
              -f owner="${GITHUB_REPOSITORY%/*}" \
              -f name="${GITHUB_REPOSITORY#*/}" \
              -F pr="$pr" \
              -f query='
                query($owner:String!, $name:String!, $pr:Int!) {
                  repository(owner:$owner, name:$name) {
                    pullRequest(number:$pr) {
                      closingIssuesReferences(first:50) { nodes { number } }
                    }
                  }
                }' \
              --jq '.data.repository.pullRequest.closingIssuesReferences.nodes[].number' \
              2>/dev/null | grep -E '^[0-9]+$' | tr '\n' ' ' || true)"
            nums+=" $(gh api "repos/$GITHUB_REPOSITORY/pulls/$pr" \
              --jq '.title + " " + (.body // "")' 2>/dev/null \
              | scan_closes | tr '\n' ' ' || true)"
          done

          # dedupe, drop anything that is a PR or does not exist, keep
          # titles (gh api prints the response body to stdout on an HTTP
          # error, so only a zero exit counts)
          closed=""
          for n in $(printf '%s' "$nums" | tr ' ' '\n' | grep -E '^[0-9]+$' | sort -un); do
            if title="$(gh api "repos/$GITHUB_REPOSITORY/issues/$n" \
                --jq 'if .pull_request then empty else .title end' \
                2>/dev/null)"; then
              [ -n "$title" ] && closed+="- #$n $title"$'\n'
            fi
          done
          closed="$(printf '%s' "$closed" | grep . | sort -t'#' -k2 -n || true)"

          # Everyone whose commits are in the range: GitHub login when the
          # commit is linked to an account, the raw git author name when not;
          # CI bots filtered out.
          base="${prev_tag:-$(git rev-list --max-parents=0 "$GITHUB_SHA" | tail -1)}"
          contributors="$(gh api --paginate \
            "repos/$GITHUB_REPOSITORY/compare/${base}...${GITHUB_SHA}" \
            --jq '.commits[] | if .author and .author.login
                              then "@" + .author.login
                              else .commit.author.name end' 2>/dev/null \
            | grep -viE '\[bot\]$' | sort -uf | sed 's/^/- /' || true)"

          notes="Download the correct version for your computer below."
          if [ -n "$closed" ]; then
            notes+=$'\n\n## Issues closed\n\n'"$closed"
          fi
          if [ -n "$contributors" ]; then
            notes+=$'\n\n## Contributors\n\n'"$contributors"
          fi
          printf 'Release notes:\n%s\n' "$notes"

          release_files=(
            "dist/release/gen1recomp-${v}-macos.zip"
            "dist/release/gen1recomp-${v}-windows.zip"
            "dist/release/gen1recomp-${v}-linux.zip"
            "dist/release/gen1recomp-${v}-linux-arm64.AppImage"
            "dist/release/gen1recomp-${v}-android.apk"
            "dist/release/gen1recomp-${v}-ios.ipa"
            "dist/release/gen1recomp-${v}-switch.zip"
            "dist/release/gen1recomp-${v}-xbox-uwp.zip"
            "dist/release/gen1recomp-${v}-rg34xxsp-stockos64-mod.zip"
            "dist/release/gen1recomp-${v}.love"
            "dist/release/sha256sums.txt"
          )

          gh release create "$tag" \
            --target "$GITHUB_SHA" \
            --title "$v" \
            --notes "$notes" \
            "${release_files[@]}"

          echo "Published release $tag"

      - name: Update iOS app repository
        if: github.repository == 'bryanthaboi/gen1recomp'
        run: |
          set -euo pipefail
          v="${{ needs.version.outputs.version }}"
          ipa="dist/release/gen1recomp-${v}-ios.ipa"
          app_repo="mobile/ios/app-repo.json"
          [ -f "$ipa" ] || { echo "::error::$ipa not found"; exit 1; }
          [ -f "$app_repo" ] || { echo "::error::$app_repo not found"; exit 1; }

          date="$(date -u +"%Y-%m-%d")"
          size="$(wc -c < "$ipa" | tr -d '[:space:]')"
          download_url="https://github.com/${GITHUB_REPOSITORY}/releases/download/v${v}/gen1recomp-${v}-ios.ipa"
          localized_description="Gen1Recomp - A native Lua / LÖVE2D recreation of Gen 1 Poke"
          release_notes="$(GH_TOKEN="${{ github.token }}" gh release view "v${v}" --json body --jq '.body // ""' 2>/dev/null || true)"
          if [ -n "$release_notes" ]; then
            localized_description="$release_notes"
          fi
          entry="$(jq -n \
            --arg version "$v" \
            --arg date "$date" \
            --arg download_url "$download_url" \
            --arg localized_description "$localized_description" \
            --argjson size "$size" \
            '{version: $version, date: $date, size: $size, downloadURL: $download_url, localizedDescription: $localized_description}')"

          if jq -e --arg version "$v" \
            'any(.apps[] | select(.bundleIdentifier == "com.theboisclub.gen1recomp").versions[]?; .version == $version)' \
            "$app_repo" >/dev/null; then
            jq --arg version "$v" --argjson entry "$entry" \
              '(.apps[] | select(.bundleIdentifier == "com.theboisclub.gen1recomp").versions) |= map(if .version == $version then $entry else . end)' \
              "$app_repo" > "$app_repo.tmp"
          else
            jq --argjson entry "$entry" \
              '(.apps[] | select(.bundleIdentifier == "com.theboisclub.gen1recomp").versions) |= [$entry] + .' \
              "$app_repo" > "$app_repo.tmp"
          fi
          mv "$app_repo.tmp" "$app_repo"

      # main is PR-only for everyone except deploy keys (the "main protection"
      # ruleset's bypass actor), so this push must authenticate with the
      # RELEASE_DEPLOY_KEY deploy key over SSH; the workflow's GITHUB_TOKEN
      # would be rejected by the branch protection.
      - name: Commit iOS app repository
        if: github.repository == 'bryanthaboi/gen1recomp'
        env:
          DEPLOY_KEY: ${{ secrets.RELEASE_DEPLOY_KEY }}
        run: |
          set -euo pipefail
          git add mobile/ios/app-repo.json
          if git diff --cached --quiet; then
            echo "app-repo.json unchanged; nothing to push"
            exit 0
          fi
          key="$RUNNER_TEMP/release-deploy-key"
          printf '%s\n' "$DEPLOY_KEY" > "$key"
          chmod 600 "$key"
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git commit -m "chore(ios): update app-repo.json [skip ci]"
          git -c core.sshCommand="ssh -i $key -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" \
            push "git@github.com:${GITHUB_REPOSITORY}.git" "HEAD:${GITHUB_REF_NAME}"
          rm -f "$key"

      - name: Clean up signing keychain
        if: ${{ always() && github.repository == 'bryanthaboi/gen1recomp' }}
        run: |
          security delete-keychain "$RUNNER_TEMP/pokemon-signing.keychain-db" 2>/dev/null || true
          rm -f "$RUNNER_TEMP/release-deploy-key"
