# This action will trigger when
# 1. when the workflow is manually triggered
# 2. ./scripts/deploy_pr.sh is run locally
# 3. when a PR is updated
name: Deploy PR
on:
  push:
    branches-ignore:
      - main
  workflow_dispatch:
    inputs:
      experiments:
        description: "Experiments to enable"
        required: false
        type: string
        default: "*"
      build:
        description: "Force new build"
        required: false
        type: boolean
        default: false
      deploy:
        description: "Force new deployment"
        required: false
        type: boolean
        default: false

env:
  REPO: ghcr.io/coder/coder-preview

permissions:
  contents: read
  packages: write
  pull-requests: write # needed for commenting on PRs

jobs:
  check_pr:
    runs-on: ubuntu-latest
    outputs:
      PR_OPEN: ${{ steps.check_pr.outputs.pr_open }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Check if PR is open
        id: check_pr
        run: |
          set -euo pipefail
          pr_open=true
          if [[ "$(gh pr view --json state | jq -r '.state')" != "OPEN" ]]; then
            echo "PR doesn't exist or is closed."
            pr_open=false
          fi
          echo "pr_open=$pr_open" >> $GITHUB_OUTPUT
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  get_info:
    needs: check_pr
    if: ${{ needs.check_pr.outputs.PR_OPEN == 'true' }}
    outputs:
      PR_NUMBER: ${{ steps.pr_info.outputs.PR_NUMBER }}
      PR_TITLE: ${{ steps.pr_info.outputs.PR_TITLE }}
      PR_URL: ${{ steps.pr_info.outputs.PR_URL }}
      CODER_BASE_IMAGE_TAG: ${{ steps.set_tags.outputs.CODER_BASE_IMAGE_TAG }}
      CODER_IMAGE_TAG: ${{ steps.set_tags.outputs.CODER_IMAGE_TAG }}
      NEW: ${{ steps.check_deployment.outputs.NEW }}
      BUILD: ${{ steps.build_conditionals.outputs.first_or_force_build == 'true' || steps.build_conditionals.outputs.automatic_rebuild == 'true' }}

    runs-on: "ubuntu-latest"
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get PR number, title, and branch name
        id: pr_info
        run: |
          set -euo pipefail
          PR_NUMBER=$(gh pr view --json number | jq -r '.number')
          PR_TITLE=$(gh pr view --json title | jq -r '.title')
          PR_URL=$(gh pr view --json url | jq -r '.url')
          echo "PR_URL=$PR_URL" >> $GITHUB_OUTPUT
          echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_OUTPUT
          echo "PR_TITLE=$PR_TITLE" >> $GITHUB_OUTPUT
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Set required tags
        id: set_tags
        run: |
          set -euo pipefail
          echo "CODER_BASE_IMAGE_TAG=$CODER_BASE_IMAGE_TAG" >> $GITHUB_OUTPUT
          echo "CODER_IMAGE_TAG=$CODER_IMAGE_TAG" >> $GITHUB_OUTPUT
        env:
          CODER_BASE_IMAGE_TAG: ghcr.io/coder/coder-preview-base:pr${{ steps.pr_info.outputs.PR_NUMBER }}
          CODER_IMAGE_TAG: ghcr.io/coder/coder-preview:pr${{ steps.pr_info.outputs.PR_NUMBER }}

      - name: Set up kubeconfig
        run: |
          set -euo pipefail
          mkdir -p ~/.kube
          echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG }}" > ~/.kube/config
          chmod 644 ~/.kube/config
          export KUBECONFIG=~/.kube/config

      - name: Check if the helm deployment already exists
        id: check_deployment
        run: |
          set -euo pipefail
          if helm status "pr${{ steps.pr_info.outputs.PR_NUMBER }}" --namespace "pr${{ steps.pr_info.outputs.PR_NUMBER }}" > /dev/null 2>&1; then
            echo "Deployment already exists. Skipping deployment."
            NEW=false
          else
            echo "Deployment doesn't exist."
            NEW=true
          fi
          echo "NEW=$NEW" >> $GITHUB_OUTPUT

      - name: Check changed files
        uses: dorny/paths-filter@v3
        id: filter
        with:
          base: ${{ github.ref }}
          filters: |
            all:
              - "**"
            ignored:
              - "docs/**"
              - "README.md"
              - "examples/web-server/**"
              - "examples/monitoring/**"
              - "examples/lima/**"
              - ".github/**"
              - "offlinedocs/**"
              - ".devcontainer/**"
              - "helm/**"
              - "*[^g][^o][^.][^s][^u][^m]*"
              - "*[^g][^o][^.][^m][^o][^d]*"
              - "*[^M][^a][^k][^e][^f][^i][^l][^e]*"
              - "scripts/**/*[^D][^o][^c][^k][^e][^r][^f][^i][^l][^e]*"
              - "scripts/**/*[^D][^o][^c][^k][^e][^r][^f][^i][^l][^e][.][b][^a][^s][^e]*"

      - name: Print number of changed files
        run: |
          set -euo pipefail
          echo "Total number of changed files: ${{ steps.filter.outputs.all_count }}"
          echo "Number of ignored files: ${{ steps.filter.outputs.ignored_count }}"

      - name: Build conditionals
        id: build_conditionals
        run: |
          set -euo pipefail
          # build if the workflow is manually triggered and the deployment doesn't exist (first build or force rebuild)
          echo "first_or_force_build=${{ (github.event_name == 'workflow_dispatch' && steps.check_deployment.outputs.NEW == 'true') || github.event.inputs.build == 'true' }}" >> $GITHUB_OUTPUT
          # build if the deployment alreday exist and there are changes in the files that we care about (automatic updates)
          echo "automatic_rebuild=${{ steps.check_deployment.outputs.NEW == 'false' && steps.filter.outputs.all_count > steps.filter.outputs.ignored_count }}" >> $GITHUB_OUTPUT

  comment-pr:
    needs: get_info
    if: needs.get_info.outputs.BUILD == 'true' || github.event.inputs.deploy == 'true'
    runs-on: "ubuntu-latest"
    steps:
      - name: Find Comment
        uses: peter-evans/find-comment@v3
        id: fc
        with:
          issue-number: ${{ needs.get_info.outputs.PR_NUMBER }}
          comment-author: "github-actions[bot]"
          body-includes: ":rocket:"
          direction: last

      - name: Comment on PR
        id: comment_id
        uses: peter-evans/create-or-update-comment@v4
        with:
          comment-id: ${{ steps.fc.outputs.comment-id }}
          issue-number: ${{ needs.get_info.outputs.PR_NUMBER }}
          edit-mode: replace
          body: |
            ---
            :rocket: Deploying PR ${{ needs.get_info.outputs.PR_NUMBER }} ...
            ---
          reactions: eyes
          reactions-edit-mode: replace

  build:
    needs: get_info
    # Run build job only if there are changes in the files that we care about or if the workflow is manually triggered with --build flag
    if: needs.get_info.outputs.BUILD == 'true'
    runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
    # This concurrency only cancels build jobs if a new build is triggred. It will avoid cancelling the current deployemtn in case of docs chnages.
    concurrency:
      group: build-${{ github.workflow }}-${{ github.ref }}-${{ needs.get_info.outputs.BUILD }}
      cancel-in-progress: true
    env:
      DOCKER_CLI_EXPERIMENTAL: "enabled"
      CODER_IMAGE_TAG: ${{ needs.get_info.outputs.CODER_IMAGE_TAG }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

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

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

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

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

      - name: Build and push Linux amd64 Docker image
        run: |
          set -euo pipefail
          go mod download
          make gen/mark-fresh
          export DOCKER_IMAGE_NO_PREREQUISITES=true
          version="$(./scripts/version.sh)"
          export CODER_IMAGE_BUILD_BASE_TAG="$(CODER_IMAGE_BASE=coder-base ./scripts/image_tag.sh --version "$version")"
          make -j build/coder_linux_amd64
          ./scripts/build_docker.sh \
            --arch amd64 \
            --target ${{ env.CODER_IMAGE_TAG }} \
            --version $version \
            --push \
            build/coder_linux_amd64

  deploy:
    needs: [build, get_info]
    # Run deploy job only if build job was successful or skipped
    if: |
      always() && (needs.build.result == 'success' || needs.build.result == 'skipped') &&
      (needs.get_info.outputs.BUILD == 'true' || github.event.inputs.deploy == 'true')
    runs-on: "ubuntu-latest"
    env:
      CODER_IMAGE_TAG: ${{ needs.get_info.outputs.CODER_IMAGE_TAG }}
      PR_NUMBER: ${{ needs.get_info.outputs.PR_NUMBER }}
      PR_TITLE: ${{ needs.get_info.outputs.PR_TITLE }}
      PR_URL: ${{ needs.get_info.outputs.PR_URL }}
      PR_HOSTNAME: "pr${{ needs.get_info.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}"
    steps:
      - name: Set up kubeconfig
        run: |
          set -euo pipefail
          mkdir -p ~/.kube
          echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG }}" > ~/.kube/config
          chmod 644 ~/.kube/config
          export KUBECONFIG=~/.kube/config

      - name: Check if image exists
        run: |
          set -euo pipefail
          foundTag=$(
            gh api /orgs/coder/packages/container/coder-preview/versions |
            jq -r --arg tag "pr${{ env.PR_NUMBER }}" '.[] |
            select(.metadata.container.tags == [$tag]) |
            .metadata.container.tags[0]'
          )
          if [ -z "$foundTag" ]; then
            echo "Image not found"
            echo "${{ env.CODER_IMAGE_TAG }} not found in ghcr.io/coder/coder-preview"
            exit 1
          else
            echo "Image found"
            echo "$foundTag tag found in ghcr.io/coder/coder-preview"
          fi
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Add DNS record to Cloudflare
        if: needs.get_info.outputs.NEW == 'true'
        run: |
          curl -X POST "https://api.cloudflare.com/client/v4/zones/${{ secrets.PR_DEPLOYMENTS_ZONE_ID }}/dns_records" \
            -H "Authorization: Bearer ${{ secrets.PR_DEPLOYMENTS_CLOUDFLARE_API_TOKEN }}" \
            -H "Content-Type:application/json" \
            --data '{"type":"CNAME","name":"*.${{ env.PR_HOSTNAME }}","content":"${{ env.PR_HOSTNAME }}","ttl":1,"proxied":false}'

      - name: Create PR namespace
        if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
        run: |
          set -euo pipefail
          # try to delete the namespace, but don't fail if it doesn't exist
          kubectl delete namespace "pr${{ env.PR_NUMBER }}" || true
          kubectl create namespace "pr${{ env.PR_NUMBER }}"

      - name: Checkout
        uses: actions/checkout@v4

      - name: Check and Create Certificate
        if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
        run: |
          # Using kubectl to check if a Certificate resource already exists
          # we are doing this to avoid letsenrypt rate limits
          if ! kubectl get certificate pr${{ env.PR_NUMBER }}-tls -n pr-deployment-certs > /dev/null 2>&1; then
            echo "Certificate doesn't exist. Creating a new one."
            envsubst < ./.github/pr-deployments/certificate.yaml | kubectl apply -f -
          else
            echo "Certificate exists. Skipping certificate creation."
          fi
          echo "Copy certificate from pr-deployment-certs to pr${{ env.PR_NUMBER }} namespace"
          until kubectl get secret pr${{ env.PR_NUMBER }}-tls -n pr-deployment-certs &> /dev/null
          do
            echo "Waiting for secret pr${{ env.PR_NUMBER }}-tls to be created..."
            sleep 5
          done
          (
            kubectl get secret pr${{ env.PR_NUMBER }}-tls -n pr-deployment-certs -o json |
            jq 'del(.metadata.namespace,.metadata.creationTimestamp,.metadata.resourceVersion,.metadata.selfLink,.metadata.uid,.metadata.managedFields)' |
            kubectl -n pr${{ env.PR_NUMBER }} apply -f -
          )

      - name: Set up PostgreSQL database
        if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
        run: |
          helm repo add bitnami https://charts.bitnami.com/bitnami
          helm install coder-db bitnami/postgresql \
            --namespace pr${{ env.PR_NUMBER }} \
            --set auth.username=coder \
            --set auth.password=coder \
            --set auth.database=coder \
            --set persistence.size=10Gi
          kubectl create secret generic coder-db-url -n pr${{ env.PR_NUMBER }} \
            --from-literal=url="postgres://coder:coder@coder-db-postgresql.pr${{ env.PR_NUMBER }}.svc.cluster.local:5432/coder?sslmode=disable"

      - name: Create a service account, role, and rolebinding for the PR namespace
        if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
        run: |
          set -euo pipefail
          # Create service account, role, rolebinding
          envsubst < ./.github/pr-deployments/rbac.yaml | kubectl apply -f -

      - name: Create values.yaml
        env:
          EXPERIMENTS: ${{ github.event.inputs.experiments }}
          PR_DEPLOYMENTS_GITHUB_OAUTH_CLIENT_ID: ${{ secrets.PR_DEPLOYMENTS_GITHUB_OAUTH_CLIENT_ID }}
          PR_DEPLOYMENTS_GITHUB_OAUTH_CLIENT_SECRET: ${{ secrets.PR_DEPLOYMENTS_GITHUB_OAUTH_CLIENT_SECRET }}
        run: |
          set -euo pipefail
          envsubst < ./.github/pr-deployments/values.yaml > ./pr-deploy-values.yaml

      - name: Install/Upgrade Helm chart
        run: |
          set -euo pipefail
          helm dependency update --skip-refresh ./helm/coder
          helm upgrade --install "pr${{ env.PR_NUMBER }}" ./helm/coder \
          --namespace "pr${{ env.PR_NUMBER }}" \
          --values ./pr-deploy-values.yaml \
          --force

      - name: Install coder-logstream-kube
        if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
        run: |
          helm repo add coder-logstream-kube https://helm.coder.com/logstream-kube
          helm upgrade --install coder-logstream-kube coder-logstream-kube/coder-logstream-kube \
            --namespace "pr${{ env.PR_NUMBER }}" \
            --set url="https://${{ env.PR_HOSTNAME }}"

      - name: Get Coder binary
        if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
        run: |
          set -euo pipefail

          DEST="${HOME}/coder"
          URL="https://${{ env.PR_HOSTNAME }}/bin/coder-linux-amd64"

          mkdir -p "$(dirname ${DEST})"

          COUNT=0
          until $(curl --output /dev/null --silent --head --fail "$URL"); do
              printf '.'
              sleep 5
              COUNT=$((COUNT+1))
              if [ $COUNT -ge 60 ]; then
                echo "Timed out waiting for URL to be available"
                exit 1
              fi
          done

          curl -fsSL "$URL" -o "${DEST}"
          chmod +x "${DEST}"
          "${DEST}" version
          mv "${DEST}" /usr/local/bin/coder

      - name: Create first user, template and workspace
        if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
        id: setup_deployment
        run: |
          set -euo pipefail

          # Create first user

          # create a masked random password 12 characters long
          password=$(openssl rand -base64 16 | tr -d "=+/" | cut -c1-12)

          # add mask so that the password is not printed to the logs
          echo "::add-mask::$password"
          echo "password=$password" >> $GITHUB_OUTPUT

          coder login \
            --first-user-username coder \
            --first-user-email pr${{ env.PR_NUMBER }}@coder.com \
            --first-user-password $password \
            --first-user-trial \
            --use-token-as-session \
            https://${{ env.PR_HOSTNAME }}

          # Create template
          cd ./.github/pr-deployments/template
          coder templates push -y --variable namespace=pr${{ env.PR_NUMBER }} kubernetes

          # Create workspace
          coder create --template="kubernetes" kube --parameter cpu=2 --parameter memory=4 --parameter home_disk_size=2 -y
          coder stop kube -y

      - name: Send Slack notification
        if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
        run: |
          curl -s -o /dev/null -X POST -H 'Content-type: application/json' \
            -d \
            '{
              "pr_number": "'"${{ env.PR_NUMBER }}"'",
              "pr_url": "'"${{ env.PR_URL }}"'",
              "pr_title": "'"${{ env.PR_TITLE }}"'",
              "pr_access_url": "'"https://${{ env.PR_HOSTNAME }}"'",
              "pr_username": "'"test"'",
              "pr_email": "'"pr${{ env.PR_NUMBER }}@coder.com"'",
              "pr_password": "'"${{ steps.setup_deployment.outputs.password }}"'",
              "pr_actor": "'"${{ github.actor }}"'"
            }' \
            ${{ secrets.PR_DEPLOYMENTS_SLACK_WEBHOOK }}
          echo "Slack notification sent"

      - name: Find Comment
        uses: peter-evans/find-comment@v3
        id: fc
        with:
          issue-number: ${{ env.PR_NUMBER }}
          comment-author: "github-actions[bot]"
          body-includes: ":rocket:"
          direction: last

      - name: Comment on PR
        uses: peter-evans/create-or-update-comment@v4
        env:
          STATUS: ${{ needs.get_info.outputs.NEW == 'true' && 'Created' || 'Updated' }}
        with:
          issue-number: ${{ env.PR_NUMBER }}
          edit-mode: replace
          comment-id: ${{ steps.fc.outputs.comment-id }}
          body: |
            ---
            :heavy_check_mark: PR ${{ env.PR_NUMBER }} ${{ env.STATUS }} successfully.
            :rocket: Access the credentials [here](${{ secrets.PR_DEPLOYMENTS_SLACK_CHANNEL_URL }}).
            ---
            cc: @${{ github.actor }}
          reactions: rocket
          reactions-edit-mode: replace
