name: Build and Release XWorkmate Packages

env:
  VAULT_ADDR: https://vault.svc.plus
  FLUTTER_VERSION: 3.41.4

on:
  push:
    branches:
      - main
      - "release/**"
    tags:
      - "v*"
    paths-ignore:
      - "README.md"
  pull_request:
    paths:
      - "lib/**"
      - "assets/**"
      - "android/**"
      - "ios/**"
      - "macos/**"
      - "linux/**"
      - "windows/**"
      - "rust/**"
      - "test/**"
      - "scripts/**"
      - "pubspec.*"
      - "Makefile"
      - ".github/actions/setup-flutter-sdk/action.yml"
      - ".github/workflows/build-and-release.yml"
  workflow_dispatch:
    inputs:
      enable_testflight:
        description: "Build & upload TestFlight (macOS/iOS App Store) artifacts"
        type: boolean
        default: false

permissions:
  contents: read
  id-token: write

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

jobs:
  prepare:
    if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' || (github.event_name == 'pull_request' && github.base_ref == 'main') }}
    runs-on: ubuntu-22.04
    needs:
      - verify
    permissions:
      contents: write
    outputs:
      should_release: ${{ steps.flags.outputs.should_release }}
      testflight_enabled: ${{ steps.flags.outputs.testflight_enabled }}
      release_tag: ${{ steps.meta.outputs.release_tag }}
      release_title: ${{ steps.meta.outputs.release_title }}
      release_notes: ${{ steps.meta.outputs.release_notes }}
    steps:
      - name: Checkout source
        uses: actions/checkout@v7
        with:
          fetch-depth: 0

      - name: Determine release mode
        id: flags
        shell: bash
        env:
          ENABLE_TESTFLIGHT_INPUT: ${{ github.event.inputs.enable_testflight }}
          ENABLE_TESTFLIGHT_VAR: ${{ vars.ENABLE_TESTFLIGHT }}
        run: |
          if [[ "${GITHUB_REF:-}" == refs/tags/v* || "${GITHUB_EVENT_NAME:-}" == "workflow_dispatch" || "${GITHUB_REF:-}" == "refs/heads/main" ]]; then
            echo "should_release=true" >> "$GITHUB_OUTPUT"
          else
            echo "should_release=false" >> "$GITHUB_OUTPUT"
          fi

          # TestFlight is opt-in (default OFF). Enabled only when explicitly
          # requested via the workflow_dispatch input or the ENABLE_TESTFLIGHT
          # repo/org variable. Keeps missing Apple signing secrets from failing
          # the normal DMG/IPA release path.
          if [[ "${ENABLE_TESTFLIGHT_INPUT:-}" == "true" || "${ENABLE_TESTFLIGHT_VAR:-}" == "true" ]]; then
            echo "testflight_enabled=true" >> "$GITHUB_OUTPUT"
          else
            echo "testflight_enabled=false" >> "$GITHUB_OUTPUT"
          fi

      - name: Compute release metadata
        id: meta
        shell: bash
        run: bash ./scripts/ci/compute_release_metadata.sh

  verify:
    runs-on: ubuntu-22.04
    steps:
      - name: Checkout source
        uses: actions/checkout@v7

      - name: Set up Flutter SDK
        uses: ./.github/actions/setup-flutter-sdk
        with:
          flutter-version: ${{ env.FLUTTER_VERSION }}

      - name: Install Linux dependencies
        shell: bash
        run: bash ./scripts/ci/setup_platform_deps.sh linux

      - name: Run Flutter verification suite
        shell: bash
        run: bash ./scripts/ci/run_flutter_ci_suite.sh

  build:
    if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' || (github.event_name == 'pull_request' && github.base_ref == 'main') }}
    name: Build ${{ matrix.platform }} ${{ matrix.package }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - platform: linux
            arch: amd64
            package: deb-rpm
            runs_on: ubuntu-22.04
            artifact_name: build-linux-amd64-deb-rpm
            artifact_paths: |
              dist/linux/*.deb
              dist/linux/*.rpm
          - platform: windows
            arch: amd64
            package: msi
            runs_on: windows-2022
            artifact_name: build-windows-amd64-msi
            artifact_paths: |
              dist/windows/*.msi
              dist/windows/*.zip
          - platform: macos
            arch: arm64
            package: dmg
            runs_on: macos-14
            artifact_name: build-macos-arm64-dmg
            artifact_paths: |
              dist/macos/*.dmg
          - platform: macos
            arch: arm64
            package: app-store-pkg
            # Quoted so the `matrix.release_only != 'true'` guard compares
            # string-to-string. A YAML boolean here coerces to a number in
            # GitHub expressions (true -> 1, 'true' -> NaN), making the guard
            # always true and building this release-only leg on every PR.
            release_only: "true"
            runs_on: macos-14
            artifact_name: build-macos-arm64-pkg
            artifact_paths: |
              dist/macos-app-store/*.pkg
          - platform: ios
            arch: arm64
            package: ipa
            runs_on: macos-14
            artifact_name: build-ios-arm64-ipa
            artifact_paths: |
              dist/ios/*.ipa
              dist/ios/*.zip
          - platform: android
            arch: arm64
            package: apk
            runs_on: ubuntu-22.04
            artifact_name: build-android-arm64-apk
            artifact_paths: |
              dist/android/*.apk
    runs-on: ${{ matrix.runs_on }}
    needs:
      - prepare
    env:
      PLATFORM: ${{ matrix.platform }}
      ARCH: ${{ matrix.arch }}
      SHOULD_RELEASE: ${{ needs.prepare.outputs.should_release }}
    steps:
      - name: Checkout source
        uses: actions/checkout@v7

      # Secrets are loaded per-platform so a missing/extra field for one OS
      # family never fails the matrix legs of the others (vault-action's
      # ignoreNotFound does NOT suppress field-level "No match data" errors).
      - name: Load Vault secrets (Apple)
        id: vault_apple
        if: ${{ (matrix.platform == 'macos' || matrix.platform == 'ios') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
        uses: hashicorp/vault-action@v4
        with:
          url: ${{ env.VAULT_ADDR }}
          method: jwt
          role: github-actions-xworkmate-app
          jwtGithubAudience: vault
          ignoreNotFound: true
          secrets: |
            kv/data/github-actions/xworkmate-app XWORKMATE_SIGN_IDENTITY | XWORKMATE_SIGN_IDENTITY ;
            kv/data/github-actions/xworkmate-app APPLE_CERT_P12_BASE64 | APPLE_CERT_P12_BASE64 ;
            kv/data/github-actions/xworkmate-app APPLE_CERT_PASSWORD | APPLE_CERT_PASSWORD ;
            kv/data/github-actions/xworkmate-app APPLE_PROVISION_PROFILE_BASE64 | APPLE_PROVISION_PROFILE_BASE64 ;
            kv/data/github-actions/xworkmate-app APPLE_MAC_PROVISION_PROFILE_BASE64 | APPLE_MAC_PROVISION_PROFILE_BASE64 ;
            kv/data/github-actions/xworkmate-app APPLE_KEYCHAIN_PASSWORD | APPLE_KEYCHAIN_PASSWORD ;
            kv/data/github-actions/xworkmate-app APPLE_EXPORT_METHOD | APPLE_EXPORT_METHOD

      - name: Load Vault secrets (Windows)
        id: vault_windows
        if: ${{ matrix.platform == 'windows' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
        uses: hashicorp/vault-action@v4
        with:
          url: ${{ env.VAULT_ADDR }}
          method: jwt
          role: github-actions-xworkmate-app
          jwtGithubAudience: vault
          ignoreNotFound: true
          secrets: |
            kv/data/github-actions/xworkmate-app WINDOWS_PFX_BASE64 | WINDOWS_PFX_BASE64 ;
            kv/data/github-actions/xworkmate-app WINDOWS_PFX_PASSWORD | WINDOWS_PFX_PASSWORD ;
            kv/data/github-actions/xworkmate-app WINDOWS_CODESIGN_SUBJECT | WINDOWS_CODESIGN_SUBJECT

      - name: Load Vault secrets (Android)
        id: vault_android
        if: ${{ matrix.platform == 'android' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
        uses: hashicorp/vault-action@v4
        with:
          url: ${{ env.VAULT_ADDR }}
          method: jwt
          role: github-actions-xworkmate-app
          jwtGithubAudience: vault
          ignoreNotFound: true
          secrets: |
            kv/data/github-actions/xworkmate-app ANDROID_KEYSTORE_BASE64 | ANDROID_KEYSTORE_BASE64 ;
            kv/data/github-actions/xworkmate-app ANDROID_KEYSTORE_PASSWORD | ANDROID_KEYSTORE_PASSWORD ;
            kv/data/github-actions/xworkmate-app ANDROID_KEY_ALIAS | ANDROID_KEY_ALIAS ;
            kv/data/github-actions/xworkmate-app ANDROID_KEY_PASSWORD | ANDROID_KEY_PASSWORD

      - name: Export signing secrets
        shell: bash
        run: |
          {
            echo "XWORKMATE_SIGN_IDENTITY=${{ steps.vault_apple.outputs.XWORKMATE_SIGN_IDENTITY }}"
            echo "APPLE_CERT_P12_BASE64=${{ steps.vault_apple.outputs.APPLE_CERT_P12_BASE64 }}"
            echo "APPLE_CERT_PASSWORD=${{ steps.vault_apple.outputs.APPLE_CERT_PASSWORD }}"
            echo "APPLE_PROVISION_PROFILE_BASE64=${{ steps.vault_apple.outputs.APPLE_PROVISION_PROFILE_BASE64 }}"
            echo "APPLE_KEYCHAIN_PASSWORD=${{ steps.vault_apple.outputs.APPLE_KEYCHAIN_PASSWORD }}"
            echo "APPLE_EXPORT_METHOD=${{ steps.vault_apple.outputs.APPLE_EXPORT_METHOD }}"
            echo "WINDOWS_PFX_BASE64=${{ steps.vault_windows.outputs.WINDOWS_PFX_BASE64 }}"
            echo "WINDOWS_PFX_PASSWORD=${{ steps.vault_windows.outputs.WINDOWS_PFX_PASSWORD }}"
            echo "WINDOWS_CODESIGN_SUBJECT=${{ steps.vault_windows.outputs.WINDOWS_CODESIGN_SUBJECT }}"
            echo "ANDROID_KEYSTORE_BASE64=${{ steps.vault_android.outputs.ANDROID_KEYSTORE_BASE64 }}"
            echo "ANDROID_KEYSTORE_PASSWORD=${{ steps.vault_android.outputs.ANDROID_KEYSTORE_PASSWORD }}"
            echo "ANDROID_KEY_ALIAS=${{ steps.vault_android.outputs.ANDROID_KEY_ALIAS }}"
            echo "ANDROID_KEY_PASSWORD=${{ steps.vault_android.outputs.ANDROID_KEY_PASSWORD }}"
          } >> "$GITHUB_ENV"

      - name: Set up Flutter SDK
        uses: ./.github/actions/setup-flutter-sdk
        with:
          flutter-version: ${{ env.FLUTTER_VERSION }}

      - name: Set up Java 17 for Android
        if: ${{ matrix.platform == 'android' }}
        uses: actions/setup-java@v5
        with:
          distribution: temurin
          java-version: "17"

      - name: Preflight platform lane
        id: preflight
        shell: bash
        run: bash ./scripts/ci/platform_preflight.sh "$PLATFORM" "$SHOULD_RELEASE"

      - name: Install platform dependencies
        if: ${{ steps.preflight.outputs.should_build_platform == 'true' }}
        shell: bash
        run: bash ./scripts/ci/setup_platform_deps.sh "$PLATFORM"

      - name: Install Go
        if: ${{ matrix.platform == 'macos' && steps.preflight.outputs.should_build_platform == 'true' }}
        uses: actions/setup-go@v6
        with:
          go-version: "1.24.1"

      - name: Build platform artifacts
        if: ${{ steps.preflight.outputs.should_build_platform == 'true' && (matrix.release_only != 'true' || env.SHOULD_RELEASE == 'true') && (matrix.package != 'app-store-pkg' || needs.prepare.outputs.testflight_enabled == 'true') }}
        shell: bash
        run: bash ./scripts/ci/build_matrix_artifacts.sh "$PLATFORM" "$ARCH" "${{ matrix.package }}" "$SHOULD_RELEASE"

      - name: Upload build artifacts
        if: ${{ steps.preflight.outputs.should_build_platform == 'true' && (matrix.release_only != 'true' || env.SHOULD_RELEASE == 'true') && (matrix.package != 'app-store-pkg' || needs.prepare.outputs.testflight_enabled == 'true') }}
        uses: actions/upload-artifact@v7
        with:
          name: ${{ matrix.artifact_name }}
          path: ${{ matrix.artifact_paths }}
          if-no-files-found: error

  remote_contract:
    name: Test - remote provider contract
    runs-on: ubuntu-22.04
    needs:
      - build
    # Test-stage quality gate: runs between build and release.
    # continue-on-error keeps it skippable so a failure never blocks release.
    continue-on-error: true
    if: ${{ github.event_name != 'push' && github.event_name != 'pull_request' }}
    steps:
      - name: Checkout source
        uses: actions/checkout@v7

      - name: Load Vault secrets
        id: vault
        uses: hashicorp/vault-action@v4
        with:
          url: ${{ env.VAULT_ADDR }}
          method: jwt
          role: github-actions-xworkmate-app
          jwtGithubAudience: vault
          ignoreNotFound: true
          secrets: |
            kv/data/github-actions/xworkmate-app REVIEW_ACCOUNT_LOGIN_PASSWORD | REVIEW_ACCOUNT_LOGIN_PASSWORD

      - name: Export remote contract secrets
        run: echo "REVIEW_ACCOUNT_LOGIN_PASSWORD=${{ steps.vault.outputs.REVIEW_ACCOUNT_LOGIN_PASSWORD }}" >> "$GITHUB_ENV"

      - name: Verify accounts to bridge provider contract
        shell: bash
        env:
          REVIEW_ACCOUNT_BASE_URL: ${{ vars.REVIEW_ACCOUNT_BASE_URL }}
          REVIEW_ACCOUNT_LOGIN_NAME: ${{ vars.REVIEW_ACCOUNT_LOGIN_NAME }}
        run: bash ./scripts/ci/verify_remote_provider_contract.sh

  release:
    # always() so release waits for the remote_contract gate to finish but is
    # never blocked by it being skipped (e.g. push events) or failing.
    # build/prepare must still genuinely succeed.
    if: ${{ always() && needs.prepare.outputs.should_release == 'true' && needs.prepare.result == 'success' && needs.build.result == 'success' }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: github_release
            runs_on: ubuntu-22.04
          - target: testflight_ios
            runs_on: macos-14
            artifact_name: build-ios-arm64-ipa
            artifact_path: release-artifacts/build-ios-arm64-ipa
            testflight_platform: ios
          - target: testflight_macos
            runs_on: macos-14
            artifact_name: build-macos-arm64-pkg
            artifact_path: release-artifacts/build-macos-arm64-pkg
            testflight_platform: macos
    runs-on: ${{ matrix.runs_on }}
    permissions:
      contents: write
    needs:
      - prepare
      - build
      - remote_contract
    steps:
      - name: Checkout source
        uses: actions/checkout@v7

      - name: Load App Store Connect secrets
        id: vault
        if: ${{ matrix.target != 'github_release' && needs.prepare.outputs.testflight_enabled == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
        uses: hashicorp/vault-action@v4
        with:
          url: ${{ env.VAULT_ADDR }}
          method: jwt
          role: github-actions-xworkmate-app
          jwtGithubAudience: vault
          ignoreNotFound: true
          secrets: |
            kv/data/github-actions/xworkmate-app APPLE_CERT_P12_BASE64 | APPLE_CERT_P12_BASE64 ;
            kv/data/github-actions/xworkmate-app APPLE_CERT_PASSWORD | APPLE_CERT_PASSWORD ;
            kv/data/github-actions/xworkmate-app APPLE_MAC_PROVISION_PROFILE_BASE64 | APPLE_MAC_PROVISION_PROFILE_BASE64 ;
            kv/data/github-actions/xworkmate-app APPLE_KEYCHAIN_PASSWORD | APPLE_KEYCHAIN_PASSWORD ;
            kv/data/github-actions/xworkmate-app APP_STORE_CONNECT_API_KEY_ID | APP_STORE_CONNECT_API_KEY_ID ;
            kv/data/github-actions/xworkmate-app APP_STORE_CONNECT_ISSUER_ID | APP_STORE_CONNECT_ISSUER_ID ;
            kv/data/github-actions/xworkmate-app APP_STORE_CONNECT_API_KEY_P8_BASE64 | APP_STORE_CONNECT_API_KEY_P8_BASE64

      - name: Export App Store Connect secrets
        if: ${{ matrix.target != 'github_release' && needs.prepare.outputs.testflight_enabled == 'true' }}
        run: |
          {
            echo "APPLE_CERT_P12_BASE64=${{ steps.vault.outputs.APPLE_CERT_P12_BASE64 }}"
            echo "APPLE_CERT_PASSWORD=${{ steps.vault.outputs.APPLE_CERT_PASSWORD }}"
            echo "APPLE_MAC_PROVISION_PROFILE_BASE64=${{ steps.vault.outputs.APPLE_MAC_PROVISION_PROFILE_BASE64 }}"
            echo "APPLE_KEYCHAIN_PASSWORD=${{ steps.vault.outputs.APPLE_KEYCHAIN_PASSWORD }}"
            echo "APP_STORE_CONNECT_API_KEY_ID=${{ steps.vault.outputs.APP_STORE_CONNECT_API_KEY_ID }}"
            echo "APP_STORE_CONNECT_ISSUER_ID=${{ steps.vault.outputs.APP_STORE_CONNECT_ISSUER_ID }}"
            echo "APP_STORE_CONNECT_API_KEY_P8_BASE64=${{ steps.vault.outputs.APP_STORE_CONNECT_API_KEY_P8_BASE64 }}"
          } >> "$GITHUB_ENV"

      - name: Download all artifacts
        if: ${{ matrix.target == 'github_release' }}
        uses: actions/download-artifact@v8
        with:
          path: release-artifacts

      - name: Upload assets to GitHub Release
        if: ${{ matrix.target == 'github_release' }}
        shell: bash
        run: bash ./scripts/ci/github_release_upload.sh release-artifacts
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }}
          RELEASE_TITLE: ${{ needs.prepare.outputs.release_title }}
          RELEASE_NOTES: ${{ needs.prepare.outputs.release_notes }}

      - name: Download TestFlight artifact
        if: ${{ matrix.target != 'github_release' && needs.prepare.outputs.testflight_enabled == 'true' }}
        uses: actions/download-artifact@v8
        with:
          name: ${{ matrix.artifact_name }}
          path: ${{ matrix.artifact_path }}

      - name: Upload to TestFlight
        if: ${{ matrix.target != 'github_release' && needs.prepare.outputs.testflight_enabled == 'true' }}
        shell: bash
        run: bash ./scripts/ci/testflight_upload.sh "${{ matrix.testflight_platform }}" "${{ matrix.artifact_path }}"
