# GitHub release workflow.
name: Release
on:
  push:
    tags:
      - "v*"
  workflow_dispatch:
    inputs:
      go_version:
        description: "Go version to use for building."
        required: false
        default: "1.20.4"
      dry_run:
        description: Perform a dry-run release (devel). Note that ref must be an annotated tag when run without dry-run.
        type: boolean
        required: true
        default: false

permissions:
  # Required to publish a release
  contents: write
  # Necessary to push docker images to ghcr.io.
  packages: write
  # Necessary for GCP authentication (https://github.com/google-github-actions/setup-gcloud#usage)
  id-token: write

concurrency: ${{ github.workflow }}-${{ github.ref }}

env:
  # Use `inputs` (vs `github.event.inputs`) to ensure that booleans are actual
  # booleans, not strings.
  # https://github.blog/changelog/2022-06-10-github-actions-inputs-unified-across-manual-and-reusable-workflows/
  CODER_RELEASE: ${{ !inputs.dry_run }}
  CODER_DRY_RUN: ${{ inputs.dry_run }}
  # For some reason, setup-go won't actually pick up a new patch version if
  # it has an old one cached. We need to manually specify the versions so we
  # can get the latest release. Never use "~1.xx" here!
  CODER_GO_VERSION: "1.20.4"

jobs:
  release:
    name: Build and publish
    runs-on: ${{ github.repository_owner == 'coder' && 'ubuntu-latest-8-cores' || 'ubuntu-latest' }}
    env:
      # Necessary for Docker manifest
      DOCKER_CLI_EXPERIMENTAL: "enabled"
    outputs:
      version: ${{ steps.version.outputs.version }}
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      # If the event that triggered the build was an annotated tag (which our
      # tags are supposed to be), actions/checkout has a bug where the tag in
      # question is only a lightweight tag and not a full annotated tag. This
      # command seems to fix it.
      # https://github.com/actions/checkout/issues/290
      - name: Fetch git tags
        run: git fetch --tags --force

      - name: Print version
        id: version
        run: |
          set -euo pipefail
          version="$(./scripts/version.sh)"
          echo "version=$version" >> $GITHUB_OUTPUT
          # Speed up future version.sh calls.
          echo "CODER_FORCE_VERSION=$version" >> $GITHUB_ENV
          echo "$version"

      - name: Create release notes
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          # We always have to set this since there might be commits on
          # main that didn't have a PR.
          CODER_IGNORE_MISSING_COMMIT_METADATA: "1"
        run: |
          set -euo pipefail
          ref=HEAD
          old_version="$(git describe --abbrev=0 "$ref^1")"
          version="$(./scripts/version.sh)"

          # Generate notes.
          release_notes_file="$(mktemp -t release_notes.XXXXXX)"
          ./scripts/release/generate_release_notes.sh --old-version "$old_version" --new-version "$version" --ref "$ref" >> "$release_notes_file"
          echo CODER_RELEASE_NOTES_FILE="$release_notes_file" >> $GITHUB_ENV

      - name: Show release notes
        run: |
          set -euo pipefail
          cat "$CODER_RELEASE_NOTES_FILE"

      - name: Docker Login
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: actions/setup-go@v4
        with:
          go-version: ${{ env.CODER_GO_VERSION }}

      - name: Cache Node
        id: cache-node
        uses: actions/cache@v3
        with:
          path: |
            **/node_modules
            .eslintcache
          key: js-${{ runner.os }}-test-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            js-${{ runner.os }}-

      - name: Install nsis and zstd
        run: sudo apt-get install -y nsis zstd

      - name: Install nfpm
        run: |
          set -euo pipefail
          wget -O /tmp/nfpm.deb https://github.com/goreleaser/nfpm/releases/download/v2.18.1/nfpm_amd64.deb
          sudo dpkg -i /tmp/nfpm.deb
          rm /tmp/nfpm.deb

      - name: Install rcodesign
        run: |
          set -euo pipefail
          wget -O /tmp/rcodesign.tar.gz https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-x86_64-unknown-linux-musl.tar.gz
          sudo tar -xzf /tmp/rcodesign.tar.gz \
            -C /usr/bin \
            --strip-components=1 \
            apple-codesign-0.22.0-x86_64-unknown-linux-musl/rcodesign
          rm /tmp/rcodesign.tar.gz

      - name: Setup Apple Developer certificate and API key
        run: |
          set -euo pipefail
          touch /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8}
          chmod 600 /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8}
          echo "$AC_CERTIFICATE_P12_BASE64" | base64 -d > /tmp/apple_cert.p12
          echo "$AC_CERTIFICATE_PASSWORD" > /tmp/apple_cert_password.txt
          echo "$AC_APIKEY_P8_BASE64" | base64 -d > /tmp/apple_apikey.p8
        env:
          AC_CERTIFICATE_P12_BASE64: ${{ secrets.AC_CERTIFICATE_P12_BASE64 }}
          AC_CERTIFICATE_PASSWORD: ${{ secrets.AC_CERTIFICATE_PASSWORD }}
          AC_APIKEY_P8_BASE64: ${{ secrets.AC_APIKEY_P8_BASE64 }}

      - name: Build binaries
        run: |
          set -euo pipefail
          go mod download

          version="$(./scripts/version.sh)"
          make gen/mark-fresh
          make -j \
            build/coder_"$version"_linux_{amd64,armv7,arm64}.{tar.gz,apk,deb,rpm} \
            build/coder_"$version"_{darwin,windows}_{amd64,arm64}.zip \
            build/coder_"$version"_windows_amd64_installer.exe \
            build/coder_helm_"$version".tgz
        env:
          CODER_SIGN_DARWIN: "1"
          AC_CERTIFICATE_FILE: /tmp/apple_cert.p12
          AC_CERTIFICATE_PASSWORD_FILE: /tmp/apple_cert_password.txt
          AC_APIKEY_ISSUER_ID: ${{ secrets.AC_APIKEY_ISSUER_ID }}
          AC_APIKEY_ID: ${{ secrets.AC_APIKEY_ID }}
          AC_APIKEY_FILE: /tmp/apple_apikey.p8

      - name: Delete Apple Developer certificate and API key
        run: rm -f /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8}

      - name: Determine base image tag
        id: image-base-tag
        run: |
          set -euo pipefail
          if [[ "${CODER_RELEASE:-}" != *t* ]] || [[ "${CODER_DRY_RUN:-}" == *t* ]]; then
            # Empty value means use the default and avoid building a fresh one.
            echo "tag=" >> $GITHUB_OUTPUT
          else
            echo "tag=$(CODER_IMAGE_BASE=ghcr.io/coder/coder-base ./scripts/image_tag.sh)" >> $GITHUB_OUTPUT
          fi

      - name: Create empty base-build-context directory
        if: steps.image-base-tag.outputs.tag != ''
        run: mkdir base-build-context

      - name: Install depot.dev CLI
        if: steps.image-base-tag.outputs.tag != ''
        uses: depot/setup-action@v1

      # This uses OIDC authentication, so no auth variables are required.
      - name: Build base Docker image via depot.dev
        if: steps.image-base-tag.outputs.tag != ''
        uses: depot/build-push-action@v1
        with:
          project: wl5hnrrkns
          context: base-build-context
          file: scripts/Dockerfile.base
          platforms: linux/amd64,linux/arm64,linux/arm/v7
          pull: true
          no-cache: true
          push: true
          tags: |
            ${{ steps.image-base-tag.outputs.tag }}

      - name: Verify that images are pushed properly
        run: |
          # retry 10 times with a 5 second delay as the images may not be
          # available immediately
          for i in {1..10}; do
            rc=0
            raw_manifests=$(docker buildx imagetools inspect --raw "${{ steps.image-base-tag.outputs.tag }}") || rc=$?
            if [[ "$rc" -eq 0 ]]; then
              break
            fi
            if [[ "$i" -eq 10 ]]; then
              echo "Failed to pull manifests after 10 retries"
              exit 1
            fi
            echo "Failed to pull manifests, retrying in 5 seconds"
            sleep 5
          done

          manifests=$(
            echo "$raw_manifests" | \
              jq -r '.manifests[].platform | .os + "/" + .architecture + (if .variant then "/" + .variant else "" end)'
          )

          # Verify all 3 platforms are present.
          set -euxo pipefail
          echo "$manifests" | grep -q linux/amd64
          echo "$manifests" | grep -q linux/arm64
          echo "$manifests" | grep -q linux/arm/v7

      - name: Build Linux Docker images
        run: |
          set -euxo pipefail

          # build Docker images for each architecture
          version="$(./scripts/version.sh)"
          make -j build/coder_"$version"_linux_{amd64,arm64,armv7}.tag

          # we can't build multi-arch if the images aren't pushed, so quit now
          # if dry-running
          if [[ "$CODER_RELEASE" != *t* ]]; then
            echo Skipping multi-arch docker builds due to dry-run.
            exit 0
          fi

          # build and push multi-arch manifest, this depends on the other images
          # being pushed so will automatically push them.
          make -j push/build/coder_"$version"_linux.tag

          # if the current version is equal to the highest (according to semver)
          # version in the repo, also create a multi-arch image as ":latest" and
          # push it
          if [[ "$(git tag | grep '^v' | grep -vE '(rc|dev|-|\+|\/)' | sort -r --version-sort | head -n1)" == "v$(./scripts/version.sh)" ]]; then
            ./scripts/build_docker_multiarch.sh \
              --push \
              --target "$(./scripts/image_tag.sh --version latest)" \
              $(cat build/coder_"$version"_linux_{amd64,arm64,armv7}.tag)
          fi
        env:
          CODER_BASE_IMAGE_TAG: ${{ steps.image-base-tag.outputs.tag }}

      - name: ls build
        run: ls -lh build

      - name: Publish release
        run: |
          set -euo pipefail

          publish_args=()
          if [[ $CODER_DRY_RUN == *t* ]]; then
            publish_args+=(--dry-run)
          fi
          declare -p publish_args

          ./scripts/release/publish.sh \
            "${publish_args[@]}" \
            --release-notes-file "$CODER_RELEASE_NOTES_FILE" \
            ./build/*_installer.exe \
            ./build/*.zip \
            ./build/*.tar.gz \
            ./build/*.tgz \
            ./build/*.apk \
            ./build/*.deb \
            ./build/*.rpm
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          CODER_GPG_RELEASE_KEY_BASE64: ${{ secrets.GPG_RELEASE_KEY_BASE64 }}

      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v1
        with:
          workload_identity_provider: ${{ secrets.GCP_WORKLOAD_ID_PROVIDER }}
          service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}

      - name: Setup GCloud SDK
        uses: "google-github-actions/setup-gcloud@v1"

      - name: Publish Helm Chart
        if: ${{ !inputs.dry_run }}
        run: |
          set -euo pipefail
          version="$(./scripts/version.sh)"
          mkdir -p build/helm
          cp "build/coder_helm_${version}.tgz" build/helm
          gsutil cp gs://helm.coder.com/v2/index.yaml build/helm/index.yaml
          helm repo index build/helm --url https://helm.coder.com/v2 --merge build/helm/index.yaml
          gsutil -h "Cache-Control:no-cache,max-age=0" cp build/helm/coder_helm_${version}.tgz gs://helm.coder.com/v2
          gsutil -h "Cache-Control:no-cache,max-age=0" cp build/helm/index.yaml gs://helm.coder.com/v2
          gsutil -h "Cache-Control:no-cache,max-age=0" cp helm/artifacthub-repo.yml gs://helm.coder.com/v2

      - name: Upload artifacts to actions (if dry-run)
        if: ${{ inputs.dry_run }}
        uses: actions/upload-artifact@v3
        with:
          name: release-artifacts
          path: |
            ./build/*_installer.exe
            ./build/*.zip
            ./build/*.tar.gz
            ./build/*.tgz
            ./build/*.apk
            ./build/*.deb
            ./build/*.rpm
          retention-days: 7

      - name: Start Packer builds
        if: ${{ !inputs.dry_run }}
        uses: peter-evans/repository-dispatch@v2
        with:
          token: ${{ secrets.CDRCI_GITHUB_TOKEN }}
          repository: coder/packages
          event-type: coder-release
          client-payload: '{"coder_version": "${{ steps.version.outputs.version }}"}'

  publish-winget:
    name: Publish to winget-pkgs
    runs-on: windows-latest
    needs: release
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      # If the event that triggered the build was an annotated tag (which our
      # tags are supposed to be), actions/checkout has a bug where the tag in
      # question is only a lightweight tag and not a full annotated tag. This
      # command seems to fix it.
      # https://github.com/actions/checkout/issues/290
      - name: Fetch git tags
        run: git fetch --tags --force

      - name: Install wingetcreate
        run: |
          Invoke-WebRequest https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe

      - name: Submit updated manifest to winget-pkgs
        run: |
          # The package version is the same as the tag minus the leading "v".
          # The version in this output already has the leading "v" removed but
          # we do it again to be safe.
          $version = "${{ needs.release.outputs.version }}".Trim('v')

          $release_assets = gh release view --repo coder/coder "v${version}" --json assets | `
            ConvertFrom-Json
          # Get the installer URL from the release assets.
          $installer_url = $release_assets.assets | `
            Where-Object name -Match ".*_windows_amd64_installer.exe$" | `
            Select -ExpandProperty url

          echo "Installer URL: ${installer_url}"
          echo "Package version: ${version}"

          # Bail if dry-run.
          if ($env:CODER_DRY_RUN -match "t") {
            echo "Skipping submission due to dry-run."
            exit 0
          }

          # The URL "|X64" suffix forces the architecture as it cannot be
          # sniffed properly from the URL. wingetcreate checks both the URL and
          # binary magic bytes for the architecture and they need to both match,
          # but they only check for `x64`, `win64` and `_64` in the URL. Our URL
          # contains `amd64` which doesn't match sadly.
          #
          # wingetcreate will still do the binary magic bytes check, so if we
          # accidentally change the architecture of the installer, it will fail
          # submission.
          .\wingetcreate.exe update Coder.Coder `
            --submit `
            --version "${version}" `
            --urls "${installer_url}|X64" `
            --token "$env:WINGET_GH_TOKEN"

        env:
          # For gh CLI:
          GH_TOKEN: ${{ github.token }}
          # For wingetcreate. We need a real token since we're pushing a commit
          # to GitHub and then making a PR in a different repo.
          WINGET_GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }}

      - name: Comment on PR
        if: ${{ !inputs.dry_run }}
        run: |
          # Find the PR that wingetcreate just made.
          $version = "${{ needs.release.outputs.version }}".Trim('v')
          $pr_list = gh pr list --repo microsoft/winget-pkgs --search "author:cdrci Coder.Coder version ${version}" --limit 1 --json number | `
            ConvertFrom-Json
          $pr_number = $pr_list[0].number

          gh pr comment --repo microsoft/winget-pkgs "${pr_number}" --body "🤖 cc: @deansheather @matifali"

        env:
          # For gh CLI. We need a real token since we're commenting on a PR in a
          # different repo.
          GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }}
