Automatic PR Merger

name: Automatic PR Merger

on:
  push: {} # update PR when base branch is updated

jobs:
  # thats's all. single step is needed - if PR is mergeable according to
  # branch protection rules it will be merged automatically
  mergepal:
    runs-on: ubuntu-22.04
    if: github.repository == 'RasaHQ/rasa'

    steps:
      - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c
      - uses: rasahq/update-pr-branch@f7012036a6d5659cfbc37f180716963511e81f95
        with:
          token: ${{ secrets.UPDATE_BRANCH_PAT }}
          # required parameter by original action -
          # check is already done through protected branches so not needed for us
          required_approval_count: 0
          # update branch despite failing check runs
          require_passed_checks: false


CI Github Actions

name: CI Github Actions

on:
  push:
    branches:
      - main
    tags:
      - "*"
  pull_request:

env:
  DEFAULT_PYTHON_VERSION: "3.10"

jobs:
  test:
    name: Run Tests
    runs-on: ubuntu-22.04
    #missing matrix
    strategy:
      fail-fast: false

    steps:
      - name: Checkout git repository 🕝
        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c

      - name: Download gomplate
        run: |-
          sudo curl -o /usr/local/bin/gomplate -sSL https://github.com/hairyhenderson/gomplate/releases/download/v3.9.0/gomplate_linux-amd64
          sudo chmod +x /usr/local/bin/gomplate

      - name: Set up Python ${{ env.DEFAULT_PYTHON_VERSION }} 🐍
        uses: actions/setup-python@57ded4d7d5e986d7296eab16560982c6dd7c923b
        with:
          python-version: ${{ env.DEFAULT_PYTHON_VERSION }}

      - name: Read Poetry Version 🔢
        run: |
          echo "POETRY_VERSION=$(scripts/poetry-version.sh)" >> $GITHUB_ENV
        shell: bash

      - name: Install poetry 🦄
        uses: Gr1N/setup-poetry@15821dc8a61bc630db542ae4baf6a7c19a994844 # v8
        with:
          poetry-version: ${{ env.POETRY_VERSION }}

      - name: Load Poetry Cached Libraries ⬇
        id: cache-poetry
        uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8
        with:
          path: .venv
          key: ${{ runner.os }}-poetry-${{ env.POETRY_VERSION }}-${{ env.DEFAULT_PYTHON_VERSION }}-${{ hashFiles('**/poetry.lock') }}-${{ secrets.POETRY_CACHE_VERSION }}
          restore-keys: ${{ runner.os }}-poetry-${{ env.DEFAULT_PYTHON_VERSION }}

      - name: Clear Poetry cache
        if: steps.cache-poetry.outputs.cache-hit == 'true' && contains(github.event.pull_request.labels.*.name, 'tools:clear-poetry-cache-unit-tests')
        run: rm -r .venv

      - name: Create virtual environment
        if: (steps.cache-poetry.outputs.cache-hit != 'true' || contains(github.event.pull_request.labels.*.name, 'tools:clear-poetry-cache-unit-tests'))
        run: python -m venv create .venv

      - name: Set up virtual environment
        run: poetry config virtualenvs.in-project true

      - name: Install Dependencies 📦
        run: |
          make install-full

      - name: Lint Code 🎎
        run: |
          poetry run ruff check .github --extend-ignore D
          poetry run black --check .github

      - name: Test Code 🔍
        run: |
          make test-gh-actions


Semgrep

# Name of this GitHub Actions workflow.
name: Semgrep

on:
  # Scan mainline branches and report all findings:
  push:
    branches:
      - main
  # Scan changed files in PRs (diff-aware scanning):
  pull_request:

jobs:
  semgrep:
    # User-definable name of this GitHub Actions job:
    name: Semgrep Workflow Security Scan
    # If you are self-hosting, change the following `runs-on` value:
    runs-on: ubuntu-22.04

    container:
      # A Docker image with Semgrep installed. Do not change this.
      image: returntocorp/semgrep@sha256:37736e4992c539f760e36e14d48924bd9fa70d0abbde39a6d86d93f66a1affd4

    # To skip any PR created by dependabot to avoid permission issues:
    if: (github.actor != 'dependabot[bot]')

    steps:
      # Fetch project source with GitHub Actions Checkout.
      - uses: actions/checkout@v3
      # Run the "semgrep ci" command on the command line of the docker image.
      - run: semgrep ci
        env:
          # Add the rules that Semgrep uses by setting the SEMGREP_RULES environment variable.
          SEMGREP_RULES: p/github-actions


Backup Rasa repo to S3

name: Backup Rasa repo to S3
on:
  schedule:
    # Run cron job at 7AM Monday to Sunday.
    - cron: "0 7 * * *"
  workflow_dispatch:

permissions:
  id-token: write
  contents: read

jobs:
  backup:
    runs-on: ubuntu-22.04
    steps:
      - name: Checkout repository
        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c
        with:
          fetch-depth: 0

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@8c3f20df09ac63af7b3ae3d7c91f105f857d8497 #v3.0.1
        with:
          role-to-assume: ${{ secrets.GIT_BACKUP_ROLE_ARN }}
          aws-region: ${{ secrets.GIT_BACKUP_REGION }}

      # Determine the date in the right format for us to tag the image.
      - name: Get current date
        id: date
        run: echo "date=$(date +'%Y%m%d')" >> $GITHUB_OUTPUT

      - name: Create Git bundle
        run: git bundle create --progress rasa-${{ steps.date.outputs.date }}.bundle --all

      - name: Verify Git bundle
        run: git bundle verify rasa-${{ steps.date.outputs.date }}.bundle

      - name: Upload bundle to S3
        run: aws s3 cp rasa-${{ steps.date.outputs.date }}.bundle s3://${{ secrets.GIT_BACKUP_BUCKET }}/rasa/


Cron Test to Check Pip Installation

# This is a installation test that we run daily. We attempt to install Rasa
# on Ubuntu, Mac and Windows and try to run `rasa init`. The goal here is to
# catch installation issues in an automated way.

name: Cron Test to Check Pip Installation

on:
  schedule:
    - cron: "0 0 * * *"

jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os:
          [ubuntu-22.04, ubuntu-18.04, macos-latest, windows-2019, windows-2022]
        python-version: [3.8, 3.9, '3.10']
      fail-fast: false

    steps:
      - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@57ded4d7d5e986d7296eab16560982c6dd7c923b
        with:
          python-version: ${{ matrix.python-version }}
      - name: Try to install Rasa.
        run: |
          python -m pip install rasa
      - name: Try to install Rasa[full].
        run: |
          python -m pip install rasa[full]
      - name: Try to run Rasa.
        run: |
          python -m rasa --version
          # Must create a folder first
          mkdir rasa-demo
          # Next we try to init.
          python -m rasa init --init-dir rasa-demo --no-prompt
      - name: Notify slack on failure
        if: failure()
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
        uses: voxmedia/github-action-slack-notify-build@3665186a8c1a022b28a1dbe0954e73aa9081ea9e
        with:
          channel_id: ${{ secrets.SLACK_ALERTS_CHANNEL_ID }}
          status: FAILED
          color: danger


Nightly Builds

name: Nightly Builds
on:
  schedule:
    # Runs every weekday at 1am
    - cron: 0 1 * * 1-5
  workflow_dispatch:

jobs:
  run_script_and_tag_nightly_release:
    name: Run release script and tag a new nightly release
    runs-on: ubuntu-22.04
    outputs:
      tag_name: ${{ steps.set_tagname.outputs.tag_name }}

    steps:
      - name: Checkout git repository 🕝
        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}

      - name: Set up Python ${{ env.DEFAULT_PYTHON_VERSION }} 🐍
        uses: actions/setup-python@57ded4d7d5e986d7296eab16560982c6dd7c923b
        with:
          python-version: ${{ env.DEFAULT_PYTHON_VERSION }}

      - name: Compose tag name
        id: set_tagname
        run: |
          DATE=$(date +'%Y%m%d')

          # Find latest rasa-oss version
          echo "Trying to find the latest rasa-oss version..."
          pip install toml
          LATEST_RASA_MINOR=$(python scripts/get_version_from_toml.py)
          echo "Current RASA version: ${LATEST_RASA_MINOR}"

          LATEST_NIGHTLY_VERSION=$(echo ${LATEST_RASA_MINOR})

          echo "Composing nightly build tag name..."
          GH_TAG=${LATEST_NIGHTLY_VERSION}.dev${DATE}
          echo "New nightly release version: ${GH_TAG}"
          echo "tag_name=${GH_TAG}" >> $GITHUB_OUTPUT

      - name: Tag latest main commit as nightly
        run: |
          git config user.name github-actions
          git config user.email github-actions@github.com

          git tag -a ${{ steps.set_tagname.outputs.tag_name }} -m "This is an internal development build"
          git push origin ${{ steps.set_tagname.outputs.tag_name }} --tags

  deploy:
    name: Deploy to PyPI
    runs-on: ubuntu-22.04

    # deploy will only be run when there is a tag available
    needs: run_script_and_tag_nightly_release  # only run after all other stages succeeded

    steps:
      - name: Checkout git repository 🕝
        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c

      - name: Set up Python ${{ env.DEFAULT_PYTHON_VERSION }} 🐍
        uses: actions/setup-python@57ded4d7d5e986d7296eab16560982c6dd7c923b
        with:
          python-version: ${{ env.DEFAULT_PYTHON_VERSION }}

      - name: Read Poetry Version 🔢
        run: |
          echo "POETRY_VERSION=$(scripts/poetry-version.sh)" >> $GITHUB_ENV
        shell: bash

      - name: Install poetry 🦄
        uses: Gr1N/setup-poetry@15821dc8a61bc630db542ae4baf6a7c19a994844 # v8
        with:
          poetry-version: ${{ env.POETRY_VERSION }}

      - name: Copy Segment write key to the package
        env:
          RASA_TELEMETRY_WRITE_KEY: ${{ secrets.RASA_OSS_TELEMETRY_WRITE_KEY }}
          RASA_EXCEPTION_WRITE_KEY: ${{ secrets.RASA_OSS_EXCEPTION_WRITE_KEY }}
        run: |
          ./scripts/write_keys_file.sh

      - name: Update version (nightly releases) 🚀
        run: |
          poetry run pip install toml pep440_version_utils
          poetry run python ./scripts/prepare_nightly_release.py --next_version "${{ needs.run_script_and_tag_nightly_release.outputs.tag_name }}"

      - name: Build ⚒ Distributions
        run: |
          poetry build

      # Authenticate and push to the release registry
      - id: 'auth-release'
        name: Authenticate with gcloud for release registry 🎫
        uses: 'google-github-actions/auth@e8df18b60c5dd38ba618c121b779307266153fbf'
        with:
          credentials_json: '${{ secrets.RASA_OSS_RELEASE_ACCOUNT_KEY }}'

      - name: 'Set up Cloud SDK'
        uses: 'google-github-actions/setup-gcloud@62d4898025f6041e16b1068643bfc5a696863587'

      - name: Release via GCP Artifact Registry
        run: |
          pip install keyring
          pip install keyrings.google-artifactregistry-auth
          pip install twine
          gcloud artifacts print-settings python --project=rasa-releases --repository=rasa --location=europe-west3 > ~/.pypirc
          twine upload --verbose --repository-url https://europe-west3-python.pkg.dev/rasa-releases/rasa/ ${{ format('{0}/dist/*', github.workspace) }}

  docker:
    name: Build Docker
    runs-on: ubuntu-22.04
    needs: run_script_and_tag_nightly_release
    env:
      GCLOUD_VERSION: "297.0.1"
      # Registry used to store Docker images used for release purposes
      DEV_REGISTRY: "europe-west3-docker.pkg.dev/rasa-ci-cd/rasa"
      IMAGE_TAG: ${{ needs.run_script_and_tag_nightly_release.outputs.tag_name }}

    steps:
      - name: Checkout git repository 🕝
        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c

      - name: Free disk space
        # tries to make sure we do not run out of disk space, see
        # https://github.community/t5/GitHub-Actions/BUG-Strange-quot-No-space-left-on-device-quot-IOExceptions-on/td-p/46101
        run: |
          sudo swapoff -a
          sudo rm -f /swapfile
          sudo apt clean
          docker rmi $(docker image ls -aq)
          df -h

      - name: Read Poetry Version 🔢
        run: |
          echo "POETRY_VERSION=$(scripts/poetry-version.sh)" >> $GITHUB_ENV
        shell: bash

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55
        id: buildx
        with:
          version: v0.5.1
          driver: docker

      - name: Copy Segment write key to the package
        env:
          RASA_TELEMETRY_WRITE_KEY: ${{ secrets.RASA_OSS_TELEMETRY_WRITE_KEY }}
          RASA_EXCEPTION_WRITE_KEY: ${{ secrets.RASA_OSS_EXCEPTION_WRITE_KEY }}
        run: |
          ./scripts/write_keys_file.sh

      - name: Build Docker image
        run: |
          docker build . -t rasa/rasa:base-localdev -f docker/Dockerfile.base
          docker build . -t rasa/rasa:base-builder-localdev -f docker/Dockerfile.base-builder --build-arg IMAGE_BASE_NAME=rasa/rasa --build-arg POETRY_VERSION=${{ env.POETRY_VERSION }}
          docker build . -t rasa/rasa:base-poetry -f docker/Dockerfile.base-poetry --build-arg IMAGE_BASE_NAME=rasa/rasa --build-arg BASE_IMAGE_HASH=localdev
          docker build . -t rasa/rasa:${IMAGE_TAG} -f Dockerfile --build-arg IMAGE_BASE_NAME=rasa/rasa --build-arg BASE_IMAGE_HASH=localdev --build-arg BASE_BUILDER_IMAGE_HASH=localdev
          docker tag rasa/rasa:${IMAGE_TAG} ${{env.DEV_REGISTRY}}/rasa:${IMAGE_TAG}

      # Authenticate and push to the release registry
      - id: 'auth-dev'
        name: Authenticate with gcloud for dev registry 🎫
        uses: 'google-github-actions/auth@e8df18b60c5dd38ba618c121b779307266153fbf'
        with:
          credentials_json: '${{ secrets.RASA_OSS_RELEASE_ACCOUNT_KEY }}'

      - name: Authenticate docker for dev registry 🎫
        run: |
          # Set up docker to authenticate via gcloud command-line tool.
          gcloud auth configure-docker europe-west3-docker.pkg.dev

      - name: Push image to release registry
        run: |
          docker push ${{env.DEV_REGISTRY}}/rasa:${IMAGE_TAG}

  send_slack_notification_on_failure:
    name: Notify Slack
    runs-on: ubuntu-22.04
    if: always()
    needs:
      - docker
      - deploy

    steps:
      - name: Notify Slack of failure ⛔
        # send notification if 'deploy' or 'docker' is skipped (previous needed job failed) or failed
        if: needs.docker.result != 'success' || needs.deploy.result != 'success'
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_RELEASE_ASSISTANT_ATOM_ALERTS_WEBHOOK }}
        uses: Ilshidur/action-slack@689ad44a9c9092315abd286d0e3a9a74d31ab78a
        with:
          args: "⛔ *Rasa nightly release* failed 😱! Please check out GitHub Actions: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"


MLflow tests

name: MLflow tests

on:
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
      - ready_for_review
  push:
    branches:
      - master
      - branch-[0-9]+.[0-9]+

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
  cancel-in-progress: true

# Use `bash --noprofile --norc -exo pipefail` by default for all `run` steps in this workflow:
# https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#defaultsrun
defaults:
  run:
    shell: bash --noprofile --norc -exo pipefail {0}

env:
  MLFLOW_HOME: /home/runner/work/mlflow/mlflow
  # Note miniconda is pre-installed in the virtual environments for GitHub Actions:
  # https://github.com/actions/virtual-environments/blob/main/images/linux/scripts/installers/miniconda.sh
  MLFLOW_CONDA_HOME: /usr/share/miniconda
  SPARK_LOCAL_IP: localhost
  PIP_EXTRA_INDEX_URL: https://download.pytorch.org/whl/cpu
  PYTHONUTF8: "1"

jobs:
  # python-skinny tests cover a subset of mlflow functionality
  # that is meant to be supported with a smaller dependency footprint.
  # The python skinny tests cover the subset of mlflow functionality
  # while also verifying certain dependencies are omitted.
  python-skinny:
    if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: ./.github/actions/untracked
      - uses: ./.github/actions/setup-python
      - name: Install dependencies
        run: |
          source ./dev/install-common-deps.sh --skinny
      - name: Run tests
        run: |
          ./dev/run-python-skinny-tests.sh

  python:
    if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    timeout-minutes: 120
    strategy:
      fail-fast: false
      matrix:
        group: [1, 2]
        include:
          - splits: 2
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: ./.github/actions/untracked
      - uses: ./.github/actions/free-disk-space
      - uses: ./.github/actions/setup-python
      - uses: ./.github/actions/setup-pyenv
      - uses: ./.github/actions/setup-java
      - uses: ./.github/actions/cache-pip
      - name: Install dependencies
        run: |
          python -m venv .venv
          source .venv/bin/activate
          source ./dev/install-common-deps.sh --ml
      - uses: ./.github/actions/pipdeptree
      - name: Import check
        run: |
          source .venv/bin/activate
          python tests/check_mlflow_lazily_imports_ml_packages.py
      - name: Run tests
        run: |
          source .venv/bin/activate
          source dev/setup-ssh.sh
          pytest --splits=${{ matrix.splits }} --group=${{ matrix.group }} --quiet --requires-ssh \
            --ignore-flavors --ignore=tests/examples --ignore=tests/recipes --ignore=tests/evaluate \
            --ignore tests/deployments/server tests

  database:
    if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    timeout-minutes: 90
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: ./.github/actions/untracked
      - name: Build
        run: |
          ./tests/db/compose.sh pull -q postgresql mysql mssql
          ./tests/db/compose.sh build --build-arg DEPENDENCIES="$(cat requirements/skinny-requirements.txt requirements/core-requirements.txt | grep -Ev '^(#|$)')"
      - name: Run tests
        run: |
          set +e
          err=0
          trap 'err=1' ERR

          for service in $(./tests/db/compose.sh config --services | grep '^mlflow-')
          do
            # Set `--no-TTY` to show container logs on GitHub Actions:
            # https://github.com/actions/virtual-environments/issues/5022
            ./tests/db/compose.sh run --rm --no-TTY $service pytest \
              tests/store/tracking/test_sqlalchemy_store.py \
              tests/store/model_registry/test_sqlalchemy_store.py \
              tests/db
          done

          test $err = 0

      - name: Run migration check
        run: |
          set +e
          err=0
          trap 'err=1' ERR

          ./tests/db/compose.sh down --volumes --remove-orphans
          for service in $(./tests/db/compose.sh config --services | grep '^migration-')
          do
            ./tests/db/compose.sh run --rm --no-TTY $service
          done

          test $err = 0

      - name: Clean up
        run: |
          ./tests/db/compose.sh down --volumes --remove-orphans --rmi all

  java:
    if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: ./.github/actions/untracked
      - uses: ./.github/actions/setup-python
      - uses: ./.github/actions/setup-java
      - name: Install dependencies
        run: |
          source ./dev/install-common-deps.sh
      - uses: ./.github/actions/pipdeptree
      - name: Run tests
        run: |
          cd mlflow/java
          mvn clean package -q

  flavors:
    if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    timeout-minutes: 120
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: ./.github/actions/untracked
      - uses: ./.github/actions/setup-python
      - uses: ./.github/actions/setup-pyenv
      - uses: ./.github/actions/setup-java
      - uses: ./.github/actions/cache-pip
      - name: Install dependencies
        run: |
          python -m venv .venv
          source .venv/bin/activate
          source ./dev/install-common-deps.sh --ml
      - uses: ./.github/actions/pipdeptree
      - name: Run tests
        run: |
          source .venv/bin/activate
          pytest \
            tests/utils/test_model_utils.py \
            tests/tracking/fluent/test_fluent_autolog.py \
            tests/autologging \
            tests/server/auth

  # It takes 9 ~ 10 minutes to run tests in `tests/models`. To make CI finish faster,
  # run these tests in a separate job.
  models:
    if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    timeout-minutes: 120
    strategy:
      fail-fast: false
      matrix:
        group: [1, 2]
        include:
          - splits: 2
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: ./.github/actions/untracked
      - uses: ./.github/actions/free-disk-space
      - uses: ./.github/actions/setup-python
      - uses: ./.github/actions/setup-pyenv
      - uses: ./.github/actions/setup-java
      - name: Install dependencies
        run: |
          source ./dev/install-common-deps.sh
          pip install pyspark langchain langchain-community
      - uses: ./.github/actions/pipdeptree
      - name: Run tests
        run: |
          pytest --splits=${{ matrix.splits }} --group=${{ matrix.group }} tests/models

  # NOTE: numpy is pinned in this suite due to its heavy reliance on shap, which internally uses
  # references to the now fully deprecated (as of 1.24.x) numpy types (i.e., np.bool).
  # When the shap cross version tests are passing in a new release version of shap, this pin should
  # be removed.
  evaluate:
    if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    timeout-minutes: 90
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: ./.github/actions/untracked
      - uses: ./.github/actions/setup-python
      - uses: ./.github/actions/setup-pyenv
      - uses: ./.github/actions/setup-java
      - name: Install dependencies
        run: |
          source ./dev/install-common-deps.sh
          pip install pyspark torch transformers langchain langchain-experimental
      - uses: ./.github/actions/pipdeptree
      - name: Run tests
        run: |
          pytest tests/evaluate --ignore=tests/evaluate/test_default_evaluator_delta.py
      - name: Run tests with delta
        run: |
          pytest tests/evaluate/test_default_evaluator_delta.py

  pyfunc:
    if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    timeout-minutes: 120
    strategy:
      fail-fast: false
      matrix:
        group: [1, 2]
        include:
          - splits: 2
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: ./.github/actions/untracked
      - uses: ./.github/actions/free-disk-space
      - uses: ./.github/actions/setup-python
      - uses: ./.github/actions/setup-pyenv
      - uses: ./.github/actions/setup-java
      - name: Install dependencies
        run: |
          source ./dev/install-common-deps.sh
          pip install tensorflow 'pyspark[connect]'
          pip install typing_extensions -U
      - uses: ./.github/actions/pipdeptree
      - name: Run tests
        run: |
          pytest --splits=${{ matrix.splits }} --group=${{ matrix.group }} --durations=30 \
            tests/pyfunc --ignore tests/pyfunc/test_spark_connect.py

          # test_spark_connect.py fails if it's run with other tests, so run it separately.
          pytest tests/pyfunc/test_spark_connect.py

  sagemaker:
    if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    timeout-minutes: 120
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: ./.github/actions/untracked
      - uses: ./.github/actions/setup-python
      - uses: ./.github/actions/setup-java
      - name: Install dependencies
        run: |
          source ./dev/install-common-deps.sh --ml
      - uses: ./.github/actions/pipdeptree
      - name: Run tests
        run: |
          pytest tests/sagemaker

  windows:
    if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
    runs-on: windows-latest
    timeout-minutes: 120
    strategy:
      fail-fast: false
      matrix:
        group: [1, 2]
        include:
          - splits: 2
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: ./.github/actions/untracked
      - uses: ./.github/actions/setup-python
      - uses: ./.github/actions/setup-pyenv
      - uses: ./.github/actions/setup-java
      - uses: ./.github/actions/cache-pip
      - name: Install python dependencies
        run: |
          python -m venv .venv
          source .venv/Scripts/activate
          python -m pip install -U pip
          pip install --no-dependencies tests/resources/mlflow-test-plugin
          pip install .[extras]
          pip install pyspark
          pip install mleap
          # Install Hugging Face datasets to test Hugging Face usage with MLflow dataset tracking
          pip install datasets
          # Install TensorFlow to test TensorFlow dataset usage with MLflow dataset tracking
          pip install tensorflow
          # Install torch and transformers to test metrics
          pip install torch transformers
          pip install -r requirements/test-requirements.txt
      - uses: ./.github/actions/pipdeptree
      - name: Download Hadoop winutils for Spark
        run: |
          git clone https://github.com/cdarlint/winutils /tmp/winutils
      - name: Run python tests
        env:
          # Starting from SQLAlchemy version 2.0, `QueuePool` is the default connection pool
          # when creating an `Engine`. `QueuePool` prevents the removal of temporary database
          # files created during tests on Windows as it keeps the DB connection open until
          # it's explicitly disposed.
          MLFLOW_SQLALCHEMYSTORE_POOLCLASS: "NullPool"
        run: |
          source .venv/Scripts/activate
          # Set Hadoop environment variables required for testing Spark integrations on Windows
          export HADOOP_HOME=/tmp/winutils/hadoop-3.2.2
          export PATH=$PATH:$HADOOP_HOME/bin
          # Run Windows tests
          pytest --splits=${{ matrix.splits }} --group=${{ matrix.group }} \
            --ignore-flavors --ignore=tests/projects --ignore=tests/examples --ignore=tests/recipes --ignore=tests/evaluate --ignore tests/deployments/server \
            tests
          # MLeap is incompatible on Windows with PySpark3.4 release.
          # Reinstate tests when MLeap has released a fix. [ML-30491]
          # pytest tests/mleap


Labeling

name: Labeling

on:
  issues:
    types:
      - opened
      - edited
  pull_request_target:
    types:
      - opened
      - edited

permissions:
  contents: read

defaults:
  run:
    shell: bash --noprofile --norc -exo pipefail {0}

jobs:
  labeling:
    if: github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write # harupy/auto-labeling
      issues: write # harupy/auto-labeling
    timeout-minutes: 120
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: harupy/auto-labeling@master
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          label-pattern: '- \[(.*?)\] ?`(.+?)`'


Examples

name: Examples

on:
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
      - ready_for_review
  schedule:
    # Run this action daily at 13:00 UTC
    - cron: "0 13 * * *"
  workflow_dispatch:
    inputs:
      repository:
        description: >
          [Optional] Repository name with owner. For example, mlflow/mlflow.
           Defaults to the repository that triggered a workflow.
        required: false
        default: ""
      ref:
        description: >
          [Optional] The branch, tag or SHA to checkout. When checking out the repository that
           triggered a workflow, this defaults to the reference or SHA for that event. Otherwise,
           uses the default branch.
        required: false
        default: ""

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
  cancel-in-progress: true

defaults:
  run:
    shell: bash --noprofile --norc -exo pipefail {0}

env:
  MLFLOW_HOME: /home/runner/work/mlflow/mlflow
  MLFLOW_CONDA_HOME: /usr/share/miniconda

jobs:
  examples:
    runs-on: ubuntu-latest
    timeout-minutes: 120
    if: github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && github.repository == 'mlflow-automation/mlflow') || (github.event_name == 'pull_request' && github.event.pull_request.draft == false)
    strategy:
      fail-fast: false
      matrix:
        group: [1, 2]
        include:
          - splits: 2
    steps:
      - uses: actions/checkout@v4
        with:
          repository: ${{ github.event_name == 'schedule' && 'mlflow/mlflow' || github.event.inputs.repository }}
          ref: ${{ github.event.inputs.ref }}
          submodules: recursive
      - uses: ./.github/actions/free-disk-space
      - name: Check diff
        id: check-diff
        if: github.event_name == 'pull_request'
        run: |
          REPO="${{ github.repository }}"
          PR_NUMBER="${{ github.event.pull_request.number }}"
          BASE_REF="${{ github.base_ref }}"
          CHANGED_FILES=$(python dev/list_changed_files.py --repository $REPO --pr-num $PR_NUMBER | grep "tests/examples\|examples" || true);
          EXAMPLES_CHANGED=$([[ ! -z "$CHANGED_FILES" ]] && echo "true" || echo "false")

          echo -e "CHANGED_FILES:\nCHANGED_FILES"
          echo "EXAMPLES_CHANGED: $EXAMPLES_CHANGED"
          echo "examples_changed=$EXAMPLES_CHANGED" >> $GITHUB_OUTPUT

      - uses: ./.github/actions/setup-python
      - uses: ./.github/actions/setup-pyenv

      - name: Install dependencies
        if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || steps.check-diff.outputs.examples_changed == 'true' }}
        run: |
          source ./dev/install-common-deps.sh --ml

      - name: Run example tests
        if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || steps.check-diff.outputs.examples_changed == 'true' }}
        env:
          SPARK_LOCAL_IP: localhost
        run: |
          pytest --splits=${{ matrix.splits }} --group=${{ matrix.group }} --serve-wheel tests/examples --durations=30

      - name: Remove conda environments
        run: |
          conda remove --all --yes --name test-environment
          ./dev/remove-conda-envs.sh

      - name: Show disk usage
        run: |
          df -h

  docker:
    runs-on: ubuntu-latest
    timeout-minutes: 120
    if: github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && github.repository == 'mlflow-automation/mlflow') || (github.event_name == 'pull_request' && github.event.pull_request.draft == false)
    steps:
      - uses: actions/checkout@v4
        with:
          repository: ${{ github.event_name == 'schedule' && 'mlflow/mlflow' || github.event.inputs.repository }}
          ref: ${{ github.event.inputs.ref }}
          submodules: recursive

      - name: Check diff
        id: check-diff
        if: github.event_name == 'pull_request'
        run: |
          REPO="${{ github.repository }}"
          PR_NUMBER="${{ github.event.pull_request.number }}"
          CHANGED_FILES=$(python dev/list_changed_files.py --repository $REPO --pr-num $PR_NUMBER | grep "Dockerfile\|\.dockerignore" || true);
          DOCKER_CHANGED=$([[ ! -z "$CHANGED_FILES" ]] && echo "true" || echo "false")
          echo -e "CHANGED_FILES:\nCHANGED_FILES"
          echo "DOCKER_CHANGED: $DOCKER_CHANGED"
          echo "docker_changed=$DOCKER_CHANGED" >> $GITHUB_OUTPUT

      - name: Run docker tests
        if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || steps.check-diff.outputs.docker_changed == 'true' }}
        run: |
          docker build -t mlflow_test_build . && docker images | grep mlflow_test_build

      - name: Show disk usage
        run: |
          df -h


Cross version tests

name: Cross version tests
run-name: ${{ inputs.uuid }}

on:
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
      - ready_for_review
  workflow_dispatch:
    inputs:
      repository:
        description: >
          [Optional] Repository name with owner. For example, mlflow/mlflow.
           Defaults to the repository that triggered a workflow.
        required: false
        default: ""
      ref:
        description: >
          [Optional] The branch, tag or SHA to checkout. When checking out the repository that
           triggered a workflow, this defaults to the reference or SHA for that event. Otherwise,
           uses the default branch.
        required: false
        default: ""
      flavors:
        description: "[Optional] Comma-separated string specifying which flavors to test (e.g. 'sklearn, xgboost'). If unspecified, all flavors are tested."
        required: false
        default: ""
      versions:
        description: "[Optional] Comma-separated string specifying which versions to test (e.g. '1.2.3, 4.5.6'). If unspecified, all versions are tested."
        required: false
        default: ""
      uuid:
        description: "[Optional] A unique identifier for this run."
        required: false
        default: ""
  schedule:
    # Run this workflow daily at 13:00 UTC
    - cron: "0 13 * * *"

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
  cancel-in-progress: true

defaults:
  run:
    shell: bash --noprofile --norc -exo pipefail {0}

env:
  MLFLOW_HOME: /home/runner/work/mlflow/mlflow
  PIP_EXTRA_INDEX_URL: https://download.pytorch.org/whl/cpu

jobs:
  set-matrix:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    if: github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && github.repository == 'mlflow-automation/mlflow') || (github.event_name == 'pull_request' && github.event.pull_request.draft == false)
    permissions:
      issues: read # listLabelsOnIssue
    outputs:
      matrix1: ${{ steps.set-matrix.outputs.matrix1 }}
      matrix2: ${{ steps.set-matrix.outputs.matrix2 }}
      is_matrix1_empty: ${{ steps.set-matrix.outputs.is_matrix1_empty }}
      is_matrix2_empty: ${{ steps.set-matrix.outputs.is_matrix2_empty }}
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
          repository: ${{ github.event_name == 'schedule' && 'mlflow/mlflow' || github.event.inputs.repository }}
          ref: ${{ github.event.inputs.ref }}
      - uses: ./.github/actions/untracked
      - uses: ./.github/actions/setup-python
      - name: Install dependencies
        run: |
          pip install -r dev/requirements.txt
          pip install pytest pytest-cov
      - name: Check labels
        uses: actions/github-script@v7
        id: check-labels
        with:
          script: |
            if (context.eventName !== "pull_request") {
              return {
                enable_dev_tests: true,
                only_latest: false,
              };
            }
            const labels = await github.rest.issues.listLabelsOnIssue({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
            });
            const labelNames = labels.data.map(({ name }) => name);
            return {
              enable_dev_tests: labelNames.includes("enable-dev-tests"),
              only_latest: labelNames.includes("only-latest"),
            };
      - name: Test set_matrix.py
        run: |
          python -m pytest --noconftest dev/tests
      - id: set-matrix
        name: Set matrix
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          EVENT_NAME="${{ github.event_name }}"
          if [ "$EVENT_NAME" = "pull_request" ]; then
            REPO="${{ github.repository }}"
            PR_NUMBER="${{ github.event.pull_request.number }}"
            BASE_REF="${{ github.base_ref }}"
            REF_VERSIONS_YAML="https://raw.githubusercontent.com/$REPO/$BASE_REF/mlflow/ml-package-versions.yml"
            CHANGED_FILES="$(python dev/list_changed_files.py --repository $REPO --pr-num $PR_NUMBER)"
            ENABLE_DEV_TESTS="${{ fromJson(steps.check-labels.outputs.result).enable_dev_tests }}"
            NO_DEV_FLAG=$([ "$ENABLE_DEV_TESTS" == "true" ] && echo "" || echo "--no-dev")
            ONLY_LATEST="${{ fromJson(steps.check-labels.outputs.result).only_latest }}"
            ONLY_LATEST_FLAG=$([ "$ONLY_LATEST" == "true" ] && echo "--only-latest" || echo "")
            python dev/set_matrix.py --ref-versions-yaml $REF_VERSIONS_YAML --changed-files "$CHANGED_FILES" $NO_DEV_FLAG $ONLY_LATEST_FLAG
          elif [ "$EVENT_NAME" = "workflow_dispatch" ]; then
            python dev/set_matrix.py --flavors "${{ github.event.inputs.flavors }}" --versions "${{ github.event.inputs.versions }}"
          else
            python dev/set_matrix.py
          fi

  test1:
    needs: set-matrix
    if: ${{ needs.set-matrix.outputs.is_matrix1_empty == 'false' }}
    runs-on: ubuntu-latest
    timeout-minutes: 120
    strategy:
      fail-fast: false
      matrix: ${{ fromJson(needs.set-matrix.outputs.matrix1) }}
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
          repository: ${{ github.event_name == 'schedule' && 'mlflow/mlflow' || github.event.inputs.repository }}
          ref: ${{ github.event.inputs.ref }}
      - uses: ./.github/actions/free-disk-space
        if: matrix.free_disk_space
      - uses: ./.github/actions/setup-python
        with:
          python-version: ${{ matrix.python }}
      - uses: ./.github/actions/setup-pyenv
      - uses: ./.github/actions/setup-java
        with:
          java-version: ${{ matrix.java }}
      - name: Install mlflow & test dependencies
        run: |
          pip install wheel
          pip install .[extras]
          pip install -r requirements/test-requirements.txt
      - name: Install ${{ matrix.package }} ${{ matrix.version }}
        run: |
          ${{ matrix.install }}
      - name: Check package versions
        run: |
          python dev/show_package_release_dates.py
      - name: Pre-test
        if: matrix.pre_test
        run: |
          ${{ matrix.pre_test }}
      - uses: ./.github/actions/pipdeptree
      - name: Run tests
        env:
          MLFLOW_CONDA_HOME: /usr/share/miniconda
          SPARK_LOCAL_IP: localhost
          JOHNSNOWLABS_LICENSE_JSON: ${{ secrets.JOHNSNOWLABS_LICENSE_JSON }}
          HF_HUB_ENABLE_HF_TRANSFER: 1
        run: |
          ${{ matrix.run }}

  test2:
    needs: set-matrix
    if: ${{ needs.set-matrix.outputs.is_matrix2_empty == 'false' }}
    runs-on: ubuntu-latest
    timeout-minutes: 120
    strategy:
      fail-fast: false
      matrix: ${{ fromJson(needs.set-matrix.outputs.matrix2) }}
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
          repository: ${{ github.event_name == 'schedule' && 'mlflow/mlflow' || github.event.inputs.repository }}
          ref: ${{ github.event.inputs.ref }}
      - uses: ./.github/actions/free-disk-space
        if: matrix.free_disk_space
      - uses: ./.github/actions/setup-python
        with:
          python-version: ${{ matrix.python }}
      - uses: ./.github/actions/setup-pyenv
      - uses: ./.github/actions/setup-java
        with:
          java-version: ${{ matrix.java }}
      - name: Install mlflow & test dependencies
        run: |
          pip install wheel
          pip install .[extras]
          pip install -r requirements/test-requirements.txt
      - name: Install ${{ matrix.package }} ${{ matrix.version }}
        run: |
          ${{ matrix.install }}
      - name: Check package versions
        run: |
          python dev/show_package_release_dates.py
      - name: Pre-test
        if: matrix.pre_test
        run: |
          ${{ matrix.pre_test }}
      - uses: ./.github/actions/pipdeptree
      - name: Run tests
        env:
          MLFLOW_CONDA_HOME: /usr/share/miniconda
          SPARK_LOCAL_IP: localhost
          JOHNSNOWLABS_LICENSE_JSON: ${{ secrets.JOHNSNOWLABS_LICENSE_JSON }}
          HF_HUB_ENABLE_HF_TRANSFER: 1
        run: |
          ${{ matrix.run }}


Test package build

name: Test package build

on:
  push:
    branches:
      - master
      - branch-[0-9]+.[0-9]+
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
      - ready_for_review

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
  cancel-in-progress: true

defaults:
  run:
    shell: bash --noprofile --norc -exo pipefail {0}

jobs:
  build:
    if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    timeout-minutes: 20
    permissions:
      actions: write # deleteArtifact
    strategy:
      matrix:
        type: ["full", "skinny"]
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: ./.github/actions/untracked
      - uses: ./.github/actions/setup-python
      - uses: actions/setup-node@v3
        with:
          node-version: "16"

      - name: Build UI
        working-directory: mlflow/server/js
        run: |
          yarn
          yarn build

      - name: Install dependencies
        run: |
          pip install build twine

      - name: Build distribution files
        id: build-dist
        run: |
          if [ "${{ matrix.type }}" == "skinny" ]; then
            ./dev/build.sh --skinny
          else
            ./dev/build.sh
          fi

          # List distribution files and check their file sizes
          ls -lh dist

          # Set step outputs
          sdist_path=$(find dist -type f -name "*.tar.gz")
          wheel_path=$(find dist -type f -name "*.whl")
          wheel_name=$(basename $wheel_path)
          wheel_size=$(stat -c %s $wheel_path)
          echo "sdist-path=${sdist_path}" >> $GITHUB_OUTPUT
          echo "wheel-path=${wheel_path}" >> $GITHUB_OUTPUT
          echo "wheel-name=${wheel_name}" >> $GITHUB_OUTPUT
          echo "wheel-size=${wheel_size}" >> $GITHUB_OUTPUT

      - name: List files in source distribution
        run: |
          tar -tf ${{ steps.build-dist.outputs.sdist-path }}

      - name: List files in binary distribution
        run: |
          unzip -l ${{ steps.build-dist.outputs.wheel-path }}

      - name: Compare files in source and binary distributions
        run: |
          tar -tzf ${{ steps.build-dist.outputs.sdist-path }} | grep -v '/$' | cut -d'/' -f2- | sort > /tmp/source.txt
          zipinfo -1 ${{ steps.build-dist.outputs.wheel-path }} | sort > /tmp/wheel.txt
          diff /tmp/source.txt /tmp/wheel.txt || true

      - name: Run twine check
        run: |
          twine check --strict ${{ steps.build-dist.outputs.wheel-path }}

      - name: Test installation from tarball
        run: |
          pip install ${{ steps.build-dist.outputs.sdist-path }}
          python -c "import mlflow; print(mlflow.__version__)"

      - name: Test installation from wheel
        run: |
          pip install --force-reinstall ${{ steps.build-dist.outputs.wheel-path }}
          python -c "import mlflow; print(mlflow.__version__)"

      # Anyone with read access can download the uploaded wheel on GitHub.
      - name: Store wheel
        uses: actions/upload-artifact@v4
        if: github.event_name == 'push'
        with:
          name: ${{ steps.build-dist.outputs.wheel-name }}
          path: ${{ steps.build-dist.outputs.wheel-path }}

      - name: Remove old wheels
        uses: actions/github-script@v7
        if: github.event_name == 'push'
        env:
          WHEEL_SIZE: ${{ steps.build-dist.outputs.wheel-size }}
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const { owner, repo } = context.repo;

            // For some reason, the newly-uploaded wheel in the previous step is not included.
            const artifactsResp = await github.rest.actions.listArtifactsForRepo({
              owner,
              repo,
            });
            const wheels = artifactsResp.data.artifacts.filter(({ name }) => name.endsWith(".whl"));

            // The storage usage limit for a free github account is up to 500 MB. See the page below for details:
            // https://docs.github.com/en/github/setting-up-and-managing-billing-and-payments-on-github/about-billing-for-github-actions
            MAX_SIZE_IN_BYTES = 300_000_000; // 300 MB

            let index = 0;
            let sum = parseInt(process.env.WHEEL_SIZE);  // include the newly-uploaded wheel
            for (const [idx, { size_in_bytes }] of wheels.entries()) {
              index = idx;
              sum += size_in_bytes;
              if (sum > MAX_SIZE_IN_BYTES) {
                break;
              }
            }

            if (sum <= MAX_SIZE_IN_BYTES) {
              return;
            }

            // Delete old wheels
            const promises = wheels.slice(index).map(({ id: artifact_id }) =>
              github.rest.actions.deleteArtifact({
                owner,
                repo,
                artifact_id,
              })
            );
            Promise.all(promises).then(data => console.log(data));


Autoformat

# See .github/workflows/autoformat.md for instructions on how to test this workflow.

name: Autoformat
on:
  issue_comment:
    types: [created, edited]

permissions:
  contents: read

defaults:
  run:
    shell: bash --noprofile --norc -exo pipefail {0}

jobs:
  check-comment:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, '@mlflow-automation') && contains(github.event.comment.body, 'autoformat') }}
    permissions:
      statuses: write # autoformat.createStatus
      pull-requests: write # autoformat.createReaction on PRs
    outputs:
      should_autoformat: ${{ fromJSON(steps.judge.outputs.result).shouldAutoformat }}
      repository: ${{ fromJSON(steps.judge.outputs.result).repository }}
      head_ref: ${{ fromJSON(steps.judge.outputs.result).head_ref }}
      head_sha: ${{ fromJSON(steps.judge.outputs.result).head_sha }}
      base_ref: ${{ fromJSON(steps.judge.outputs.result).base_ref }}
      base_sha: ${{ fromJSON(steps.judge.outputs.result).base_sha }}
      base_repo: ${{ fromJSON(steps.judge.outputs.result).base_repo }}
      pull_number: ${{ fromJSON(steps.judge.outputs.result).pull_number }}
    steps:
      - uses: actions/checkout@v4
      - name: judge
        id: judge
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            core.debug(JSON.stringify(context, null, 2));
            const autoformat = require('./.github/workflows/autoformat.js');
            const { comment } = context.payload;
            const pullInfo = await autoformat.getPullInformation(context, github);
            const isMaintainer = autoformat.isMlflowMaintainer(comment.author_association) && autoformat.isMlflowMaintainer(pullInfo.author_association);
            if (!isMaintainer) {
              core.setFailed("Only MLflow maintainers can trigger this workflow in their PRs.");
            }
            const shouldAutoformat = autoformat.shouldAutoformat(comment);
            if (shouldAutoformat) {
              await autoformat.createReaction(context, github);
              await autoformat.createStatus(context, github, core);
            }
            return { ...pullInfo, shouldAutoformat };

  format:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    needs: check-comment
    if: ${{ needs.check-comment.outputs.should_autoformat == 'true' }}
    permissions:
      pull-requests: read # view files modified in PR
    outputs:
      reformatted: ${{ steps.patch.outputs.reformatted }}
    steps:
      - uses: actions/checkout@v4
        with:
          repository: ${{ needs.check-comment.outputs.repository }}
          ref: ${{ needs.check-comment.outputs.head_ref }}
          # Set fetch-depth to merge the base branch
          fetch-depth: 300
      - name: Check diff
        id: diff
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          changed_files="$(gh pr view --repo ${{ github.repository }} ${{ needs.check-comment.outputs.pull_number }} --json files --jq '.files.[].path')"
          protos=$([[ -z $(echo "$changed_files" | grep '^\(mlflow/protos\|tests/protos\)') ]] && echo "false" || echo "true")
          js=$([[ -z $(echo "$changed_files" | grep '^mlflow/server/js') ]] && echo "false" || echo "true")
          r=$([[ -z $(echo "$changed_files" | grep '^mlflow/R/mlflow') ]] && echo "false" || echo "true")
          echo "protos=$protos" >> $GITHUB_OUTPUT
          echo "js=$js" >> $GITHUB_OUTPUT
          echo "r=$r" >> $GITHUB_OUTPUT
      # Merge the base branch (which is usually master) to apply formatting using the latest configurations.
      - name: Merge base branch
        run: |
          git config user.name 'mlflow-automation'
          git config user.email 'mlflow-automation@users.noreply.github.com'
          git remote add base https://github.com/${{ needs.check-comment.outputs.base_repo }}.git
          git fetch base ${{ needs.check-comment.outputs.base_ref }}
          git merge base/${{ needs.check-comment.outputs.base_ref }}
      - uses: ./.github/actions/setup-python
      # ************************************************************************
      # pre-commit
      # ************************************************************************
      - run: |
          pip install -r requirements/lint-requirements.txt
          # TODO: Use a precompiled binary once it's available: https://github.com/tamasfe/taplo/issues/542
          cargo install taplo-cli@0.9.0 --locked
          pre-commit install
          pre-commit run --all-files --color=always || true
      # ************************************************************************
      # protos
      # ************************************************************************
      - if: steps.diff.outputs.protos == 'true'
        env:
          DOCKER_BUILDKIT: 1
        run: |
          wget https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protoc-3.19.4-linux-x86_64.zip -O /tmp/protoc.zip
          sudo unzip /tmp/protoc.zip -d /tmp/protoc
          sudo chmod -R 777 /tmp/protoc
          echo "/tmp/protoc/bin" >> $GITHUB_PATH
          pip install --upgrade pip
          pip install .
      - if: steps.diff.outputs.protos == 'true'
        run: |
          python ./dev/generate_protos.py
      # ************************************************************************
      # js
      # ************************************************************************
      - if: steps.diff.outputs.js == 'true'
        uses: actions/setup-node@v3
        with:
          node-version: "16"
      - if: steps.diff.outputs.js == 'true'
        working-directory: mlflow/server/js
        run: |
          yarn install
      - if: steps.diff.outputs.js == 'true'
        working-directory: mlflow/server/js
        run: |
          yarn lint:fix
          yarn prettier:fix
      - if: steps.diff.outputs.js == 'true'
        working-directory: mlflow/server/js
        run: |
          yarn i18n
      # ************************************************************************
      # R
      # ************************************************************************
      - if: steps.diff.outputs.r == 'true'
        working-directory: docs
        run: |
          ./build-rdoc.sh
      # ************************************************************************
      # Upload patch
      # ************************************************************************
      - name: Create patch
        id: patch
        run: |
          git diff > ${{ github.run_id }}.diff
          reformatted=$([[ -s ${{ github.run_id }}.diff ]] && echo "true" || echo "false")
          echo "reformatted=$reformatted" >> $GITHUB_OUTPUT

      - name: Upload patch
        uses: actions/upload-artifact@v4
        with:
          name: ${{ github.run_id }}.diff
          path: ${{ github.run_id }}.diff

  push:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    needs: [check-comment, format]
    if: ${{ needs.format.outputs.reformatted == 'true' }}
    steps:
      - uses: actions/checkout@v4
        with:
          repository: ${{ needs.check-comment.outputs.repository }}
          ref: ${{ needs.check-comment.outputs.head_ref }}
          # Set fetch-depth to merge the base branch
          fetch-depth: 300
          token: ${{ secrets.MLFLOW_AUTOMATION_TOKEN }}

      - name: Merge base branch
        run: |
          git config user.name 'mlflow-automation'
          git config user.email 'mlflow-automation@users.noreply.github.com'
          git remote add base https://github.com/${{ needs.check-comment.outputs.base_repo }}.git
          git fetch base ${{ needs.check-comment.outputs.base_ref }}
          git merge base/${{ needs.check-comment.outputs.base_ref }}

      - name: Download patch
        uses: actions/download-artifact@v4
        with:
          name: ${{ github.run_id }}.diff
          path: /tmp

      - name: Apply patch and push
        run: |
          git apply /tmp/${{ github.run_id }}.diff
          git commit -sam "Autoformat: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
          git push

  update-status:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    needs: [check-comment, format, push]
    if: always() && needs.check-comment.outputs.should_autoformat == 'true'
    permissions:
      statuses: write # autoformat.updateStatus
    steps:
      - uses: actions/checkout@v4
      - name: Update status
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const needs = ${{ toJson(needs) }};
            const head_sha = '${{ needs.check-comment.outputs.head_sha }}'
            const autoformat = require('./.github/workflows/autoformat.js');
            await autoformat.updateStatus(context, github, head_sha, needs);


JS

name: JS

on:
  push:
    paths:
      - mlflow/server/js/**
      - .github/workflows/js.yml
    branches:
      - master
      - branch-[0-9]+.[0-9]+
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
      - ready_for_review
    paths:
      - mlflow/server/js/**
      - .github/workflows/js.yml

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  js:
    if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest]
        option: [--testPathPattern, --testPathIgnorePatterns]
        include:
          - os: ubuntu-latest
            shell: bash
          - os: windows-latest
            shell: pwsh
    runs-on: ${{ matrix.os }}
    defaults:
      run:
        shell: ${{ matrix.shell }}
        working-directory: mlflow/server/js
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: actions/setup-node@v3
        with:
          node-version: "16"
      - name: Disable problem matcher
        if: runner.os == 'Windows'
        run: |
          echo "::remove-matcher owner=eslint-compact::"
          echo "::remove-matcher owner=eslint-stylish::"
      - name: Install dependencies (windows)
        if: runner.os == 'Windows'
        run: |
          # On Windows, `yarn install` changes hash of @databricks/design-system in yarn.lock.
          # Use `--no-immutable` to allow the change.
          yarn install --no-immutable
          git diff
      - name: Install dependencies (non-windows)
        if: runner.os != 'Windows'
        run: |
          yarn install --immutable
      - name: Run lint
        run: |
          yarn lint
      - name: Run prettier
        run: |
          yarn prettier:check
      - name: Run extract-i18n lint
        run: |
          yarn i18n:check
      - name: Run tests
        run: |
          yarn test --silent ${{ matrix.option }} src/experiment-tracking/components
      - name: Run build
        if: runner.os == 'Linux'
        env:
          # Prevent warnings (emitted from react-pdf) from being treated as errors
          # https://github.com/wojtekmaj/react-pdf/issues/280
          CI: false
        run: |
          yarn build


R

name: R

on:
  push:
    branches:
      - master
      - branch-[0-9]+.[0-9]+
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
      - ready_for_review
  schedule:
    # Run this workflow daily at 13:00 UTC
    - cron: "0 13 * * *"

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
  cancel-in-progress: true

env:
  MLFLOW_HOME: /home/runner/work/mlflow/mlflow

jobs:
  r:
    runs-on: ubuntu-latest
    timeout-minutes: 120
    if: github.event_name == 'push' || (github.event_name == 'schedule' && github.repository == 'mlflow-automation/mlflow') || (github.event_name == 'pull_request' && github.event.pull_request.draft == false)
    defaults:
      run:
        shell: bash --noprofile --norc -exo pipefail {0}
        working-directory: mlflow/R/mlflow
    steps:
      - uses: actions/checkout@v4
        with:
          repository: ${{ github.event_name == 'schedule' && 'mlflow/mlflow' || null }}
          submodules: recursive
      - uses: ./.github/actions/setup-python
      - uses: ./.github/actions/setup-pyenv
      - uses: ./.github/actions/setup-java
      - uses: r-lib/actions/setup-r@v2
      # This step dumps the current set of R dependencies and R version into files to be used
      # as a cache key when caching/restoring R dependencies.
      - name: Dump dependencies
        run: |
          Rscript -e 'source(".dump-r-dependencies.R", echo = TRUE)'
      - name: Get OS name
        id: os-name
        run: |
          # `os_name` will be like "Ubuntu-20.04.1-LTS"
          os_name=$(lsb_release -ds | sed 's/\s/-/g')
          echo "os-name=$os_name" >> $GITHUB_OUTPUT
      - name: Cache R packages
        uses: actions/cache@v4
        continue-on-error: true
        # https://github.com/actions/cache/issues/810
        env:
          SEGMENT_DOWNLOAD_TIMEOUT_MINS: 5
        with:
          path: ${{ env.R_LIBS_USER }}
          # We cache R dependencies based on a tuple of the current OS, the R version, and the list of
          # R dependencies
          key: ${{ steps.os-name.outputs.os-name }}-${{ hashFiles('mlflow/R/mlflow/R-version') }}-0-${{ hashFiles('mlflow/R/mlflow/depends.Rds') }}
      - name: Install dependencies
        run: |
          sudo apt-get install -y libcurl4-openssl-dev libharfbuzz-dev libfribidi-dev
          Rscript -e 'source(".install-deps.R", echo=TRUE)'
      - name: Set USE_R_DEVEL
        run: |
          if [ "$GITHUB_EVENT_NAME" = "schedule" ]; then
            USE_R_DEVEL=true
          elif [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
            # Use r-devel on a pull request targeted to a release branch
            USE_R_DEVEL=$([[ $GITHUB_BASE_REF =~ branch-[0-9]+\.[0-9]+$ ]] && echo true || echo false)
          else
            # Use r-devel on a push to a release branch
            USE_R_DEVEL=$([[ $GITHUB_REF_NAME =~ branch-[0-9]+\.[0-9]+$ ]] && echo true || echo false)
          fi
          echo "USE_R_DEVEL=$USE_R_DEVEL" >> $GITHUB_ENV
      - name: Build package
        run: |
          ./build-package.sh
      - name: Create test environment
        run: |
          pip install $(git rev-parse --show-toplevel)
          H2O_VERSION=$(Rscript -e "print(packageVersion('h2o'))" | grep -Eo '[0-9][0-9.]+')
          # test-keras-model.R fails with tensorflow>=2.13.0
          pip install xgboost 'tensorflow<2.13.0' "h2o==$H2O_VERSION"
          Rscript -e 'source(".install-mlflow-r.R", echo=TRUE)'
      - name: Run tests
        env:
          LINTR_COMMENT_BOT: false
        run: |
          cd tests
          Rscript -e 'source("../.run-tests.R", echo=TRUE)'


Dev environment setup

name: Dev environment setup

on:
  push:
    paths:
      - "dev/dev-env-setup.sh"
      - "dev/test-dev-env-setup.sh"
      - ".github/workflows/dev-setup.yml"
  schedule:
    - cron: "42 7 * * 0"
  workflow_dispatch:
    inputs:
      repository:
        description: >
          [Optional] Repository name with owner. For example, mlflow/mlflow.
           Defaults to the repository that triggered a workflow.
        required: false
        default: ""
      ref:
        description: >
          [Optional] The branch, tag or SHA to checkout. When checking out the repository that
           triggered a workflow, this defaults to the reference or SHA for that event. Otherwise,
           uses the default branch.
        required: false
        default: ""

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
  cancel-in-progress: true

defaults:
  run:
    shell: bash --noprofile --norc -exo pipefail {0}

jobs:
  linux-env-setup:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    if: github.event_name != 'schedule' || github.repository == 'mlflow-automation/mlflow'
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
          repository: ${{ github.event_name == 'schedule' && 'mlflow/mlflow' || github.event.inputs.repository }}
          ref: ${{ github.event.inputs.ref }}
      - name: Setup environment
        run: |
          git config --global user.name "test"
          git config --global user.email "test@mlflow.org"
      - name: Run Environment tests
        run: |
          TERM=xterm bash ./dev/test-dev-env-setup.sh


Closing PR

name: Closing PR

on:
  pull_request_target:
    types:
      - opened
      - edited

defaults:
  run:
    shell: bash --noprofile --norc -exo pipefail {0}

permissions:
  contents: read

jobs:
  closing-pr:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    permissions:
      pull-requests: read # closing-pr.js reads the PR body
      issues: write # closing-pr.js labels issues
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: actions/github-script@v7
        with:
          script: |
            const script = require(
              `${process.env.GITHUB_WORKSPACE}/.github/workflows/closing-pr.js`
            );
            script({ context, github });


Stale

name: Stale
on:
  schedule:
    - cron: "0 0 * * *"

permissions:
  contents: read

jobs:
  stale:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    if: github.event_name != 'schedule' || github.repository == 'mlflow/mlflow'
    permissions:
      issues: write # harupy/stale
      pull-requests: write # harupy/stale
    steps:
      - uses: harupy/stale@mlflow-stale-bot
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}
          stale-issue-message: "This issue is stale because it has been open 14 days with no activity. Remove stale label or comment or this will be closed in 35 days."
          close-issue-message: "This issue was closed because it has been stalled for 14 days with no activity."
          days-before-stale: 14
          days-before-close: 35
          # Only process issues created after 2022-07-01 as an experiment. We can remove this option
          # once we confirm the bot works fine.
          start-date: "2022-07-01T00:00:00Z"
          # Prevent the bot from staling/closing PRs
          days-before-pr-stale: -1
          days-before-pr-close: -1
          days-before-triage-reminder: 7
          days-before-reply-reminder: 14


Preview docs

name: Preview docs

on:
  pull_request_target:

permissions:
  contents: read

jobs:
  main:
    if: github.repository == 'mlflow/mlflow'
    runs-on: ubuntu-latest
    permissions:
      statuses: read # preview_docs.py checks PR statuses
      pull-requests: write # preview_docs.py comments on PRs
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
      - name: Install dependencies
        run: |
          pip install requests
      - name: Create preview link
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          CIRCLE_TOKEN: ${{ secrets.CIRCLE_TOKEN }}
        run: |
          python dev/preview_docs.py \
            --commit-sha ${{ github.event.pull_request.head.sha }} \
            --pull-number ${{ github.event.pull_request.number }} \
            --workflow-run-id ${{ github.run_id }}


Test requirements

name: Test requirements

on:
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
      - ready_for_review
    paths:
      - requirements/core-requirements.yaml
      - requirements/skinny-requirements.yaml
      - requirements/gateway-requirements.yaml
      - .github/workflows/requirements.yml
  schedule:
    - cron: "0 13 * * *"

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
  cancel-in-progress: true

env:
  MLFLOW_HOME: /home/runner/work/mlflow/mlflow
  MLFLOW_CONDA_HOME: /usr/share/miniconda
  SPARK_LOCAL_IP: localhost
  PYTHON_VERSION: "3.8"

defaults:
  run:
    shell: bash --noprofile --norc -exo pipefail {0}

jobs:
  skinny:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    if: (github.event_name == 'schedule' && github.repository == 'mlflow-automation/mlflow') || (github.event_name == 'pull_request' && github.event.pull_request.draft == false)
    steps:
      - uses: actions/checkout@v4
        with:
          repository: ${{ github.event_name == 'schedule' && 'mlflow/mlflow' || null }}
          submodules: recursive
      - uses: ./.github/actions/setup-python
        with:
          python-version: ${{ env.PYTHON_VERSION }}
      - name: Install dev script dependencies
        run: |
          pip install -r dev/requirements.txt
      - uses: ./.github/actions/update-requirements
        if: github.event_name == 'schedule'
      - name: Install dependencies
        run: |
          source ./dev/install-common-deps.sh --skinny
      - name: Run tests
        run: |
          ./dev/run-python-skinny-tests.sh

  core:
    runs-on: ubuntu-latest
    timeout-minutes: 120
    if: (github.event_name == 'schedule' && github.repository == 'mlflow-automation/mlflow') || (github.event_name == 'pull_request' && github.event.pull_request.draft == false)
    steps:
      - uses: actions/checkout@v4
        with:
          repository: ${{ github.event_name == 'schedule' && 'mlflow/mlflow' || null }}
          submodules: recursive
      - uses: ./.github/actions/free-disk-space
      - uses: ./.github/actions/setup-python
        with:
          python-version: ${{ env.PYTHON_VERSION }}
      - uses: ./.github/actions/setup-pyenv
      - uses: ./.github/actions/setup-java
      - name: Install dev script dependencies
        run: |
          pip install -r dev/requirements.txt
      - uses: ./.github/actions/update-requirements
        if: github.event_name == 'schedule'
      - name: Install dependencies
        run: |
          source ./dev/install-common-deps.sh --ml
          pip install '.[gateway]'
      - name: Run tests
        run: |
          source dev/setup-ssh.sh
          pytest tests --quiet --requires-ssh --ignore-flavors \
            --ignore=tests/examples --ignore=tests/recipes --ignore=tests/evaluate \
            --ignore=tests/deployments


Protos

name: Protos

on:
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
      - ready_for_review
    paths:
      - .github/workflows/protos.yml
      - mlflow/protos/**
      - mlflow/java/client/src/main/java/com/databricks/api/proto/**
      - mlflow/server/graphql/graphql_schema_extensions.py # schema extension changes could trigger changes in the autogenerated schema
      - requirements/skinny-requirements.txt
      - requirements/core-requirements.txt

env:
  MLFLOW_HOME: /home/runner/work/mlflow/mlflow

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
  cancel-in-progress: true

defaults:
  run:
    shell: bash --noprofile --norc -exo pipefail {0}

jobs:
  protos:
    if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
        with:
          repository: ${{ github.event.inputs.repository }}
          ref: ${{ github.event.inputs.ref }}
          submodules: recursive
      - name: Test building Docker image
        working-directory: dev
        env:
          DOCKER_BUILDKIT: 1
        run: |
          docker build -t gen-protos -f Dockerfile.protos .
          docker run --rm gen-protos protoc --version
      - name: Install dependencies
        run: |
          wget https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protoc-3.19.4-linux-x86_64.zip -O /tmp/protoc.zip
          sudo unzip /tmp/protoc.zip -d /tmp/protoc
          sudo chmod -R 777 /tmp/protoc
          echo "/tmp/protoc/bin" >> $GITHUB_PATH
          pip install --upgrade pip
          pip install .
      - name: Run tests
        run: |
          ./dev/test-generate-protos.sh

      - name: Test
        run: |
          python -c "from google.protobuf.internal import api_implementation; assert api_implementation.Type() != 'python'"
          python -c "import mlflow"

          # Ensure mlflow works fine with the python backend. See the following link for more details:
          # https://github.com/protocolbuffers/protobuf/tree/main/python#implementation-backends
          export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python
          python -c "from google.protobuf.internal import api_implementation; assert api_implementation.Type() == 'python'"
          python -c "import mlflow"


Devcontainer

name: Devcontainer

on:
  push:
    branches:
      - "master"
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
      - ready_for_review
    paths:
      - .devcontainer/**

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
  cancel-in-progress: true

defaults:
  run:
    shell: bash --noprofile --norc -exo pipefail {0}

env:
  DOCKER_BUILDKIT: 1
  BUILDKIT_PROGRESS: plain

jobs:
  devcontainer:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    if: github.repository == 'mlflow/mlflow'
    permissions:
      packages: write # to push to ghcr.io
    steps:
      - uses: actions/checkout@v4
      - name: Get image name
        run: |
          IMAGE_NAME=$(yq '.services.mlflow.image' .devcontainer/docker-compose.yml)
          echo "IMAGE_NAME=$IMAGE_NAME" >> $GITHUB_ENV
      - name: Build image
        run: |
          docker build -f .devcontainer/Dockerfile.devcontainer -t $IMAGE_NAME .
      - name: Show layer sizes
        run: |
          docker history $IMAGE_NAME
      - name: Inspect image
        run: |
          docker image inspect $IMAGE_NAME
      - name: Test Image
        run: |
          docker run --rm -v $(pwd):/workspaces/mlflow $IMAGE_NAME bash -c "pip install --no-deps -e . && pytest tests/test_version.py"
      - uses: docker/login-action@v2
        if: github.event_name == 'push'
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Push image
        if: github.event_name == 'push'
        run: |
          docker push $IMAGE_NAME


Push-Images

name: Push-Images

on:
  release:
    types:
      - published
      - edited

permissions:
  contents: read

jobs:
  push-images:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    permissions:
      packages: write # to push to ghcr.io
    steps:
      - uses: actions/checkout@v4

      - uses: ./.github/actions/setup-python

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Gather Docker Metadata for Tracking
        id: meta
        uses: docker/metadata-action@v4
        with:
          # list of Docker images to use as base name for tags
          images: |
            ghcr.io/mlflow/mlflow
          # generate Docker tags based on the following events/attributes
          tags: |
            type=ref,event=tag

      - name: Build and Push Base Image
        uses: docker/build-push-action@v3
        with:
          context: docker
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          platforms: linux/amd64,linux/arm64
          build-args: |
            VERSION=${{ steps.meta.outputs.version }}

      - name: Gather Docker Metadata for Model Server
        id: modelmeta
        uses: docker/metadata-action@v4
        with:
          # list of Docker images to use as base name for tags
          images: |
            ghcr.io/mlflow/model-server
          # generate Docker tags based on the following events/attributes
          tags: |
            type=ref,event=tag

      - name: Build Model Server Image
        run: |
          pip install .
          mlflow models build-docker -n model-server:latest --mlflow-home `pwd`

      - name: Push Model Server Image
        run: |
          set -x

          tags=$(echo -n "${{ steps.modelmeta.outputs.tags }}" | tr '\n' ' ')
          for tag in $tags; do
            docker tag model-server:latest $tag
            docker push $tag
          done


Recipe tests

name: Recipe tests

on:
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
      - ready_for_review
  push:
    branches:
      - master
      - branch-[0-9]+.[0-9]+

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
  cancel-in-progress: true

# Use `bash --noprofile --norc -exo pipefail` by default for all `run` steps in this workflow:
# https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#defaultsrun
defaults:
  run:
    shell: bash --noprofile --norc -exo pipefail {0}

env:
  MLFLOW_HOME: /home/runner/work/mlflow/mlflow
  # Note miniconda is pre-installed in the virtual environments for GitHub Actions:
  # https://github.com/actions/virtual-environments/blob/main/images/linux/scripts/installers/miniconda.sh
  MLFLOW_CONDA_HOME: /usr/share/miniconda
  SPARK_LOCAL_IP: localhost
  PYTHONUTF8: "1"

jobs:
  recipes:
    if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    timeout-minutes: 120
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: ./.github/actions/untracked
      - uses: ./.github/actions/setup-python
      - uses: ./.github/actions/setup-pyenv
      - name: Install dependencies
        run: |
          source ./dev/install-common-deps.sh
          # FLAML is incompatible with xgboost 2.1.0: https://github.com/microsoft/FLAML/issues/1314
          pip install pyspark 'xgboost<2.1.0'
      - name: Run tests
        run: |
          pytest tests/recipes

  recipes-windows:
    if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
    runs-on: windows-latest
    strategy:
      fail-fast: false
      matrix:
        group: [1, 2]
        include:
          - splits: 2
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: ./.github/actions/untracked
      - uses: ./.github/actions/setup-python
      - uses: ./.github/actions/setup-pyenv
      - name: Install python dependencies
        run: |
          pip install -r requirements/test-requirements.txt
          pip install --no-dependencies tests/resources/mlflow-test-plugin
          pip install .
          # FLAML is incompatible with xgboost 2.1.0: https://github.com/microsoft/FLAML/issues/1314
          pip install pyspark 'xgboost<2.1.0'
          # TODO: Importing datasets in a pandas UDF (created by mlflow.pyfunc.spark_udf) crashes
          # the Python worker. To avoid this, uninstall `datasets`. This is a temporary workaround.
          pip uninstall -y datasets
      - name: Download Hadoop winutils for Spark
        run: |
          git clone https://github.com/cdarlint/winutils /tmp/winutils
      - name: Run tests
        env:
          # The default pooling implmentation 'QueuePool' can lead to errors on Windows when
          # multiple threads access the same SQLite database file simultaneously.
          MLFLOW_SQLALCHEMYSTORE_POOLCLASS: "NullPool"
        run: |
          # Set Hadoop environment variables required for testing Spark integrations on Windows
          export HADOOP_HOME=/tmp/winutils/hadoop-3.2.2
          export PATH=$PATH:$HADOOP_HOME/bin
          export MLFLOW_HOME=$(pwd)
          pytest --splits ${{ matrix.splits }} --group ${{ matrix.group }} tests/recipes


Recipe template tests

name: Recipe template tests

on:
  workflow_dispatch:
    inputs:
      repository:
        description: >
          [Optional] Repository name with owner. For example, mlflow/recipes-examples.
        required: true
        default: "mlflow/recipes-examples"
      ref:
        description: >
          [Optional] The branch, tag or SHA to checkout. When checking out the repository that
           triggered a workflow, this defaults to the reference or SHA for that event. Otherwise,
           uses the default branch.
        required: false
        default: ""

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
  cancel-in-progress: true

# Use `bash --noprofile --norc -exo pipefail` by default for all `run` steps in this workflow:
# https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#defaultsrun
defaults:
  run:
    shell: bash --noprofile --norc -exo pipefail {0}

env:
  # Note miniconda is pre-installed in the virtual environments for GitHub Actions:
  # https://github.com/actions/virtual-environments/blob/main/images/linux/scripts/installers/miniconda.sh
  MLFLOW_CONDA_HOME: /usr/share/miniconda
  PYTHONUTF8: "1"

jobs:
  lint:
    runs-on: ubuntu-latest
    timeout-minutes: 120
    steps:
      - uses: actions/checkout@v4
      - uses: actions/checkout@v4
        with:
          path: ${{ github.event.inputs.repository }}
          repository: ${{ github.event.inputs.repository }}
          ref: ${{ github.event.inputs.ref }}
      - uses: ./.github/actions/setup-python
      - uses: ./.github/actions/cache-pip
      - name: Add problem matchers
        run: |
          echo "::add-matcher::.github/workflows/matchers/clint.json"
          echo "::add-matcher::.github/workflows/matchers/black.json"
          echo "::add-matcher::.github/workflows/matchers/ruff.json"
      - name: Install dependencies
        run: |
          pip install -r ${{ github.event.inputs.repository }}/requirements/lint-requirements.txt
      - name: Install pre-commit hooks
        run: |
          cd ${{ github.event.inputs.repository }}
          pre-commit install -t pre-commit -t prepare-commit-msg
      - name: Run pre-commit
        id: pre-commit
        run: |
          cd ${{ github.event.inputs.repository }}
          pre-commit run --all-files
  recipe:
    runs-on: ubuntu-latest
    timeout-minutes: 120
    steps:
      - uses: actions/checkout@v4
      - uses: actions/checkout@v4
        with:
          path: ${{ github.event.inputs.repository }}
          repository: ${{ github.event.inputs.repository }}
          ref: ${{ github.event.inputs.ref }}
      - uses: ./.github/actions/setup-python
      - name: Install dependencies
        run: |
          pip install .
          pip install -r ${{ github.event.inputs.repository }}/requirements.txt
          pip install -r ${{ github.event.inputs.repository }}/requirements/test-requirements.txt
          # This dependency is needed pytest.ini uses pytest-cov
          pip install pytest-cov
      - name: Run tests
        run: |
          cd ${{ github.event.inputs.repository }} && pytest tests --rootdir .
  recipe-windows:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/checkout@v4
        with:
          path: ${{ github.event.inputs.repository }}
          repository: ${{ github.event.inputs.repository }}
          ref: ${{ github.event.inputs.ref }}
      - uses: ./.github/actions/setup-python
      - name: Install dependencies
        run: |
          pip install .
          pip install -r ${{ github.event.inputs.repository }}/requirements.txt
          pip install -r ${{ github.event.inputs.repository }}/requirements/test-requirements.txt
          # This dependency is needed pytest.ini uses pytest-cov
          pip install pytest-cov
      - name: Run tests
        run: |
          cd ${{ github.event.inputs.repository }} &&  pytest tests --rootdir .


Advice

name: Advice

on:
  pull_request_target:
    types:
      - opened

permissions:
  contents: read

jobs:
  notify:
    if: github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    timeout-minutes: 10
    permissions:
      pull-requests: write # advice.js comments on PRs
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: actions/github-script@v7
        with:
          script: |
            const script = require(
              `${process.env.GITHUB_WORKSPACE}/.github/workflows/advice.js`
            );
            script({ context, github });


⛩ Gateway ⛩

name: ⛩ Gateway ⛩

on:
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
      - ready_for_review
  push:
    branches:
      - master
      - branch-[0-9]+.[0-9]+

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

defaults:
  run:
    shell: bash --noprofile --norc -exo pipefail {0}

env:
  PIP_EXTRA_INDEX_URL: https://download.pytorch.org/whl/cpu

jobs:
  gateway:
    if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/untracked
      - uses: ./.github/actions/setup-python
      - name: Install dependencies
        run: |
          pip install .[gateway] \
            pytest pytest-timeout pytest-asyncio \
            httpx psutil sentence-transformers transformers torchvision openai databricks-sdk
          # Ensure pydantic v2 is installed
          pip freeze | tail -n +1 | grep -q '^pydantic==2\.'
      - name: Run tests with pydantic v2
        run: |
          pytest tests/gateway
      - name: Downgrade pydantic to v1
        run: |
          pip install 'pydantic<2.0'
          # Ensure pydantic v1 is installed
          pip freeze | tail -n +1 | grep -q '^pydantic==1\.'
      - name: Run tests with pydantic v1
        run: |
          pytest tests/gateway


Cancel

# Cancel workflow runs associated with a pull request when it is closed or merged.
name: Cancel

on:
  pull_request_target:
    types:
      - closed

permissions:
  contents: read

jobs:
  cancel:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    permissions:
      actions: write # to cancel workflow runs
    steps:
      - uses: actions/checkout@v4
      - uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const script = require(
              `${process.env.GITHUB_WORKSPACE}/.github/workflows/cancel.js`
            );
            await script({ context, github });


release-note

# A workflow to validate at least one release-note category is selected

name: release-note

on:
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
      - ready_for_review
      - labeled
      - unlabeled
  # post-merge job requires write access to add a label and post a comment.
  pull_request_target:
    types:
      - closed

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
  cancel-in-progress: true

defaults:
  run:
    shell: bash --noprofile --norc -exo pipefail {0}

jobs:
  validate-labeled:
    if: github.event.pull_request.draft == false && github.event.action != 'closed'
    runs-on: ubuntu-latest
    permissions:
      pull-requests: read # validateLabeled looks at PR's labels
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4
      - name: validate-labeled
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            // https://github.com/actions/github-script#run-a-separate-file
            const script = require("./.github/workflows/release-note.js");
            await script.validateLabeled({ core, context, github });

  post-merge:
    if: github.event.action == 'closed' && github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    timeout-minutes: 5
    permissions:
      pull-requests: write # postMerge labels PRs
    steps:
      - uses: actions/checkout@v4
      - name: post-merge
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            // https://github.com/actions/github-script#run-a-separate-file
            const script = require("./.github/workflows/release-note.js");
            await script.postMerge({ context, github });


rerun-cross-version-tests

# Cross version tests sometimes fail due to transient errors. This workflow reruns failed tests.
name: rerun-cross-version-tests

on:
  schedule:
    # Run this workflow daily at 17:00 UTC (4 hours after cross-version-tests.yml workflow)
    - cron: "0 17 * * *"

concurrency:
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
  cancel-in-progress: true

permissions:
  contents: read

jobs:
  set-matrix:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    if: github.repository == 'mlflow-automation/mlflow'
    permissions:
      actions: write # to rerun workflows
    steps:
      - uses: actions/checkout@v4
      - uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const rerun = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/rerun.js`);
            await rerun({ context, github, workflow_id: "cross-version-tests.yml" });


Lint

name: Lint

on:
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
      - ready_for_review
  push:
    branches:
      - master
      - branch-[0-9]+.[0-9]+

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
  cancel-in-progress: true

defaults:
  run:
    shell: bash --noprofile --norc -exo pipefail {0}

env:
  PIP_EXTRA_INDEX_URL: https://download.pytorch.org/whl/cpu

jobs:
  lint:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/untracked
      - uses: ./.github/actions/setup-python
        id: setup-python
      - name: Add problem matchers
        run: |
          echo "::add-matcher::.github/workflows/matchers/clint.json"
          echo "::add-matcher::.github/workflows/matchers/format.json"
          echo "::add-matcher::.github/workflows/matchers/ruff.json"
      - uses: ./.github/actions/cache-pip
      - name: Install dependencies
        run: |
          python -m venv .venv
          source .venv/bin/activate
          pip install -r requirements/lint-requirements.txt
          # TODO: Use a precompiled binary once it's available: https://github.com/tamasfe/taplo/issues/542
          cargo install taplo-cli@0.9.0 --locked
      - uses: ./.github/actions/pipdeptree
      - name: Install pre-commit hooks
        run: |
          source .venv/bin/activate
          pre-commit install
      - name: Run pre-commit
        id: pre-commit
        env:
          IS_MAINTAINER: ${{ contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association )}}
          NO_FIX: "true"
        run: |
          source .venv/bin/activate
          pre-commit run --all-files


Deployments

name: Deployments

on:
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
      - ready_for_review
  push:
    branches:
      - master
      - branch-[0-9]+.[0-9]+

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
  cancel-in-progress: true

defaults:
  run:
    shell: bash --noprofile --norc -exo pipefail {0}

jobs:
  deployments:
    if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/untracked
      - uses: ./.github/actions/setup-python
      - name: Install dependencies
        run: |
          pip install --no-dependencies tests/resources/mlflow-test-plugin
          pip install .[gateway] \
            pytest pytest-timeout pytest-asyncio httpx psutil sentence-transformers transformers
      - name: Run tests
        run: |
          pytest tests/deployments


Protect

name: Protect

on:
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
      - ready_for_review

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  protect:
    # This job prevents accidental auto-merging of PRs when jobs that are conditionally
    # triggered (for example, those defined in `cross-version-tests.yml`) are either still
    # in the process of running or have resulted in failures.
    runs-on: ubuntu-latest
    timeout-minutes: 120
    steps:
      - uses: actions/checkout@v4
      - uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const script = require('./.github/workflows/protect.js');
            await script({ github, context });


Team review

name: Team review

on:
  pull_request_target:
    types: [review_requested]

jobs:
  review:
    runs-on: ubuntu-latest
    if: ${{ github.event.requested_reviewer.login == 'mlflow-automation'}}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.MLFLOW_AUTOMATION_TOKEN }}
          script: |
            const { owner, repo } = context.repo;
            const pull_number = context.payload.pull_request.number;
            const members = [
              "B-Step62",
              "BenWilson2",
              "daniellok-db",
              "harupy",
              "serena-ruan",
              "WeichenXu123",
            ];
            const author = context.payload.pull_request.user.login;
            const reviews = await github.rest.pulls.listReviews({
              owner,
              repo,
              pull_number,
            });
            const approved = reviews.data
              .filter((r) => r.state === "APPROVED")
              .map((r) => r.user.login);
            await github.rest.pulls.requestReviewers({
              owner,
              repo,
              pull_number,
              reviewers: members.filter((m) => !approved.includes(m) && m !== author),
            });
            await github.rest.pulls.removeRequestedReviewers({
              owner,
              repo,
              pull_number,
              reviewers: ["mlflow-automation"],
            });


MLflow Slow Tests

# A daily job to run slow tests with MLFLOW_RUN_SLOW_TESTS environment variable set to true.
name: MLflow Slow Tests

on:
  pull_request:
    paths:
      # Run this workflow in PR when relevant files change
      - ".github/workflows/slow-tests.yml"
      - "tests/pyfunc/docker/**"
  workflow_dispatch:
    inputs:
      repository:
        description: >
          [Optional] Repository name with owner. For example, mlflow/mlflow.
           Defaults to the repository that triggered a workflow.
        required: false
        default: ""
      ref:
        description: >
          [Optional] The branch, tag or SHA to checkout. When checking out the repository that
           triggered a workflow, this defaults to the reference or SHA for that event. Otherwise,
           uses the default branch.
        required: false
        default: ""
  schedule:
    # Run this workflow daily at 13:00 UTC
    - cron: "0 13 * * *"

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
  cancel-in-progress: true

defaults:
  run:
    shell: bash --noprofile --norc -exo pipefail {0}

env:
  MLFLOW_RUN_SLOW_TESTS: "true"
  MLFLOW_HOME: /home/runner/work/mlflow/mlflow
  PIP_EXTRA_INDEX_URL: https://download.pytorch.org/whl/cpu

jobs:
  docker-build:
    runs-on: ubuntu-latest
    timeout-minutes: 120
    if: (github.event_name == 'schedule' && github.repository == 'mlflow-automation/mlflow') || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false)
    strategy:
      fail-fast: false
      matrix:
        group: [1, 2, 3]
        include:
          - splits: 3
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: ./.github/actions/free-disk-space
      - uses: ./.github/actions/untracked
      - uses: ./.github/actions/setup-python
      - uses: ./.github/actions/setup-pyenv
      - uses: ./.github/actions/setup-java
      - name: Install dependencies
        run: |
          python -m venv .venv
          source .venv/bin/activate
          source ./dev/install-common-deps.sh --ml
          python -m pip install langchain_experimental
      - uses: ./.github/actions/pipdeptree
      - name: Run tests
        run: |
          source .venv/bin/activate
          pytest --splits=${{ matrix.splits }} --group=${{ matrix.group }} tests/pyfunc/docker


name: Maintainer approval for requirement edits

on:
  pull_request:

permissions:
  pull-requests: read

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Fail without core maintainer approval
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const prNumber = context.payload.pull_request.number;
            const repo = context.repo;
            const { data: files } = await github.rest.pulls.listFiles({
              owner: repo.owner,
              repo: repo.repo,
              pull_number: prNumber,
            });

            const editedPaths = files.map(file => file.filename);
            const protectedPaths = new Set([
              "setup.py",
              "requirements/skinny-requirements.yaml",
            ]);

            if (editedPaths.some(path => protectedPaths.has(path))) {
              const script = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/require-core-maintainer-approval.js`);
              const reason = (
                "Editing requirements files can cause conflicts in downstream processes " +
                "that depend on MLflow. Please ensure that one of the MLflow Core Maintainers " +
                "has a chance to review this PR, and rerun this workflow once approved."
              );
              await script({ context, github, core, reason });
            }


Patch

name: Patch

on:
  pull_request_target:
    types:
      - opened
      - edited
      - synchronize

permissions:
  contents: read

defaults:
  run:
    shell: bash --noprofile --norc -exo pipefail {0}

jobs:
  patch:
    if: github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    timeout-minutes: 120
    steps:
      - uses: actions/checkout@v4
      - uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const script = require('./.github/workflows/patch.js');
            await script({ github, context, core });


Cross version test runner

name: Cross version test runner
on:
  issue_comment:
    types: [created]

permissions:
  contents: read

defaults:
  run:
    shell: bash --noprofile --norc -exo pipefail {0}

jobs:
  run:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    if: ${{ github.event.issue.pull_request && github.event.comment.body }}
    permissions:
      pull-requests: write
      actions: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/github-script@v7
        id: get-ref
        with:
          result-encoding: string
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const runner = require('./.github/workflows/cross-version-test-runner.js');
            await runner.main({ context, github });