# See .github/workflows/autoformat.md for instructions on how to test this workflow. name: Autoformat on: issue_comment: types: [created] defaults: run: shell: bash permissions: {} jobs: check-comment: runs-on: ubuntu-slim timeout-minutes: 10 if: github.event.issue.pull_request && startsWith(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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | .github - name: judge id: judge uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: retries: 3 script: | core.debug(JSON.stringify(context, null, 2)); const autoformat = require('./.github/workflows/autoformat.js'); const { comment } = context.payload; const shouldAutoformat = autoformat.shouldAutoformat(comment); if (shouldAutoformat) { await autoformat.validatePermissions(context, github); await autoformat.createReaction(context, github); await autoformat.createStatus(context, github, core); } const pullInfo = await autoformat.getPullInfo(context, github); return { ...pullInfo, shouldAutoformat }; - name: Check maintainer access if: fromJSON(steps.judge.outputs.result).shouldAutoformat uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: retries: 3 script: | const autoformat = require('./.github/workflows/autoformat.js'); await autoformat.checkMaintainerAccess(context, github); 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false repository: ${{ needs.check-comment.outputs.repository }} ref: ${{ needs.check-comment.outputs.head_ref }} # Set fetch-depth to merge the base branch fetch-depth: 100 - name: Verify head SHA env: EXPECTED_SHA: ${{ needs.check-comment.outputs.head_sha }} run: | actual_sha="$(git rev-parse HEAD)" if [[ "$actual_sha" != "$EXPECTED_SHA" ]]; then echo "::error::HEAD has changed since the /autoformat comment (expected $EXPECTED_SHA, got $actual_sha). Please re-comment /autoformat." exit 1 fi - name: Check diff id: diff env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO: ${{ github.repository }} PULL_NUMBER: ${{ needs.check-comment.outputs.pull_number }} run: | changed_files="$(gh pr view --repo $REPO $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") docs=$([[ -z $(echo "$changed_files" | grep '^docs/') ]] && echo "false" || echo "true") r=$([[ -z $(echo "$changed_files" | grep '^mlflow/R/mlflow') ]] && echo "false" || echo "true") db=$([[ -z $(echo "$changed_files" | grep '^mlflow/store/db_migrations/') ]] && echo "false" || echo "true") api=$([[ -z $(echo "$changed_files" | grep -E '(^mlflow/.*\.py$|^docs/api_reference/.*\.rst$)') ]] && echo "false" || echo "true") echo "protos=$protos" >> $GITHUB_OUTPUT echo "js=$js" >> $GITHUB_OUTPUT echo "docs=$docs" >> $GITHUB_OUTPUT echo "r=$r" >> $GITHUB_OUTPUT echo "db=$db" >> $GITHUB_OUTPUT echo "api=$api" >> $GITHUB_OUTPUT # Merge the base branch (which is usually master) to apply formatting using the latest configurations. - name: Merge base branch env: BASE_REPO: ${{ needs.check-comment.outputs.base_repo }} BASE_REF: ${{ needs.check-comment.outputs.base_ref }} run: | # This identity is only used for the temporary merge commit and is not pushed. git config user.name 'name' git config user.email 'email' git remote add base https://github.com/$BASE_REPO.git git fetch base $BASE_REF git merge base/$BASE_REF - uses: ./.github/actions/setup-python # ************************************************************************ # pre-commit # ************************************************************************ - run: | uv run --only-group lint pre-commit install --install-hooks uv run --only-group lint pre-commit run install-bin -a -v uv run --only-group lint pre-commit run --all-files --color=always || true # ************************************************************************ # protos # ************************************************************************ - if: steps.diff.outputs.protos == 'true' env: DOCKER_BUILDKIT: 1 run: | # Run the script multiple times. The changes generated by the first run # may trigger additional changes, which need to be applied in subsequent runs. for i in {1..3}; do ./dev/generate-protos.sh done # ************************************************************************ # DB # ************************************************************************ - if: steps.diff.outputs.db == 'true' run: | tests/db/update_schemas.sh # ************************************************************************ # js # ************************************************************************ - if: steps.diff.outputs.js == 'true' uses: ./.github/actions/setup-node - if: steps.diff.outputs.js == 'true' working-directory: mlflow/server/js run: | yarn install --immutable - 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 - if: steps.diff.outputs.docs == 'true' working-directory: docs run: | npm ci - if: steps.diff.outputs.docs == 'true' working-directory: docs run: | npm run prettier:fix # ************************************************************************ # R # ************************************************************************ - if: steps.diff.outputs.r == 'true' working-directory: docs/api_reference run: | ./build-rdoc.sh # ************************************************************************ # API Reference # ************************************************************************ - if: steps.diff.outputs.api == 'true' run: | uv sync --group docs --extra gateway uv pip install -r requirements/torch.txt uv run --directory docs/api_reference make dummy # ************************************************************************ # Upload patch # ************************************************************************ - name: Create patch id: patch env: RUN_ID: ${{ github.run_id }} run: | git add -N . git diff > $RUN_ID.diff reformatted=$([[ -s $RUN_ID.diff ]] && echo "true" || echo "false") echo "reformatted=$reformatted" >> $GITHUB_OUTPUT - name: Upload patch uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ github.run_id }}.diff path: ${{ github.run_id }}.diff retention-days: 1 if-no-files-found: ignore push: runs-on: ubuntu-slim timeout-minutes: 5 needs: [check-comment, format] if: needs.format.outputs.reformatted == 'true' permissions: contents: read outputs: head_sha: ${{ steps.push.outputs.head_sha }} steps: - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 id: app-token with: client-id: ${{ secrets.APP_CLIENT_ID }} # See https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/managing-private-keys-for-github-apps # for how to rotate the private key private-key: ${{ secrets.APP_PRIVATE_KEY }} permission-contents: write - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: true repository: ${{ needs.check-comment.outputs.repository }} ref: ${{ needs.check-comment.outputs.head_ref }} # Set fetch-depth to merge the base branch fetch-depth: 100 # As reported in https://github.com/orgs/community/discussions/25702, if an action pushes # code using `GITHUB_TOKEN`, that won't trigger new workflow runs on the PR. # A personal access token is required to trigger new workflow runs. token: ${{ steps.app-token.outputs.token }} - name: Verify head SHA env: EXPECTED_SHA: ${{ needs.check-comment.outputs.head_sha }} run: | actual_sha="$(git rev-parse HEAD)" if [[ "$actual_sha" != "$EXPECTED_SHA" ]]; then echo "::error::HEAD has changed since the /autoformat comment (expected $EXPECTED_SHA, got $actual_sha). Please re-comment /autoformat." exit 1 fi - name: Configure git env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ needs.check-comment.outputs.pull_number }} REPO: ${{ github.repository }} run: | AUTHOR=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json author --jq '.author') IS_BOT=$(echo "$AUTHOR" | jq -r '.is_bot') if [ "$IS_BOT" = "false" ]; then LOGIN=$(echo "$AUTHOR" | jq -r '.login') else LOGIN="mlflow-app[bot]" fi ID=$(gh api "users/$LOGIN" --jq '.id') git config user.name "$LOGIN" git config user.email "${ID}+${LOGIN}@users.noreply.github.com" - name: Merge base branch env: BASE_REPO: ${{ needs.check-comment.outputs.base_repo }} BASE_REF: ${{ needs.check-comment.outputs.base_ref }} run: | git remote add base https://github.com/${BASE_REPO}.git git fetch base $BASE_REF git merge base/${BASE_REF} - name: Download patch uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ github.run_id }}.diff path: /tmp - name: Apply patch and push id: push env: RUN_ID: ${{ github.run_id }} REPOSITORY: ${{ github.repository }} run: | git apply /tmp/${RUN_ID}.diff git add . git commit -sm "Autoformat: https://github.com/${REPOSITORY}/actions/runs/${RUN_ID}" echo "head_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT git push update-status: runs-on: ubuntu-slim timeout-minutes: 10 needs: [check-comment, format, push] if: always() && needs.check-comment.outputs.should_autoformat == 'true' permissions: statuses: write # To update check statuses actions: write # To approve workflow runs steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | .github - name: Update status uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: NEEDS_JSON: ${{ toJson(needs) }} HEAD_SHA: ${{ needs.check-comment.outputs.head_sha }} PUSH_HEAD_SHA: ${{ needs.push.outputs.head_sha }} with: retries: 3 script: | const needs = JSON.parse(process.env.NEEDS_JSON); const head_sha = process.env.HEAD_SHA; const autoformat = require('./.github/workflows/autoformat.js'); const push_head_sha = process.env.PUSH_HEAD_SHA; if (push_head_sha) { await autoformat.approveWorkflowRuns(context, github, push_head_sha); } await autoformat.updateStatus(context, github, head_sha, needs);