name: release

on:
  workflow_dispatch:

permissions:
  id-token: write
  contents: write

jobs:
  prereqs:
    name: Prerequisites
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.version.outputs.version }}
    steps:
      - uses: actions/checkout@v4

      - name: Set version
        run: echo "version=$(cat VERSION | sed -E 's/.[0-9]+$//')" >> $GITHUB_OUTPUT
        id: version

# ================================
#             macOS
# ================================
  create-macos-artifacts:
    name: Create macOS artifacts
    runs-on: macos-latest
    environment: release
    needs: prereqs
    strategy:
      matrix:
        runtime: [ osx-x64, osx-arm64 ]
    steps:
    - uses: actions/checkout@v4

    - name: Set up .NET
      uses: actions/setup-dotnet@v4.1.0
      with:
        dotnet-version: 8.0.x

    - name: Build
      run: |
        dotnet build src/osx/Installer.Mac/*.csproj \
         --configuration=MacRelease --no-self-contained \
         --runtime=${{ matrix.runtime }}

    - name: Run macOS unit tests
      run: |
        dotnet test --configuration=MacRelease

    - name: Lay out payload and symbols
      run: |
        src/osx/Installer.Mac/layout.sh \
         --configuration=MacRelease --output=payload \
         --symbol-output=symbols --runtime=${{ matrix.runtime }}

    - name: Set up signing/notarization infrastructure
      env:
        A1: ${{ secrets.GATEWATCHER_DEVELOPER_ID_CERT }}
        A2: ${{ secrets.GATEWATCHER_DEVELOPER_ID_PASSWORD }}
        I1: ${{ secrets.INSTALLER_CERTIFICATE_BASE64 }}
        I2: ${{ secrets.INSTALLER_CERTIFICATE_PASSWORD }}
        N1: ${{ secrets.APPLE_TEAM_ID }}
        N2: ${{ secrets.APPLE_DEVELOPER_ID }}
        N3: ${{ secrets.APPLE_DEVELOPER_PASSWORD }}
        N4: ${{ secrets.APPLE_KEYCHAIN_PROFILE }}
      run: |
        echo "Setting up signing certificates"
        security create-keychain -p pwd $RUNNER_TEMP/buildagent.keychain
        security default-keychain -s $RUNNER_TEMP/buildagent.keychain
        security unlock-keychain -p pwd $RUNNER_TEMP/buildagent.keychain

        echo $A1 | base64 -D > $RUNNER_TEMP/cert.p12
        security import $RUNNER_TEMP/cert.p12 \
          -k $RUNNER_TEMP/buildagent.keychain \
          -P $A2 \
          -T /usr/bin/codesign
        security set-key-partition-list \
          -S apple-tool:,apple:,codesign: \
          -s -k pwd \
          $RUNNER_TEMP/buildagent.keychain

        echo $I1 | base64 -D > $RUNNER_TEMP/cert.p12
        security import $RUNNER_TEMP/cert.p12 \
          -k $RUNNER_TEMP/buildagent.keychain \
          -P $I2 \
          -T /usr/bin/productbuild
        security set-key-partition-list \
          -S apple-tool:,apple:,productbuild: \
          -s -k pwd \
          $RUNNER_TEMP/buildagent.keychain

        echo "Setting up notarytool"
        xcrun notarytool store-credentials \
          --team-id $N1 \
          --apple-id $N2 \
          --password $N3 \
          "$N4"

    - name: Run codesign against payload
      env:
        A3: ${{ secrets.APPLE_APPLICATION_SIGNING_IDENTITY }}
      run: |
        ./src/osx/Installer.Mac/codesign.sh "payload" "$A3" \
          "$GITHUB_WORKSPACE/src/osx/Installer.Mac/entitlements.xml"

    - name: Create component package
      run: |
        src/osx/Installer.Mac/pack.sh --payload="payload" \
         --version="${{ needs.prereqs.outputs.version }}" \
         --output="components/com.microsoft.gitcredentialmanager.component.pkg"

    - name: Create and sign product archive
      env:
        I3: ${{ secrets.APPLE_INSTALLER_SIGNING_IDENTITY }}
      run: |
        src/osx/Installer.Mac/dist.sh --package-path=components \
         --version="${{ needs.prereqs.outputs.version }}" \
         --runtime="${{ matrix.runtime }}" \
         --output="pkg/gcm-${{ matrix.runtime }}-${{ needs.prereqs.outputs.version }}.pkg" \
         --identity="$I3" || exit 1

    - name: Notarize product archive
      env:
        N4: ${{ secrets.APPLE_KEYCHAIN_PROFILE }}
      run: |
        src/osx/Installer.Mac/notarize.sh \
          --package="pkg/gcm-${{ matrix.runtime }}-${{ needs.prereqs.outputs.version }}.pkg" \
          --keychain-profile="$N4"

    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: macos-${{ matrix.runtime }}-artifacts
        path: |
          ./pkg/*
          ./symbols/*
          ./payload/*

# ================================
#            Windows
# ================================
  create-windows-artifacts:
    name: Create Windows Artifacts
    runs-on: windows-latest
    environment: release
    needs: prereqs
    steps:
    - uses: actions/checkout@v4

    - name: Set up .NET
      uses: actions/setup-dotnet@v4.1.0
      with:
        dotnet-version: 8.0.x

    - name: Build
      run: |
        dotnet build --configuration=WindowsRelease

    - name: Run Windows unit tests
      run: |
        dotnet test --configuration=WindowsRelease

    - name: Lay out Windows payload and symbols
      run: |
        cd $env:GITHUB_WORKSPACE\src\windows\Installer.Windows\
        ./layout.ps1 -Configuration WindowsRelease `
          -Output $env:GITHUB_WORKSPACE\payload `
          -SymbolOutput $env:GITHUB_WORKSPACE\symbols

    - name: Log into Azure
      uses: azure/login@v2
      with:
        client-id: ${{ secrets.AZURE_CLIENT_ID }}
        tenant-id: ${{ secrets.AZURE_TENANT_ID }}
        subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

    - name: Sign payload files with Azure Code Signing
      uses: azure/trusted-signing-action@v0.5.0
      with:
        endpoint: https://wus2.codesigning.azure.net/
        trusted-signing-account-name: git-fundamentals-signing
        certificate-profile-name: git-fundamentals-windows-signing
        files-folder: ${{ github.workspace }}\payload
        files-folder-filter: exe,dll
        file-digest: SHA256
        timestamp-rfc3161: http://timestamp.acs.microsoft.com
        timestamp-digest: SHA256

    # The Azure Code Signing action overrides the .NET version, so we reset it.
    - name: Set up .NET
      uses: actions/setup-dotnet@v4.1.0
      with:
        dotnet-version: 8.0.x

    - name: Build with signed payload
      run: |
        dotnet build $env:GITHUB_WORKSPACE\src\windows\Installer.Windows `
          /p:PayloadPath=$env:GITHUB_WORKSPACE\payload /p:NoLayout=true `
          --configuration=WindowsRelease
        mkdir installers
        Move-Item -Path .\out\windows\Installer.Windows\bin\Release\net472\*.exe `
         -Destination $env:GITHUB_WORKSPACE\installers

    - name: Sign installers with Azure Code Signing
      uses: azure/trusted-signing-action@v0.5.0
      with:
        endpoint: https://wus2.codesigning.azure.net/
        trusted-signing-account-name: git-fundamentals-signing
        certificate-profile-name: git-fundamentals-windows-signing
        files-folder: ${{ github.workspace }}\installers
        files-folder-filter: exe
        file-digest: SHA256
        timestamp-rfc3161: http://timestamp.acs.microsoft.com
        timestamp-digest: SHA256

    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: windows-artifacts
        path: |
          payload
          installers
          symbols

# ================================
#             Linux
# ================================
  create-linux-artifacts:
    name: Create Linux Artifacts
    runs-on: ubuntu-latest
    environment: release
    needs: prereqs
    steps:
    - uses: actions/checkout@v4

    - name: Set up .NET
      uses: actions/setup-dotnet@v4.1.0
      with:
        dotnet-version: 8.0.x

    - name: Build
      run: dotnet build --configuration=LinuxRelease

    - name: Run Linux unit tests
      run: |
        dotnet test --configuration=LinuxRelease

    - name: Log into Azure
      uses: azure/login@v2
      with:
        client-id: ${{ secrets.AZURE_CLIENT_ID }}
        tenant-id: ${{ secrets.AZURE_TENANT_ID }}
        subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

    - name: Prepare for GPG signing
      env:
        AZURE_VAULT: ${{ secrets.AZURE_VAULT }}
        GPG_KEY_SECRET_NAME: ${{ secrets.GPG_KEY_SECRET_NAME }}
        GPG_PASSPHRASE_SECRET_NAME: ${{ secrets.GPG_PASSPHRASE_SECRET_NAME }}
        GPG_KEYGRIP_SECRET_NAME: ${{ secrets.GPG_KEYGRIP_SECRET_NAME }}
      run: |
        # Install debsigs
        sudo apt install debsigs

        # Download GPG key, passphrase, and keygrip from Azure Key Vault
        key=$(az keyvault secret show --name $GPG_KEY_SECRET_NAME --vault-name $AZURE_VAULT --query "value")
        passphrase=$(az keyvault secret show --name $GPG_PASSPHRASE_SECRET_NAME --vault-name $AZURE_VAULT --query "value")
        keygrip=$(az keyvault secret show --name $GPG_KEYGRIP_SECRET_NAME --vault-name $AZURE_VAULT --query "value")

        # Remove quotes from downloaded values
        key=$(sed -e 's/^"//' -e 's/"$//' <<<"$key")
        passphrase=$(sed -e 's/^"//' -e 's/"$//' <<<"$passphrase")
        keygrip=$(sed -e 's/^"//' -e 's/"$//' <<<"$keygrip")

        # Import GPG key
        echo "$key" | base64 -d | gpg --import --no-tty --batch --yes

        # Configure GPG
        echo "allow-preset-passphrase" > ~/.gnupg/gpg-agent.conf
        gpg-connect-agent RELOADAGENT /bye
        /usr/lib/gnupg2/gpg-preset-passphrase --preset "$keygrip" <<<"$passphrase"

    - name: Sign Debian package and tarball
      run: |
        # Sign Debian package
        version=${{ needs.prereqs.outputs.version }}
        mv out/linux/Packaging.Linux/Release/deb/gcm-linux_amd64.$version.deb .
        debsigs --sign=origin --verify --check gcm-linux_amd64.$version.deb

        # Generate tarball signature file
        mv -v out/linux/Packaging.Linux/Release/tar/* .
        gpg --batch --yes --armor --output gcm-linux_amd64.$version.tar.gz.asc \
          --detach-sig gcm-linux_amd64.$version.tar.gz

    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: linux-artifacts
        path: |
          ./*.deb
          ./*.asc
          ./*.tar.gz

# ================================
#           .NET Tool
# ================================
  dotnet-tool-build:
    name: Build .NET tool
    runs-on: ubuntu-latest
    needs: prereqs
    steps:
    - uses: actions/checkout@v4

    - name: Set up .NET
      uses: actions/setup-dotnet@v4.1.0
      with:
        dotnet-version: 8.0.x

    - name: Build .NET tool
      run: |
        src/shared/DotnetTool/layout.sh --configuration=Release

    - name: Upload .NET tool artifacts
      uses: actions/upload-artifact@v4
      with:
        name: tmp.dotnet-tool-build
        path: |
          out/shared/DotnetTool/nupkg/Release

  dotnet-tool-payload-sign:
    name: Sign .NET tool payload
    runs-on: windows-latest
    environment: release
    needs: dotnet-tool-build
    steps:
    - uses: actions/checkout@v4

    - name: Download payload
      uses: actions/download-artifact@v4
      with:
        name: tmp.dotnet-tool-build

    - name: Log into Azure
      uses: azure/login@v2
      with:
        client-id: ${{ secrets.AZURE_CLIENT_ID }}
        tenant-id: ${{ secrets.AZURE_TENANT_ID }}
        subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

    - name: Install sign CLI tool
      run: |
        dotnet tool install -g sign --version 0.9.1-beta.24325.5

    - name: Sign payload
      run: |
        sign.exe code trusted-signing payload/* `
          -tse https://wus2.codesigning.azure.net/ `
          -tsa git-fundamentals-signing `
          -tscp git-fundamentals-windows-signing

    - name: Lay out signed payload, images, and symbols
      shell: bash
      run: |
        mkdir dotnet-tool-payload-sign
        mv images payload.sym payload -t dotnet-tool-payload-sign

    - name: Upload signed payload
      uses: actions/upload-artifact@v4
      with:
        name: dotnet-tool-payload-sign
        path: |
          dotnet-tool-payload-sign

  dotnet-tool-pack:
    name: Package .NET tool
    runs-on: ubuntu-latest
    needs: [ prereqs, dotnet-tool-payload-sign ]
    steps:
    - uses: actions/checkout@v4

    - name: Download signed payload
      uses: actions/download-artifact@v4
      with:
        name: dotnet-tool-payload-sign
        path: signed

    - name: Set up .NET
      uses: actions/setup-dotnet@v4.1.0
      with:
        dotnet-version: 8.0.x

    - name: Package tool
      run: |
        src/shared/DotnetTool/pack.sh --configuration=Release \
          --version="${{ needs.prereqs.outputs.version }}" \
          --publish-dir=$(pwd)/signed

    - name: Upload unsigned package
      uses: actions/upload-artifact@v4
      with:
        name: tmp.dotnet-tool-package-unsigned
        path: |
          out/shared/DotnetTool/nupkg/Release/*.nupkg

  dotnet-tool-sign:
    name: Sign .NET tool package
    runs-on: windows-latest
    environment: release
    needs: dotnet-tool-pack
    steps:
    - uses: actions/checkout@v4

    - name: Download unsigned package
      uses: actions/download-artifact@v4
      with:
        name: tmp.dotnet-tool-package-unsigned
        path: nupkg

    - name: Log into Azure
      uses: azure/login@v2
      with:
        client-id: ${{ secrets.AZURE_CLIENT_ID }}
        tenant-id: ${{ secrets.AZURE_TENANT_ID }}
        subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

    - name: Install sign CLI tool
      run: |
        dotnet tool install -g sign --version 0.9.1-beta.24325.5

    - name: Sign package
      run: |
        sign.exe code trusted-signing nupkg/* `
          -tse https://wus2.codesigning.azure.net/ `
          -tsa git-fundamentals-signing `
          -tscp git-fundamentals-windows-signing

        mv nupkg/* .

    # Remove this once NuGet supports the subscriber identity validation EKU:
    # https://github.com/NuGet/NuGetGallery/issues/10027
    - name: Extract signing certificate from package
      shell: pwsh
      run: |
        dotnet tool install --global Knapcode.CertificateExtractor
        $nupkg = gci *.nupkg
        nuget-cert-extractor --file $nupkg --output certs --code-signing --author --leaf
        $cert = gci certs\*.cer
        mv $cert .\nuget-signing.cer

    - name: Publish signed package and certificate
      uses: actions/upload-artifact@v4
      with:
        name: dotnet-tool-sign
        path: |
          *.nupkg
          *.cer

# ================================
#           Validate
# ================================
  validate:
    name: Validate installers
    strategy:
      matrix:
        component:
          - os: ubuntu-latest
            artifact: linux-artifacts
            command: git-credential-manager
            description: linux
          - os: macos-latest
            artifact: macos-osx-x64-artifacts
            command: git-credential-manager
            description: osx-x64
          - os: windows-latest
            artifact: windows-artifacts
            # Even when a standalone GCM version is installed, GitHub actions
            # runners still only recognize the version bundled with Git for
            # Windows due to its placement on the PATH. For this reason, we use
            # the full path to our installation to validate the Windows version.
            command: "$PROGRAMFILES (x86)/Git Credential Manager/git-credential-manager.exe"
            description: windows
          - os: ubuntu-latest
            artifact: dotnet-tool-sign
            command: git-credential-manager
            description: dotnet-tool
    runs-on: ${{ matrix.component.os }}
    needs: [ create-macos-artifacts, create-windows-artifacts, create-linux-artifacts, dotnet-tool-sign ]
    steps:
      - uses: actions/checkout@v4

      - name: Set up .NET
        uses: actions/setup-dotnet@v4.1.0
        with:
          dotnet-version: 8.0.x

      - name: Download artifacts
        uses: actions/download-artifact@v4
        with:
          name: ${{ matrix.component.artifact }}

      - name: Install Windows
        if: contains(matrix.component.description, 'windows')
        shell: pwsh
        run: |
          $exePaths = Get-ChildItem -Path ./installers/*.exe | %{$_.FullName}
          foreach ($exePath in $exePaths)
          {
            Start-Process -Wait -FilePath "$exePath" -ArgumentList "/SILENT /VERYSILENT /NORESTART"
          }

      - name: Install Linux (Debian package)
        if: contains(matrix.component.description, 'linux')
        run: |
          debpath=$(find ./*.deb)
          sudo apt install $debpath
          "${{ matrix.component.command }}" configure

      - name: Install Linux (tarball)
        if: contains(matrix.component.description, 'linux')
        run: |
          # Ensure we find only the source tarball, not the symbols
          tarpath=$(find . -name '*[[:digit:]].tar.gz')
          tar -xvf $tarpath -C /usr/local/bin
          "${{ matrix.component.command }}" configure

      - name: Install macOS
        if: contains(matrix.component.description, 'osx-x64')
        run: |
          # Only validate x64, given arm64 agents are not available
          pkgpath=$(find ./pkg/*.pkg)
          sudo installer -pkg $pkgpath -target /

      - name: Install .NET tool
        if: contains(matrix.component.description, 'dotnet-tool')
        run: |
          nupkgpath=$(find ./*.nupkg)
          dotnet tool install -g --add-source $(dirname "$nupkgpath") git-credential-manager
          "${{ matrix.component.command }}" configure

      - name: Validate
        shell: bash
        run: |
          "${{ matrix.component.command }}" --version | sed 's/+.*//' >actual
          cat VERSION | sed -E 's/.[0-9]+$//' >expect
          cmp expect actual || exit 1

# ================================
#             Publish
# ================================
  create-github-release:
    name: Publish GitHub draft release
    runs-on: ubuntu-latest
    env:
      AZURE_VAULT: ${{ secrets.AZURE_VAULT }}
      GPG_PUBLIC_KEY_SECRET_NAME: ${{ secrets.GPG_PUBLIC_KEY_SECRET_NAME }}
    environment: release
    needs: [ prereqs, validate ]
    steps:
      - uses: actions/checkout@v4

      - name: Set up .NET
        uses: actions/setup-dotnet@v4.1.0
        with:
          dotnet-version: 8.0.x

      - name: Download artifacts
        uses: actions/download-artifact@v4

      - name: Archive macOS payload and symbols
        run: |
          version="${{ needs.prereqs.outputs.version }}"
          mkdir osx-payload-and-symbols

          tar -C macos-osx-x64-artifacts/payload -czf osx-payload-and-symbols/gcm-osx-x64-$version.tar.gz .
          tar -C macos-osx-x64-artifacts/symbols -czf osx-payload-and-symbols/gcm-osx-x64-$version-symbols.tar.gz .

          tar -C macos-osx-arm64-artifacts/payload -czf osx-payload-and-symbols/gcm-osx-arm64-$version.tar.gz .
          tar -C macos-osx-arm64-artifacts/symbols -czf osx-payload-and-symbols/gcm-osx-arm64-$version-symbols.tar.gz .

      - name: Archive Windows payload and symbols
        run: |
          version="${{ needs.prereqs.outputs.version }}"
          mkdir win-x86-payload-and-symbols
          zip -jr win-x86-payload-and-symbols/gcm-win-x86-$version.zip windows-artifacts/payload
          zip -jr win-x86-payload-and-symbols/gcm-win-x86-$version-symbols.zip windows-artifacts/symbols

      - name: Log into Azure
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Download GPG public key signature file
        run: |
          az keyvault secret show --name "$GPG_PUBLIC_KEY_SECRET_NAME" \
            --vault-name "$AZURE_VAULT" --query "value" \
            | sed -e 's/^"//' -e 's/"$//' | base64 -d >gcm-public.asc
          mv gcm-public.asc linux-artifacts

      - uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const path = require('path');
            const version = "${{ needs.prereqs.outputs.version }}"

            var releaseMetadata = {
              owner: context.repo.owner,
              repo: context.repo.repo
            };

            // Create the release
            var tagName = `v${version}`;
            var createdRelease = await github.rest.repos.createRelease({
              ...releaseMetadata,
              draft: true,
              tag_name: tagName,
              target_commitish: context.sha,
              name: `GCM ${version}`
            });
            releaseMetadata.release_id = createdRelease.data.id;

            // Uploads contents of directory to the release created above
            async function uploadDirectoryToRelease(directory, includeExtensions=[]) {
              return fs.promises.readdir(directory)
                .then(async(files) => Promise.all(
                  files.filter(file => {
                    return includeExtensions.length==0 || includeExtensions.includes(path.extname(file).toLowerCase());
                  })
                  .map(async (file) => {
                    var filePath = path.join(directory, file);
                    github.rest.repos.uploadReleaseAsset({
                      ...releaseMetadata,
                      name: file,
                      headers: {
                        "content-length": (await fs.promises.stat(filePath)).size
                      },
                      data: fs.createReadStream(filePath)
                    });
                  }))
                );
            }

            await Promise.all([
              // Upload Windows artifacts
              uploadDirectoryToRelease('windows-artifacts/installers'),
              uploadDirectoryToRelease('win-x86-payload-and-symbols'),

              // Upload macOS artifacts
              uploadDirectoryToRelease('macos-osx-x64-artifacts/pkg'),
              uploadDirectoryToRelease('macos-osx-arm64-artifacts/pkg'),
              uploadDirectoryToRelease('osx-payload-and-symbols'),

              // Upload Linux artifacts
              uploadDirectoryToRelease('linux-artifacts'),

              // Upload .NET tool package
              uploadDirectoryToRelease('dotnet-tool-sign'),
            ]);
