name: Build/Release Corso
env:
  IMAGE_NAME: ghcr.io/alcionai/corso
on:
  workflow_dispatch:

  pull_request:

  push:
    branches: [main]
    tags: ["v*.*.*"]

  repository_dispatch:
    types: [ok-to-test-command]

permissions:
  # required to retrieve AWS credentials
  id-token: write
  contents: write
  packages: write
  pull-requests: read

# cancel currently running jobs if a new version of the branch is pushed
concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  # ----------------------------------------------------------------------------------------------------
  # --- Prechecks and Checkouts ------------------------------------------------------------------------
  # ----------------------------------------------------------------------------------------------------
  Precheck:
    uses: alcionai/corso/.github/workflows/_filechange_checker.yml@main

  Checkout:
    needs: [Precheck]
    environment: Testing
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: src
    steps:
      - uses: actions/checkout@v3

      # single setup and sum cache handling here.
      # the results will cascade onto both testing and linting.
      - name: Setup Golang with cache
        uses: ./.github/actions/go-setup-cache
        if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' || needs.precheck.outputs.websitefileschanged == 'true'
        with:
          go-version-file: src/go.mod

  # SetM365App will decide which M365 app to use for this CI run
  SetM365App:
    uses: alcionai/corso/.github/workflows/accSelector.yaml@main

  SetEnv:
    environment: Testing
    runs-on: ubuntu-latest
    outputs:
      environment: ${{ steps.environment.outputs.environment }}
      version: ${{ steps.version.outputs.version }}
      website-bucket: ${{ steps.website-bucket.outputs.website-bucket }}
      website-cfid: ${{ steps.website-cfid.outputs.website-cfid }}
    steps:
      - uses: actions/checkout@v3

      - name: Figure out environment
        id: environment
        run: |
          if ${{ startsWith(github.ref, 'refs/tags/') }}; then
            echo "environment=Production" | tee -a $GITHUB_OUTPUT
          else
            echo "environment=Testing" | tee -a $GITHUB_OUTPUT
          fi

      - name: Get version string
        id: version
        run: |
          if ${{ startsWith(github.ref, 'refs/tags/') }}; then
            echo "version=$(git describe --exact-match --tags $(git rev-parse HEAD))" | tee -a $GITHUB_OUTPUT
          else
            echo "version=$(echo unreleased-$(git rev-parse --short HEAD))" | tee -a $GITHUB_OUTPUT
          fi

      - name: Get bucket name for website
        id: website-bucket
        run: |
          if ${{ startsWith(github.ref, 'refs/tags/') }}; then
            echo "website-bucket=corsobackup.io" | tee -a $GITHUB_OUTPUT
          else
            echo "website-bucket=test-corso-docs" | tee -a $GITHUB_OUTPUT
          fi

      - name: Get cfid for website
        id: website-cfid
        run: |
          if ${{ startsWith(github.ref, 'refs/tags/') }}; then
            echo "website-cfid=E1W9NGI9YTVZ1A" | tee -a $GITHUB_OUTPUT
          else
            echo "website-cfid=ESFTEIYTIP7Y3" | tee -a $GITHUB_OUTPUT
          fi


  # ----------------------------------------------------------------------------------------------------
  # --- Website Linting -----------------------------------------------------------------------------------
  # ----------------------------------------------------------------------------------------------------

  Website-Linting:
    needs: [Precheck, Checkout, SetEnv]
    environment: Testing
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' || needs.precheck.outputs.websitefileschanged == 'true' # websitefileschanged also includes srcfileschanged

    steps:
      - uses: actions/checkout@v3

      - name: Lint Website
        uses: ./.github/actions/website-linting
        with:
          version: ${{ needs.SetEnv.outputs.version }}

  # ----------------------------------------------------------------------------------------------------
  # --- Integration and Unit Testing -------------------------------------------------------------------
  # ----------------------------------------------------------------------------------------------------

  Test-Suite-Trusted:
    needs: [Precheck, Checkout, SetM365App]
    environment: Testing
    runs-on: ubuntu-latest
    if: (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main') || (needs.precheck.outputs.srcfileschanged == 'true' && github.event.pull_request.head.repo.full_name == github.repository)
    defaults:
      run:
        working-directory: src
    env:
      # Resolve the name of the secret that contains the Azure client ID/secret
      AZURE_CLIENT_ID_NAME: ${{ needs.SetM365App.outputs.client_id_env }}
      AZURE_CLIENT_SECRET_NAME: ${{ needs.SetM365App.outputs.client_secret_env }}
      CLIENT_APP_SLOT: ${{ needs.SetM365App.outputs.client_app_slot }}
      CORSO_LOG_FILE: ${{ github.workspace }}/src/testlog/run-ci.log
      LOG_GRAPH_REQUESTS: true
    steps:
      - uses: actions/checkout@v3

      - name: Setup Golang with cache
        uses: magnetikonline/action-golang-cache@v4
        with:
          go-version-file: src/go.mod

      - run: mkdir testlog

      # Install gotestfmt
      - name: Set up gotestfmt
        run: go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest

      # AWS creds
      - name: Configure AWS credentials from Test account
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: ${{ secrets.AWS_IAM_ROLE }}
          role-session-name: integration-testing
          aws-region: us-east-1

      # run the tests
      - name: Integration Tests
        env:
          AZURE_CLIENT_ID: ${{ secrets[env.AZURE_CLIENT_ID_NAME] }}
          AZURE_CLIENT_SECRET: ${{ secrets[env.AZURE_CLIENT_SECRET_NAME] }}
          AZURE_TENANT_ID: ${{ secrets.TENANT_ID }}
          CORSO_CI_TESTS: true
          CORSO_M365_TEST_USER_ID: ${{ vars.CORSO_M365_TEST_USER_ID }}
          CORSO_SECONDARY_M365_TEST_USER_ID: ${{ vars.CORSO_SECONDARY_M365_TEST_USER_ID }}
          CORSO_PASSPHRASE: ${{ secrets.INTEGRATION_TEST_CORSO_PASSPHRASE }}
        run: |
          set -euo pipefail
          go test       \
          -tags testing \
          -json         \
          -v            \
          -failfast     \
          -p 1          \
          -timeout 15m  \
          ./... \
          2>&1 | tee ./testlog/gotest-ci.log | gotestfmt -hide successful-tests

      # Upload the original go test output as an artifact for later review.
      - name: Upload test log
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: ci-test-log
          path: src/testlog/*
          if-no-files-found: error
          retention-days: 14

  Unit-Test-Suite:
    needs: [Precheck, Checkout]
    environment: Testing
    runs-on: ubuntu-latest
    if: needs.precheck.outputs.srcfileschanged == 'true'
    defaults:
      run:
        working-directory: src
    env:
      CORSO_LOG_FILE: ${{ github.workspace }}/src/testlog/run-unit.log
      LOG_GRAPH_REQUESTS: true
    steps:
      - uses: actions/checkout@v3

      - name: Setup Golang with cache
        uses: magnetikonline/action-golang-cache@v4
        with:
          go-version-file: src/go.mod

      - run: mkdir testlog

      # Install gotestfmt
      - name: Set up gotestfmt
        run: go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest

      # run the tests
      - name: Unit Tests
        env:
          # Set these to a bad value so we don't accidentally fall back to
          # something elsewhere.
          CORSO_M365_TEST_USER_ID: 'foo'
          CORSO_SECONDARY_M365_TEST_USER_ID: 'foo'
        run: |
          set -euo pipefail
          go test       \
          -tags testing \
          -json         \
          -v            \
          -failfast     \
          -p 1          \
          -timeout 15m  \
          ./... \
          2>&1 | tee ./testlog/gotest-unit.log | gotestfmt -hide successful-tests

      # Upload the original go test output as an artifact for later review.
      - name: Upload test log
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: unit-test-log
          path: src/testlog/*
          if-no-files-found: error
          retention-days: 14

  Test-Suite-Fork:
    needs: [Precheck]
    environment: Testing
    if: (!startsWith(github.ref , 'refs/tags/') && github.ref != 'refs/heads/main') && (needs.precheck.outputs.srcfileschanged == 'true' && github.event.pull_request.head.repo.full_name != github.repository)
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: src
    env:
      CORSO_LOG_FILE: ${{ github.workspace }}/testlog/run-fork.log
      LOG_GRAPH_REQUESTS: true
    steps:
      - name: Fail check if not repository_dispatch
        if: github.event_name != 'repository_dispatch'
        run: |
          echo "Workflow requires approval from a maintainer to run. It will be automatically rerun on approval."
          exit 1

      - uses: marocchino/sticky-pull-request-comment@v2
        if: github.event.client_payload.slash_command.args.named.sha != '' && contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.args.named.sha)
        with:
          message: |
            Workflow run sha specified via `ok-to-test` is not the latest commit on PR. Run canceled.

      - name: Fail check if not head of PR
        if: github.event.client_payload.slash_command.args.named.sha != '' && contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.args.named.sha)
        run: |
          echo "Workflow run sha specified is not the latest commit on PR. Exiting."
          exit 1

      # add comment to PR with link to workflow run
      - uses: marocchino/sticky-pull-request-comment@v2
        with:
          message: |
            Test suite run will be available at https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID

      # Check out merge commit
      - name: Fork based /ok-to-test checkout
        uses: actions/checkout@v3
        with:
          ref: "refs/pull/${{ github.event.client_payload.pull_request.number }}/merge"

      - name: Setup Golang with cache
        uses: magnetikonline/action-golang-cache@v4
        with:
          go-version-file: src/go.mod

      - run: mkdir testlog

      # Install gotestfmt
      - name: Set up gotestfmt
        run: go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest

      # AWS creds
      - name: Configure AWS credentials from Test account
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: ${{ secrets.AWS_IAM_ROLE }}
          role-session-name: integration-testing
          aws-region: us-east-1

      # run the tests
      - name: Integration Tests
        env:
          AZURE_CLIENT_ID: ${{ secrets.CLIENT_ID }}
          AZURE_CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }}
          AZURE_TENANT_ID: ${{ secrets.TENANT_ID }}
          CORSO_CI_TESTS: true
          CORSO_M365_TEST_USER_ID: ${{ vars.CORSO_M365_TEST_USER_ID }}
          CORSO_PASSPHRASE: ${{ secrets.INTEGRATION_TEST_CORSO_PASSPHRASE }}
        run: |
          set -euo pipefail
          go test \
          -json   \
          -v      \
          -timeout 15m  \
          ./... \
          2>&1 | tee ./testlog/gotest-fork.log | gotestfmt -hide successful-tests

      # Upload the original go test log as an artifact for later review.
      - name: Upload test log
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: fork-test-log
          path: src/testlog/*
          if-no-files-found: error
          retention-days: 14

      # Update check run called "Test-Suite-Fork"
      - uses: actions/github-script@v6
        id: update-check-run
        if: failure()
        env:
          number: ${{ github.event.client_payload.pull_request.number }}
          job: ${{ github.job }}
          # Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run
          conclusion: ${{ job.status }}
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const { data: pull } = await github.rest.pulls.get({
              ...context.repo,
              pull_number: process.env.number
            });
            const ref = pull.head.sha;
            const { data: checks } = await github.rest.checks.listForRef({
              ...context.repo,
              ref
            });
            const check = checks.check_runs.filter(c => c.name === process.env.job);
            const { data: result } = await github.rest.checks.update({
              ...context.repo,
              check_run_id: check[0].id,
              status: 'completed',
              conclusion: process.env.conclusion
            });
            return result;

  # ----------------------------------------------------------------------------------------------------
  # --- Source Code Linting ----------------------------------------------------------------------------
  # ----------------------------------------------------------------------------------------------------

  Source-Code-Linting:
    needs: [Precheck, Checkout]
    environment: Testing
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' || needs.precheck.outputs.srcfileschanged == 'true'
    defaults:
      run:
        working-directory: src
    steps:
      - uses: actions/checkout@v3

      - name: Setup Golang with cache
        uses: magnetikonline/action-golang-cache@v4
        with:
          go-version-file: src/go.mod

      - name: Go Lint
        uses: golangci/golangci-lint-action@v3
        with:
          # Keep pinned to a verson as sometimes updates will add new lint
          # failures in unchanged code.
          version: v1.52.2
          working-directory: src
          skip-pkg-cache: true
          skip-build-cache: true

      # check licenses
      - name: Get go-licenses
        run: go install github.com/google/go-licenses@latest

      - name: Run go-licenses
        run: go-licenses check github.com/alcionai/corso/src --ignore github.com/alcionai/corso/src

      - name: Run staticcheck
        uses: dominikh/staticcheck-action@v1.3.0
        with:
          install-go: false
          working-directory: src


  # ----------------------------------------------------------------------------------------------------
  # --- GitHub Actions Linting -------------------------------------------------------------------------
  # ----------------------------------------------------------------------------------------------------

  Actions-Lint:
    needs: [Precheck]
    environment: Testing
    runs-on: ubuntu-latest
    if: needs.precheck.outputs.actionsfileschanged == 'true'
    steps:
      - uses: actions/checkout@v3

      - name: actionlint
        uses: raven-actions/actionlint@v1
        with:
          fail-on-error: true
          cache: true
          # Ignore
          #   * combining commands into a subshell and using single output
          #     redirect
          #   * various variable quoting patterns
          #   * possible ineffective echo commands
          flags: "-ignore SC2129 -ignore SC2086 -ignore SC2046 -ignore 2116"

  # ----------------------------------------------------------------------------------------------------
  # --- Publish steps ----------------------------------------------------------------------------------
  # ----------------------------------------------------------------------------------------------------

  Publish-Binary:
    needs: [Test-Suite-Trusted, Source-Code-Linting, Website-Linting, SetEnv]
    environment: ${{ needs.SetEnv.outputs.environment }}
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v3

      - name: Publish Binary
        uses: ./.github/actions/publish-binary
        with:
          version: ${{ needs.SetEnv.outputs.version }}
          github_token: ${{ secrets.GITHUB_TOKEN }}
          rudderstack_write_key: ${{ secrets.RUDDERSTACK_CORSO_WRITE_KEY }}
          rudderstack_data_plane_url: ${{ secrets.RUDDERSTACK_CORSO_DATA_PLANE_URL }}

  Publish-Image:
    needs: [Test-Suite-Trusted, Source-Code-Linting, Website-Linting, SetEnv]
    environment: ${{ needs.SetEnv.outputs.environment }}
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/')
    defaults:
      run:
        working-directory: build
    env:
      PLATFORMS: linux/amd64,linux/arm64
    steps:
      - uses: actions/checkout@v3

      # Setup buildx
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v2
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      # retrieve credentials for ghcr.io
      - name: Login to Github Packages
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: ${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=tag
            type=sha,format=short,prefix=
            type=raw,value=nightly

      # deploy the image
      - name: Build image and push to GitHub Container Registry
        uses: docker/build-push-action@v3
        with:
          context: .
          file: ./build/Dockerfile
          platforms: ${{ env.PLATFORMS }}
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          build-args: |
            CORSO_BUILD_LDFLAGS=-X 'github.com/alcionai/corso/src/internal/events.RudderStackWriteKey=${{ secrets.RUDDERSTACK_CORSO_WRITE_KEY }}' -X 'github.com/alcionai/corso/src/internal/events.RudderStackDataPlaneURL=${{ secrets.RUDDERSTACK_CORSO_DATA_PLANE_URL }}' -X 'github.com/alcionai/corso/src/internal/version.Version=${{ needs.SetEnv.outputs.version }}'
          # use the github cache
          cache-from: type=gha
          cache-to: type=gha,mode=max

  Validate-Linux-Artifacts:
    needs: [Publish-Binary, Publish-Image, SetEnv]
    environment: Testing
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/')
    env:
      CORSO_VERSION: ${{ needs.SetEnv.outputs.version }}
    steps:
      - name: Validate x86_64 binary artifacts
        run: |
          set -ex
          curl -L https://github.com/alcionai/corso/releases/download/${{ env.CORSO_VERSION }}/corso_${{ env.CORSO_VERSION }}_Linux_x86_64.tar.gz > corso.tar.gz
          tar -xf corso.tar.gz
          ./corso --help
          ./corso --version 2>&1 | grep -E "version: ${{ env.CORSO_VERSION }}$"
      - name: Validate arm64 binary artifacts
        uses: uraimo/run-on-arch-action@v2
        with:
          arch: armv7
          distro: ubuntu18.04
          install: |
            apt-get -y update && apt-get -y install curl
          run: |
            set -ex
            sudo apt-get update && sudo apt-get install curl
            curl -L https://github.com/alcionai/corso/releases/download/${{ env.CORSO_VERSION }}/corso_${{ env.CORSO_VERSION }}_Linux_arm64.tar.gz > corso.tar.gz
            tar -xf corso.tar.gz
            ./corso --help
            ./corso --version 2>&1 | grep -E "version: ${{ env.CORSO_VERSION }}$"

  Validate-Docker-Artifacts:
    needs: [Publish-Binary, Publish-Image, SetEnv]
    environment: Testing
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/')
    env:
      CORSO_VERSION: ${{ needs.SetEnv.outputs.version }}
    steps:
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v2
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Validate amd64 container images
        run: |
          docker run --platform linux/amd64 ${{ env.IMAGE_NAME }}:${{ env.CORSO_VERSION }} --help
          docker run --platform linux/amd64 ${{ env.IMAGE_NAME }}:${{ env.CORSO_VERSION }} --version | grep -E "version: ${{ env.CORSO_VERSION }}$"
      - name: Validate arm64 container images
        run: |
          docker run --platform linux/arm64 ${{ env.IMAGE_NAME }}:${{ env.CORSO_VERSION }} --help
          docker run --platform linux/amd64 ${{ env.IMAGE_NAME }}:${{ env.CORSO_VERSION }} --version | grep -E "version: ${{ env.CORSO_VERSION }}$"

  Validate-MacOS-Artifacts:
    needs: [Publish-Binary, Publish-Image, SetEnv]
    environment: Testing
    runs-on: macos-latest
    if: startsWith(github.ref, 'refs/tags/')
    env:
      CORSO_VERSION: ${{ needs.SetEnv.outputs.version }}
    steps:
      - name: Validate x86_64 binary artifacts
        run: |
          set -ex
          curl -L https://github.com/alcionai/corso/releases/download/${{ env.CORSO_VERSION }}/corso_${{ env.CORSO_VERSION }}_Darwin_x86_64.tar.gz > corso.tar.gz
          tar -xf corso.tar.gz
          ./corso --help
          ./corso --version 2>&1 | grep -E "version: ${{ env.CORSO_VERSION }}$"
      - name: Validate arm64 binary artifacts
        run: |
          set -ex
          curl -L https://github.com/alcionai/corso/releases/download/${{ env.CORSO_VERSION }}/corso_${{ env.CORSO_VERSION }}_Darwin_arm64.tar.gz > corso.tar.gz
          tar -xf corso.tar.gz
          # TODO: test/validate arm64 executable once we have arm64 CI

  Validate-Windows-Artifacts:
    needs: [Publish-Binary, Publish-Image, SetEnv]
    environment: Testing
    runs-on: windows-latest
    if: startsWith(github.ref, 'refs/tags/')
    env:
      CORSO_VERSION: ${{ needs.SetEnv.outputs.version }}
    steps:
      - name: Validate x86_64 binary artifacts
        run: |
          curl -L https://github.com/alcionai/corso/releases/download/${{ env.CORSO_VERSION }}/corso_${{ env.CORSO_VERSION }}_Windows_x86_64.zip -o corso.zip
          7z x corso.zip
          ./corso.exe --help
          ./corso.exe --version 2>&1 | grep -E "version: ${{ env.CORSO_VERSION }}$"

  Publish-Website-Test:
    needs: [Test-Suite-Trusted, Source-Code-Linting, Website-Linting, SetEnv]
    environment: ${{ needs.SetEnv.outputs.environment }}
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v3 # need to checkout to make the action available

      - name: Publish website
        uses: ./.github/actions/publish-website
        with:
          aws-iam-role: ${{ secrets.AWS_IAM_ROLE }}
          bucket: ${{ needs.SetEnv.outputs.website-bucket }}
          cfid: ${{ needs.SetEnv.outputs.website-cfid }}

  Publish-Website-Prod:
    needs: [SetEnv, Validate-Linux-Artifacts, Validate-MacOS-Artifacts, Validate-Docker-Artifacts, Validate-Windows-Artifacts]
    environment: ${{ needs.SetEnv.outputs.environment }}
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/')

    steps:
      - uses: actions/checkout@v3 # need to checkout to make the action available

      - name: Publish website
        uses: ./.github/actions/publish-website
        with:
          aws-iam-role: ${{ secrets.AWS_IAM_ROLE }}
          bucket: ${{ needs.SetEnv.outputs.website-bucket }}
          cfid: ${{ needs.SetEnv.outputs.website-cfid }}

  Validate-Website-Artifacts:
    needs: [Publish-Website-Prod, SetEnv]
    environment: Testing
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/')
    env:
      CORSO_VERSION: ${{ needs.SetEnv.outputs.version }}
    steps:
      - name: Validate docs
        run: |
          curl https://corsobackup.io/docs/quickstart/ | grep https://github.com/alcionai/corso/releases/download/${{ env.CORSO_VERSION }}/corso_${{ env.CORSO_VERSION }}_Linux_x86_64.tar.gz
