From 4ceae9cf85c8364c48dc758505dfe7aa51a63757 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:18:46 +1200 Subject: [PATCH] [ci] Replace pre-commit with prek (#18029) --- .github/workflows/ci.yml | 76 ++++++----------------- .github/workflows/sync-device-classes.yml | 22 +++---- AGENTS.md | 6 +- requirements_test.txt | 2 +- script/determine-jobs.py | 8 +-- script/setup | 6 +- script/setup.bat | 6 +- 7 files changed, 49 insertions(+), 77 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e020e90018..e37cde5a47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: python -m venv venv . venv/bin/activate python --version - uv pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt pre-commit + uv pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt uv pip install -e . determine-jobs: @@ -208,69 +208,34 @@ jobs: run: script/ci-suggest-changes if: always() - pre-commit-ci-lite: - name: pre-commit.ci lite + lint-format: + name: Check lint and formatting runs-on: ubuntu-latest needs: - - common - determine-jobs if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') && needs.determine-jobs.outputs.core-ci == 'true' steps: - name: Check out code from GitHub uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Restore Python - uses: ./.github/actions/restore-python + - name: Run prek + uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0 with: - python-version: ${{ env.DEFAULT_PYTHON }} - cache-key: ${{ needs.common.outputs.cache-key }} - # Inlined from esphome/pre-commit-action with a restore-only cache - # step: the pre-commit-seed-cache job owns saving this cache, so - # pull request runs never write per-PR copies. - - name: Restore pre-commit cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/.cache/pre-commit - # Must match the key pre-commit-seed-cache saves - # yamllint disable-line rule:line-length - key: pre-commit-3|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }} - - name: Run pre-commit + # Keep in sync with requirements_test.txt. + prek-version: "0.4.11" + # This job only runs on pull requests, so nothing ever populates + # the cache on dev. Every run would miss and then write a per-pull + # request copy, which is what the old seed-cache job existed to + # avoid. Building the hooks from scratch takes seconds, so skip it. + cache: false env: - SKIP: pylint,ci-custom - run: | - python -m pip install pre-commit - pre-commit run --show-diff-on-failure --color=always --all-files + PREK_SKIP: pylint,ci-custom + # Pushes any fixes the hooks made back to the pull request. This step + # must keep its default name: the GitHub App that performs the push + # locates the workflow run by that name. - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 if: always() - - pre-commit-seed-cache: - name: Seed pre-commit cache - runs-on: ubuntu-latest - needs: - - common - # Saves a dev-scoped pre-commit cache that pull request runs can - # restore, since pre-commit.ci lite itself never runs on dev pushes. - if: github.event_name == 'push' && github.ref == 'refs/heads/dev' - steps: - - name: Check out code from GitHub - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Restore Python - uses: ./.github/actions/restore-python with: - python-version: ${{ env.DEFAULT_PYTHON }} - cache-key: ${{ needs.common.outputs.cache-key }} - - name: Cache pre-commit environments - id: cache-pre-commit - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/.cache/pre-commit - # Must match the restore key in pre-commit-ci-lite - # yamllint disable-line rule:line-length - key: pre-commit-3|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }} - - name: Install pre-commit hook environments - if: steps.cache-pre-commit.outputs.cache-hit != 'true' - run: | - python -m pip install pre-commit - pre-commit install-hooks + msg: apply automatic formatting fixes pytest: name: Run pytest @@ -1456,16 +1421,15 @@ jobs: ci-status: name: CI Status runs-on: ubuntu-24.04 - # Listed in the same order the jobs are defined above. Two jobs are + # Listed in the same order the jobs are defined above. One job is # deliberately left out: "benchmarks" reports through CodSpeed rather than - # this check, and "pre-commit-seed-cache" only populates a cache on pushes - # to dev. + # this check. needs: - common - determine-jobs - ci-custom - pylint - - pre-commit-ci-lite + - lint-format - pytest - codecov-empty-upload - integration-tests diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 5d250b97eb..a299e76584 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -44,7 +44,7 @@ jobs: - name: Set up uv # An order of magnitude faster than pip on cold boots, with its # own wheel cache. ``--system`` (below) installs into the - # setup-python interpreter so subsequent ``pre-commit`` / + # setup-python interpreter so subsequent ``prek`` / # ``script/run-in-env.py`` steps find the deps without a # ``uv run`` prefix. uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 @@ -58,19 +58,19 @@ jobs: - name: Install Home Assistant run: | uv pip install --system -e lib/home-assistant - uv pip install --system -r requirements.txt -r requirements_test.txt pre-commit + uv pip install --system -r requirements.txt -r requirements_test.txt - name: Sync run: | python ./script/sync-device_class.py - - name: Apply pre-commit auto-fixes + - name: Apply prek auto-fixes # First pass: let formatters (ruff, end-of-file-fixer, etc.) modify - # files. pre-commit exits non-zero whenever a hook touches anything, + # files. prek exits non-zero whenever a hook touches anything, # which would otherwise abort the workflow before the auto-fixes # can flow into the sync PR. # - # SKIP: + # PREK_SKIP: # - no-commit-to-branch is a local guard against committing on # dev/release/beta; CI runs on dev by definition, and # peter-evans/create-pull-request creates the branch itself. @@ -79,18 +79,18 @@ jobs: # the runtime deps (HA + requirements*.txt); main CI already # gates pylint on real PRs. env: - SKIP: pylint,no-commit-to-branch - run: python script/run-in-env.py pre-commit run --all-files || true + PREK_SKIP: pylint,no-commit-to-branch + run: python script/run-in-env.py prek run --all-files || true - - name: Verify pre-commit clean + - name: Verify prek clean # Second pass: re-run all hooks against the now-fixed tree. # Auto-fixers exit 0 (nothing to change); any remaining failure # from a check-only hook (flake8 / yamllint / ci-custom) is a - # real issue and fails the workflow loudly. Same SKIP list as + # real issue and fails the workflow loudly. Same PREK_SKIP list as # above for the same reasons. env: - SKIP: pylint,no-commit-to-branch - run: python script/run-in-env.py pre-commit run --all-files + PREK_SKIP: pylint,no-commit-to-branch + run: python script/run-in-env.py prek run --all-files - name: Commit changes uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 diff --git a/AGENTS.md b/AGENTS.md index b067482d18..40381030cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -412,7 +412,7 @@ This document provides essential context for AI models interacting with this pro * **Configuration:** * `pyproject.toml`: Defines the Python project metadata and dependencies. * `platformio.ini`: Configures the PlatformIO build environments for different microcontrollers. - * `.pre-commit-config.yaml`: Configures the pre-commit hooks for linting and formatting. + * `.pre-commit-config.yaml`: Configures the lint and format hooks, run by `prek`. * **CI/CD Pipeline:** Defined in `.github/workflows`. * **Static Analysis & Development:** * `esphome/core/defines.h`: A comprehensive header file containing all `#define` directives that can be added by components using `cg.add_define()` in Python. This file is used exclusively for development, static analysis tools, and CI testing - it is not used during runtime compilation. When developing components that add new defines, they must be added to this file to ensure proper IDE support and static analysis coverage. The file includes feature flags, build configurations, and platform-specific defines that help static analyzers understand the complete codebase without needing to compile for specific platforms. @@ -420,7 +420,7 @@ This document provides essential context for AI models interacting with this pro ## 6. Development & Testing Workflow * **Local Development Environment:** Use the provided Docker container or create a Python virtual environment and install dependencies from `requirements_dev.txt`. -* **Running Commands:** Use the `script/run-in-env.py` script to execute commands within the project's virtual environment. For example, to run the linter: `python3 script/run-in-env.py pre-commit run`. +* **Running Commands:** Use the `script/run-in-env.py` script to execute commands within the project's virtual environment. For example, to run the linter: `python3 script/run-in-env.py prek run`. * **Testing:** * **Python:** Run unit tests with `pytest`. * **C++:** Use `clang-tidy` for static analysis. @@ -493,7 +493,7 @@ This document provides essential context for AI models interacting with this pro 1. **Fork & Branch:** Create a new branch based on the `dev` branch (always use `git checkout -b dev` to ensure you're branching from `dev`, not the currently checked out branch). 2. **Make Changes:** Adhere to all coding conventions and patterns. 3. **Test:** Create component tests for all supported platforms and run the full test suite locally. - 4. **Lint:** Run `pre-commit` to ensure code is compliant. + 4. **Lint:** Run `prek` to ensure code is compliant. 5. **Commit:** Commit your changes. There is no strict format for commit messages. 6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title must start with a `[tag]` prefix. For component work, use the component name (e.g., `[display] Fix bug`, `[abc123] Add new component`); for changes to shared/core code that isn't tied to a single component, use `[core]` (e.g., `[core] Add validator`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template. diff --git a/requirements_test.txt b/requirements_test.txt index 389bf6dbf0..943d6b597b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -2,7 +2,7 @@ pylint==4.0.6 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating ruff==0.16.1 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating -pre-commit +prek==0.4.11 # also change in .github/workflows/ci.yml when updating # Unit tests pytest==9.1.1 diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 061485c76c..8039aff83f 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -23,7 +23,7 @@ what files have changed. It outputs JSON with the following structure: } The CI workflow uses this information to: -- Gate the unconditional jobs (ci-custom, pytest, pre-commit-ci-lite) via core_ci; +- Gate the unconditional jobs (ci-custom, pytest, lint-format) via core_ci; false when a pull_request only touches CI-irrelevant meta paths (other workflow files, .github/actions/build-image/*, .yamllint, .github/dependabot.yml, docker/**) so workflow-only PRs satisfy the required CI Status check without running the @@ -708,7 +708,7 @@ def should_run_benchmarks(branch: str | None = None) -> bool: # Files / path patterns whose changes alone don't warrant running the -# unconditional CI jobs (`ci-custom`, `pytest`, `pre-commit-ci-lite`). +# unconditional CI jobs (`ci-custom`, `pytest`, `lint-format`). # Single source of truth for what we treat as "CI-irrelevant" on # pull_request events; ci.yml used to encode this in its own # `pull_request.paths` filter, but that hid the required `CI Status` @@ -752,7 +752,7 @@ def _is_ci_irrelevant_path(path: str) -> bool: def should_run_core_ci(branch: str | None = None) -> bool: - """Determine if the unconditional CI jobs (ci-custom/pytest/pre-commit-ci-lite) should run. + """Determine if the unconditional CI jobs (ci-custom/pytest/lint-format) should run. Returns False only when every changed file is in the CI-irrelevant set above (see ``_is_ci_irrelevant_path``). Empty diffs return True so we @@ -1177,7 +1177,7 @@ def main() -> None: # Determine what should run # core_ci gates the unconditional jobs in ci.yml (ci-custom, pytest, - # pre-commit-ci-lite). Non-pull_request events (push to dev/beta/release + # lint-format). Non-pull_request events (push to dev/beta/release # and merge_group) always run them so behavior like venv-cache saves on # push to dev is preserved. event_name = os.environ.get("GITHUB_EVENT_NAME", "") diff --git a/script/setup b/script/setup index 709eaee0f3..5dfc0efe5d 100755 --- a/script/setup +++ b/script/setup @@ -25,7 +25,11 @@ fi uv pip install setuptools wheel uv pip install -e ".[dev,test]" --config-settings editable_mode=compat -pre-commit install +# --overwrite replaces any hook already in place. Without it, prek finds a +# previously installed pre-commit hook, moves it aside to +# .git/hooks/pre-commit.legacy and keeps calling it, so every commit would +# run both tools. +prek install --overwrite mkdir -p .temp diff --git a/script/setup.bat b/script/setup.bat index 003ea31b36..809d05ae93 100644 --- a/script/setup.bat +++ b/script/setup.bat @@ -17,7 +17,11 @@ pip3 install -r requirements.txt -r requirements_test.txt -r requirements_dev.tx pip3 install setuptools wheel pip3 install -e ".[dev,test]" --config-settings editable_mode=compat -pre-commit install +rem --overwrite replaces any hook already in place. Without it, prek finds a +rem previously installed pre-commit hook, moves it aside to +rem .git/hooks/pre-commit.legacy and keeps calling it, so every commit would +rem run both tools. +prek install --overwrite echo . echo .