name: ci
on:
  push:
    branches:
      - main
      - r[0-9]+ # Trigger builds after a push to weekly branches
    tags:
      # The following regex matches the Mimir release tag. Tag filters not as strict due to different regex system on Github Actions.
      - mimir-[0-9]+.[0-9]+.[0-9]+**
  pull_request:

concurrency:
  # Cancel any running workflow for the same branch when new commits are pushed.
  # We group both by ref_name (available when CI is triggered by a push to a branch/tag)
  # and head_ref (available when CI is triggered by a PR).
  group: "${{ github.ref_name }}-${{ github.head_ref }}"
  cancel-in-progress: true

jobs:
  prepare:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v4
      - name: Get build image from Makefile
        id: build_image_step
        run: echo "build_image=$(make print-build-image)" >> "$GITHUB_OUTPUT"
    outputs:
      build_image: ${{ steps.build_image_step.outputs.build_image }}
      # Determine if we will deploy (aka push) the image to the registry.
      is_deploy: ${{ (startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/heads/r')) && github.event_name == 'push' && github.repository == 'grafana/mimir' }}

  goversion:
    runs-on: ubuntu-latest
    needs: prepare
    container:
      image: ${{ needs.prepare.outputs.build_image }}
    steps:
      - uses: actions/checkout@v4
      - name: Run Git Config
        run: git config --global --add safe.directory '*'
      - name: Get Go Version
        id: go-version
        run: |
          echo "version=$(make BUILD_IN_CONTAINER=false print-go-version)" >> "$GITHUB_OUTPUT"
    outputs:
      version: ${{ steps.go-version.outputs.version }}

  lint:
    runs-on: ubuntu-latest
    needs: prepare
    container:
      image: ${{ needs.prepare.outputs.build_image }}
    steps:
      - name: Check out repository
        uses: actions/checkout@v4
      - name: Run Git Config
        run: git config --global --add safe.directory '*'
        # Commands in the Makefile are hardcoded with an assumed file structure of the CI container
        # Symlink ensures paths specified in previous commands don’t break
      - name: Symlink Expected Path to Workspace
        run: |
          mkdir -p /go/src/github.com/grafana/mimir
          ln -s $GITHUB_WORKSPACE/* /go/src/github.com/grafana/mimir
      - name: Get golangci-lint cache path
        id: golangcilintcache
        run: |
          echo "path=$(golangci-lint cache status | grep 'Dir: ' | cut -d ' ' -f2)" >> "$GITHUB_OUTPUT"
      - name: Cache golangci-lint cache
        uses: actions/cache@v4
        with:
          key: lint-golangci-lint-${{ runner.os }}-${{ hashFiles('go.mod', 'go.sum', '.golangci.yml', 'Makefile') }}
          path: ${{ steps.golangcilintcache.outputs.path }}
      - name: Get Go cache paths
        id: goenv
        run: |
          echo "gocache=$(go env GOCACHE)" >> "$GITHUB_OUTPUT"
          echo "gomodcache=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT"
      - name: Cache Go build cache
        uses: actions/cache@v4
        with:
          key: lint-go-build-${{ runner.os }}-${{ hashFiles('go.mod', 'go.sum') }}
          path: ${{ steps.goenv.outputs.gocache }}
      # Although we use vendoring, this linting job downloads all modules to verify that what is vendored is correct,
      # so it'll use GOMODCACHE. Other jobs don't need this.
      - name: Cache Go module cache
        uses: actions/cache@v4
        with:
          key: lint-go-mod-${{ runner.os }}-${{ hashFiles('go.mod', 'go.sum') }}
          path: ${{ steps.goenv.outputs.gomodcache }}
      - name: Lint
        run: make BUILD_IN_CONTAINER=false lint
      - name: Check Vendor Directory
        run: make BUILD_IN_CONTAINER=false mod-check
      - name: Check Protos
        run: make BUILD_IN_CONTAINER=false check-protos
      - name: Check Generated Documentation
        run: make BUILD_IN_CONTAINER=false check-doc
      - name: Check White Noise
        run: make BUILD_IN_CONTAINER=false check-white-noise
      - name: Check License Header
        run: make BUILD_IN_CONTAINER=false check-license
      - name: Check Docker-Compose YAML
        run: make BUILD_IN_CONTAINER=false check-mimir-microservices-mode-docker-compose-yaml check-mimir-read-write-mode-docker-compose-yaml
      - name: Check Generated OTLP Code
        run: make BUILD_IN_CONTAINER=false check-generated-otlp-code

  doc-validator:
    runs-on: ubuntu-latest
    container:
      image: grafana/doc-validator:v5.2.0
    steps:
      - name: Check out repository
        uses: actions/checkout@v4
      - name: Run Git Config
        run: git config --global --add safe.directory '*'
      - name: Run doc-validator tool (mimir)
        run: >
          doc-validator
          '--skip-checks=^canonical-does-not-match-pretty-URL|image.+$'
          docs/sources/mimir
          /docs/mimir/latest
          | reviewdog
          -f=rdjsonl
          --fail-on-error
          --filter-mode=nofilter
          --name=doc-validator
          --reporter=github-pr-review
        env:
          REVIEWDOG_GITHUB_API_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
      - name: Run doc-validator tool (helm-charts)
        run: >
          doc-validator
          docs/sources/helm-charts
          /docs/helm-charts/mimir-distributed/latest
          | reviewdog
          -f=rdjsonl
          --fail-on-error
          --filter-mode=nofilter
          --name=doc-validator
          --reporter=github-pr-review
        env:
          REVIEWDOG_GITHUB_API_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

  lint-jsonnet:
    runs-on: ubuntu-latest
    needs:
      - prepare
    container:
      image: ${{ needs.prepare.outputs.build_image }}
    steps:
      - name: Check out repository
        uses: actions/checkout@v4
      - name: Run Git Config
        run: git config --global --add safe.directory '*'
        # Commands in the Makefile are hardcoded with an assumed file structure of the CI container
        # Symlink ensures paths specified in previous commands don’t break
      - name: Symlink Expected Path to Workspace
        run: |
          mkdir -p /go/src/github.com/grafana/mimir
          ln -s $GITHUB_WORKSPACE/* /go/src/github.com/grafana/mimir
      - name: Check Mixin
        run: make BUILD_IN_CONTAINER=false check-mixin
      - name: Check Mixin Tests
        run: make BUILD_IN_CONTAINER=false check-mixin-tests
      - name: Check Mixin with Mimirtool rules check
        run: make BUILD_IN_CONTAINER=false check-mixin-mimirtool-rules
      - name: Check Jsonnet Manifests
        run: make BUILD_IN_CONTAINER=false check-jsonnet-manifests
      - name: Check Jsonnet Getting Started
        run: make BUILD_IN_CONTAINER=false check-jsonnet-getting-started
      - name: Check Jsonnet Tests
        run: make BUILD_IN_CONTAINER=false check-jsonnet-tests

  lint-helm:
    runs-on: ubuntu-latest
    needs:
      - prepare
    container:
      image: ${{ needs.prepare.outputs.build_image }}
    steps:
      - name: Check out repository
        uses: actions/checkout@v4
      - name: Run Git Config
        run: git config --global --add safe.directory '*'
        # Commands in the Makefile are hardcoded with an assumed file structure of the CI container
        # Symlink ensures paths specified in previous commands don’t break
      - name: Symlink Expected Path to Workspace
        run: |
          mkdir -p /go/src/github.com/grafana/mimir
          ln -s $GITHUB_WORKSPACE/* /go/src/github.com/grafana/mimir
      - name: Set up Helm
        uses: azure/setup-helm@v4
        with:
          version: v3.8.2
      - name: Check Helm Tests
        run: make BUILD_IN_CONTAINER=false check-helm-tests

  test:
    runs-on: ubuntu-latest
    strategy:
      # Do not abort other groups when one fails.
      fail-fast: false
      # Split tests into 4 groups.
      matrix:
        test_group_id:    [0, 1, 2, 3]
        test_group_total: [4]
    needs:
      - prepare
    container:
      image: ${{ needs.prepare.outputs.build_image }}
    steps:
      - name: Check out repository
        uses: actions/checkout@v4
      - name: Run Git Config
        run: git config --global --add safe.directory '*'
      - name: Symlink Expected Path to Workspace
        run: |
          mkdir -p /go/src/github.com/grafana/mimir
          ln -s $GITHUB_WORKSPACE/* /go/src/github.com/grafana/mimir
      - name: Get Go build cache path
        id: gocache
        run: |
          echo "path=$(go env GOCACHE)" >> "$GITHUB_OUTPUT"
      - name: Cache Go build cache
        uses: actions/cache@v4
        with:
          # Cache is shared between test groups.
          key: test-go-build-${{ runner.os }}-${{ hashFiles('go.mod', 'go.sum') }}
          path: ${{ steps.gocache.outputs.path }}
      - name: Run Tests
        run: |
          echo "Running unit tests (group ${{ matrix.test_group_id }} of ${{ matrix.test_group_total }}) with Go version: $(go version)"
          ./.github/workflows/scripts/run-unit-tests-group.sh --index ${{ matrix.test_group_id }} --total ${{ matrix.test_group_total }}

  test-docs:
    uses: ./.github/workflows/test-docs.yml

  build:
    runs-on: ubuntu-latest
    needs:
      - prepare
    container:
      image: ${{ needs.prepare.outputs.build_image }}
    steps:
      - name: Check out repository
        uses: actions/checkout@v4
      - name: Run Git Config
        run: git config --global --add safe.directory '*'
      - name: Install Docker Client
        run: ./.github/workflows/scripts/install-docker.sh
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
      - name: Set up Docker Buildx
        id: buildx
        uses: docker/setup-buildx-action@v3
      - name: Symlink Expected Path to Workspace
        run: |
          mkdir -p /go/src/github.com/grafana/mimir
          ln -s $GITHUB_WORKSPACE/* /go/src/github.com/grafana/mimir
      - name: Get Go build cache path
        id: gocache
        run: |
          echo "path=$(go env GOCACHE)" >> "$GITHUB_OUTPUT"
      - name: Cache Go build cache
        uses: actions/cache@v4
        with:
          key: build-go-build-${{ runner.os }}-${{ hashFiles('go.mod', 'go.sum') }}
          path: ${{ steps.gocache.outputs.path }}
      - name: Build Multiarch Docker Images Locally
        # Ignore mimir-build-image and mimir-rules-action.
        run: |
          ./.github/workflows/scripts/build-images.sh /tmp/images $(make list-image-targets | grep -v -E '/mimir-build-image/|/mimir-rules-action/')
      - name: Build Archive With Docker Images
        run: |
          tar cvf images.tar /tmp/images
      - name: Upload Archive with Docker Images
        uses: actions/upload-artifact@v4
        with:
          name: Docker Images
          path: ./images.tar
      - name: Build Mimir with race-detector
        run: |
          make BUILD_IN_CONTAINER=false cmd/mimir/.uptodate_race
          export IMAGE_TAG_RACE=$(make image-tag-race)
          export MIMIR_DISTROLESS_IMAGE="grafana/mimir:$IMAGE_TAG_RACE"
          docker save $MIMIR_DISTROLESS_IMAGE -o ./mimir_race_image_distroless
      - name: Upload archive with race-enabled Mimir
        uses: actions/upload-artifact@v4
        with:
          name: Race-enabled Mimir
          path: |
            ./mimir_race_image_distroless

  integration:
    needs: [goversion, build, prepare]
    runs-on: ubuntu-latest
    strategy:
      # Do not abort other groups when one fails.
      fail-fast: false
      # Split tests into 6 groups.
      matrix:
        test_group_id:    [0, 1, 2, 3, 4, 5]
        test_group_total: [6]
    steps:
      - name: Upgrade golang
        uses: actions/setup-go@v5
        with:
          go-version: ${{ needs.goversion.outputs.version }}
          cache: false # We manage caching ourselves below to maintain consistency with the other jobs that don't use setup-go.
      - name: Check out repository
        uses: actions/checkout@v4
      - name: Run Git Config
        run: git config --global --add safe.directory '*'
      - name: Install Docker Client
        run: sudo ./.github/workflows/scripts/install-docker.sh
      - name: Symlink Expected Path to Workspace
        run: |
          sudo mkdir -p /go/src/github.com/grafana/mimir
          sudo ln -s $GITHUB_WORKSPACE/* /go/src/github.com/grafana/mimir
      - name: Get Go build cache path
        id: gocache
        run: |
          echo "path=$(go env GOCACHE)" >> "$GITHUB_OUTPUT"
      - name: Cache Go build cache
        uses: actions/cache@v4
        with:
          # Cache is shared between test groups.
          key: integration-go-build-${{ runner.os }}-${{ hashFiles('go.mod', 'go.sum') }}
          path: ${{ steps.gocache.outputs.path }}
      - name: Download Archive with Docker Images
        uses: actions/download-artifact@v4
        with:
          name: Docker Images
      - name: Extract Docker Images from Archive
        run: tar xvf images.tar -C /
      - name: Load Mimirtool Image into Docker
        run: |
          export IMAGE_TAG=$(make image-tag)
          # skopeo will by default load system-specific version of the image (linux/amd64).
          # note that this doesn't use skopeo version from our build-image, because we don't use build-image when running integration tests.
          # that's why we use docker run to run latest version.
          docker run -v /tmp/images:/tmp/images -v /var/run/docker.sock:/var/run/docker.sock quay.io/skopeo/stable:v1.15.1 copy oci-archive:/tmp/images/mimirtool.oci "docker-daemon:grafana/mimirtool:$IMAGE_TAG"
      - name: Download Archive with Docker Images
        uses: actions/download-artifact@v4
        with:
          name: Race-enabled Mimir
      - name: Load race-enabled mimir into Docker
        run: |
          export IMAGE_TAG_RACE=$(make image-tag-race)
          docker load -i ./mimir_race_image_distroless
          docker run "grafana/mimir:$IMAGE_TAG_RACE" --version
      - name: Preload Images
        # We download docker images used by integration tests so that all images are available
        # locally and the download time doesn't account in the test execution time, which is subject
        # to a timeout
        run: go run ./tools/pre-pull-images | xargs -n1 -P4 docker pull
      - name: Integration Tests
        run: |
          export IMAGE_TAG_RACE=$(make image-tag-race)
          export MIMIR_IMAGE="grafana/mimir:$IMAGE_TAG_RACE"
          export IMAGE_TAG=$(make image-tag)
          export MIMIRTOOL_IMAGE="grafana/mimirtool:$IMAGE_TAG"
          export MIMIR_CHECKOUT_DIR="/go/src/github.com/grafana/mimir"
          echo "Running integration tests with image: $MIMIR_IMAGE (Mimir), $MIMIRTOOL_IMAGE (Mimirtool)"
          echo "Running integration tests (group ${{ matrix.test_group_id }} of ${{ matrix.test_group_total }}) with Go version: $(go version)"
          ./.github/workflows/scripts/run-integration-tests-group.sh --index ${{ matrix.test_group_id }} --total ${{ matrix.test_group_total }}

  deploy:
    needs: [prepare, build, test, lint, integration]
    # Only deploy images on pushes to the grafana/mimir repo, which either are tag pushes or weekly release branch pushes.
    if: needs.prepare.outputs.is_deploy == 'true'
    runs-on: ubuntu-latest
    container:
      image: ${{ needs.prepare.outputs.build_image }}
    steps:
      - name: Check out repository
        uses: actions/checkout@v4
      - name: Run Git Config
        run: git config --global --add safe.directory '*'
      - name: Install Docker Client
        run: ./.github/workflows/scripts/install-docker.sh
      - name: Symlink Expected Path to Workspace
        run: |
          mkdir -p /go/src/github.com/grafana/mimir
          ln -s $GITHUB_WORKSPACE/* /go/src/github.com/grafana/mimir
      - name: Download Archive with Docker Images
        uses: actions/download-artifact@v4
        with:
          name: Docker Images
      - name: Extract Docker Images from Archive
        run: tar xvf images.tar -C /
      - name: Deploy
        run: |
          if [ -n "$DOCKER_PASSWORD" ]; then
            printenv DOCKER_PASSWORD | skopeo login -u "$DOCKER_USERNAME" --password-stdin docker.io
          fi
          ./.github/workflows/scripts/push-images.sh /tmp/images grafana/ $(make image-tag)
        env:
          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
          DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}