name: Release

on:
  push:
    tags:
      - "v*.*.*"

permissions:
  contents: read

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

jobs:
  preflight:
    name: Verify release tag
    if: ${{ vars.RELEASE_PIPELINE_ENABLED == 'true' }}
    runs-on: ubuntu-24.04
    timeout-minutes: 10
    outputs:
      version: ${{ steps.version.outputs.app }}
      android_code: ${{ steps.version.outputs.android_code }}

    steps:
      - name: Checkout release history
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          fetch-depth: 0
          persist-credentials: false

      - name: Verify canonical beta tag on current master
        id: version
        run: |
          set -euo pipefail
          packaging/version/resolve-version.sh verify >/dev/null
          version="$(packaging/version/resolve-version.sh product)"
          channel="$(packaging/version/resolve-version.sh channel)"
          master_sha="$(git rev-parse origin/master)"
          if [ "$GITHUB_SHA" != "$master_sha" ]; then
            echo "Release tags must point at the current master commit" >&2
            exit 1
          fi
          if [ "$channel" != "beta" ]; then
            echo "Only beta closed-testing releases are enabled" >&2
            exit 1
          fi
          echo "app=$version" >> "$GITHUB_OUTPUT"
          echo "android_code=$(packaging/version/resolve-version.sh android-code)" >> "$GITHUB_OUTPUT"

      - name: Refuse an existing GitHub Release
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
            echo "GitHub Release $GITHUB_REF_NAME already exists" >&2
            exit 1
          fi

  linux:
    name: Linux packages
    needs: preflight
    uses: ./.github/workflows/linux-packages.yml

  windows:
    name: Windows Store package
    needs: preflight
    uses: ./.github/workflows/windows-store.yml

  macos:
    name: Signed and notarized macOS package
    needs: preflight
    uses: ./.github/workflows/apple-release.yml
    secrets: inherit

  android:
    name: Signed Android package
    needs: preflight
    uses: ./.github/workflows/android-release.yml
    secrets: inherit

  play-closed-testing:
    name: Stage Play closed-testing draft
    needs:
      - preflight
      - linux
      - windows
      - macos
      - android
    runs-on: ubuntu-24.04
    timeout-minutes: 20
    environment: play-closed-testing
    permissions:
      contents: read
      id-token: write

    steps:
      - name: Checkout
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Download signed Android artifacts
        uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
        with:
          name: vnidrop-${{ needs.preflight.outputs.version }}-android-release
          path: build/release/android

      - name: Validate closed-testing configuration
        env:
          WORKLOAD_IDENTITY_PROVIDER: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}
          PLAY_SERVICE_ACCOUNT: ${{ vars.GCP_PLAY_SERVICE_ACCOUNT }}
          PLAY_PACKAGE_NAME: ${{ vars.PLAY_PACKAGE_NAME }}
          PLAY_CLOSED_TRACK: ${{ vars.PLAY_CLOSED_TRACK }}
          PLAY_APP_SIGNING_CERT_SHA256: ${{ vars.PLAY_APP_SIGNING_CERT_SHA256 }}
        run: |
          for name in \
            WORKLOAD_IDENTITY_PROVIDER \
            PLAY_SERVICE_ACCOUNT \
            PLAY_PACKAGE_NAME \
            PLAY_CLOSED_TRACK \
            PLAY_APP_SIGNING_CERT_SHA256; do
            if [ -z "${!name:-}" ]; then
              echo "Missing Play closed-testing configuration: $name" >&2
              exit 1
            fi
          done
          case "${PLAY_CLOSED_TRACK,,}" in
            production|*:production)
              echo "Production Play tracks are forbidden" >&2
              exit 1
              ;;
          esac
          if [ "$PLAY_PACKAGE_NAME" != "com.vnidrop.app" ]; then
            echo "Unexpected Play package name: $PLAY_PACKAGE_NAME" >&2
            exit 1
          fi

      - name: Authenticate to Google with GitHub OIDC
        id: google-auth
        uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3
        with:
          workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}
          service_account: ${{ vars.GCP_PLAY_SERVICE_ACCOUNT }}
          token_format: access_token
          access_token_scopes: https://www.googleapis.com/auth/androidpublisher

      - name: Stage AAB and download Play-signed APK
        env:
          GOOGLE_PLAY_ACCESS_TOKEN: ${{ steps.google-auth.outputs.access_token }}
          PLAY_PACKAGE_NAME: ${{ vars.PLAY_PACKAGE_NAME }}
          PLAY_CLOSED_TRACK: ${{ vars.PLAY_CLOSED_TRACK }}
          PLAY_APP_SIGNING_CERT_SHA256: ${{ vars.PLAY_APP_SIGNING_CERT_SHA256 }}
          VERSION: ${{ needs.preflight.outputs.version }}
          VERSION_CODE: ${{ needs.preflight.outputs.android_code }}
        run: |
          set -euo pipefail
          shopt -s nullglob
          bundles=(build/release/android/*.aab)
          if [ "${#bundles[@]}" -ne 1 ]; then
            echo "Expected exactly one signed AAB" >&2
            exit 1
          fi
          mkdir -p build/release/play
          python3 packaging/android/publish_play.py \
            --bundle "${bundles[0]}" \
            --package-name "$PLAY_PACKAGE_NAME" \
            --track "$PLAY_CLOSED_TRACK" \
            --version-code "$VERSION_CODE" \
            --release-name "$VERSION" \
            --expected-app-certificate "$PLAY_APP_SIGNING_CERT_SHA256" \
            --apk-output "build/release/play/VniDrop-${VERSION}-${VERSION_CODE}-play-universal.apk" \
            --metadata-output build/release/play/play-release.json

      - name: Set up Android SDK verification tools
        uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3
        with:
          packages: "platform-tools build-tools;36.0.0"

      - name: Verify Play-signed universal APK
        env:
          EXPECTED_CERT_SHA256: ${{ vars.PLAY_APP_SIGNING_CERT_SHA256 }}
          VERSION: ${{ needs.preflight.outputs.version }}
          VERSION_CODE: ${{ needs.preflight.outputs.android_code }}
        run: |
          set -euo pipefail
          apk="build/release/play/VniDrop-${VERSION}-${VERSION_CODE}-play-universal.apk"
          apkanalyzer_path="$(
            find "$ANDROID_SDK_ROOT/cmdline-tools" -type f -name apkanalyzer -perm -111 |
              sort -r |
              head -1
          )"
          if [ -z "$apkanalyzer_path" ]; then
            echo "apkanalyzer was not found" >&2
            exit 1
          fi
          packaging/android/verify-apk-signature.sh \
            "$apk" \
            "$EXPECTED_CERT_SHA256" \
            >/dev/null
          if [ "$("$apkanalyzer_path" manifest application-id "$apk")" != "com.vnidrop.app" ]; then
            echo "Play APK package name mismatch" >&2
            exit 1
          fi
          if [ "$("$apkanalyzer_path" manifest version-name "$apk")" != "$VERSION" ]; then
            echo "Play APK version name mismatch" >&2
            exit 1
          fi
          if [ "$("$apkanalyzer_path" manifest version-code "$apk")" != "$VERSION_CODE" ]; then
            echo "Play APK version code mismatch" >&2
            exit 1
          fi
          (
            cd build/release/play
            sha256sum \
              "VniDrop-${VERSION}-${VERSION_CODE}-play-universal.apk" \
              play-release.json \
              > SHA256SUMS
          )

      - name: Upload Play-signed APK
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: vnidrop-${{ needs.preflight.outputs.version }}-android-play
          path: build/release/play/
          if-no-files-found: error
          retention-days: 90
          compression-level: 0

  publish-microsoft-store:
    name: Submit Microsoft Store update
    needs:
      - preflight
      - linux
      - windows
      - macos
      - play-closed-testing
    runs-on: windows-2025
    timeout-minutes: 30
    environment: microsoft-store
    permissions:
      contents: read

    steps:
      - name: Download Windows Store package
        uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
        with:
          name: vnidrop-${{ needs.preflight.outputs.version }}-windows-store-x64
          path: build/release/windows

      - name: Validate Microsoft Store configuration
        id: store-package
        shell: pwsh
        env:
          AZURE_AD_TENANT_ID: ${{ secrets.AZURE_AD_TENANT_ID }}
          AZURE_AD_APPLICATION_CLIENT_ID: ${{ secrets.AZURE_AD_APPLICATION_CLIENT_ID }}
          AZURE_AD_APPLICATION_SECRET: ${{ secrets.AZURE_AD_APPLICATION_SECRET }}
          SELLER_ID: ${{ secrets.SELLER_ID }}
          MICROSOFT_STORE_PRODUCT_ID: ${{ vars.MICROSOFT_STORE_PRODUCT_ID }}
        run: |
          $configuration = @{
            AZURE_AD_TENANT_ID = $env:AZURE_AD_TENANT_ID
            AZURE_AD_APPLICATION_CLIENT_ID = $env:AZURE_AD_APPLICATION_CLIENT_ID
            AZURE_AD_APPLICATION_SECRET = $env:AZURE_AD_APPLICATION_SECRET
            SELLER_ID = $env:SELLER_ID
            MICROSOFT_STORE_PRODUCT_ID = $env:MICROSOFT_STORE_PRODUCT_ID
          }
          foreach ($entry in $configuration.GetEnumerator()) {
            if ([string]::IsNullOrWhiteSpace($entry.Value) -or $entry.Value -eq "REPLACE_ME") {
              throw "Missing Microsoft Store configuration: $($entry.Key)"
            }
          }
          if ($env:MICROSOFT_STORE_PRODUCT_ID -ne "9NJ5Q0FG7TGL") {
            throw "Unexpected Microsoft Store product ID: $env:MICROSOFT_STORE_PRODUCT_ID"
          }
          $packages = @(
            Get-ChildItem build/release/windows -File -Filter *.msixupload -Recurse
          )
          if ($packages.Count -ne 1) {
            throw "Expected exactly one msixupload package, found $($packages.Count)"
          }
          "path=$($packages[0].FullName)" >> $env:GITHUB_OUTPUT

      - name: Set up Microsoft Store Developer CLI
        uses: microsoft/microsoft-store-apppublisher@15abd1c50fcc164b19cb240fb04ef3c49bf715a2 # v1.1
        with:
          version: v0.3.9

      - name: Authenticate and verify Store access
        shell: pwsh
        env:
          AZURE_AD_TENANT_ID: ${{ secrets.AZURE_AD_TENANT_ID }}
          AZURE_AD_APPLICATION_CLIENT_ID: ${{ secrets.AZURE_AD_APPLICATION_CLIENT_ID }}
          AZURE_AD_APPLICATION_SECRET: ${{ secrets.AZURE_AD_APPLICATION_SECRET }}
          SELLER_ID: ${{ secrets.SELLER_ID }}
          MICROSOFT_STORE_PRODUCT_ID: ${{ vars.MICROSOFT_STORE_PRODUCT_ID }}
        run: |
          msstore reconfigure `
            --tenantId "$env:AZURE_AD_TENANT_ID" `
            --sellerId "$env:SELLER_ID" `
            --clientId "$env:AZURE_AD_APPLICATION_CLIENT_ID" `
            --clientSecret "$env:AZURE_AD_APPLICATION_SECRET"
          if ($LASTEXITCODE -ne 0) {
            throw "Microsoft Store authentication failed"
          }
          msstore settings --enableTelemetry false
          if ($LASTEXITCODE -ne 0) {
            throw "Failed to disable Microsoft Store CLI telemetry"
          }
          msstore apps get "$env:MICROSOFT_STORE_PRODUCT_ID"
          if ($LASTEXITCODE -ne 0) {
            throw "The Microsoft Store application is not accessible"
          }

      - name: Publish package to Microsoft Store
        shell: pwsh
        env:
          MICROSOFT_STORE_PRODUCT_ID: ${{ vars.MICROSOFT_STORE_PRODUCT_ID }}
          STORE_PACKAGE: ${{ steps.store-package.outputs.path }}
        run: |
          msstore publish "$env:STORE_PACKAGE" `
            --appId "$env:MICROSOFT_STORE_PRODUCT_ID"
          if ($LASTEXITCODE -ne 0) {
            throw "Microsoft Store package publication failed"
          }

      - name: Summarize Store submission
        shell: pwsh
        env:
          VERSION: ${{ needs.preflight.outputs.version }}
          MICROSOFT_STORE_PRODUCT_ID: ${{ vars.MICROSOFT_STORE_PRODUCT_ID }}
        run: |
          "### Microsoft Store submission" >> $env:GITHUB_STEP_SUMMARY
          "- App version: $env:VERSION" >> $env:GITHUB_STEP_SUMMARY
          "- Product ID: $env:MICROSOFT_STORE_PRODUCT_ID" >> $env:GITHUB_STEP_SUMMARY
          "- Package submitted for certification" >> $env:GITHUB_STEP_SUMMARY

  publish-github:
    name: Publish coordinated GitHub Release
    needs:
      - preflight
      - linux
      - windows
      - macos
      - play-closed-testing
      - publish-microsoft-store
    runs-on: ubuntu-24.04
    timeout-minutes: 20
    permissions:
      contents: write
      id-token: write
      attestations: write

    steps:
      - name: Checkout
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Download Debian package
        uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
        with:
          name: vnidrop-${{ needs.preflight.outputs.version }}-linux-deb-x64
          path: build/release/downloads/deb

      - name: Download RPM package
        uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
        with:
          name: vnidrop-${{ needs.preflight.outputs.version }}-linux-rpm-x64
          path: build/release/downloads/rpm

      - name: Download macOS package
        uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
        with:
          name: vnidrop-${{ needs.preflight.outputs.version }}-macos-dmg
          path: build/release/downloads/macos

      - name: Download Windows Store package
        uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
        with:
          name: vnidrop-${{ needs.preflight.outputs.version }}-windows-store-x64
          path: build/release/downloads/windows

      - name: Download Play-signed APK
        uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
        with:
          name: vnidrop-${{ needs.preflight.outputs.version }}-android-play
          path: build/release/downloads/play

      - name: Verify and assemble public release assets
        run: packaging/release/assemble-release.sh

      - name: Attest release provenance
        uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4
        with:
          subject-path: build/release/final/*

      - name: Create GitHub Release
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh release create "$GITHUB_REF_NAME" \
            build/release/final/* \
            --repo "$GITHUB_REPOSITORY" \
            --verify-tag \
            --title "VniDrop ${{ needs.preflight.outputs.version }}" \
            --generate-notes

  update-homebrew:
    name: Update Homebrew cask
    needs:
      - preflight
      - macos
      - publish-github
    runs-on: ubuntu-24.04
    timeout-minutes: 15

    steps:
      - name: Checkout
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Download macOS package
        uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
        with:
          name: vnidrop-${{ needs.preflight.outputs.version }}-macos-dmg
          path: dist

      - name: Render Homebrew cask
        env:
          VERSION: ${{ needs.preflight.outputs.version }}
        run: |
          set -euo pipefail
          sha="$(sha256sum "dist/VniDrop-${VERSION}.dmg" | cut -d' ' -f1)"
          sed -e "s/^  version \".*\"/  version \"${VERSION}\"/" \
              -e "s/^  sha256 \".*\"/  sha256 \"${sha}\"/" \
              packaging/homebrew/vnidrop.rb > /tmp/vnidrop.rb

      - name: Push cask to tap
        env:
          TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
          VERSION: ${{ needs.preflight.outputs.version }}
        run: |
          set -euo pipefail
          git clone \
            "https://x-access-token:${TAP_TOKEN}@github.com/sudosylabs/homebrew-vnidrop.git" \
            tap
          mkdir -p tap/Casks
          cp /tmp/vnidrop.rb tap/Casks/vnidrop.rb
          cd tap
          git config user.name "vnidrop-release-bot"
          git config user.email "release-bot@users.noreply.github.com"
          git add Casks/vnidrop.rb
          git commit -m "vnidrop ${VERSION}" || {
            echo "Homebrew cask already matches ${VERSION}"
            exit 0
          }
          git push
