name: Build Desktop

permissions:
  contents: write
  id-token: write
  attestations: write
  actions: write # needed for ccache action to be able to delete gha caches

on:
  push:
    branches:
      - master
    paths:
      - '*/**'    # matches all files not in root
      - '!**.md'
      - '!.github/**'
      - '!.husky/**'
      - '!.tx/**'
      - '!doc/**'
      - '!webclient/**'
      - '.github/workflows/desktop-build.yml'
      - 'CMakeLists.txt'
      - 'vcpkg.json'
      - 'vcpkg'
    tags:
      - '*'
  pull_request:
    paths:
      - '*/**'    # matches all files not in root
      - '!**.md'
      - '!.github/**'
      - '!.husky/**'
      - '!.tx/**'
      - '!doc/**'
      - '!webclient/**'
      - '.github/workflows/desktop-build.yml'
      - 'CMakeLists.txt'
      - 'vcpkg.json'
      - 'vcpkg'

# Cancel earlier, unfinished runs of this workflow on the same branch (unless on master)
concurrency:
  group: "${{ github.workflow }} @ ${{ github.ref_name }}"
  cancel-in-progress: ${{ github.ref_name != 'master' }}

jobs:
  configure:
    name: Configure
    runs-on: ubuntu-latest
    outputs:
      tag: ${{steps.configure.outputs.tag}}
      sha: ${{steps.configure.outputs.sha}}

    steps:  
      - name: Configure
        id: configure
        shell: bash
        run: |
          tag_regex='^refs/tags/'
          if [[ $GITHUB_EVENT_NAME == pull-request ]]; then    # pull request
            sha="${{github.event.pull_request.head.sha}}"
          elif [[ $GITHUB_REF =~ $tag_regex ]]; then    # release
            sha="$GITHUB_SHA"
            tag="${GITHUB_REF/refs\/tags\//}"
            echo "tag=$tag" >>"$GITHUB_OUTPUT"
          else    # push to branch
            sha="$GITHUB_SHA"
          fi
          echo "sha=$sha" >>"$GITHUB_OUTPUT"

      - name: Checkout
        if: steps.configure.outputs.tag != null
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Prepare release parameters
        id: prepare
        if: steps.configure.outputs.tag != null
        shell: bash
        env:
          TAG: ${{steps.configure.outputs.tag}}
        run: .ci/prep_release.sh

      - name: Create release
        if: steps.configure.outputs.tag != null
        id: create_release
        shell: bash
        env:
          GH_TOKEN: ${{github.token}}
          tag_name: ${{steps.configure.outputs.tag}}
          target: ${{steps.configure.outputs.sha}}
          release_name: ${{steps.prepare.outputs.title}}
          body_path: ${{steps.prepare.outputs.body_path}}
          prerelease: ${{steps.prepare.outputs.is_beta}}
        run: |
          if [[ $prerelease == true ]]; then
            args="--prerelease"
          fi
          gh release create "$tag_name" --draft --verify-tag $args \
              --target "$target" --title "$release_name" \
              --notes-file "$body_path"

  build-linux:
    strategy:
      fail-fast: false
      matrix:
        # These names correspond to the files in ".ci/$distro$version"
        include:
          - distro: Arch
            package: false    # We are packaged in Arch already
            allow-failure: true

          - distro: Debian
            version: 11
            package: DEB

          - distro: Servatrice_Debian
            version: 11
            package: DEB
            test: false
            server_only: true

          - distro: Debian
            version: 12
            package: DEB
            test: false    # Running tests on all distros is superfluous

          - distro: Debian
            version: 13
            package: DEB

          - distro: Fedora
            version: 42
            package: RPM
            test: false    # Running tests on all distros is superfluous

          - distro: Fedora
            version: 43
            package: RPM

          - distro: Ubuntu
            version: 22.04
            package: DEB
            test: false    # Running tests on all distros is superfluous

          - distro: Ubuntu
            version: 24.04
            package: DEB
            test: false    # Running tests on all distros is superfluous

          - distro: Ubuntu
            version: 26.04
            package: DEB

    name: ${{matrix.distro}} ${{matrix.version}}
    needs: configure
    runs-on: ubuntu-latest
    continue-on-error: ${{matrix.allow-failure == 'true'}}
    env:
      NAME: ${{matrix.distro}}${{matrix.version}}
      CACHE: ${{github.workspace}}/.cache/${{matrix.distro}}${{matrix.version}}   # directory for caching docker image and ccache
      # Cache size over the entire repo is 10Gi:
      # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
      CCACHE_SIZE: 550M
      CMAKE_GENERATOR: 'Ninja'

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

      - name: Restore compiler cache (ccache)
        id: ccache_restore
        uses: actions/cache/restore@v5
        env:
          BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
        with:
          path: ${{env.CACHE}}
          key: ccache-${{matrix.distro}}${{matrix.version}}-${{env.BRANCH_NAME}}
          restore-keys: ccache-${{matrix.distro}}${{matrix.version}}-

      - name: Build ${{matrix.distro}} ${{matrix.version}} Docker image
        shell: bash
        run: source .ci/docker.sh --build

      - name: Build debug and test
        if: matrix.test != 'false'
        shell: bash
        env:
          CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}'
        run: |
          source .ci/docker.sh
          RUN --server --debug --test --ccache "$CCACHE_SIZE"

      - name: Build release package
        id: build
        if: matrix.package != 'false'
        shell: bash
        env:
          BUILD_DIR: build
          SUFFIX: '-${{matrix.distro}}${{matrix.version}}'
          package: '${{matrix.package}}'
          CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}'
          NO_CLIENT: ${{matrix.server_only == 'true' && '--no-client' || '' }}
        run: |
          source .ci/docker.sh
          RUN --server --release --package "$package" --dir "$BUILD_DIR" \
                  --ccache "$CCACHE_SIZE" $NO_CLIENT
          .ci/name_build.sh

      # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342.
      - name: Delete remote compiler cache (ccache)
        if: github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit
        env:
          GH_TOKEN: ${{ github.token }}
        run: gh cache delete --repo ${{ github.repository }} ${{ steps.ccache_restore.outputs.cache-primary-key }}

      - name: Save updated compiler cache (ccache)
        if: github.ref == 'refs/heads/master'
        uses: actions/cache/save@v5
        with:
          path: ${{env.CACHE}}
          key: ${{ steps.ccache_restore.outputs.cache-primary-key }}

      - name: Upload artifact
        id: upload_artifact
        if: matrix.package != 'false'
        uses: actions/upload-artifact@v7
        with:
          path: ${{steps.build.outputs.path}}
          archive: false
          if-no-files-found: error

      - name: Upload to release
        id: upload_release
        if: matrix.package != 'false' && needs.configure.outputs.tag != null
        shell: bash
        env:
          GH_TOKEN: ${{github.token}}
          tag_name: ${{needs.configure.outputs.tag}}
          asset_name: ${{steps.build.outputs.fullname}}
          asset_path: ${{steps.build.outputs.path}}
        run: gh release upload "$tag_name" "$asset_path#$asset_name"

      - name: Attest binary provenance
        id: attestation
        if: steps.upload_release.outcome == 'success'
        uses: actions/attest@v4
        with:
          subject-path: ${{steps.build.outputs.path}}
          show-summary: false

      - name: Verify binary attestation
        if: steps.attestation.outcome == 'success'
        shell: bash
        env:
          GH_TOKEN: ${{github.token}}
        run: gh attestation verify ${{steps.build.outputs.path}} --repo Cockatrice/Cockatrice

  build-vcpkg:
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: macOS
            target: 13
            runner: macos-15-intel
            soc: Intel
            xcode: "16.4"
            type: Release
            override_target: 13
            make_package: 1
            package_suffix: "-macOS13_Intel"
            qt_version: 6.10.*
            qt_arch: clang_64
            qt_modules: qtimageformats qtmultimedia qtwebsockets
            cmake_generator: Ninja
            use_ccache: 1

          - os: macOS
            target: 14
            runner: macos-14
            soc: Apple
            xcode: "15.4"
            type: Release
            make_package: 1
            package_suffix: "-macOS14"
            qt_version: 6.10.*
            qt_arch: clang_64
            qt_modules: qtimageformats qtmultimedia qtwebsockets
            cmake_generator: Ninja
            use_ccache: 1

          - os: macOS
            target: 15
            runner: macos-15
            soc: Apple
            xcode: "16.4"
            type: Release
            make_package: 1
            package_suffix: "-macOS15"
            qt_version: 6.10.*
            qt_arch: clang_64
            qt_modules: qtimageformats qtmultimedia qtwebsockets
            cmake_generator: Ninja
            use_ccache: 1

          - os: macOS
            target: 15
            runner: macos-15
            soc: Apple
            xcode: "16.4"
            type: Debug
            qt_version: 6.10.*
            qt_arch: clang_64
            qt_modules: qtimageformats qtmultimedia qtwebsockets
            cmake_generator: Ninja
            use_ccache: 1

          - os: Windows
            target: 10
            runner: windows-2025
            type: Release
            make_package: 1
            package_suffix: "-Win10"
            qt_version: 6.10.*
            qt_arch: win64_msvc2022_64
            qt_modules: qtimageformats qtmultimedia qtwebsockets
            cmake_generator: "Visual Studio 17 2022"
            cmake_generator_platform: x64

    name: ${{matrix.os}} ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
    needs: configure
    runs-on: ${{matrix.runner}}
    env:
      CCACHE_DIR: ${{github.workspace}}/.cache/
      # Cache size over the entire repo is 10Gi:
      # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
      CCACHE_SIZE: 550M

    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          submodules: recursive

      - name: Add msbuild to PATH
        if: matrix.os == 'Windows'
        id: add-msbuild
        uses: microsoft/setup-msbuild@v3
        with:
          msbuild-architecture: x64

      - name: Setup ccache
        if: matrix.use_ccache == 1 && matrix.os == 'macOS'
        run: brew install ccache

      - name: Restore compiler cache (ccache)
        if: matrix.use_ccache == 1
        id: ccache_restore
        uses: actions/cache/restore@v5
        env:
          BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
        with:
          path: ${{env.CCACHE_DIR}}
          key: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}-${{env.BRANCH_NAME}}
          restore-keys: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}-

      - name: Install aqtinstall
        run: pipx install aqtinstall

      # Resolve given wildcard versions (e.g. Qt 6.6.*) to latest version via aqtinstall to avoid stale caches on new releases
      - name: Resolve latest Qt patch version
        id: resolve_qt_version
        shell: bash
        run: .ci/resolve_latest_aqt_qt_version.sh "${{matrix.qt_version}}"

      - name: Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries (${{ matrix.soc }} macOS)
        if: matrix.os == 'macOS'
        id: restore_qt
        uses: actions/cache/restore@v5
        with:
          path: ${{ github.workspace }}/Qt
          key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}

      # Using jurplel/install-qt-action to install Qt without using brew
      # qt build using vcpkg either just fails or takes too long to build
      - name: Install fat Qt ${{ steps.resolve_qt_version.outputs.version }} (${{ matrix.soc }} macOS)
        if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
        uses: jurplel/install-qt-action@v4
        with:
          version: ${{ steps.resolve_qt_version.outputs.version }}
          arch: ${{matrix.qt_arch}}
          modules: ${{matrix.qt_modules}}
          cache: false
          dir: ${{github.workspace}}

      - name: Thin Qt libraries (${{ matrix.soc }} macOS)
        if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
        run: .ci/thin_macos_qtlib.sh

      - name: Cache thin Qt libraries (${{ matrix.soc }} macOS)
        if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
        uses: actions/cache/save@v5
        with:
          path: ${{ github.workspace }}/Qt
          key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}

      - name: Install Qt ${{matrix.qt_version}} (Windows)
        if: matrix.os == 'Windows'
        uses: jurplel/install-qt-action@v4
        with:
          version: ${{ steps.resolve_qt_version.outputs.version }}
          arch: ${{matrix.qt_arch}}
          modules: ${{matrix.qt_modules}}
          cache: true

      - name: Install NSIS
        if: matrix.os == 'Windows'
        shell: bash
        run: choco install nsis

      - name: Setup vcpkg cache
        id: vcpkg-cache
        uses: TAServers/vcpkg-cache@v3
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

      # uses environment variables, see compile.sh for more details
      - name: Build Cockatrice
        id: build
        shell: bash
        env:
          BUILDTYPE: '${{matrix.type}}'
          MAKE_PACKAGE: '${{matrix.make_package}}'
          PACKAGE_SUFFIX: '${{matrix.package_suffix}}'
          CMAKE_GENERATOR: ${{matrix.cmake_generator}}
          CMAKE_GENERATOR_PLATFORM: ${{matrix.cmake_generator_platform}}
          USE_CCACHE: ${{matrix.use_ccache}}
          VCPKG_DISABLE_METRICS: 1
          VCPKG_BINARY_SOURCES: 'clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite'
          # macOS-specific environment variables, will be ignored on Windows
          MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
          MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
          MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
          MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
          DEVELOPER_DIR: '/Applications/Xcode_${{matrix.xcode}}.app/Contents/Developer'
          TARGET_MACOS_VERSION: ${{ matrix.override_target }}
        run: .ci/compile.sh --server --test --vcpkg

      # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342.
      - name: Delete remote compiler cache (ccache)
        if: github.ref == 'refs/heads/master' && matrix.use_ccache == 1 && steps.ccache_restore.outputs.cache-hit
        env:
          GH_TOKEN: ${{ github.token }}
        run: gh cache delete --repo ${{ github.repository }} ${{ steps.ccache_restore.outputs.cache-primary-key }}

      - name: Save updated compiler cache (ccache)
        if: github.ref == 'refs/heads/master' && matrix.use_ccache == 1
        uses: actions/cache/save@v5
        with:
          path: ${{env.CCACHE_DIR}}
          key: ${{ steps.ccache_restore.outputs.cache-primary-key }}

      - name: Sign app bundle
        if: matrix.os == 'macOS' && matrix.make_package && needs.configure.outputs.tag != null
        id: sign_macos
        env:
          MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
          MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
        run: |
          if [[ -n "$MACOS_CERTIFICATE_NAME" ]]
          then
            security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
            /usr/bin/codesign --sign="$MACOS_CERTIFICATE_NAME" --entitlements=".ci/macos.entitlements" --options=runtime --force --deep --timestamp --verbose ${{steps.build.outputs.path}}
          fi

      - name: Notarize app bundle
        if: steps.sign_macos.outcome == 'success'
        env:
          MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
          MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
          MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}
        run: |
          if [[ -n "$MACOS_NOTARIZATION_APPLE_ID" ]]
          then
            # Store the notarization credentials so that we can prevent a UI password dialog from blocking the CI
            echo "Create keychain profile"
            xcrun notarytool store-credentials "notarytool-profile" --apple-id "$MACOS_NOTARIZATION_APPLE_ID" --team-id "$MACOS_NOTARIZATION_TEAM_ID" --password "$MACOS_NOTARIZATION_PWD"

            # We can't notarize an app bundle directly, but we need to compress it as an archive.
            # Therefore, we create a zip file containing our app bundle, so that we can send it to the
            # notarization service
            echo "Creating temp notarization archive"
            ditto -c -k --keepParent ${{steps.build.outputs.path}} "notarization.zip"

            # Here we send the notarization request to the Apple's Notarization service, waiting for the result.
            # This typically takes a few seconds inside a CI environment, but it might take more depending on the App
            # characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if
            # you're curious
            echo "Notarize app"
            xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait

            # Finally, we need to "attach the staple" to our executable, which will allow our app to be
            # validated by macOS even when an internet connection is not available.
            echo "Attach staple"
            xcrun stapler staple ${{steps.build.outputs.path}}
          fi

      - name: Upload artifact
        if: matrix.make_package
        id: upload_artifact
        uses: actions/upload-artifact@v7
        with:
          path: ${{steps.build.outputs.path}}
          archive: false
          if-no-files-found: error

      - name: Upload PDBs (Program Databases)
        if: matrix.os == 'Windows' && github.ref_type != 'tag'
        uses: actions/upload-artifact@v7
        with:
          name: ${{steps.build.outputs.name}}-PDBs
          path: |
            build/cockatrice/Release/*.pdb
            build/oracle/Release/*.pdb
            build/servatrice/Release/*.pdb
          if-no-files-found: error

      - name: Upload to release
        if: needs.configure.outputs.tag != null && matrix.make_package == '1'
        id: upload_release
        shell: bash
        env:
          GH_TOKEN: ${{github.token}}
          tag_name: ${{needs.configure.outputs.tag}}
          asset_name: ${{steps.build.outputs.fullname}}
          asset_path: ${{steps.build.outputs.path}}
        run: gh release upload "$tag_name" "$asset_path#$asset_name"

      - name: Attest binary provenance
        if: steps.upload_release.outcome == 'success'
        id: attestation
        uses: actions/attest@v4
        with:
          subject-path: ${{steps.build.outputs.path}}
          show-summary: false

      - name: Verify binary attestation
        if: steps.attestation.outcome == 'success'
        shell: bash
        env:
          GH_TOKEN: ${{github.token}}
        run: gh attestation verify ${{steps.build.outputs.path}} --repo Cockatrice/Cockatrice
