name: Turborepo Test
on:
  push:
    branches: [main]
  pull_request:

concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
  actions: write
  contents: read
  pull-requests: read

jobs:
  find-changes:
    name: Find path changes
    runs-on: ubuntu-latest
    outputs:
      docs: ${{ steps.filter.outputs.docs }}
      basic-example: ${{ steps.filter.outputs.basic-example }}
      kitchen-sink-example: ${{ steps.filter.outputs.kitchen-sink-example }}
      non-monorepo-example: ${{ steps.filter.outputs.non-monorepo-example }}
      with-svelte-example: ${{ steps.filter.outputs.with-svelte-example }}
      with-tailwind-example: ${{ steps.filter.outputs.with-tailwind-example }}
      rest: ${{ steps.filter.outputs.rest }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Check path changes
        id: filter
        run: |
          # Determine the base and head commits to compare
          if [ "${{ github.event_name }}" == "pull_request" ]; then
            # For pull requests, compare the base branch to the current HEAD
            git fetch origin ${{ github.base_ref }}
            BASE_COMMIT="origin/${{ github.base_ref }}"
            HEAD_COMMIT="HEAD"
          else
            # For pushes, use the before and after SHAs
            BASE_COMMIT="${{ github.event.before }}"
            HEAD_COMMIT="${{ github.event.after }}"
          fi

          echo "Comparing changes between $BASE_COMMIT and $HEAD_COMMIT"

          # Function to check if files in given paths have changed
          check_path_changes() {
            local name=$1
            shift
            local paths=("$@")

            # Create a command that checks all paths
            local cmd="git diff --name-only $BASE_COMMIT $HEAD_COMMIT -- "
            for path in "${paths[@]}"; do
              cmd+="\"$path\" "
            done

            # Run the command and check if there are any results
            if eval "$cmd" | grep -q .; then
              echo "$name=true" >> $GITHUB_OUTPUT
              echo "Changes detected in $name paths"
            else
              echo "$name=false" >> $GITHUB_OUTPUT
              echo "No changes in $name paths"
            fi
          }

          # Function to make path checking more readable
          check_paths() {
            local name=$1
            local path_string=$2

            # Convert the comma-separated string to an array
            IFS=',' read -ra path_array <<< "$path_string"

            # Call the check_path_changes function with the name and paths
            check_path_changes "$name" "${path_array[@]}"
          }

          # Check each path pattern with a more readable syntax
          echo "Checking path patterns..."

          check_paths "docs" "docs/"
          check_paths "basic-example" "examples/basic/,turborepo-tests/example-basic-*/,turborepo-tests/helpers/"
          check_paths "kitchen-sink-example" "examples/kitchen-sink/,turborepo-tests/example-kitchen-sink-*/,turborepo-tests/helpers/"
          check_paths "non-monorepo-example" "examples/non-monorepo/,turborepo-tests/example-non-monorepo-*/,turborepo-tests/helpers/"
          check_paths "with-svelte-example" "examples/with-svelte/,turborepo-tests/example-with-svelte-*/,turborepo-tests/helpers/"
          check_paths "with-tailwind-example" "examples/with-tailwind/,turborepo-tests/example-with-svelte-*/,turborepo-tests/helpers/"

          # Handle the "rest" pattern - files that are NOT in examples/ or docs/
          CHANGED_FILES=$(git diff --name-only $BASE_COMMIT $HEAD_COMMIT)

          # Filter to only include files that do NOT start with examples/ or docs/
          FILES_NOT_IN_EXAMPLES_OR_DOCS=$(echo "$CHANGED_FILES" | grep -v -E "^(examples/|docs/)" || true)

          if [ -n "$FILES_NOT_IN_EXAMPLES_OR_DOCS" ]; then
            echo "rest=true" >> $GITHUB_OUTPUT
            echo "Changes detected outside examples/ and docs/ directories"
          else
            echo "rest=false" >> $GITHUB_OUTPUT
            echo "No changes outside examples/ and docs/ directories"
          fi

  integration:
    name: Turborepo Integration
    needs:
      - find-changes
    runs-on: ${{ matrix.os.runner }}
    timeout-minutes: 45
    if: ${{ needs.find-changes.outputs.rest == 'true' }}
    strategy:
      fail-fast: false
      matrix:
        os:
          - runner: ubuntu-latest
          - runner: macos-13
          - runner: windows-latest
    steps:
      # On Windows, set autocrlf to input so that when the repo is cloned down
      # the fixtures retain their line endings and don't get updated to CRLF.
      # We want this because this repo also contains the fixtures for our test cases
      # and these fixtures have files that need stable file hashes. If we let git update
      # the line endings on checkout, the file hashes will change.
      # https://www.git-scm.com/book/en/v2/Customizing-Git-Git-Configuration#_core_autocrlf
      - name: set crlf
        if: matrix.os.runner == 'windows-latest'
        shell: bash
        run: git config --global core.autocrlf input
      - uses: actions/checkout@v4

      - name: Setup Turborepo Environment
        uses: ./.github/actions/setup-turborepo-environment
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"
          node-version: "18.20.2" # TODO: Update integration tests with changed log output in Node.js 22

      - name: Install Global Turbo
        uses: ./.github/actions/install-global-turbo

      - name: Install Bun
        uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1

      - name: Setup Graphviz
        uses: ts-graphviz/setup-graphviz@v2
        with:
          macos-skip-brew-update: "true"
        env:
          HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: true

      - name: Cache Prysk
        id: cache-prysk
        uses: actions/cache@v4
        with:
          path: cli/.cram_env
          key: prysk-venv-${{ matrix.os.runner }}

      - name: Run sccache-cache
        uses: mozilla-actions/sccache-action@v0.0.6

      - name: Integration Tests
        run: |
          if [ -z "${RUSTC_WRAPPER}" ]; then
            unset RUSTC_WRAPPER
          fi
          turbo run test --filter=turborepo-tests-integration --color --env-mode=strict --token=${{ secrets.TURBO_TOKEN }} --team=${{ vars.TURBO_TEAM }}
        shell: bash
        env:
          SCCACHE_BUCKET: turborepo-sccache
          SCCACHE_REGION: us-east-2
          # Only use sccache if we're in the Vercel repo.
          RUSTC_WRAPPER: ${{ !github.event.pull_request.head.repo.fork && 'sccache' || '' }}
          CARGO_INCREMENTAL: 0
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

  rust_lint:
    name: Rust lints
    needs:
      - find-changes
    if: ${{ needs.find-changes.outputs.rest == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Turborepo Environment
        uses: ./.github/actions/setup-turborepo-environment
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"

      - name: Run cargo fmt check
        run: |
          cargo fmt --check

      - name: Check Cargo.toml formatting (taplo)
        run: npx @taplo/cli@0.5.2 format --check

      - name: Check licenses
        uses: EmbarkStudios/cargo-deny-action@v2
        with:
          command: check licenses

      - name: Run cargo clippy
        run: |
          cargo clippy --workspace --features rustls-tls --all-targets -- --deny clippy::all

  rust_check:
    # We test dependency changes only on main
    name: Turborepo rust check
    needs:
      - find-changes
    if: ${{ needs.find-changes.outputs.rest == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Turborepo Environment
        uses: ./.github/actions/setup-turborepo-environment
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"

      - name: Run cargo check
        run: |
          cargo check --workspace

  turbo_types_check:
    name: "@turbo/types codegen check"
    needs:
      - find-changes
    if: ${{ needs.find-changes.outputs.rest == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

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

      - name: Install Global Turbo
        uses: ./.github/actions/install-global-turbo

      - name: Build turbo-types schemas
        run: |
          turbo build --filter=@turbo/types

      - name: Check for uncommitted changes
        run: |
          if ! git diff --exit-code; then
            echo "::error::Generated schema files are out of sync with TypeScript types"
            echo "::error::Please run 'turbo build --filter=@turbo/types' and commit the changes"
            git diff
            exit 1
          fi

  rust_test:
    strategy:
      fail-fast: false
      matrix:
        os:
          - name: ubuntu
            runner: ubuntu-latest
          - name: macos
            runner: macos-13
          - name: windows
            runner: windows-latest
    runs-on: ${{ matrix.os.runner }}
    needs:
      - find-changes
    if: ${{ needs.find-changes.outputs.rest == 'true' }}
    name: Turborepo Rust testing on ${{ matrix.os.name }}
    steps:
      - name: Set git to use LF line endings
        run: |
          git config --global core.autocrlf false
          git config --global core.eol lf
        if: matrix.os.name == 'windows'

      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Turborepo Environment
        uses: ./.github/actions/setup-turborepo-environment
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"

      - name: Run sccache-cache
        uses: mozilla-actions/sccache-action@v0.0.6

      - name: Run tests
        timeout-minutes: 120
        # We explicitly unset RUSTC_WRAPPER if it is an empty string as causes build issues
        run: |
          if [ -z "${RUSTC_WRAPPER}" ]; then
            unset RUSTC_WRAPPER
          fi
          if [ "$RUNNER_OS" == "Windows" ]; then
              cargo test --workspace --exclude turborepo-napi
          else
              cargo test --workspace
          fi
        shell: bash
        env:
          SCCACHE_BUCKET: turborepo-sccache
          SCCACHE_REGION: us-east-2
          # Only use sccache if we're in the Vercel repo.
          RUSTC_WRAPPER: ${{ !github.event.pull_request.head.repo.fork && 'sccache' || '' }}
          CARGO_INCREMENTAL: 0
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

  basic-example:
    name: "`basic` example"
    timeout-minutes: 40
    needs:
      - find-changes
    if: ${{ needs.find-changes.outputs.basic-example == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: "Run `basic` example tests"
        uses: ./.github/actions/examples-tests
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"
          turbo-token: "${{ secrets.TURBO_TOKEN }}"
          turbo-team: "${{ vars.TURBO_TEAM }}"
          test-filter: "@turborepo-examples-tests/basic-*"

  kitchen-sink-example:
    name: "`kitchen-sink` example"
    needs:
      - find-changes
    timeout-minutes: 40
    if: ${{ needs.find-changes.outputs.kitchen-sink-example == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: "Run `kitchen-sink` example tests"
        uses: ./.github/actions/examples-tests
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"
          turbo-token: "${{ secrets.TURBO_TOKEN }}"
          turbo-team: "${{ vars.TURBO_TEAM }}"
          test-filter: "@turborepo-examples-tests/kitchen-sink-*"

  non-monorepo-example:
    name: "`non-monorepo` example"
    timeout-minutes: 40
    needs:
      - find-changes
    if: ${{ needs.find-changes.outputs.non-monorepo-example == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: "Run `non-monorepo` example tests"
        uses: ./.github/actions/examples-tests
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"
          turbo-token: "${{ secrets.TURBO_TOKEN }}"
          turbo-team: "${{ vars.TURBO_TEAM }}"
          test-filter: "@turborepo-examples-tests/non-monorepo-*"

  with-svelte-example:
    name: "`with-svelte` example"
    timeout-minutes: 40
    needs:
      - find-changes
    if: ${{ needs.find-changes.outputs.with-svelte-example == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: "Run `with-svelte` example tests"
        uses: ./.github/actions/examples-tests
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"
          turbo-token: "${{ secrets.TURBO_TOKEN }}"
          turbo-team: "${{ vars.TURBO_TEAM }}"
          test-filter: "@turborepo-examples-tests/with-svelte-*"

  with-tailwind-example:
    name: "`with-tailwind` example"
    timeout-minutes: 40
    needs:
      - find-changes
    if: ${{ needs.find-changes.outputs.with-tailwind-example == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: "Run `with-tailwind` example tests"
        uses: ./.github/actions/examples-tests
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"
          turbo-token: "${{ secrets.TURBO_TOKEN }}"
          turbo-team: "${{ vars.TURBO_TEAM }}"
          test-filter: "@turborepo-examples-tests/with-tailwind-*"

  summary:
    name: Turborepo Test Summary
    runs-on: ubuntu-latest
    if: always()
    needs:
      - find-changes
      - integration
      - rust_lint
      - rust_check
      - turbo_types_check
      - rust_test
      - basic-example
      - kitchen-sink-example
      - non-monorepo-example
      - with-svelte-example
      - with-tailwind-example
    steps:
      - name: Compute info
        id: info
        if: always()
        run: |
          cancelled=false
          failure=false

          subjob () {
            local result=$1
            if [ "$result" = "cancelled" ]; then
              cancelled=true
            elif [ "$result" != "success" ] && [ "$result" != "skipped" ]; then
              failure=true
            fi
          }

          subjob ${{needs.integration.result}}
          subjob ${{needs.rust_lint.result}}
          subjob ${{needs.rust_check.result}}
          subjob ${{needs.turbo_types_check.result}}
          subjob ${{needs.rust_test.result}}
          subjob ${{needs.basic-example.result}}
          subjob ${{needs.kitchen-sink-example.result}}
          subjob ${{needs.non-monorepo-example.result}}
          subjob ${{needs.with-svelte-example.result}}
          subjob ${{needs.with-tailwind-example.result}}

          if [ "$cancelled" = "true" ]; then
            echo "cancelled=true" >> $GITHUB_OUTPUT
          elif [ "$failure" = "true" ]; then
            echo "failure=true" >> $GITHUB_OUTPUT
          else
            echo "success=true" >> $GITHUB_OUTPUT
          fi

      - name: Failed
        if: steps.info.outputs.failure == 'true'
        run: exit 1

      - name: Succeeded
        if: steps.info.outputs.success == 'true'
        run: echo Ok

  cleanup:
    name: Cleanup
    needs: summary
    if: always()
    uses: ./.github/workflows/pr-clean-caches.yml
    secrets: inherit
