This is an automated email from the ASF dual-hosted git repository.
vatsrahul1001 pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 91508249689d955af02f7aeadc6b316821e23baa
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Oct 29 16:59:59 2026 +0800
[v3-4-test] Run the slow root prek checks in parallel (#75437) (#75933)
Co-authored-by: PoAn Yang <[email protected]>
Signed-off-by: PoAn Yang <[email protected]>
---
.pre-commit-config.yaml | 50 +++++++++--
contributing-docs/08_static_code_checks.rst | 6 ++
scripts/ci/prek/AGENTS.md | 30 ++++++-
scripts/ci/prek/check_prek_parallel_hooks.py | 97 ++++++++++++++++++++++
.../ci/prek/test_check_prek_parallel_hooks.py | 75 +++++++++++++++++
5 files changed, 249 insertions(+), 9 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 92e716ce9b3..8a470cbdf5e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -27,6 +27,8 @@ default_env:
# the committed ones. Hooks in nested configs do not inherit this and must
pass --frozen to uv themselves.
UV_FROZEN: "1"
exclude: ^.*/.*_vendor/
+# Hooks with `priority: 1000` run in parallel, after every other hook in this
file. Only the hooks
+# listed in scripts/ci/prek/check_prek_parallel_hooks.py may set a priority.
repos:
- repo: meta
hooks:
@@ -251,6 +253,19 @@ repos:
^scripts/ci/license-templates/
- repo: local
hooks:
+ # Hooks with `priority: 1000` start in the order they appear in this
file. This hook takes the
+ # longest of them, so it comes first. Because it needs the CI image, it
would normally go in the
+ # CI image section at the end of this file. Its priority still makes it
run after every hook that
+ # has no priority.
+ - id: check-template-fields-valid
+ name: Check templated fields mapped in operators/sensors
+ env:
+ PYTHONPYCACHEPREFIX: .build/pycache
+ priority: 1000
+ language: python
+ entry: ./scripts/ci/prek/check_template_fields.py
+ files:
^(providers/[^/]+|airflow-core)/src/airflow/(.*/)?(sensors|operators)/.*\.py$
+ require_serial: true
- id: check-min-python-version
name: Check minimum Python version
env:
@@ -739,10 +754,12 @@ repos:
name: Detect accidentally committed debug statements
env:
PYTHONPYCACHEPREFIX: .build/pycache
+ priority: 1000
- id: check-builtin-literals
name: Require literal syntax when initializing builtins
env:
PYTHONPYCACHEPREFIX: .build/pycache
+ priority: 1000
- id: detect-private-key
name: Detect if private key is added to the repository
env:
@@ -795,6 +812,7 @@ repos:
name: Check if there are no deprecate log warn
env:
PYTHONPYCACHEPREFIX: .build/pycache
+ priority: 1000
- repo: https://github.com/adrienverge/yamllint
rev: cba56bcde1fdd01c1deb3f945e69764c291a6530 # frozen: v1.38.0
hooks:
@@ -802,6 +820,7 @@ repos:
name: Check YAML files with yamllint
env:
PYTHONPYCACHEPREFIX: .build/pycache
+ priority: 1000
entry: yamllint -c yamllint-config.yml --strict
types: [yaml]
exclude: >
@@ -841,6 +860,7 @@ repos:
name: Run codespell
env:
PYTHONPYCACHEPREFIX: .build/pycache
+ priority: 1000
description: Run codespell to check for common misspellings in files
entry: bash -c 'echo "If you think that this failure is an error,
consider adding the word(s)
to the codespell dictionary at docs/spelling_wordlist.txt.
@@ -871,6 +891,7 @@ repos:
name: Run zizmor to check for github workflow syntax errors
env:
PYTHONPYCACHEPREFIX: .build/pycache
+ priority: 1000
types: [yaml]
files: ^\.github/workflows/.*$|^\.github/actions/.*$
require_serial: true
@@ -888,6 +909,7 @@ repos:
name: Check internal Markdown links with lychee (offline mode)
env:
PYTHONPYCACHEPREFIX: .build/pycache
+ priority: 1000
description: >-
Validate intra-repo links in Markdown files (relative paths, anchors,
and fragments) without making any HTTP requests. Auto-generated
client
@@ -956,6 +978,7 @@ repos:
name: Check for test-only imports in production source
env:
PYTHONPYCACHEPREFIX: .build/pycache
+ priority: 1000
entry: ./scripts/ci/prek/check_test_only_imports_in_src.py
language: python
pass_filenames: true
@@ -1028,6 +1051,7 @@ repos:
name: Lint Dockerfile
env:
PYTHONPYCACHEPREFIX: .build/pycache
+ priority: 1000
language: python
entry: ./scripts/ci/prek/lint_dockerfile.py
files: Dockerfile.*$
@@ -1074,6 +1098,7 @@ repos:
name: Prevent deprecated sqlalchemy usage
env:
PYTHONPYCACHEPREFIX: .build/pycache
+ priority: 1000
entry: ./scripts/ci/prek/prevent_deprecated_sqlalchemy_usage.py
language: python
files: >
@@ -1202,6 +1227,7 @@ repos:
- id: check-for-inclusive-language
env:
PYTHONPYCACHEPREFIX: .build/pycache
+ priority: 1000
language: pygrep
name: Check for language that we do not accept as community
description: Please use more appropriate words for community
documentation.
@@ -1415,6 +1441,7 @@ repos:
name: Check if licenses are OK for Apache
env:
PYTHONPYCACHEPREFIX: .build/pycache
+ priority: 1000
entry: ./scripts/ci/prek/check_license.py
language: python
files: ^LICENSE$
@@ -1423,6 +1450,7 @@ repos:
name: Check that metrics in the codebase are in sync with the metrics
registry YAML file.
env:
PYTHONPYCACHEPREFIX: .build/pycache
+ priority: 1000
entry: ./scripts/ci/prek/check_metrics_synced_with_the_registry.py
language: python
files:
\.py$|^shared/observability/src/airflow_shared/observability/metrics/metrics_template\.yaml$
@@ -1469,6 +1497,7 @@ repos:
name: Check Shell scripts syntax correctness
env:
PYTHONPYCACHEPREFIX: .build/pycache
+ priority: 1000
language: docker_image
entry: koalaman/shellcheck:v0.8.0 -x -a
files: \.(bash|sh)$|^hooks/build$|^hooks/push$
@@ -1692,6 +1721,7 @@ repos:
name: Run markdownlint
env:
PYTHONPYCACHEPREFIX: .build/pycache
+ priority: 1000
description: Checks the style of Markdown files.
entry: markdownlint
language: node
@@ -1856,6 +1886,7 @@ repos:
name: Check that all logging methods are lazy
env:
PYTHONPYCACHEPREFIX: .build/pycache
+ priority: 1000
entry: ./scripts/ci/prek/check_lazy_logging.py
language: python
pass_filenames: true
@@ -1864,6 +1895,7 @@ repos:
name: Check XComModel columns are read through xcom_entity, not
directly in SQLAlchemy queries
env:
PYTHONPYCACHEPREFIX: .build/pycache
+ priority: 1000
entry: ./scripts/ci/prek/check_xcom_model_columns.py
language: python
pass_filenames: true
@@ -1910,6 +1942,7 @@ repos:
name: bandit
env:
PYTHONPYCACHEPREFIX: .build/pycache
+ priority: 1000
description: "Bandit is a tool for finding common security issues in
Python code"
entry: bandit
language: python
@@ -1979,6 +2012,14 @@ repos:
entry: ./scripts/ci/prek/check_prek_hooks_pycache_prefix.py
pass_filenames: false
files:
(^|/)\.pre-commit-config\.yaml$|^scripts/ci/prek/check_prek_hooks_pycache_prefix\.py$
+ - id: check-prek-parallel-hooks
+ name: Only audited hooks run in parallel in the root prek config
+ env:
+ PYTHONPYCACHEPREFIX: .build/pycache
+ language: python
+ entry: ./scripts/ci/prek/check_prek_parallel_hooks.py
+ pass_filenames: false
+ files:
^\.pre-commit-config\.yaml$|^scripts/ci/prek/check_prek_parallel_hooks\.py$
- id: check-migration-patterns
name: Check migration files for anti-patterns and style (MIG001-MIG004)
env:
@@ -1992,14 +2033,6 @@ repos:
## ADD MOST PREK HOOK ABOVE THAT LINE
# The below prek hooks are those requiring CI image to be built
## ONLY ADD PREK HOOKS HERE THAT REQUIRE CI IMAGE
- - id: check-template-fields-valid
- name: Check templated fields mapped in operators/sensors
- env:
- PYTHONPYCACHEPREFIX: .build/pycache
- language: python
- entry: ./scripts/ci/prek/check_template_fields.py
- files:
^(providers/[^/]+|airflow-core)/src/airflow/(.*/)?(sensors|operators)/.*\.py$
- require_serial: true
- id: check-execution-api-versions
name: Check execution API datamodel changes have corresponding version
updates
env:
@@ -2073,6 +2106,7 @@ repos:
name: Check for problematic context manager class decorators
env:
PYTHONPYCACHEPREFIX: .build/pycache
+ priority: 1000
entry: ./scripts/ci/prek/check_contextmanager_class_decorators.py
language: python
files: .*test.*\.py$
diff --git a/contributing-docs/08_static_code_checks.rst
b/contributing-docs/08_static_code_checks.rst
index 11eee3ebb53..91bd8f22f7b 100644
--- a/contributing-docs/08_static_code_checks.rst
+++ b/contributing-docs/08_static_code_checks.rst
@@ -161,6 +161,12 @@ After installation, prek hooks are run automatically when
you commit the
code or push it to the repository (depending on stages configured for the
hooks). Some of the
hooks are configured to run on "manual" stage only and are not run
automatically.
+The hooks of the root ``.pre-commit-config.yaml`` run one at a time, except
for some slow checks
+that set ``priority: 1000``. Those checks run in parallel at the end. Before
you set that priority
+on another hook, read the rules in ``scripts/ci/prek/AGENTS.md``.
``check-prek-parallel-hooks``
+fails when a hook sets a priority other than 1000, or sets 1000 but is not
listed in
+``PARALLEL_HOOKS`` in ``scripts/ci/prek/check_prek_parallel_hooks.py``.
+
By default when you run ``prek``, the ``pre-commit`` stage hooks are run.
But you can run prek hooks manually as needed.
diff --git a/scripts/ci/prek/AGENTS.md b/scripts/ci/prek/AGENTS.md
index d8b605f6684..64427787af2 100644
--- a/scripts/ci/prek/AGENTS.md
+++ b/scripts/ci/prek/AGENTS.md
@@ -22,7 +22,9 @@ When adding a new breeze-dependent hook:
1. Import and use `run_command_via_breeze_run` from `common_prek_utils` — do
not shell out
to `breeze` directly.
2. Register the hook at the **end** of the relevant `.pre-commit-config.yaml`
file (breeze
- hooks are slow and should run after fast, local checks).
+ hooks are slow and should run after fast, local checks). A hook in the
`priority: 1000` group
+ described below runs after the other hooks no matter where it sits in the
file, so this rule
+ does not apply to that hook.
## Adding new hooks
@@ -36,3 +38,29 @@ When adding a new breeze-dependent hook:
breeze deletes `__pycache__` directories on start-up, so a hook writing
`.pyc` next to the
sources can make a concurrent breeze hook fail with `Directory not empty`.
`check-prek-hooks-pycache-prefix` enforces this.
+- In `/.pre-commit-config.yaml`, the hooks with `priority: 1000` form one
group. prek runs them in
+ parallel, after every other hook in the file. Leave `priority:` unset on a
hook unless it joins
+ this group. A hook may join the group only if it follows all of these rules:
+ - It never changes a tracked file, not even when it finds drift. prek runs
`git diff` before and
+ after the group. When tracked files change, prek fails the whole group and
does not say which
+ hook changed them.
+ - It writes untracked files only where no other hook in the group reads
them, for example under
+ `.build/`.
+ - It does not change the git index, refs or worktrees, and it does not use
fixed Docker
+ container, network or volume names.
+ - It shares a virtualenv or cache with another hook only if the tool that
writes the virtualenv
+ or cache locks it or writes each file atomically. For example, uv locks a
virtualenv while
+ installing into it, and Python writes each `.pyc` file atomically.
+ - Unless the hook is `check-template-fields-valid`, it does not run Breeze
and it does not walk a
+ directory tree with `Path.rglob()` on Python 3.11.
`check-template-fields-valid` is the only
+ hook in the group that runs Breeze, and Breeze deletes `__pycache__`
directories at startup.
+ On Python 3.11, `rglob()` fails when a directory disappears during the
walk. So while
+ `check-template-fields-valid` runs, a hook that walks a tree with
`rglob()` on Python 3.11 can
+ fail. `check-template-fields-valid` itself walks `providers/` with
`rglob()` on Python 3.11
+ inside the Breeze container, so a second hook that runs Breeze can make
that walk fail.
+
+ To add a hook to the group, set `priority: 1000` on it and list it in
`PARALLEL_HOOKS` in
+ `check_prek_parallel_hooks.py`. The `check-prek-parallel-hooks` hook fails
if a hook in
+ `/.pre-commit-config.yaml` sets a priority other than 1000, or sets 1000 but
is not listed in
+ `PARALLEL_HOOKS`. If you change a listed hook so that it breaks one of the
rules above, take it
+ out of the group.
diff --git a/scripts/ci/prek/check_prek_parallel_hooks.py
b/scripts/ci/prek/check_prek_parallel_hooks.py
new file mode 100755
index 00000000000..1042766cdd4
--- /dev/null
+++ b/scripts/ci/prek/check_prek_parallel_hooks.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# /// script
+# requires-python = ">=3.11"
+# dependencies = [
+# "pyyaml>=6.0.3",
+# ]
+# ///
+"""Fail when a hook of the root prek config sets a priority that is not
allowed.
+
+prek runs the hooks that share a ``priority`` value in parallel. In the root
+``.pre-commit-config.yaml``, only the hooks in ``PARALLEL_HOOKS`` may set a
priority, and they
+must all use ``PARALLEL_PRIORITY``. ``scripts/ci/prek/AGENTS.md`` lists the
rules that a hook must
+follow before it can be added to ``PARALLEL_HOOKS``.
+
+The check rejects every value other than ``PARALLEL_PRIORITY`` because hooks
that set no priority
+get their position in the file, counted from 0, as their priority. For
example, a hook that sets
+``50`` would run together with the 51st hook.
+"""
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+import yaml
+
+ROOT_CONFIG = Path(".pre-commit-config.yaml")
+PARALLEL_PRIORITY = 1000
+PARALLEL_HOOKS = frozenset(
+ {
+ "bandit",
+ "check-apache-license-rat",
+ "check-builtin-literals",
+ "check-contextmanager-class-decorators",
+ "check-for-inclusive-language",
+ "check-lazy-logging",
+ "check-metrics-synced-with-registry",
+ "check-template-fields-valid",
+ "check-test-only-imports-in-src",
+ "check-xcom-model-columns",
+ "codespell",
+ "debug-statements",
+ "lint-dockerfile",
+ "lint-markdown",
+ "lychee-docker",
+ "prevent-deprecated-sqlalchemy-usage",
+ "python-no-log-warn",
+ "shellcheck",
+ "yamllint",
+ "zizmor",
+ }
+)
+
+
+def find_unaudited_priority_hooks(config_path: Path) -> list[str]:
+ config = yaml.safe_load(config_path.read_text()) or {}
+ return [
+ f"{hook['id']} (priority {hook['priority']!r})"
+ for repo in config.get("repos", [])
+ for hook in repo.get("hooks", [])
+ if "priority" in hook and (hook["priority"] != PARALLEL_PRIORITY or
hook["id"] not in PARALLEL_HOOKS)
+ ]
+
+
+def main() -> int:
+ errors = find_unaudited_priority_hooks(ROOT_CONFIG)
+ if errors:
+ print(f"ERROR: These hooks in {ROOT_CONFIG} set a priority that is not
allowed:\n")
+ print("\n".join(f" {error}" for error in errors))
+ print(
+ "\nOnly the hooks listed in PARALLEL_HOOKS in
scripts/ci/prek/check_prek_parallel_hooks.py"
+ f" may set a priority, and the value must be {PARALLEL_PRIORITY}.
Those hooks run in parallel."
+ " Before you add a hook to PARALLEL_HOOKS, check that it follows
the rules in"
+ " scripts/ci/prek/AGENTS.md."
+ )
+ return 1
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/scripts/tests/ci/prek/test_check_prek_parallel_hooks.py
b/scripts/tests/ci/prek/test_check_prek_parallel_hooks.py
new file mode 100644
index 00000000000..d56cf844a3d
--- /dev/null
+++ b/scripts/tests/ci/prek/test_check_prek_parallel_hooks.py
@@ -0,0 +1,75 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import pytest
+from check_prek_parallel_hooks import PARALLEL_HOOKS,
find_unaudited_priority_hooks, main
+
+AUDITED_HOOK = sorted(PARALLEL_HOOKS)[0]
+
+CONFIG = f"""\
+repos:
+ - repo: meta
+ hooks:
+ - id: no-priority
+ - repo: local
+ hooks:
+ - id: {AUDITED_HOOK}
+ priority: 1000
+ - id: not-audited
+ priority: 1000
+ - id: {AUDITED_HOOK}
+ priority: 50
+ - id: alias
+ priority: checks
+"""
+
+
+def test_find_unaudited_priority_hooks(tmp_path):
+ config = tmp_path / ".pre-commit-config.yaml"
+ config.write_text(CONFIG)
+
+ assert find_unaudited_priority_hooks(config) == [
+ "not-audited (priority 1000)",
+ f"{AUDITED_HOOK} (priority 50)",
+ "alias (priority 'checks')",
+ ]
+
+
+def test_find_unaudited_priority_hooks_empty_config(tmp_path):
+ config = tmp_path / ".pre-commit-config.yaml"
+ config.write_text("")
+
+ assert find_unaudited_priority_hooks(config) == []
+
+
[email protected](
+ ("config", "expected_returncode"),
+ [
+ pytest.param(CONFIG, 1, id="unaudited-priority"),
+ pytest.param(
+ f"repos:\n - repo: local\n hooks:\n - id:
{AUDITED_HOOK}\n priority: 1000\n",
+ 0,
+ id="audited-priority",
+ ),
+ ],
+)
+def test_main(tmp_path, monkeypatch, config, expected_returncode):
+ (tmp_path / ".pre-commit-config.yaml").write_text(config)
+ monkeypatch.chdir(tmp_path)
+
+ assert main() == expected_returncode