name: frontend

on:
  push:
    branches:
      - master
  pull_request:

# Cancel in progress workflows on pull_requests.
# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value
concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

# hack for https://github.com/actions/cache/issues/810#issuecomment-1222550359
env:
  SEGMENT_DOWNLOAD_TIMEOUT_MINS: 3
  NODE_OPTIONS: '--max-old-space-size=4096'

jobs:
  files-changed:
    name: detect what files changed
    runs-on: ubuntu-22.04
    timeout-minutes: 3
    # Map a step output to a job output
    outputs:
      lintable_css_in_js_modified: ${{ steps.changes.outputs.lintable_css_in_js_modified }}
      lintable_css_in_js_modified_files: ${{ steps.changes.outputs.lintable_css_in_js_modified_files }}
      lintable_css_in_js_rules_changed: ${{ steps.changes.output.lintable_css_in_js_rules_changed }}
      lintable_modified: ${{ steps.changes.outputs.lintable_modified }}
      lintable_modified_files: ${{ steps.changes.outputs.lintable_modified_files }}
      lintable_rules_changed: ${{ steps.changes.outputs.lintable_rules_changed }}
      testable_modified: ${{ steps.changes.outputs.testable_modified }}
      testable_modified_files: ${{ steps.changes.outputs.testable_modified_files }}
      testable_rules_changed: ${{ steps.changes.outputs.testable_rules_changed }}
      frontend_all: ${{ steps.changes.outputs.frontend_all }}
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

      - name: Check for frontend file changes
        uses: dorny/paths-filter@0bc4621a3135347011ad047f9ecf449bf72ce2bd # v3.0.0
        id: changes
        with:
          token: ${{ github.token }}
          filters: .github/file-filters.yml
          list-files: shell

  typescript-and-lint:
    if: needs.files-changed.outputs.frontend_all == 'true'
    needs: files-changed
    name: typescript and lint
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

      - name: Internal github app token
        id: token
        uses: getsentry/action-github-app-token@d4b5da6c5e37703f8c3b3e43abb5705b46e159cc # v3.0.0
        continue-on-error: true
        with:
          app_id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
          private_key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }}

      - uses: getsentry/action-setup-volta@e4939d337b83760d13a9d7030a6f68c9d0ee7581 # v2.0.0

      - name: Install dependencies
        id: dependencies
        run: yarn install --frozen-lockfile

      # Setup custom tsc matcher, see https://github.com/actions/setup-node/issues/97
      - name: setup matchers
        run: |
          echo "::remove-matcher owner=masters::"
          echo "::add-matcher::.github/tsc.json"
          echo "::add-matcher::.github/eslint-stylish.json"

      # When we're on master we can run all tasks across all files
      # or if lint rules have changed, run the related task across all files
      - name: biome (all files)
        if: github.ref == 'refs/heads/master'
        run: yarn lint:biome

      - name: prettier (all files)
        if: github.ref == 'refs/heads/master' || needs.files-changed.outputs.lintable_rules_changed == 'true'
        run: yarn lint:prettier

      - name: eslint (all files)
        if: github.ref == 'refs/heads/master' || needs.files-changed.outputs.lintable_rules_changed == 'true'
        run: yarn lint:js

      - name: stylelint (all files)
        if: github.ref == 'refs/heads/master' || needs.files-changed.outputs.lintable_css_in_js_rules_changed == 'true'
        run: yarn lint:css

      # When on a PR branch, we only need to look at the changed files
      - name: biome (fix)
        if: github.ref != 'refs/heads/master'
        run: yarn fix:biome

      - name: prettier (changed files only)
        if: github.ref != 'refs/heads/master' && needs.files-changed.outputs.lintable_rules_changed != 'true' && needs.files-changed.outputs.lintable_modified == 'true'
        run: yarn prettier --write ${{ needs.files-changed.outputs.lintable_modified_files }}

      - name: eslint (changed files only)
        if: github.ref != 'refs/heads/master' && needs.files-changed.outputs.lintable_rules_changed != 'true' && needs.files-changed.outputs.lintable_modified == 'true'
        run: yarn eslint --fix ${{ needs.files-changed.outputs.lintable_modified_files }}

      - name: stylelint (changed files only)
        if: github.ref != 'refs/heads/master' && needs.files-changed.outputs.lintable_css_in_js_rules_changed != 'true' && needs.files-changed.outputs.lintable_css_in_js_modified == 'true'
        run: yarn stylelint ${{ needs.files-changed.outputs.lintable_css_in_js_modified_files }}

      # Check (and error) for dirty working tree for forks
      # Reason being we need a different token to auto commit changes and
      # forks do not have access to said token
      - name: Check for dirty git working tree (forks)
        if: github.ref != 'refs/heads/master' && steps.token.outcome != 'success'
        run: |
          git diff --quiet || (echo '::error ::lint produced file changes, run linter locally and try again' && exit 1)

      # If working tree is dirty, commit and update if we have a token
      - name: Commit any eslint fixed files
        if: github.ref != 'refs/heads/master' && steps.token.outcome == 'success'
        uses: getsentry/action-github-commit@31f6706ca1a7b9ad6d22c1b07bf3a92eabb05632 # v2.0.0
        with:
          github-token: ${{ steps.token.outputs.token }}
          message: ':hammer_and_wrench: apply eslint style fixes'

      - name: tsc
        id: tsc
        if: steps.dependencies.outcome == 'success'
        run: yarn tsc -p config/tsconfig.ci.json

  frontend-jest-tests:
    if: needs.files-changed.outputs.testable_rules_changed == 'true' || needs.files-changed.outputs.testable_modified == 'true'
    needs: files-changed
    name: Jest
    # If you change the runs-on image, you must also change the runner in jest-balance.yml
    # so that the balancer runs in the same environment as the tests.
    runs-on: ubuntu-22.04
    timeout-minutes: 30
    strategy:
      # This helps not having to run multiple jobs because one fails, thus, reducing resource usage
      # and reducing the risk that one of many runs would turn red again (read: intermittent tests)
      fail-fast: false
      matrix:
        # XXX: When updating this, make sure you also update CI_NODE_TOTAL.

        instance: [0, 1, 2, 3]

    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
        name: Checkout sentry

        with:
          # Avoid codecov error message related to SHA resolution:
          # https://github.com/codecov/codecov-bash/blob/7100762afbc822b91806a6574658129fe0d23a7d/codecov#L891
          fetch-depth: '2'

      - uses: getsentry/action-setup-volta@e4939d337b83760d13a9d7030a6f68c9d0ee7581 # v2.0.0

      - name: node_modules cache
        uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
        id: nodemodulescache
        with:
          path: node_modules
          key: ${{ runner.os }}-node-modules-${{ hashFiles('yarn.lock', 'api-docs/yarn.lock') }}

      - name: Install Javascript Dependencies
        if: steps.nodemodulescache.outputs.cache-hit != 'true'
        run: yarn install --frozen-lockfile

      - name: jest
        env:
          GITHUB_PR_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
          GITHUB_PR_REF: ${{ github.event.pull_request.head.ref || github.ref }}
          # XXX: CI_NODE_TOTAL must be hardcoded to the length of strategy.matrix.instance.
          #      Otherwise, if there are other things in the matrix, using strategy.job-total
          #      wouldn't be correct. Also, if this increases, make sure to also increase
          #      `flags.frontend.after_n_builds` in `codecov.yml`.
          CI_NODE_TOTAL: 4
          CI_NODE_INDEX: ${{ matrix.instance }}
          # Disable testing-library from printing out any of of the DOM to
          # stdout. No one actually looks through this in CI, they're just
          # going to run it locally.
          #
          # This quiets up the logs quite a bit.
          DEBUG_PRINT_LIMIT: 0
        run: |
          JEST_TESTS=$(yarn -s jest --listTests --json) yarn test-ci --forceExit

      # We only upload coverage data for FE changes since it conflicts with
      # codecov's carry forward functionality.
      # Upload coverage data even if running the tests step fails since
      # it reduces large coverage fluctuations.
      - name: Handle artifacts
        uses: ./.github/actions/artifacts
        if: always()
        with:
          files: .artifacts/coverage/*
          type: frontend
          token: ${{ secrets.CODECOV_TOKEN }}

  # This check runs once all dependant jobs have passed
  # It symbolizes that all required Frontend checks have succesfully passed (Or skipped)
  # This check is the only required Github check
  frontend-required-check:
    needs: [files-changed, frontend-jest-tests, typescript-and-lint]
    name: Frontend
    # This is necessary since a failed/skipped dependent job would cause this job to be skipped
    if: always()
    runs-on: ubuntu-22.04
    steps:
      # If any jobs we depend on fail, we will fail since this is a required check
      # NOTE: A timeout is considered a failure
      - name: Check for failures
        if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
        run: |
          echo "One of the dependent jobs have failed. You may need to re-run it." && exit 1