name: ci

on:
  push:
    branches:
      - main

  pull_request:
  workflow_dispatch:

permissions:
  actions: none
  checks: none
  contents: read
  deployments: none
  issues: none
  packages: none
  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.event_name == 'pull_request' }}

env:
  CODER_GO_VERSION: "~1.20"

jobs:
  lint:
    runs-on: ${{ github.repository_owner == 'coder' && 'ubuntu-latest-8-cores' || 'ubuntu-latest' }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3

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

      # Check for any typos!
      - name: Check for typos
        uses: crate-ci/typos@v1.14.10
        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"

      # Check for Go linting errors!
      - name: Lint Go
        uses: golangci/golangci-lint-action@v3.3.1
        with:
          version: v1.51.0

      - name: Lint shell scripts
        uses: ludeeus/action-shellcheck@2.0.0
        env:
          SHELLCHECK_OPTS: --external-sources
        with:
          ignore: node_modules

      # Lint our dashboard!
      - name: Cache node_modules
        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 node_modules
        run: ./scripts/yarn_install.sh
      - name: Lint TypeScript
        run: yarn lint
        working-directory: site

      # Make sure the Helm chart is linted!
      - name: Install helm
        uses: azure/setup-helm@v3
        with:
          version: v3.9.2
      - name: Lint Helm chart
        run: |
          cd helm
          make lint

      # Ensure AGPL and Enterprise are separated!
      - name: Check for AGPL code importing Enterprise...
        run: ./scripts/check_enterprise_imports.sh

  changes:
    runs-on: ubuntu-latest
    outputs:
      docs-only: ${{ steps.filter.outputs.docs_count == steps.filter.outputs.all_count }}
      sh: ${{ steps.filter.outputs.sh }}
      ts: ${{ steps.filter.outputs.ts }}
      k8s: ${{ steps.filter.outputs.k8s }}
    steps:
      - uses: actions/checkout@v3
      # For pull requests it's not necessary to checkout the code
      - uses: dorny/paths-filter@v2
        id: filter
        with:
          filters: |
            all:
              - '**'
            docs:
              - 'docs/**'
              # For testing:
              # - '.github/**'
            sh:
              - "**.sh"
            ts:
              - 'site/**'
            k8s:
              - 'helm/**'
              - scripts/Dockerfile
              - scripts/Dockerfile.base
              - scripts/helm.sh
      - id: debug
        run: |
          echo "${{ toJSON(steps.filter )}}"

  gen:
    timeout-minutes: 8
    runs-on: ${{ github.repository_owner == 'coder' && 'ubuntu-latest-8-cores' || 'ubuntu-latest' }}
    needs: changes
    if: needs.changes.outputs.docs-only == 'false'
    steps:
      - uses: actions/checkout@v3

      - 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 node_modules
        run: ./scripts/yarn_install.sh

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

      - name: Echo Go Cache Paths
        id: go-cache-paths
        run: |
          echo "GOCACHE=$(go env GOCACHE)" >> $GITHUB_OUTPUT
          echo "GOMODCACHE=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT

      - name: Go Build Cache
        uses: actions/cache@v3
        with:
          path: ${{ steps.go-cache-paths.outputs.GOCACHE }}
          key: ${{ github.job }}-go-build-${{ hashFiles('**/go.sum', '**/**.go') }}

      - name: Go Mod Cache
        uses: actions/cache@v3
        with:
          path: ${{ steps.go-cache-paths.outputs.GOMODCACHE }}
          key: ${{ github.job }}-go-mod-${{ hashFiles('**/go.sum') }}

      - name: Install sqlc
        run: |
          curl -sSL https://github.com/kyleconroy/sqlc/releases/download/v1.17.2/sqlc_1.17.2_linux_amd64.tar.gz | sudo tar -C /usr/bin -xz sqlc
      - name: Install protoc-gen-go
        run: go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
      - name: Install protoc-gen-go-drpc
        run: go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.26
      - name: Install goimports
        run: go install golang.org/x/tools/cmd/goimports@latest
      - name: Install yq
        run: go run github.com/mikefarah/yq/v4@v4.30.6
      - name: Install mockgen
        run: go install github.com/golang/mock/mockgen@v1.6.0

      - name: Install Protoc
        run: |
          # protoc must be in lockstep with our dogfood Dockerfile or the
          # version in the comments will differ. This is also defined in
          # security.yaml
          set -x
          cd dogfood
          DOCKER_BUILDKIT=1 docker build . --target proto -t protoc
          protoc_path=/usr/local/bin/protoc
          docker run --rm --entrypoint cat protoc /tmp/bin/protoc > $protoc_path
          chmod +x $protoc_path
          protoc --version

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

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

  fmt:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
          submodules: true

      - 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 node_modules
        run: ./scripts/yarn_install.sh

      - name: Install shfmt
        run: go install mvdan.cc/sh/v3/cmd/shfmt@v3.5.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' && 'ubuntu-latest-8-cores' ||  matrix.os == 'windows-2022' && github.repository_owner == 'coder' && 'windows-latest-8-cores'|| matrix.os }}
    timeout-minutes: 20
    strategy:
      matrix:
        os:
          - ubuntu-latest
          - macos-latest
          - windows-2022
    steps:
      - uses: actions/checkout@v3

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

      - name: Echo Go Cache Paths
        id: go-cache-paths
        run: |
          echo "GOCACHE=$(go env GOCACHE)" >> ${{ runner.os == 'Windows' && '$env:' || '$' }}GITHUB_OUTPUT
          echo "GOMODCACHE=$(go env GOMODCACHE)" >> ${{ runner.os == 'Windows' && '$env:' || '$' }}GITHUB_OUTPUT

      - name: Go Build Cache
        uses: actions/cache@v3
        with:
          path: ${{ steps.go-cache-paths.outputs.GOCACHE }}
          key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.**', '**.go') }}

      - name: Go Mod Cache
        uses: actions/cache@v3
        with:
          path: ${{ steps.go-cache-paths.outputs.GOMODCACHE }}
          key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}

      - name: Install gotestsum
        uses: jaxxstorm/action-install-gh-release@v1.10.0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          repo: gotestyourself/gotestsum
          tag: v1.9.0

      - uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: 1.1.9
          terraform_wrapper: false

      - 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

          export TS_DEBUG_DISCO=true
          gotestsum --junitfile="gotests.xml" --jsonfile="gotests.json" --packages="./..." -- -parallel=8 -timeout=7m -short -failfast $COVERAGE_FLAGS

      - name: Print test stats
        if: success() || failure()
        run: |
          # Artifacts are not available after rerunning a job,
          # so we need to print the test stats to the log.
          go run ./scripts/ci-report/main.go gotests.json | tee gotests_stats.json

      - uses: actions/upload-artifact@v3
        if: success() || failure()
        with:
          name: gotests-${{ matrix.os }}.xml
          path: ./gotests.xml
          retention-days: 30

      - uses: codecov/codecov-action@v3
        # 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-psql:
    runs-on: ${{ github.repository_owner == 'coder' && 'ubuntu-latest-8-cores' || 'ubuntu-latest' }}
    # 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:
      - uses: actions/checkout@v3

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

      - name: Echo Go Cache Paths
        id: go-cache-paths
        run: |
          echo "GOCACHE=$(go env GOCACHE)" >> $GITHUB_OUTPUT
          echo "GOMODCACHE=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT

      - name: Go Build Cache
        uses: actions/cache@v3
        with:
          path: ${{ steps.go-cache-paths.outputs.GOCACHE }}
          key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum', '**/**.go') }}

      - name: Go Mod Cache
        uses: actions/cache@v3
        with:
          path: ${{ steps.go-cache-paths.outputs.GOMODCACHE }}
          key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}

      - name: Install gotestsum
        uses: jaxxstorm/action-install-gh-release@v1.10.0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          repo: gotestyourself/gotestsum
          tag: v1.9.0

      - uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: 1.1.9
          terraform_wrapper: false

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

      - name: Print test stats
        if: success() || failure()
        run: |
          # Artifacts are not available after rerunning a job,
          # so we need to print the test stats to the log.
          go run ./scripts/ci-report/main.go gotests.json | tee gotests_stats.json

      - uses: actions/upload-artifact@v3
        if: success() || failure()
        with:
          name: gotests-postgres.xml
          path: ./gotests.xml
          retention-days: 30

      - uses: codecov/codecov-action@v3
        # 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

  deploy:
    name: "deploy"
    runs-on: ${{ github.repository_owner == 'coder' && 'ubuntu-latest-8-cores' || 'ubuntu-latest' }}
    timeout-minutes: 30
    needs: changes
    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:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v1
        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@v1

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

      - name: Echo Go Cache Paths
        id: go-cache-paths
        run: |
          echo "GOCACHE=$(go env GOCACHE)" >> $GITHUB_OUTPUT
          echo "GOMODCACHE=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT

      - name: Go Build Cache
        uses: actions/cache@v3
        with:
          path: ${{ steps.go-cache-paths.outputs.GOCACHE }}
          key: ${{ runner.os }}-release-go-build-${{ hashFiles('**/go.sum') }}

      - name: Go Mod Cache
        uses: actions/cache@v3
        with:
          path: ${{ steps.go-cache-paths.outputs.GOMODCACHE }}
          key: ${{ runner.os }}-release-go-mod-${{ hashFiles('**/go.sum') }}

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

      - name: Install goimports
        run: go install golang.org/x/tools/cmd/goimports@latest
      - name: Install nfpm
        run: go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.16.0

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

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

          version="$(./scripts/version.sh)"
          make gen/mark-fresh
          make -j \
            build/coder_"$version"_windows_amd64.zip \
            build/coder_"$version"_linux_amd64.{tar.gz,deb}

      - name: Install Release
        run: |
          set -euo pipefail

          regions=(
            # gcp-region-id instance-name systemd-service-name
            "us-central1-a coder coder"
            "australia-southeast1-b coder-sydney coder-workspace-proxy"
            "europe-west3-c coder-europe coder-workspace-proxy"
            "southamerica-east1-b coder-brazil coder-workspace-proxy"
          )

          deb_pkg="./build/coder_$(./scripts/version.sh)_linux_amd64.deb"
          if [ ! -f "$deb_pkg" ]; then
            echo "deb package not found: $deb_pkg"
            ls -l ./build
            exit 1
          fi

          gcloud config set project coder-dogfood
          for region in "${regions[@]}"; do
            echo "::group::$region"
            set -- $region

            set -x
            gcloud config set compute/zone "$1"
            gcloud compute scp "$deb_pkg" "${2}:/tmp/coder.deb"
            gcloud compute ssh "$2" -- /bin/sh -c "set -eux; sudo dpkg -i --force-confdef /tmp/coder.deb; sudo systemctl daemon-reload; sudo service '$3' restart"
            set +x

            echo "::endgroup::"
          done

      - uses: actions/upload-artifact@v3
        with:
          name: coder
          path: |
            ./build/*.zip
            ./build/*.tar.gz
            ./build/*.deb
          retention-days: 7

  test-js:
    runs-on: ${{ github.repository_owner == 'coder' && 'ubuntu-latest-8-cores' || 'ubuntu-latest' }}
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v3

      - 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 }}-

      - uses: actions/setup-node@v3
        with:
          node-version: "16.16.0"

      - name: Install node_modules
        run: ./scripts/yarn_install.sh

      - run: yarn test:ci --max-workers ${{ steps.cpu-cores.outputs.count }}
        working-directory: site

      - uses: codecov/codecov-action@v3
        # 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:
    needs:
      - changes
    if: needs.changes.outputs.docs-only == 'false'
    runs-on: ${{ github.repository_owner == 'coder' && 'ubuntu-latest-8-cores' || 'ubuntu-latest' }}
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v3

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

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

      - uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: 1.1.9
          terraform_wrapper: false

      - uses: actions/setup-node@v3
        with:
          node-version: "16.16.0"

      - name: Echo Go Cache Paths
        id: go-cache-paths
        run: |
          echo "GOCACHE=$(go env GOCACHE)" >> $GITHUB_OUTPUT
          echo "GOMODCACHE=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT

      - name: Go Build Cache
        uses: actions/cache@v3
        with:
          path: ${{ steps.go-cache-paths.outputs.GOCACHE }}
          key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}

      - name: Go Mod Cache
        uses: actions/cache@v3
        with:
          path: ${{ steps.go-cache-paths.outputs.GOMODCACHE }}
          key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}

      - name: Build
        run: |
          sudo npm install -g prettier
          make -B site/out/index.html

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

      - run: yarn playwright:test
        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@v3
        with:
          name: failed-test-videos
          path: ./site/test-results/**/*.webm
          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'
    steps:
      - uses: actions/checkout@v3
        with:
          # Required by Chromatic for build-over-build history, otherwise we
          # only get 1 commit on shallow checkout.
          fetch-depth: 0

      - uses: actions/setup-node@v3
        with:
          node-version: "16.16.0"

      - name: Install dependencies
        run: cd site && yarn

      # 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@v1
        env:
          NODE_OPTIONS: "--max_old_space_size=4096"
          STORYBOOK: true
        with:
          buildScriptName: "storybook:build"
          exitOnceUploaded: true
          # Chromatic states its fine to make this token public. See:
          # https://www.chromatic.com/docs/github-actions#forked-repositories
          projectToken: 695c25b6cb65
          workingDir: "./site"

      # 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@v1
        env:
          NODE_OPTIONS: "--max_old_space_size=4096"
          STORYBOOK: true
        with:
          autoAcceptChanges: true
          buildScriptName: "storybook:build"
          projectToken: 695c25b6cb65
          workingDir: "./site"
