This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch v3-2-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-2-test by this push:
     new a985a1e4f51 [v3-2-test] CI: Block new modules under 
`airflow-core/src/airflow/utils/` (#66105) (#66133)
a985a1e4f51 is described below

commit a985a1e4f51ebe8474d88513fdd8a1bea626bd07
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri May 1 17:27:11 2026 +0200

    [v3-2-test] CI: Block new modules under `airflow-core/src/airflow/utils/` 
(#66105) (#66133)
    
    (cherry picked from commit db454a83ce6fc6e1cc5b02c3e6b52073ab2d0605)
    
    Co-authored-by: Jason(Zhe-You) Liu 
<[email protected]>
---
 .pre-commit-config.yaml                            |   6 +
 .../check_no_new_airflow_core_utils_modules.py     | 182 +++++++++++++++++++++
 .../ci/prek/known_airflow_core_utils_modules.txt   |  40 +++++
 3 files changed, 228 insertions(+)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 042d192a222..cec279736d7 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1007,6 +1007,12 @@ repos:
         language: python
         pass_filenames: true
         files: ^(airflow-core|airflow-ctl|task-sdk|providers|shared)/.*\.py$
+      - id: check-no-new-airflow-core-utils-modules
+        name: Check that no new modules are added under 
airflow-core/src/airflow/utils
+        entry: ./scripts/ci/prek/check_no_new_airflow_core_utils_modules.py
+        language: python
+        pass_filenames: false
+        files: 
^airflow-core/src/airflow/utils/[^/]+$|^scripts/ci/prek/known_airflow_core_utils_modules\.txt$|^scripts/ci/prek/check_no_new_airflow_core_utils_modules\.py$
       - id: bandit
         name: bandit
         description: "Bandit is a tool for finding common security issues in 
Python code"
diff --git a/scripts/ci/prek/check_no_new_airflow_core_utils_modules.py 
b/scripts/ci/prek/check_no_new_airflow_core_utils_modules.py
new file mode 100755
index 00000000000..a339679ebed
--- /dev/null
+++ b/scripts/ci/prek/check_no_new_airflow_core_utils_modules.py
@@ -0,0 +1,182 @@
+#!/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.10"
+# dependencies = [
+#   "rich>=13.0.0",
+# ]
+# ///
+"""Check that no new top-level modules are added under 
``airflow-core/src/airflow/utils/``.
+
+The set of allowed top-level modules (files and sub-packages) is frozen in
+``known_airflow_core_utils_modules.txt`` next to this script. New code should
+generally land in one of the following locations instead:
+
+* ``shared/`` - for utilities reused across distributions (core, task-sdk,
+  providers, ...).
+* ``airflow-core/src/airflow/<module-dir>/`` - a dedicated sub-package
+  alongside the feature it belongs to (e.g. ``models/``, ``serialization/``,
+  ``api_fastapi/``), rather than a grab-bag entry in ``utils/``.
+* ``task-sdk/`` - for code used by Dag authoring or the task execution
+  runtime.
+
+If a new module under ``airflow-core/src/airflow/utils/`` is genuinely the
+right home, discuss it with the maintainers first, then regenerate the
+allowlist with ``--generate`` and commit the updated text file together with
+the new module.
+
+Modes
+-----
+Default:
+    Scan ``airflow-core/src/airflow/utils/`` and fail if any top-level module
+    is missing from the allowlist. Stale entries (modules that were removed)
+    are pruned automatically and the hook exits non-zero so prek reports the
+    modified allowlist - just stage the updated file and re-run.
+
+``--generate``:
+    Rewrite the allowlist to match the current state of the directory.
+    Use this only when intentionally accepting a new module after review.
+"""
+
+from __future__ import annotations
+
+import argparse
+import sys
+from pathlib import Path
+
+from rich.console import Console
+from rich.panel import Panel
+
+console = Console(width=400, color_system="standard")
+
+REPO_ROOT = Path(__file__).parents[3]
+UTILS_DIR = REPO_ROOT / "airflow-core" / "src" / "airflow" / "utils"
+ALLOWLIST_FILE = Path(__file__).parent / "known_airflow_core_utils_modules.txt"
+
+
+def _current_modules() -> set[str]:
+    """Return top-level module names directly under 
``airflow-core/src/airflow/utils/``.
+
+    A module is either a ``.py`` file (other than ``__init__.py``) or a
+    sub-package directory containing an ``__init__.py``.
+    """
+    modules: set[str] = set()
+    for entry in UTILS_DIR.iterdir():
+        if entry.name.startswith((".", "__")):
+            continue
+        if entry.is_file() and entry.suffix == ".py":
+            modules.add(entry.stem)
+        elif entry.is_dir() and (entry / "__init__.py").exists():
+            modules.add(entry.name)
+    return modules
+
+
+def _load_allowlist() -> set[str]:
+    if not ALLOWLIST_FILE.exists():
+        return set()
+    return {line.strip() for line in ALLOWLIST_FILE.read_text().splitlines() 
if line.strip()}
+
+
+def _save_allowlist(modules: set[str]) -> None:
+    ALLOWLIST_FILE.write_text("\n".join(sorted(modules)) + "\n")
+
+
+def _generate() -> int:
+    modules = _current_modules()
+    _save_allowlist(modules)
+    rel = ALLOWLIST_FILE.relative_to(REPO_ROOT)
+    console.print(f"[green]Wrote {len(modules)} entries to[/green] 
[cyan]{rel}[/cyan]")
+    return 0
+
+
+def _check() -> int:
+    current = _current_modules()
+    allowed = _load_allowlist()
+
+    new_modules = sorted(current - allowed)
+    removed_modules = sorted(allowed - current)
+
+    exit_code = 0
+
+    if removed_modules:
+        _save_allowlist(current & allowed)
+        rel = ALLOWLIST_FILE.relative_to(REPO_ROOT)
+        console.print(
+            f"[yellow]Pruned {len(removed_modules)} stale entr"
+            f"{'y' if len(removed_modules) == 1 else 'ies'} from[/yellow] "
+            f"[cyan]{rel}[/cyan] (stage the updated file):"
+        )
+        for name in removed_modules:
+            console.print(f"  [dim]-[/dim] {name}")
+        exit_code = 1
+
+    if new_modules:
+        rel = ALLOWLIST_FILE.relative_to(REPO_ROOT)
+        console.print(
+            Panel.fit(
+                "New top-level module(s) detected under 
[cyan]airflow-core/src/airflow/utils/[/cyan]:\n\n"
+                + "\n".join(f"  + {name}" for name in new_modules)
+                + "\n\nAdding new modules under this directory is discouraged. 
"
+                "Prefer placing the code in one of:\n"
+                "  - [cyan]shared/[/cyan] (when shared across distributions)\n"
+                "  - [cyan]airflow-core/src/airflow/<module-dir>/[/cyan] "
+                "(a dedicated sub-package alongside the feature it belongs 
to)\n"
+                "  - [cyan]task-sdk/[/cyan] (when used by Dag authoring or 
task execution)\n\n"
+                "If the addition is intentional and has been agreed with the 
maintainers, "
+                "regenerate the allowlist with:\n\n"
+                "  [cyan]uv run 
./scripts/ci/prek/check_no_new_airflow_core_utils_modules.py 
--generate[/cyan]\n\n"
+                f"and commit the updated [cyan]{rel}[/cyan].",
+                title="[red]New airflow.utils module rejected[/red]",
+                border_style="red",
+            )
+        )
+        exit_code = 1
+
+    if exit_code == 0:
+        console.print("[green]airflow.utils module set matches the 
allowlist.[/green]")
+    return exit_code
+
+
+def main(argv: list[str] | None = None) -> int:
+    parser = argparse.ArgumentParser(
+        description=__doc__,
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+    )
+    parser.add_argument(
+        "files",
+        nargs="*",
+        help="Ignored - the hook always scans the full utils/ directory.",
+    )
+    parser.add_argument(
+        "--generate",
+        action="store_true",
+        help="Rewrite the allowlist from the current directory state.",
+    )
+    args = parser.parse_args(argv)
+
+    if not UTILS_DIR.is_dir():
+        console.print(f"[red]Expected directory does not exist: 
{UTILS_DIR}[/red]")
+        return 1
+
+    if args.generate:
+        return _generate()
+    return _check()
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/scripts/ci/prek/known_airflow_core_utils_modules.txt 
b/scripts/ci/prek/known_airflow_core_utils_modules.txt
new file mode 100644
index 00000000000..bdd3a2aa51f
--- /dev/null
+++ b/scripts/ci/prek/known_airflow_core_utils_modules.txt
@@ -0,0 +1,40 @@
+cli
+cli_action_loggers
+code_utils
+context
+dag_cycle_tester
+dag_edges
+dag_version_inflation_checker
+dates
+db
+db_cleanup
+db_manager
+deprecation_tools
+docs
+dot_renderer
+edgemodifier
+email
+event_scheduler
+file
+hashlib_wrapper
+helpers
+json
+log
+memray_utils
+net
+operator_helpers
+orm_event_handlers
+platform
+process_utils
+providers_configuration_loader
+retries
+scheduler_health
+serve_logs
+session
+span_status
+sqlalchemy
+state
+strings
+thread_safe_dict
+types
+yaml

Reply via email to