name: ci

on:
  push:
    branches:
      - main

  pull_request:
  workflow_dispatch:

permissions:
  actions: none
  checks: none
  contents: read
  deployments: none
  issues: none
  packages: write
  pull-requests: none
  repository-projects: none
  security-events: none
  statuses: none

# Cancel in-progress runs for pull requests when developers push
# additional changes
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      docs-only: ${{ steps.filter.outputs.docs_count == steps.filter.outputs.all_count }}
      docs: ${{ steps.filter.outputs.docs }}
      go: ${{ steps.filter.outputs.go }}
      ts: ${{ steps.filter.outputs.ts }}
      k8s: ${{ steps.filter.outputs.k8s }}
      ci: ${{ steps.filter.outputs.ci }}
      db: ${{ steps.filter.outputs.db }}
      offlinedocs-only: ${{ steps.filter.outputs.offlinedocs_count == steps.filter.outputs.all_count }}
      offlinedocs: ${{ steps.filter.outputs.offlinedocs }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 1
      # For pull requests it's not necessary to checkout the code
      - name: check changed files
        uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            all:
              - "**"
            docs:
              - "docs/**"
              - "README.md"
              - "examples/web-server/**"
              - "examples/monitoring/**"
              - "examples/lima/**"
            db:
              - "**.sql"
              - "coderd/database/**"
            go:
              - "**.sql"
              - "**.go"
              - "**.golden"
              - "go.mod"
              - "go.sum"
              # Other non-Go files that may affect Go code:
              - "**.rego"
              - "**.sh"
              - "**.tpl"
              - "**.gotmpl"
              - "**.gotpl"
              - "Makefile"
              - "site/static/error.html"
              # Main repo directories for completeness in case other files are
              # touched:
              - "agent/**"
              - "cli/**"
              - "cmd/**"
              - "coderd/**"
              - "enterprise/**"
              - "examples/*"
              - "provisioner/**"
              - "provisionerd/**"
              - "provisionersdk/**"
              - "pty/**"
              - "scaletest/**"
              - "tailnet/**"
              - "testutil/**"
            ts:
              - "site/**"
              - "Makefile"
            k8s:
              - "helm/**"
              - "scripts/Dockerfile"
              - "scripts/Dockerfile.base"
              - "scripts/helm.sh"
            ci:
              - ".github/actions/**"
              - ".github/workflows/ci.yaml"
            offlinedocs:
              - "offlinedocs/**"

      - id: debug
        run: |
          echo "${{ toJSON(steps.filter )}}"

  lint:
    needs: changes
    if: needs.changes.outputs.offlinedocs-only == 'false' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
    runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 1

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

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

      - name: Get golangci-lint cache dir
        run: |
          linter_ver=$(egrep -o 'GOLANGCI_LINT_VERSION=\S+' dogfood/Dockerfile | cut -d '=' -f 2)
          go install github.com/golangci/golangci-lint/cmd/golangci-lint@v$linter_ver
          dir=$(golangci-lint cache status | awk '/Dir/ { print $2 }')
          echo "LINT_CACHE_DIR=$dir" >> $GITHUB_ENV

      - name: golangci-lint cache
        uses: buildjet/cache@v4
        with:
          path: |
            ${{ env.LINT_CACHE_DIR }}
          key: golangci-lint-${{ runner.os }}-${{ hashFiles('**/*.go') }}
          restore-keys: |
            golangci-lint-${{ runner.os }}-

      # Check for any typos
      - name: Check for typos
        uses: crate-ci/typos@v1.18.2
        with:
          config: .github/workflows/typos.toml

      - name: Fix the typos
        if: ${{ failure() }}
        run: |
          echo "::notice:: you can automatically fix typos from your CLI:
          cargo install typos-cli
          typos -c .github/workflows/typos.toml -w"

      # Needed for helm chart linting
      - name: Install helm
        uses: azure/setup-helm@v3
        with:
          version: v3.9.2

      - name: make lint
        run: |
          make --output-sync=line -j lint

  gen:
    timeout-minutes: 8
    runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
    needs: changes
    if: needs.changes.outputs.docs-only == 'false' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 1

      - 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: go install tools
        run: |
          go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30
          go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.33
          go install golang.org/x/tools/cmd/goimports@latest
          go install github.com/mikefarah/yq/v4@v4.30.6
          go install go.uber.org/mock/mockgen@v0.4.0

      - name: Install Protoc
        run: |
          mkdir -p /tmp/proto
          pushd /tmp/proto
          curl -L -o protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v23.3/protoc-23.3-linux-x86_64.zip
          unzip protoc.zip
          cp -r ./bin/* /usr/local/bin
          cp -r ./include /usr/local/bin/include
          popd

      - name: make gen
        run: "make --output-sync -j -B gen"

      - name: Check for unstaged files
        run: ./scripts/check_unstaged.sh

  fmt:
    needs: changes
    if: needs.changes.outputs.offlinedocs-only == 'false' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
    runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
    timeout-minutes: 7
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 1

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

      - name: Setup Go
        uses: buildjet/setup-go@v5
        with:
          # This doesn't need caching. It's super fast anyways!
          cache: false
          go-version: 1.21.5

      - name: Install shfmt
        run: go install mvdan.cc/sh/v3/cmd/shfmt@v3.7.0

      - name: make fmt
        run: |
          export PATH=${PATH}:$(go env GOPATH)/bin
          make --output-sync -j -B fmt

      - name: Check for unstaged files
        run: ./scripts/check_unstaged.sh

  test-go:
    runs-on: ${{ matrix.os == 'ubuntu-latest' && github.repository_owner == 'coder' && 'buildjet-4vcpu-ubuntu-2204' || matrix.os == 'macos-latest' && github.repository_owner == 'coder' && 'macos-latest-xlarge' || matrix.os == 'windows-2022' && github.repository_owner == 'coder' && 'windows-latest-16-cores' || matrix.os }}
    needs: changes
    if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
    timeout-minutes: 20
    strategy:
      fail-fast: false
      matrix:
        os:
          - ubuntu-latest
          - macos-latest
          - windows-2022
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 1

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

      - name: Setup Terraform
        uses: ./.github/actions/setup-tf

      - name: Test with Mock Database
        id: test
        shell: bash
        run: |
          # Code coverage is more computationally expensive and also
          # prevents test caching, so we disable it on alternate operating
          # systems.
          if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then
            echo "cover=true" >> $GITHUB_OUTPUT
            export COVERAGE_FLAGS='-covermode=atomic -coverprofile="gotests.coverage" -coverpkg=./...'
          else
            echo "cover=false" >> $GITHUB_OUTPUT
          fi

          # if macOS, install google-chrome for scaletests. As another concern,
          # should we really have this kind of external dependency requirement
          # on standard CI?
          if [ "${{ matrix.os }}" == "macos-latest" ]; then
            brew install google-chrome
          fi

          # By default Go will use the number of logical CPUs, which
          # is a fine default.
          PARALLEL_FLAG=""

          # macOS will output "The default interactive shell is now zsh"
          # intermittently in CI...
          if [ "${{ matrix.os }}" == "macos-latest" ]; then
            touch ~/.bash_profile && echo "export BASH_SILENCE_DEPRECATION_WARNING=1" >> ~/.bash_profile
          fi
          export TS_DEBUG_DISCO=true
          gotestsum --junitfile="gotests.xml" --jsonfile="gotests.json" \
            --packages="./..." -- $PARALLEL_FLAG -short -failfast $COVERAGE_FLAGS

      - name: Upload test stats to Datadog
        timeout-minutes: 1
        continue-on-error: true
        uses: ./.github/actions/upload-datadog
        if: success() || failure()
        with:
          api-key: ${{ secrets.DATADOG_API_KEY }}

      - name: Check code coverage
        uses: codecov/codecov-action@v4
        # This action has a tendency to error out unexpectedly, it has
        # the `fail_ci_if_error` option that defaults to `false`, but
        # that is no guarantee, see:
        # https://github.com/codecov/codecov-action/issues/788
        continue-on-error: true
        if: steps.test.outputs.cover && github.actor != 'dependabot[bot]' && !github.event.pull_request.head.repo.fork
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ./gotests.coverage
          flags: unittest-go-${{ matrix.os }}

  test-go-pg:
    runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
    needs:
      - changes
    if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
    # This timeout must be greater than the timeout set by `go test` in
    # `make test-postgres` to ensure we receive a trace of running
    # goroutines. Setting this to the timeout +5m should work quite well
    # even if some of the preceding steps are slow.
    timeout-minutes: 25
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 1

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

      - name: Setup Terraform
        uses: ./.github/actions/setup-tf

      - name: Test with PostgreSQL Database
        run: |
          export TS_DEBUG_DISCO=true
          make test-postgres

      - name: Upload test stats to Datadog
        timeout-minutes: 1
        continue-on-error: true
        uses: ./.github/actions/upload-datadog
        if: success() || failure()
        with:
          api-key: ${{ secrets.DATADOG_API_KEY }}

      - name: Check code coverage
        uses: codecov/codecov-action@v4
        # This action has a tendency to error out unexpectedly, it has
        # the `fail_ci_if_error` option that defaults to `false`, but
        # that is no guarantee, see:
        # https://github.com/codecov/codecov-action/issues/788
        continue-on-error: true
        if: github.actor != 'dependabot[bot]' && !github.event.pull_request.head.repo.fork
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ./gotests.coverage
          flags: unittest-go-postgres-linux

  test-go-race:
    runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
    needs: changes
    if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
    timeout-minutes: 25
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 1

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

      - name: Setup Terraform
        uses: ./.github/actions/setup-tf

      - name: Run Tests
        run: |
          gotestsum --junitfile="gotests.xml" -- -race ./...

      - name: Upload test stats to Datadog
        timeout-minutes: 1
        continue-on-error: true
        uses: ./.github/actions/upload-datadog
        if: always()
        with:
          api-key: ${{ secrets.DATADOG_API_KEY }}

  test-js:
    runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
    needs: changes
    if: needs.changes.outputs.ts == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
    timeout-minutes: 20
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 1

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

      - run: pnpm test:ci --max-workers $(nproc)
        working-directory: site

      - name: Check code coverage
        uses: codecov/codecov-action@v4
        # This action has a tendency to error out unexpectedly, it has
        # the `fail_ci_if_error` option that defaults to `false`, but
        # that is no guarantee, see:
        # https://github.com/codecov/codecov-action/issues/788
        continue-on-error: true
        if: github.actor != 'dependabot[bot]' && !github.event.pull_request.head.repo.fork
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ./site/coverage/lcov.info
          flags: unittest-js

  test-e2e:
    runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-16vcpu-ubuntu-2204' || 'ubuntu-latest' }}
    needs: changes
    if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ts == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
    timeout-minutes: 20
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 1

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

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

      - name: Setup Terraform
        uses: ./.github/actions/setup-tf

      - name: go install tools
        run: |
          go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30
          go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.33
          go install golang.org/x/tools/cmd/goimports@latest
          go install github.com/mikefarah/yq/v4@v4.30.6
          go install go.uber.org/mock/mockgen@v0.4.0

      - name: Install Protoc
        run: |
          mkdir -p /tmp/proto
          pushd /tmp/proto
          curl -L -o protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v23.3/protoc-23.3-linux-x86_64.zip
          unzip protoc.zip
          cp -r ./bin/* /usr/local/bin
          cp -r ./include /usr/local/bin/include
          popd

      - name: Build
        run: |
          make -B site/out/index.html

      - run: pnpm playwright:install
        working-directory: site

      - run: pnpm playwright:test --workers 1
        env:
          DEBUG: pw:api
        working-directory: site

      - name: Upload Playwright Failed Tests
        if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux' && !github.event.pull_request.head.repo.fork
        uses: actions/upload-artifact@v4
        with:
          name: failed-test-videos
          path: ./site/test-results/**/*.webm
          retention-days: 7

      - name: Upload pprof dumps
        if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux' && !github.event.pull_request.head.repo.fork
        uses: actions/upload-artifact@v4
        with:
          name: debug-pprof-dumps
          path: ./site/test-results/**/debug-pprof-*.txt
          retention-days: 7

  chromatic:
    # REMARK: this is only used to build storybook and deploy it to Chromatic.
    runs-on: ubuntu-latest
    needs: changes
    if: needs.changes.outputs.ts == 'true' || needs.changes.outputs.ci == 'true'
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          # Required by Chromatic for build-over-build history, otherwise we
          # only get 1 commit on shallow checkout.
          fetch-depth: 0

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

      # This step is not meant for mainline because any detected changes to
      # storybook snapshots will require manual approval/review in order for
      # the check to pass. This is desired in PRs, but not in mainline.
      - name: Publish to Chromatic (non-mainline)
        if: github.ref != 'refs/heads/main' && github.repository_owner == 'coder'
        uses: chromaui/action@v10
        env:
          NODE_OPTIONS: "--max_old_space_size=4096"
          STORYBOOK: true
        with:
          # Do a fast, testing build for change previews
          buildScriptName: "storybook:ci"
          exitOnceUploaded: true
          # This will prevent CI from failing when Chromatic detects visual changes
          exitZeroOnChanges: true
          # Chromatic states its fine to make this token public. See:
          # https://www.chromatic.com/docs/github-actions#forked-repositories
          projectToken: 695c25b6cb65
          workingDir: "./site"
          storybookBaseDir: "./site"
          # Prevent excessive build runs on minor version changes
          skip: "@(renovate/**|dependabot/**)"
          # Run TurboSnap to trace file dependencies to related stories
          # and tell chromatic to only take snapshots of relevent stories
          onlyChanged: true
          # Avoid uploading single files, because that's very slow
          zip: true

      # This is a separate step for mainline only that auto accepts and changes
      # instead of holding CI up. Since we squash/merge, this is defensive to
      # avoid the same changeset from requiring review once squashed into
      # main. Chromatic is supposed to be able to detect that we use squash
      # commits, but it's good to be defensive in case, otherwise CI remains
      # infinitely "in progress" in mainline unless we re-review each build.
      - name: Publish to Chromatic (mainline)
        if: github.ref == 'refs/heads/main' && github.repository_owner == 'coder'
        uses: chromaui/action@v10
        env:
          NODE_OPTIONS: "--max_old_space_size=4096"
          STORYBOOK: true
        with:
          autoAcceptChanges: true
          # This will prevent CI from failing when Chromatic detects visual changes
          exitZeroOnChanges: true
          # Do a full build with documentation for mainline builds
          buildScriptName: "storybook:build"
          projectToken: 695c25b6cb65
          workingDir: "./site"
          storybookBaseDir: "./site"
          # Run TurboSnap to trace file dependencies to related stories
          # and tell chromatic to only take snapshots of relevent stories
          onlyChanged: true
          # Avoid uploading single files, because that's very slow
          zip: true

  offlinedocs:
    name: offlinedocs
    needs: changes
    runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
    if: needs.changes.outputs.offlinedocs == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.docs == 'true'

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          # 0 is required here for version.sh to work.
          fetch-depth: 0

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

      - name: Install Protoc
        run: |
          mkdir -p /tmp/proto
          pushd /tmp/proto
          curl -L -o protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v23.3/protoc-23.3-linux-x86_64.zip
          unzip protoc.zip
          cp -r ./bin/* /usr/local/bin
          cp -r ./include /usr/local/bin/include
          popd

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

      - name: Install go tools
        run: |
          go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30
          go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.33
          go install golang.org/x/tools/cmd/goimports@latest
          go install github.com/mikefarah/yq/v4@v4.30.6
          go install go.uber.org/mock/mockgen@v0.4.0

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

      - name: Format
        run: |
          cd offlinedocs
          pnpm format:check

      - name: Lint
        run: |
          cd offlinedocs
          pnpm lint

      - name: Build
        run: |
          make -j build/coder_docs_"$(./scripts/version.sh)".tgz

  required:
    runs-on: ubuntu-latest
    needs:
      - fmt
      - lint
      - gen
      - test-go
      - test-go-pg
      - test-go-race
      - test-js
      - test-e2e
      - offlinedocs
      - sqlc-vet
    # Allow this job to run even if the needed jobs fail, are skipped or
    # cancelled.
    if: always()
    steps:
      - name: Ensure required checks
        run: |
          echo "Checking required checks"
          echo "- fmt: ${{ needs.fmt.result }}"
          echo "- lint: ${{ needs.lint.result }}"
          echo "- gen: ${{ needs.gen.result }}"
          echo "- test-go: ${{ needs.test-go.result }}"
          echo "- test-go-pg: ${{ needs.test-go-pg.result }}"
          echo "- test-go-race: ${{ needs.test-go-race.result }}"
          echo "- test-js: ${{ needs.test-js.result }}"
          echo "- test-e2e: ${{ needs.test-e2e.result }}"
          echo "- offlinedocs: ${{ needs.offlinedocs.result }}"
          echo

          # We allow skipped jobs to pass, but not failed or cancelled jobs.
          if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" || "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
            echo "One of the required checks has failed or has been cancelled"
            exit 1
          fi

          echo "Required checks have passed"

  build:
    # This builds and publishes ghcr.io/coder/coder-preview:main for each commit
    # to main branch. We are only building this for amd64 platform. (>95% pulls
    # are for amd64)
    needs: changes
    if: needs.changes.outputs.docs-only == 'false' && !github.event.pull_request.head.repo.fork
    runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
    env:
      DOCKER_CLI_EXPERIMENTAL: "enabled"
    outputs:
      IMAGE: ghcr.io/coder/coder-preview:${{ steps.build-docker.outputs.tag }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

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

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

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

      - name: Install nfpm
        run: go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.35.1

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

      - name: Build
        run: |
          set -euxo pipefail
          go mod download

          version="$(./scripts/version.sh)"
          tag="main-$(echo "$version" | sed 's/+/-/g')"
          echo "tag=$tag" >> $GITHUB_OUTPUT

          make gen/mark-fresh
          make -j \
            build/coder_linux_{amd64,arm64,armv7} \
            build/coder_"$version"_windows_amd64.zip \
            build/coder_"$version"_linux_amd64.{tar.gz,deb}

      - name: Build Linux Docker images
        id: build-docker
        env:
          CODER_IMAGE_BASE: ghcr.io/coder/coder-preview
          CODER_IMAGE_TAG_PREFIX: main
          DOCKER_CLI_EXPERIMENTAL: "enabled"
        run: |
          set -euxo pipefail

          # build Docker images for each architecture
          version="$(./scripts/version.sh)"
          tag="main-$(echo "$version" | sed 's/+/-/g')"
          echo "tag=$tag" >> $GITHUB_OUTPUT

          # build images for each architecture
          make -j build/coder_"$version"_linux_{amd64,arm64,armv7}.tag

          # only push if we are on main branch
          if [ "${{ github.ref }}" == "refs/heads/main" ]; then
            # 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_{amd64,arm64,armv7}.tag

            # Define specific tags
            tags=("$tag" "main" "latest")

            # Create and push a multi-arch manifest for each tag
            # we are adding `latest` tag and keeping `main` for backward
            # compatibality
            for t in "${tags[@]}"; do
                ./scripts/build_docker_multiarch.sh \
                    --push \
                    --target "ghcr.io/coder/coder-preview:$t" \
                    --version $version \
                    $(cat build/coder_"$version"_linux_{amd64,arm64,armv7}.tag)
            done
          fi

      - name: Prune old images
        if: github.ref == 'refs/heads/main'
        uses: vlaurin/action-ghcr-prune@v0.6.0
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          organization: coder
          container: coder-preview
          keep-younger-than: 7 # days
          keep-tags: latest
          keep-tags-regexes: ^pr
          prune-tags-regexes: |
            ^main-
            ^v
          prune-untagged: true

      - name: Upload build artifacts
        if: github.ref == 'refs/heads/main'
        uses: actions/upload-artifact@v4
        with:
          name: coder
          path: |
            ./build/*.zip
            ./build/*.tar.gz
            ./build/*.deb
          retention-days: 7

  deploy:
    name: "deploy"
    runs-on: ubuntu-latest
    timeout-minutes: 30
    needs:
      - changes
      - build
    if: |
      github.ref == 'refs/heads/main' && !github.event.pull_request.head.repo.fork
      && needs.changes.outputs.docs-only == 'false'
    permissions:
      contents: read
      id-token: write
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: projects/573722524737/locations/global/workloadIdentityPools/github/providers/github
          service_account: coder-ci@coder-dogfood.iam.gserviceaccount.com

      - name: Set up Google Cloud SDK
        uses: google-github-actions/setup-gcloud@v2

      - name: Set up Flux CLI
        uses: fluxcd/flux2/action@main
        with:
          # Keep this up to date with the version of flux installed in dogfood cluster
          version: "2.2.1"

      - name: Get Cluster Credentials
        uses: "google-github-actions/get-gke-credentials@v2"
        with:
          cluster_name: dogfood-v2
          location: us-central1-a
          project_id: coder-dogfood-v2

      - name: Reconcile Flux
        run: |
          set -euxo pipefail
          flux --namespace flux-system reconcile source git flux-system
          flux --namespace flux-system reconcile source git coder-main
          flux --namespace flux-system reconcile kustomization flux-system
          flux --namespace flux-system reconcile kustomization coder
          flux --namespace flux-system reconcile source chart coder-coder
          flux --namespace flux-system reconcile source chart coder-coder-provisioner
          flux --namespace coder reconcile helmrelease coder
          flux --namespace coder reconcile helmrelease coder-provisioner

      # Just updating Flux is usually not enough. The Helm release may get
      # redeployed, but unless something causes the Deployment to update the
      # pods won't be recreated. It's important that the pods get recreated,
      # since we use `imagePullPolicy: Always` to ensure we're running the
      # latest image.
      - name: Rollout Deployment
        run: |
          set -euxo pipefail
          kubectl --namespace coder rollout restart deployment/coder
          kubectl --namespace coder rollout status deployment/coder
          kubectl --namespace coder rollout restart deployment/coder-provisioner
          kubectl --namespace coder rollout status deployment/coder-provisioner

  deploy-wsproxies:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main' && !github.event.pull_request.head.repo.fork
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup flyctl
        uses: superfly/flyctl-actions/setup-flyctl@master

      - name: Deploy workspace proxies
        run: |
          flyctl deploy --image "$IMAGE" --app paris-coder --config ./.github/fly-wsproxies/paris-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_PARIS" --yes
          flyctl deploy --image "$IMAGE" --app sydney-coder --config ./.github/fly-wsproxies/sydney-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_SYDNEY" --yes
          flyctl deploy --image "$IMAGE" --app sao-paulo-coder --config ./.github/fly-wsproxies/sao-paulo-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_SAO_PAULO" --yes
          flyctl deploy --image "$IMAGE" --app jnb-coder --config ./.github/fly-wsproxies/jnb-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_JNB" --yes
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
          IMAGE: ${{ needs.build.outputs.IMAGE }}
          TOKEN_PARIS: ${{ secrets.FLY_PARIS_CODER_PROXY_SESSION_TOKEN }}
          TOKEN_SYDNEY: ${{ secrets.FLY_SYDNEY_CODER_PROXY_SESSION_TOKEN }}
          TOKEN_SAO_PAULO: ${{ secrets.FLY_SAO_PAULO_CODER_PROXY_SESSION_TOKEN }}
          TOKEN_JNB: ${{ secrets.FLY_JNB_CODER_PROXY_SESSION_TOKEN }}

  # sqlc-vet runs a postgres docker container, runs Coder migrations, and then
  # runs sqlc-vet to ensure all queries are valid. This catches any mistakes
  # in migrations or sqlc queries that makes a query unable to be prepared.
  sqlc-vet:
    runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
    needs: changes
    if: needs.changes.outputs.db == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
    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: Setup and run sqlc vet
        run: |
          make sqlc-vet
