# GitHub release workflow.
name: Release
on:
  push:
    tags:
      - "v*"
  workflow_dispatch:
    inputs:
      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 }}

jobs:
  release:
    name: Build and publish
    runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
    env:
      # Necessary for Docker manifest
      DOCKER_CLI_EXPERIMENTAL: "enabled"
    outputs:
      version: ${{ steps.version.outputs.version }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        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="v$(./scripts/version.sh)"

          # Generate notes.
          release_notes_file="$(mktemp -t release_notes.XXXXXX)"
          ./scripts/release/generate_release_notes.sh --check-for-changelog --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@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Setup Go
        uses: ./.github/actions/setup-go

      - name: Setup Node
        uses: ./.github/actions/setup-node

      - 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.35.1/nfpm_2.35.1_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 \
            build/provisioner_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: Generate offline docs
        run: |
          version="$(./scripts/version.sh)"
          make -j build/coder_docs_"$version".tgz

      - 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@v2
        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@v2"

      - 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
          cp "build/provisioner_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/provisioner_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@v4
        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@v3
        with:
          token: ${{ secrets.CDRCI_GITHUB_TOKEN }}
          repository: coder/packages
          event-type: coder-release
          client-payload: '{"coder_version": "${{ steps.version.outputs.version }}"}'

  publish-homebrew:
    name: Publish to Homebrew tap
    runs-on: ubuntu-latest
    needs: release
    if: ${{ !inputs.dry_run }}

    steps:
      # TODO: skip this if it's not a new release (i.e. a backport). This is
      #       fine right now because it just makes a PR that we can close.
      - name: Update homebrew
        env:
          # Variables used by the `gh` command
          GH_REPO: coder/homebrew-coder
          GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }}
        run: |
          # Keep version number around for reference, removing any potential leading v
          coder_version="$(echo "${{ needs.release.outputs.version }}" | tr -d v)"

          set -euxo pipefail

          # Setup Git
          git config --global user.email "ci@coder.com"
          git config --global user.name "Coder CI"
          git config --global credential.helper "store"

          temp_dir="$(mktemp -d)"
          cd "$temp_dir"

          # Download checksums
          checksums_url="$(gh release view --repo coder/coder "v$coder_version" --json assets \
            | jq -r ".assets | map(.url) | .[]" \
            | grep -e ".checksums.txt\$")"
          wget "$checksums_url" -O checksums.txt

          # Get the SHAs
          darwin_arm_sha="$(cat checksums.txt | grep "darwin_arm64.zip" | awk '{ print $1 }')"
          darwin_intel_sha="$(cat checksums.txt | grep "darwin_amd64.zip" | awk '{ print $1 }')"
          linux_sha="$(cat checksums.txt | grep "linux_amd64.tar.gz" | awk '{ print $1 }')"

          echo "macOS arm64: $darwin_arm_sha"
          echo "macOS amd64: $darwin_intel_sha"
          echo "Linux amd64: $linux_sha"

          # Check out the homebrew repo
          git clone "https://github.com/$GH_REPO" homebrew-coder
          brew_branch="auto-release/$coder_version"
          cd homebrew-coder

          # Check if a PR already exists.
          pr_count="$(gh pr list --search "head:$brew_branch" --json id,closed | jq -r ".[] | select(.closed == false) | .id" | wc -l)"
          if [[ "$pr_count" > 0 ]]; then
            echo "Bailing out as PR already exists" 2>&1
            exit 0
          fi

          # Set up cdrci credentials for pushing to homebrew-coder
          echo "https://x-access-token:$GH_TOKEN@github.com" >> ~/.git-credentials
          # Update the formulae and push
          git checkout -b "$brew_branch"
          ./scripts/update-v2.sh "$coder_version" "$darwin_arm_sha" "$darwin_intel_sha" "$linux_sha"
          git add .
          git commit -m "coder $coder_version"
          git push -u origin -f "$brew_branch"

          # Create PR
          gh pr create \
            -B master -H "$brew_branch" \
            -t "coder $coder_version" \
            -b "" \
            -r "${{ github.actor }}" \
            -a "${{ github.actor }}" \
            -b "This automatic PR was triggered by the release of Coder v$coder_version"

  publish-winget:
    name: Publish to winget-pkgs
    runs-on: windows-latest
    needs: release
    if: ${{ !inputs.dry_run }}

    steps:
      - name: Sync fork
        run: gh repo sync cdrci/winget-pkgs -b master
        env:
          GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }}

      - name: Checkout
        uses: actions/checkout@v4
        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 URLs from the release assets.
          $amd64_installer_url = $release_assets.assets | `
            Where-Object name -Match ".*_windows_amd64_installer.exe$" | `
            Select -ExpandProperty url
          $amd64_zip_url = $release_assets.assets | `
            Where-Object name -Match ".*_windows_amd64.zip$" | `
            Select -ExpandProperty url
          $arm64_zip_url = $release_assets.assets | `
            Where-Object name -Match ".*_windows_arm64.zip$" | `
            Select -ExpandProperty url

          echo "amd64 Installer URL: ${amd64_installer_url}"
          echo "amd64 zip URL: ${amd64_zip_url}"
          echo "arm64 zip URL: ${arm64_zip_url}"
          echo "Package version: ${version}"

          .\wingetcreate.exe update Coder.Coder `
            --submit `
            --version "${version}" `
            --urls "${amd64_installer_url}" "${amd64_zip_url}" "${arm64_zip_url}" `
            --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
        run: |
          # wait 30 seconds
          Start-Sleep -Seconds 30.0
          # 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 }}

  # publish-sqlc pushes the latest schema to sqlc cloud.
  # At present these pushes cannot be tagged, so the last push is always the latest.
  publish-sqlc:
    name: "Publish to schema sqlc cloud"
    runs-on: "ubuntu-latest"
    needs: release
    if: ${{ !inputs.dry_run }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 1

      # We need golang to run the migration main.go
      - name: Setup Go
        uses: ./.github/actions/setup-go

      - name: Setup sqlc
        uses: ./.github/actions/setup-sqlc

      - name: Push schema to sqlc cloud
        # Don't block a release on this
        continue-on-error: true
        run: |
          make sqlc-push
