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

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 4395fe94773 Add tests for scripts and remove redundant sys.path.insert 
calls (#63598)
4395fe94773 is described below

commit 4395fe947730feaca9135c2071ab3bca7ad431cf
Author: Jarek Potiuk <[email protected]>
AuthorDate: Sat Mar 14 17:22:46 2026 +0100

    Add tests for scripts and remove redundant sys.path.insert calls (#63598)
    
    * Add tests for scripts and remove redundant sys.path.insert calls
    
    - Remove 85 redundant `sys.path.insert(0, 
str(Path(__file__).parent.resolve()))`
      calls from scripts in ci/prek/, cov/, and in_container/. Python already
      adds the script's directory to sys.path when running a file directly,
      making these calls unnecessary.
    - Keep 6 cross-directory sys.path.insert calls that are genuinely needed
      (AIRFLOW_CORE_SOURCES_PATH, AIRFLOW_ROOT, etc.).
    - Add __init__.py files to scripts/ci/ and scripts/ci/prek/ to make them
      proper Python packages.
    - Add scripts/pyproject.toml with package discovery and pytest config.
    - Add 176 tests covering: common_prek_utils (insert_documentation,
      check_list_sorted, get_provider_id_from_path, ConsoleDiff, etc.),
      new_session_in_provide_session, check_deprecations, unittest_testcase,
      changelog_duplicates, newsfragments, checkout_no_credentials, and
      check_order_dockerfile_extras.
    - Add scripts tests to CI: new SCRIPTS_FILES file group in selective
      checks, run-scripts-tests output, and tests-scripts job in
      basic-tests.yml.
    - Document scripts as a workspace distribution in CLAUDE.md.
    
    * Add pytest as dev dependency for scripts distribution
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    
    * Use devel-common instead of pytest for scripts dev dependencies
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    
    * Fix xdist test collection order for newsfragment tests
    
    Sort the VALID_CHANGE_TYPES set when passing to parametrize to ensure
    deterministic test ordering across xdist workers.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    
    * Update scripts/ci/prek/changelog_duplicates.py
    
    Co-authored-by: Dev-iL <[email protected]>
    
    * Refactor scripts tests: convert setup methods to fixtures and extract 
constants
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
    Co-authored-by: Dev-iL <[email protected]>
---
 .github/workflows/basic-tests.yml                  |  23 ++
 .github/workflows/ci-amd-arm.yml                   |   2 +
 .pre-commit-config.yaml                            |  10 +-
 AGENTS.md                                          |   8 +-
 .../src/airflow_breeze/utils/selective_checks.py   |  11 +
 pyproject.toml                                     |   4 +
 .../{cov/restapi_coverage.py => ci/__init__.py}    |  16 -
 .../restapi_coverage.py => ci/prek/__init__.py}    |  16 -
 scripts/ci/prek/breeze_cmd_line.py                 |   2 -
 scripts/ci/prek/capture_airflowctl_help.py         |   4 -
 scripts/ci/prek/changelog_duplicates.py            |  49 +--
 .../ci/prek/check_airflow_bug_report_template.py   |   3 -
 scripts/ci/prek/check_airflow_imports.py           |   1 -
 scripts/ci/prek/check_airflow_imports_in_shared.py |   1 -
 .../ci/prek/check_airflow_v_imports_in_tests.py    |   5 +-
 .../ci/prek/check_airflowctl_command_coverage.py   |   2 -
 .../prek/check_base_operator_partial_arguments.py  |   2 -
 scripts/ci/prek/check_cli_definition_imports.py    |   1 -
 scripts/ci/prek/check_common_sql_dependency.py     |   4 +-
 scripts/ci/prek/check_core_imports_in_sdk.py       |   1 -
 scripts/ci/prek/check_core_imports_in_shared.py    |   1 -
 scripts/ci/prek/check_default_configuration.py     |   4 -
 scripts/ci/prek/check_execution_api_versions.py    |   2 -
 scripts/ci/prek/check_extra_packages_ref.py        |  10 +-
 scripts/ci/prek/check_i18n_json.py                 |   3 -
 scripts/ci/prek/check_imports_in_providers.py      |   4 -
 scripts/ci/prek/check_init_decorator_arguments.py  |   2 -
 scripts/ci/prek/check_integrations_list.py         |   1 -
 scripts/ci/prek/check_k8s_schemas_published.py     |   2 -
 scripts/ci/prek/check_kubeconform.py               |   2 -
 scripts/ci/prek/check_min_python_version.py        |   3 -
 scripts/ci/prek/check_order_dockerfile_extras.py   |   4 +-
 scripts/ci/prek/check_provider_docs.py             |   5 +-
 scripts/ci/prek/check_provider_yaml_files.py       |   2 -
 .../check_providers_subpackages_all_have_init.py   |   1 -
 scripts/ci/prek/check_revision_heads_map.py        |   2 -
 scripts/ci/prek/check_schema_defaults.py           |   4 -
 scripts/ci/prek/check_sdk_imports.py               |   1 -
 .../prek/check_shared_distributions_structure.py   |   1 -
 .../ci/prek/check_shared_distributions_usage.py    |   1 -
 .../ci/prek/check_system_tests_hidden_in_index.py  |   1 -
 .../check_template_context_variable_in_sync.py     |   3 -
 scripts/ci/prek/check_template_fields.py           |   2 -
 scripts/ci/prek/check_test_only_imports_in_src.py  |   1 -
 scripts/ci/prek/check_tests_in_right_folders.py    |   2 -
 scripts/ci/prek/check_ti_vs_tis_attributes.py      |   2 -
 scripts/ci/prek/check_version_consistency.py       |   8 +-
 scripts/ci/prek/common_prek_utils.py               |   2 +-
 scripts/ci/prek/compile_ui_assets.py               |   2 -
 scripts/ci/prek/compile_ui_assets_dev.py           |   3 -
 scripts/ci/prek/download_k8s_schemas.py            |   3 -
 scripts/ci/prek/generate_airflow_diagrams.py       |   1 -
 scripts/ci/prek/generate_openapi_spec.py           |   4 -
 scripts/ci/prek/generate_openapi_spec_providers.py |   2 -
 scripts/ci/prek/generate_volumes_for_sources.py    |   4 -
 scripts/ci/prek/lint_helm.py                       |   2 -
 scripts/ci/prek/lint_json_schema.py                |   2 -
 scripts/ci/prek/local_yml_mounts.py                |   3 -
 scripts/ci/prek/migration_reference.py             |   4 -
 scripts/ci/prek/mypy.py                            |   6 +-
 scripts/ci/prek/mypy_folder.py                     |   3 -
 scripts/ci/prek/newsfragments.py                   |  57 +--
 scripts/ci/prek/sync_translation_namespaces.py     |   2 -
 scripts/ci/prek/ts_compile_lint_common_ai.py       |   1 -
 scripts/ci/prek/ts_compile_lint_edge.py            |   1 -
 .../prek/ts_compile_lint_simple_auth_manager_ui.py |   1 -
 scripts/ci/prek/ts_compile_lint_ui.py              |   1 -
 scripts/ci/prek/update_airflow_pyproject_toml.py   |   4 +-
 scripts/ci/prek/update_chart_dependencies.py       |   3 -
 scripts/ci/prek/update_example_dags_paths.py       |   1 -
 scripts/ci/prek/update_providers_build_files.py    |   1 -
 scripts/ci/prek/update_providers_dependencies.py   |   2 -
 scripts/ci/prek/update_source_date_epoch.py        |   4 -
 scripts/ci/prek/update_versions.py                 |   3 -
 scripts/ci/prek/upgrade_important_versions.py      |   4 +-
 scripts/ci/prek/validate_chart_annotations.py      |   3 -
 scripts/cov/cli_coverage.py                        |   5 -
 scripts/cov/core_coverage.py                       |   5 -
 scripts/cov/other_coverage.py                      |   5 -
 scripts/cov/restapi_coverage.py                    |   5 -
 .../in_container/install_airflow_and_providers.py  |   1 -
 .../in_container/install_airflow_python_client.py  |   2 -
 .../install_development_dependencies.py            |   2 -
 .../in_container/run_capture_airflowctl_help.py    |   2 -
 .../in_container/run_check_imports_in_providers.py |   1 -
 scripts/in_container/run_generate_constraints.py   |   2 -
 scripts/in_container/run_generate_openapi_spec.py  |   5 +-
 .../run_generate_openapi_spec_providers.py         |   1 -
 .../in_container/run_provider_yaml_files_check.py  |  12 +-
 scripts/pyproject.toml                             |  76 ++++
 .../{cov/restapi_coverage.py => tests/__init__.py} |  16 -
 .../restapi_coverage.py => tests/ci/__init__.py}   |  16 -
 .../ci/prek/__init__.py}                           |  16 -
 scripts/tests/ci/prek/conftest.py                  |  76 ++++
 scripts/tests/ci/prek/test_changelog_duplicates.py | 101 +++++
 scripts/tests/ci/prek/test_check_deprecations.py   | 206 ++++++++++
 .../ci/prek/test_check_order_dockerfile_extras.py  | 118 ++++++
 .../tests/ci/prek/test_checkout_no_credentials.py  | 251 ++++++++++++
 scripts/tests/ci/prek/test_common_prek_utils.py    | 425 +++++++++++++++++++++
 .../ci/prek/test_new_session_in_provide_session.py | 243 ++++++++++++
 scripts/tests/ci/prek/test_newsfragments.py        |  81 ++++
 scripts/tests/ci/prek/test_unittest_testcase.py    |  93 +++++
 102 files changed, 1806 insertions(+), 328 deletions(-)

diff --git a/.github/workflows/basic-tests.yml 
b/.github/workflows/basic-tests.yml
index b026316889d..c3be7603724 100644
--- a/.github/workflows/basic-tests.yml
+++ b/.github/workflows/basic-tests.yml
@@ -40,6 +40,10 @@ on:  # yamllint disable-line rule:truthy
         description: "Whether to run breeze integration tests (true/false)"
         required: true
         type: string
+      run-scripts-tests:
+        description: "Whether to run scripts tests (true/false)"
+        required: true
+        type: string
       basic-checks-only:
         description: "Whether to run only basic checks (true/false)"
         required: true
@@ -145,6 +149,25 @@ jobs:
       - name: "Run shared ${{ matrix.shared-distribution }} tests"
         run: uv run --group dev pytest --color=yes -n auto
         working-directory: shared/${{ matrix.shared-distribution }}
+  tests-scripts:
+    timeout-minutes: 10
+    name: Scripts tests
+    runs-on: ${{ fromJSON(inputs.runners) }}
+    if: inputs.run-scripts-tests == 'true'
+    steps:
+      - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
+        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # 
v6.0.2
+        with:
+          fetch-depth: 1
+          persist-credentials: false
+      - name: "Install uv"
+        run: pip install "uv==${UV_VERSION}"
+        env:
+          UV_VERSION: ${{ inputs.uv-version }}
+      - name: "Run scripts tests"
+        run: uv run --project . pytest --color=yes -n auto
+        working-directory: ./scripts/
+
   tests-ui:
     timeout-minutes: 15
     name: React UI tests
diff --git a/.github/workflows/ci-amd-arm.yml b/.github/workflows/ci-amd-arm.yml
index 50f5ba8c95e..9e509925317 100644
--- a/.github/workflows/ci-amd-arm.yml
+++ b/.github/workflows/ci-amd-arm.yml
@@ -120,6 +120,7 @@ jobs:
       run-task-sdk-tests: ${{ 
steps.selective-checks.outputs.run-task-sdk-tests }}
       run-task-sdk-integration-tests: ${{ 
steps.selective-checks.outputs.run-task-sdk-integration-tests }}
       run-breeze-integration-tests: ${{ 
steps.selective-checks.outputs.run-breeze-integration-tests }}
+      run-scripts-tests: ${{ steps.selective-checks.outputs.run-scripts-tests 
}}
       runner-type: ${{ steps.selective-checks.outputs.runner-type }}
       run-ui-tests: ${{ steps.selective-checks.outputs.run-ui-tests }}
       run-ui-e2e-tests: ${{ steps.selective-checks.outputs.run-ui-e2e-tests }}
@@ -204,6 +205,7 @@ jobs:
       skip-prek-hooks: ${{ needs.build-info.outputs.skip-prek-hooks }}
       canary-run: ${{needs.build-info.outputs.canary-run}}
       run-breeze-integration-tests: 
${{needs.build-info.outputs.run-breeze-integration-tests}}
+      run-scripts-tests: ${{needs.build-info.outputs.run-scripts-tests}}
       latest-versions-only: ${{needs.build-info.outputs.latest-versions-only}}
       use-uv: ${{needs.build-info.outputs.use-uv}}
       platform: ${{ needs.build-info.outputs.platform }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 702496555b3..84bcfbe669c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -700,7 +700,15 @@ repos:
         name: Verify usage of Airflow deprecation classes in core
         entry: category=DeprecationWarning|category=PendingDeprecationWarning
         files: \.py$
-        exclude: 
^airflow-core/src/airflow/configuration\.py$|^airflow-core/tests/.*$|^providers/.*/src/airflow/providers/|^scripts/in_container/verify_providers\.py$|^providers/.*/tests/.*$|^devel-common/
+        exclude: >
+          (?x)
+          ^airflow-core/src/airflow/configuration\.py$|
+          ^airflow-core/tests/.*$|
+          ^providers/.*/src/airflow/providers/|
+          ^scripts/in_container/verify_providers\.py$|
+          ^providers/.*/tests/.*$|
+          ^scripts/tests/.*$|
+          ^devel-common/
         pass_filenames: true
       - id: check-provide-create-sessions-imports
         language: pygrep
diff --git a/AGENTS.md b/AGENTS.md
index 236d953c115..32cedcee6be 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -27,7 +27,8 @@
 - **Run Helm tests in parallel with xdist** `breeze testing helm-tests 
--use-xdist`
 - **Run Helm tests with specific K8s version:** `breeze testing helm-tests 
--use-xdist --kubernetes-version 1.35.0`
 - **Run specific Helm test type:** `breeze testing helm-tests --use-xdist 
--test-type <type>` (types: `airflow_aux`, `airflow_core`, `apiserver`, 
`dagprocessor`, `other`, `redis`, `security`, `statsd`, `webserver`)
-- **Run other suites of tests** `breeze testing <test_group>` (test groups: 
`airflow-ctl-tests`, `docker-compose-tests`, `task-sdk-tests`
+- **Run other suites of tests** `breeze testing <test_group>` (test groups: 
`airflow-ctl-tests`, `docker-compose-tests`, `task-sdk-tests`)
+- **Run scripts tests:** `uv run --project scripts pytest scripts/tests/ -xvs`
 - **Run Airflow CLI:** `breeze run airflow dags list`
 - **Type-check:** `breeze run mypy path/to/code`
 - **Lint with ruff only:** `prek run ruff --from-ref <target_branch>`
@@ -56,6 +57,11 @@ UV workspace monorepo. Key paths:
 - `providers/` — 100+ provider packages, each with its own `pyproject.toml`
 - `airflow-ctl/` — management CLI tool
 - `chart/` — Helm chart for Kubernetes deployment
+- `dev/` — development utilities and scripts used to bootstrap the 
environment, releases, breeze dev env
+- `scripts/` — utility scripts for CI, Docker, and prek hooks (workspace 
distribution `apache-airflow-scripts`)
+  - `ci/prek/` — prek (pre-commit) hook scripts; shared utilities in 
`common_prek_utils.py`
+  - `tests/` — pytest tests for the scripts; run with `uv run --project 
scripts pytest scripts/tests/`
+
 
 ## Architecture Boundaries
 
diff --git a/dev/breeze/src/airflow_breeze/utils/selective_checks.py 
b/dev/breeze/src/airflow_breeze/utils/selective_checks.py
index 4a349c459ac..f39f4462a4e 100644
--- a/dev/breeze/src/airflow_breeze/utils/selective_checks.py
+++ b/dev/breeze/src/airflow_breeze/utils/selective_checks.py
@@ -135,6 +135,7 @@ class FileGroupForCi(Enum):
     UNIT_TEST_FILES = auto()
     DEVEL_TOML_FILES = auto()
     UI_ENGLISH_TRANSLATION_FILES = auto()
+    SCRIPTS_FILES = auto()
 
 
 class AllProvidersSentinel:
@@ -333,6 +334,12 @@ CI_FILE_GROUP_MATCHES: HashableDict[FileGroupForCi] = 
HashableDict(
         FileGroupForCi.UI_ENGLISH_TRANSLATION_FILES: [
             r"^airflow-core/src/airflow/ui/public/i18n/locales/en/.*\.json$",
         ],
+        FileGroupForCi.SCRIPTS_FILES: [
+            r"^scripts/ci/.*\.py$",
+            r"^scripts/cov/.*\.py$",
+            r"^scripts/tools/.*\.py$",
+            r"^scripts/tests/.*\.py$",
+        ],
     }
 )
 
@@ -945,6 +952,10 @@ class SelectiveChecks:
             FileGroupForCi.AIRFLOW_CTL_INTEGRATION_TEST_FILES
         )
 
+    @cached_property
+    def run_scripts_tests(self) -> bool:
+        return self._should_be_run(FileGroupForCi.SCRIPTS_FILES)
+
     @cached_property
     def run_kubernetes_tests(self) -> bool:
         return self._should_be_run(FileGroupForCi.KUBERNETES_FILES)
diff --git a/pyproject.toml b/pyproject.toml
index 878533fff2f..b4a5b024cf6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -785,6 +785,7 @@ testing = ["dev", "providers.tests", "tests_common", 
"tests", "system", "unit",
 "providers/**/tests/*" = ["D", "TID253", "S101", "TRY002"]
 "performance/tests/*" = ["S101"]
 "dev/registry/tests/*" = ["S101"]
+"scripts/tests/*" = ["S101"]
 
 # Shared distributions SHOULD use relative imports when referencing each other
 # This one disables 'ban-relative-imports'.
@@ -1288,6 +1289,7 @@ dev = [
     "apache-airflow[all]",
     "apache-airflow-breeze",
     "apache-airflow-dev",
+    "apache-airflow-scripts",
     "apache-airflow-devel-common[no-doc]",
     "apache-airflow-docker-tests",
     "apache-airflow-task-sdk-integration-tests",
@@ -1344,6 +1346,7 @@ no-build-isolation-package = ["sphinx-redoc"]
 apache-airflow = {workspace = true}
 apache-airflow-breeze = {workspace = true}
 apache-airflow-dev = {workspace = true}
+apache-airflow-scripts = {workspace = true}
 apache-airflow-core = {workspace = true}
 apache-airflow-ctl = {workspace = true}
 apache-airflow-ctl-tests = {workspace = true}
@@ -1480,6 +1483,7 @@ members = [
     "airflow-ctl-tests",
     "dev",
     "devel-common",
+    "scripts",
     "docker-tests",
     "task-sdk-integration-tests",
     "helm-tests",
diff --git a/scripts/cov/restapi_coverage.py b/scripts/ci/__init__.py
similarity index 67%
copy from scripts/cov/restapi_coverage.py
copy to scripts/ci/__init__.py
index cc80db9241d..13a83393a91 100644
--- a/scripts/cov/restapi_coverage.py
+++ b/scripts/ci/__init__.py
@@ -14,19 +14,3 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-from __future__ import annotations
-
-import sys
-from pathlib import Path
-
-from cov_runner import run_tests
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
-source_files = ["airflow-core/tests/unit/api_fastapi"]
-
-files_not_fully_covered: list[str] = []
-
-if __name__ == "__main__":
-    args = ["-qq"] + source_files
-    run_tests(args, source_files, files_not_fully_covered)
diff --git a/scripts/cov/restapi_coverage.py b/scripts/ci/prek/__init__.py
similarity index 67%
copy from scripts/cov/restapi_coverage.py
copy to scripts/ci/prek/__init__.py
index cc80db9241d..13a83393a91 100644
--- a/scripts/cov/restapi_coverage.py
+++ b/scripts/ci/prek/__init__.py
@@ -14,19 +14,3 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-from __future__ import annotations
-
-import sys
-from pathlib import Path
-
-from cov_runner import run_tests
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
-source_files = ["airflow-core/tests/unit/api_fastapi"]
-
-files_not_fully_covered: list[str] = []
-
-if __name__ == "__main__":
-    args = ["-qq"] + source_files
-    run_tests(args, source_files, files_not_fully_covered)
diff --git a/scripts/ci/prek/breeze_cmd_line.py 
b/scripts/ci/prek/breeze_cmd_line.py
index c040bcca657..d95cc4e8552 100755
--- a/scripts/ci/prek/breeze_cmd_line.py
+++ b/scripts/ci/prek/breeze_cmd_line.py
@@ -27,9 +27,7 @@ from __future__ import annotations
 import os
 import subprocess
 import sys
-from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import AIRFLOW_ROOT_PATH, console, 
initialize_breeze_prek
 
 BREEZE_INSTALL_DIR = AIRFLOW_ROOT_PATH / "dev" / "breeze"
diff --git a/scripts/ci/prek/capture_airflowctl_help.py 
b/scripts/ci/prek/capture_airflowctl_help.py
index b988fbf9c8f..d9618c58ad0 100755
--- a/scripts/ci/prek/capture_airflowctl_help.py
+++ b/scripts/ci/prek/capture_airflowctl_help.py
@@ -24,10 +24,6 @@
 # ///
 from __future__ import annotations
 
-import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import (
     initialize_breeze_prek,
     run_command_via_breeze_shell,
diff --git a/scripts/ci/prek/changelog_duplicates.py 
b/scripts/ci/prek/changelog_duplicates.py
index b96526f2e85..e3b7f636911 100755
--- a/scripts/ci/prek/changelog_duplicates.py
+++ b/scripts/ci/prek/changelog_duplicates.py
@@ -38,25 +38,30 @@ known_exceptions = [
 
 pr_number_re = re.compile(r".*\(#([0-9]{1,6})\)`?`?$")
 
-files = sys.argv[1:]
-
-failed = False
-for filename in files:
-    seen = []
-    dups = []
-    with open(filename) as f:
-        for line in f:
-            match = pr_number_re.search(line)
-            if match:
-                pr_number = match.group(1)
-                if pr_number not in seen:
-                    seen.append(pr_number)
-                elif pr_number not in known_exceptions:
-                    dups.append(pr_number)
-
-    if dups:
-        print(f"Duplicate changelog entries found for {filename}: {dups}")
-        failed = True
-
-if failed:
-    sys.exit(1)
+
+def find_duplicates(lines: list[str]) -> list[str]:
+    """Find duplicate PR numbers in changelog lines, excluding known 
exceptions."""
+    seen: list[str] = []
+    dups: list[str] = []
+    for line in lines:
+        if (match := pr_number_re.search(line)) and (pr := match.group(1)):
+            if pr not in seen:
+                seen.append(pr)
+            elif pr not in known_exceptions:
+                dups.append(pr)
+    return dups
+
+
+def main(filenames: list[str]) -> int:
+    failed = False
+    for filename in filenames:
+        with open(filename) as f:
+            dups = find_duplicates(f.readlines())
+        if dups:
+            print(f"Duplicate changelog entries found for {filename}: {dups}")
+            failed = True
+    return 1 if failed else 0
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv[1:]))
diff --git a/scripts/ci/prek/check_airflow_bug_report_template.py 
b/scripts/ci/prek/check_airflow_bug_report_template.py
index dd36a06b3af..88358654424 100755
--- a/scripts/ci/prek/check_airflow_bug_report_template.py
+++ b/scripts/ci/prek/check_airflow_bug_report_template.py
@@ -26,11 +26,8 @@
 from __future__ import annotations
 
 import sys
-from pathlib import Path
 
 import yaml
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import AIRFLOW_ROOT_PATH, check_list_sorted, console
 
 BUG_REPORT_TEMPLATE = AIRFLOW_ROOT_PATH / ".github" / "ISSUE_TEMPLATE" / 
"3-airflow_providers_bug_report.yml"
diff --git a/scripts/ci/prek/check_airflow_imports.py 
b/scripts/ci/prek/check_airflow_imports.py
index 9513cb06976..74d4097366d 100755
--- a/scripts/ci/prek/check_airflow_imports.py
+++ b/scripts/ci/prek/check_airflow_imports.py
@@ -29,7 +29,6 @@ import re
 import sys
 from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import console, get_imports_from_file
 
 
diff --git a/scripts/ci/prek/check_airflow_imports_in_shared.py 
b/scripts/ci/prek/check_airflow_imports_in_shared.py
index 19b974cd1fc..63dd94e090f 100755
--- a/scripts/ci/prek/check_airflow_imports_in_shared.py
+++ b/scripts/ci/prek/check_airflow_imports_in_shared.py
@@ -29,7 +29,6 @@ import ast
 import sys
 from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import console
 
 
diff --git a/scripts/ci/prek/check_airflow_v_imports_in_tests.py 
b/scripts/ci/prek/check_airflow_v_imports_in_tests.py
index 43c6f1f7936..866da90e866 100755
--- a/scripts/ci/prek/check_airflow_v_imports_in_tests.py
+++ b/scripts/ci/prek/check_airflow_v_imports_in_tests.py
@@ -31,10 +31,7 @@ import ast
 import sys
 from pathlib import Path
 
-from common_prek_utils import AIRFLOW_ROOT_PATH
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
-from common_prek_utils import console
+from common_prek_utils import AIRFLOW_ROOT_PATH, console
 
 
 def check_airflow_v_imports_and_fix(test_file: Path) -> list[str]:
diff --git a/scripts/ci/prek/check_airflowctl_command_coverage.py 
b/scripts/ci/prek/check_airflowctl_command_coverage.py
index 8a81dd098c6..2b808db9c00 100755
--- a/scripts/ci/prek/check_airflowctl_command_coverage.py
+++ b/scripts/ci/prek/check_airflowctl_command_coverage.py
@@ -31,9 +31,7 @@ from __future__ import annotations
 import ast
 import re
 import sys
-from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import AIRFLOW_ROOT_PATH, console
 
 OPERATIONS_FILE = AIRFLOW_ROOT_PATH / "airflow-ctl" / "src" / "airflowctl" / 
"api" / "operations.py"
diff --git a/scripts/ci/prek/check_base_operator_partial_arguments.py 
b/scripts/ci/prek/check_base_operator_partial_arguments.py
index 6c6201e0134..ee2c7639f08 100755
--- a/scripts/ci/prek/check_base_operator_partial_arguments.py
+++ b/scripts/ci/prek/check_base_operator_partial_arguments.py
@@ -26,11 +26,9 @@ from __future__ import annotations
 
 import ast
 import itertools
-import pathlib
 import sys
 import typing
 
-sys.path.insert(0, str(pathlib.Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import AIRFLOW_TASK_SDK_SOURCES_PATH, console
 
 SDK_BASEOPERATOR_PY = AIRFLOW_TASK_SDK_SOURCES_PATH / "airflow" / "sdk" / 
"bases" / "operator.py"
diff --git a/scripts/ci/prek/check_cli_definition_imports.py 
b/scripts/ci/prek/check_cli_definition_imports.py
index ba14b8e2f71..c8a9d1730af 100755
--- a/scripts/ci/prek/check_cli_definition_imports.py
+++ b/scripts/ci/prek/check_cli_definition_imports.py
@@ -36,7 +36,6 @@ import argparse
 import sys
 from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import console, get_imports_from_file
 
 # Allowed modules that can be imported in CLI definition files
diff --git a/scripts/ci/prek/check_common_sql_dependency.py 
b/scripts/ci/prek/check_common_sql_dependency.py
index a59445a0a15..4677464da10 100755
--- a/scripts/ci/prek/check_common_sql_dependency.py
+++ b/scripts/ci/prek/check_common_sql_dependency.py
@@ -32,12 +32,10 @@ import sys
 from collections.abc import Iterable
 
 import yaml
+from common_prek_utils import get_provider_base_dir_from_path
 from packaging.specifiers import SpecifierSet
 from rich.console import Console
 
-sys.path.insert(0, str(pathlib.Path(__file__).parent.resolve()))
-from common_prek_utils import get_provider_base_dir_from_path
-
 console = Console(color_system="standard", width=200)
 
 
diff --git a/scripts/ci/prek/check_core_imports_in_sdk.py 
b/scripts/ci/prek/check_core_imports_in_sdk.py
index 393ae5a4c7b..700d81f5ea1 100755
--- a/scripts/ci/prek/check_core_imports_in_sdk.py
+++ b/scripts/ci/prek/check_core_imports_in_sdk.py
@@ -29,7 +29,6 @@ import ast
 import sys
 from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import console
 
 
diff --git a/scripts/ci/prek/check_core_imports_in_shared.py 
b/scripts/ci/prek/check_core_imports_in_shared.py
index f7b16153410..738d2a8ba5b 100644
--- a/scripts/ci/prek/check_core_imports_in_shared.py
+++ b/scripts/ci/prek/check_core_imports_in_shared.py
@@ -29,7 +29,6 @@ import ast
 import sys
 from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import console
 
 
diff --git a/scripts/ci/prek/check_default_configuration.py 
b/scripts/ci/prek/check_default_configuration.py
index 7288a7a0644..17e7731201f 100755
--- a/scripts/ci/prek/check_default_configuration.py
+++ b/scripts/ci/prek/check_default_configuration.py
@@ -23,10 +23,6 @@
 # ///
 from __future__ import annotations
 
-import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import (
     initialize_breeze_prek,
     run_command_via_breeze_shell,
diff --git a/scripts/ci/prek/check_execution_api_versions.py 
b/scripts/ci/prek/check_execution_api_versions.py
index 6ce1f5b7644..749f4020e02 100755
--- a/scripts/ci/prek/check_execution_api_versions.py
+++ b/scripts/ci/prek/check_execution_api_versions.py
@@ -26,9 +26,7 @@ from __future__ import annotations
 import os
 import subprocess
 import sys
-from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import console
 
 DATAMODELS_PREFIX = 
"airflow-core/src/airflow/api_fastapi/execution_api/datamodels/"
diff --git a/scripts/ci/prek/check_extra_packages_ref.py 
b/scripts/ci/prek/check_extra_packages_ref.py
index 004e3b4699a..6e3bd8bcd6b 100755
--- a/scripts/ci/prek/check_extra_packages_ref.py
+++ b/scripts/ci/prek/check_extra_packages_ref.py
@@ -32,9 +32,8 @@ from __future__ import annotations
 
 import re
 import sys
-from pathlib import Path
 
-from common_prek_utils import AIRFLOW_ROOT_PATH
+from common_prek_utils import AIRFLOW_ROOT_PATH, console
 from tabulate import tabulate
 
 try:
@@ -42,16 +41,9 @@ try:
 except ImportError:
     import tomli as tomllib
 
-
-COMMON_PREK_PATH = Path(__file__).parent.resolve()
 EXTRA_PACKAGES_REF_FILE = AIRFLOW_ROOT_PATH / "airflow-core" / "docs" / 
"extra-packages-ref.rst"
 PYPROJECT_TOML_FILE_PATH = AIRFLOW_ROOT_PATH / "pyproject.toml"
 
-sys.path.insert(0, COMMON_PREK_PATH.as_posix())  # make sure common_prek_utils 
is imported
-from common_prek_utils import console
-
-sys.path.insert(0, AIRFLOW_ROOT_PATH.as_posix())  # make sure airflow root is 
imported
-
 doc_ref_content = EXTRA_PACKAGES_REF_FILE.read_text()
 
 errors: list[str] = []
diff --git a/scripts/ci/prek/check_i18n_json.py 
b/scripts/ci/prek/check_i18n_json.py
index e093632c2e1..79aa43a8fc7 100755
--- a/scripts/ci/prek/check_i18n_json.py
+++ b/scripts/ci/prek/check_i18n_json.py
@@ -33,9 +33,6 @@ import json
 import sys
 from pathlib import Path
 
-COMMON_PREK_PATH = Path(__file__).parent.resolve()
-
-sys.path.insert(0, COMMON_PREK_PATH.as_posix())  # make sure common_prek_utils 
is imported
 from common_prek_utils import AIRFLOW_ROOT_PATH, console
 
 LOCALES_DIR = AIRFLOW_ROOT_PATH / "airflow-core" / "src" / "airflow" / "ui" / 
"public" / "i18n" / "locales"
diff --git a/scripts/ci/prek/check_imports_in_providers.py 
b/scripts/ci/prek/check_imports_in_providers.py
index 113e4479321..dbd533e2db2 100755
--- a/scripts/ci/prek/check_imports_in_providers.py
+++ b/scripts/ci/prek/check_imports_in_providers.py
@@ -24,10 +24,6 @@
 # ///
 from __future__ import annotations
 
-import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import (
     initialize_breeze_prek,
     run_command_via_breeze_shell,
diff --git a/scripts/ci/prek/check_init_decorator_arguments.py 
b/scripts/ci/prek/check_init_decorator_arguments.py
index 3032a06a21f..683fca120a7 100755
--- a/scripts/ci/prek/check_init_decorator_arguments.py
+++ b/scripts/ci/prek/check_init_decorator_arguments.py
@@ -28,11 +28,9 @@ from __future__ import annotations
 import ast
 import collections.abc
 import itertools
-import pathlib
 import sys
 from typing import TYPE_CHECKING
 
-sys.path.insert(0, str(pathlib.Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import AIRFLOW_CORE_SOURCES_PATH, 
AIRFLOW_TASK_SDK_SOURCES_PATH, console
 
 SDK_DEFINITIONS_PKG = AIRFLOW_TASK_SDK_SOURCES_PATH / "airflow" / "sdk" / 
"definitions"
diff --git a/scripts/ci/prek/check_integrations_list.py 
b/scripts/ci/prek/check_integrations_list.py
index bf42d3f76b2..161049175ca 100755
--- a/scripts/ci/prek/check_integrations_list.py
+++ b/scripts/ci/prek/check_integrations_list.py
@@ -41,7 +41,6 @@ from typing import Any
 import yaml
 
 # make sure common_prek_utils is imported
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import (
     AIRFLOW_ROOT_PATH,
     console,
diff --git a/scripts/ci/prek/check_k8s_schemas_published.py 
b/scripts/ci/prek/check_k8s_schemas_published.py
index 53648fb4aa5..c7ec8f36f12 100755
--- a/scripts/ci/prek/check_k8s_schemas_published.py
+++ b/scripts/ci/prek/check_k8s_schemas_published.py
@@ -26,11 +26,9 @@ If any version returns non-200 the hook fails with 
instructions.
 from __future__ import annotations
 
 import sys
-from pathlib import Path
 from urllib.error import HTTPError, URLError
 from urllib.request import Request, urlopen
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import console, read_allowed_kubernetes_versions
 
 PROBE_URL_TEMPLATE = 
"https://airflow.apache.org/k8s-schemas/v{version}-standalone-strict/configmap-v1.json";
diff --git a/scripts/ci/prek/check_kubeconform.py 
b/scripts/ci/prek/check_kubeconform.py
index 2162ff6139d..18b03aa6aef 100755
--- a/scripts/ci/prek/check_kubeconform.py
+++ b/scripts/ci/prek/check_kubeconform.py
@@ -28,9 +28,7 @@ from __future__ import annotations
 import os
 import subprocess
 import sys
-from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import AIRFLOW_ROOT_PATH, console, 
initialize_breeze_prek
 
 initialize_breeze_prek(__name__, __file__)
diff --git a/scripts/ci/prek/check_min_python_version.py 
b/scripts/ci/prek/check_min_python_version.py
index 809fc37546f..adaba0c81a3 100755
--- a/scripts/ci/prek/check_min_python_version.py
+++ b/scripts/ci/prek/check_min_python_version.py
@@ -25,9 +25,6 @@ from __future__ import annotations
 
 import subprocess
 import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 
 from common_prek_utils import console
 
diff --git a/scripts/ci/prek/check_order_dockerfile_extras.py 
b/scripts/ci/prek/check_order_dockerfile_extras.py
index e239dec08ae..a5202449e92 100755
--- a/scripts/ci/prek/check_order_dockerfile_extras.py
+++ b/scripts/ci/prek/check_order_dockerfile_extras.py
@@ -31,10 +31,8 @@ from __future__ import annotations
 import sys
 from pathlib import Path
 
-from rich import print
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import AIRFLOW_ROOT_PATH, check_list_sorted
+from rich import print
 
 errors: list[str] = []
 
diff --git a/scripts/ci/prek/check_provider_docs.py 
b/scripts/ci/prek/check_provider_docs.py
index 1255e3be422..fb41ad4c191 100755
--- a/scripts/ci/prek/check_provider_docs.py
+++ b/scripts/ci/prek/check_provider_docs.py
@@ -29,16 +29,13 @@ import sys
 from collections import defaultdict
 from pathlib import Path
 
-from rich.console import Console
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure common 
utils are importable
-
 from common_prek_utils import (
     AIRFLOW_CORE_SOURCES_PATH,
     AIRFLOW_PROVIDERS_ROOT_PATH,
     AIRFLOW_ROOT_PATH,
     get_all_provider_info_dicts,
 )
+from rich.console import Console
 
 sys.path.insert(0, str(AIRFLOW_CORE_SOURCES_PATH))  # make sure setup is 
imported from Airflow
 
diff --git a/scripts/ci/prek/check_provider_yaml_files.py 
b/scripts/ci/prek/check_provider_yaml_files.py
index 7348d4f0bb1..46f5e942ad4 100755
--- a/scripts/ci/prek/check_provider_yaml_files.py
+++ b/scripts/ci/prek/check_provider_yaml_files.py
@@ -24,9 +24,7 @@
 from __future__ import annotations
 
 import sys
-from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import (
     initialize_breeze_prek,
     run_command_via_breeze_shell,
diff --git a/scripts/ci/prek/check_providers_subpackages_all_have_init.py 
b/scripts/ci/prek/check_providers_subpackages_all_have_init.py
index b50b3485cb2..c1511f21319 100755
--- a/scripts/ci/prek/check_providers_subpackages_all_have_init.py
+++ b/scripts/ci/prek/check_providers_subpackages_all_have_init.py
@@ -27,7 +27,6 @@ import os
 import sys
 from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import (
     AIRFLOW_PROVIDERS_ROOT_PATH,
     AIRFLOW_ROOT_PATH,
diff --git a/scripts/ci/prek/check_revision_heads_map.py 
b/scripts/ci/prek/check_revision_heads_map.py
index 02a87c7e56b..a223d07e740 100755
--- a/scripts/ci/prek/check_revision_heads_map.py
+++ b/scripts/ci/prek/check_revision_heads_map.py
@@ -28,9 +28,7 @@ from __future__ import annotations
 import os
 import re
 import sys
-from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import AIRFLOW_CORE_SOURCES_PATH, 
AIRFLOW_PROVIDERS_ROOT_PATH, console
 
 DB_FILE = AIRFLOW_CORE_SOURCES_PATH / "airflow" / "utils" / "db.py"
diff --git a/scripts/ci/prek/check_schema_defaults.py 
b/scripts/ci/prek/check_schema_defaults.py
index 3a1fb75cfbe..dcfa4b462f5 100755
--- a/scripts/ci/prek/check_schema_defaults.py
+++ b/scripts/ci/prek/check_schema_defaults.py
@@ -23,10 +23,6 @@
 # ///
 from __future__ import annotations
 
-import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import (
     initialize_breeze_prek,
     run_command_via_breeze_shell,
diff --git a/scripts/ci/prek/check_sdk_imports.py 
b/scripts/ci/prek/check_sdk_imports.py
index f266fd3480e..f900390695f 100755
--- a/scripts/ci/prek/check_sdk_imports.py
+++ b/scripts/ci/prek/check_sdk_imports.py
@@ -29,7 +29,6 @@ import ast
 import sys
 from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import console
 
 
diff --git a/scripts/ci/prek/check_shared_distributions_structure.py 
b/scripts/ci/prek/check_shared_distributions_structure.py
index 41a9fb8112e..ac08c28924b 100755
--- a/scripts/ci/prek/check_shared_distributions_structure.py
+++ b/scripts/ci/prek/check_shared_distributions_structure.py
@@ -37,7 +37,6 @@ try:
 except ImportError:
     import tomli as tomllib
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import AIRFLOW_ROOT_PATH, console
 
 SHARED_DIR = AIRFLOW_ROOT_PATH / "shared"
diff --git a/scripts/ci/prek/check_shared_distributions_usage.py 
b/scripts/ci/prek/check_shared_distributions_usage.py
index c9cd41e92dc..c5759d76d73 100755
--- a/scripts/ci/prek/check_shared_distributions_usage.py
+++ b/scripts/ci/prek/check_shared_distributions_usage.py
@@ -41,7 +41,6 @@ try:
 except ImportError:
     import tomli as tomllib
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # for 
common_prek_utils import
 from common_prek_utils import AIRFLOW_ROOT_PATH, console, insert_documentation
 
 SHARED_DIR = AIRFLOW_ROOT_PATH / "shared"
diff --git a/scripts/ci/prek/check_system_tests_hidden_in_index.py 
b/scripts/ci/prek/check_system_tests_hidden_in_index.py
index cbb81c7435e..65edabe155d 100755
--- a/scripts/ci/prek/check_system_tests_hidden_in_index.py
+++ b/scripts/ci/prek/check_system_tests_hidden_in_index.py
@@ -35,7 +35,6 @@ if __name__ not in ("__main__", "__mp_main__"):
     )
 
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import AIRFLOW_PROVIDERS_ROOT_PATH, console
 
 errors: list[Any] = []
diff --git a/scripts/ci/prek/check_template_context_variable_in_sync.py 
b/scripts/ci/prek/check_template_context_variable_in_sync.py
index 61f4445f274..a6fdef35f28 100755
--- a/scripts/ci/prek/check_template_context_variable_in_sync.py
+++ b/scripts/ci/prek/check_template_context_variable_in_sync.py
@@ -26,13 +26,10 @@
 from __future__ import annotations
 
 import ast
-import pathlib
 import re
 import sys
 import typing
 
-sys.path.insert(0, str(pathlib.Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
-
 from common_prek_utils import AIRFLOW_CORE_ROOT_PATH, 
AIRFLOW_TASK_SDK_SOURCES_PATH
 
 TASKRUNNER_PY = AIRFLOW_TASK_SDK_SOURCES_PATH / "airflow" / "sdk" / 
"execution_time" / "task_runner.py"
diff --git a/scripts/ci/prek/check_template_fields.py 
b/scripts/ci/prek/check_template_fields.py
index deed0d25900..fb68c8dec22 100755
--- a/scripts/ci/prek/check_template_fields.py
+++ b/scripts/ci/prek/check_template_fields.py
@@ -24,9 +24,7 @@
 from __future__ import annotations
 
 import sys
-from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import (
     initialize_breeze_prek,
     run_command_via_breeze_shell,
diff --git a/scripts/ci/prek/check_test_only_imports_in_src.py 
b/scripts/ci/prek/check_test_only_imports_in_src.py
index 848cd82df94..0670409a18f 100755
--- a/scripts/ci/prek/check_test_only_imports_in_src.py
+++ b/scripts/ci/prek/check_test_only_imports_in_src.py
@@ -42,7 +42,6 @@ import re
 import sys
 from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import console
 
 # Top-level modules that are dev-only and must never be imported at runtime.
diff --git a/scripts/ci/prek/check_tests_in_right_folders.py 
b/scripts/ci/prek/check_tests_in_right_folders.py
index 1756e61474e..1a2bf5b53bc 100755
--- a/scripts/ci/prek/check_tests_in_right_folders.py
+++ b/scripts/ci/prek/check_tests_in_right_folders.py
@@ -26,9 +26,7 @@ from __future__ import annotations
 
 import re
 import sys
-from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import console, initialize_breeze_prek
 
 initialize_breeze_prek(__name__, __file__)
diff --git a/scripts/ci/prek/check_ti_vs_tis_attributes.py 
b/scripts/ci/prek/check_ti_vs_tis_attributes.py
index bca2d521e49..3ae595c7398 100755
--- a/scripts/ci/prek/check_ti_vs_tis_attributes.py
+++ b/scripts/ci/prek/check_ti_vs_tis_attributes.py
@@ -25,9 +25,7 @@ from __future__ import annotations
 
 import ast
 import sys
-from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import AIRFLOW_CORE_SOURCES_PATH, console
 
 TI_PATH = AIRFLOW_CORE_SOURCES_PATH / "airflow" / "models" / "taskinstance.py"
diff --git a/scripts/ci/prek/check_version_consistency.py 
b/scripts/ci/prek/check_version_consistency.py
index c7bfaee9450..b068cd0c49d 100755
--- a/scripts/ci/prek/check_version_consistency.py
+++ b/scripts/ci/prek/check_version_consistency.py
@@ -28,24 +28,20 @@ from __future__ import annotations
 import ast
 import re
 import sys
-from pathlib import Path
 
 try:
     import tomllib
 except ImportError:
     import tomli as tomllib
 
-from packaging.specifiers import SpecifierSet
-from packaging.version import Version
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
 from common_prek_utils import (
     AIRFLOW_CORE_SOURCES_PATH,
     AIRFLOW_ROOT_PATH,
     AIRFLOW_TASK_SDK_SOURCES_PATH,
     console,
 )
+from packaging.specifiers import SpecifierSet
+from packaging.version import Version
 
 
 def read_airflow_version() -> str:
diff --git a/scripts/ci/prek/common_prek_utils.py 
b/scripts/ci/prek/common_prek_utils.py
index 5a9024e00ac..4bfb9b09ac2 100644
--- a/scripts/ci/prek/common_prek_utils.py
+++ b/scripts/ci/prek/common_prek_utils.py
@@ -133,7 +133,7 @@ def read_allowed_kubernetes_versions() -> list[str]:
     raise RuntimeError("ALLOWED_KUBERNETES_VERSIONS not found in 
global_constants.py")
 
 
-def pre_process_files(files: list[str]) -> list[str]:
+def pre_process_mypy_files(files: list[str]) -> list[str]:
     """Pre-process files passed to mypy.
 
     * Exclude conftest.py files and __init__.py files
diff --git a/scripts/ci/prek/compile_ui_assets.py 
b/scripts/ci/prek/compile_ui_assets.py
index d9c2ba4f32c..054845fe78d 100755
--- a/scripts/ci/prek/compile_ui_assets.py
+++ b/scripts/ci/prek/compile_ui_assets.py
@@ -29,8 +29,6 @@ from pathlib import Path
 # Cannot have additional Python dependencies installed. We should not import 
any of the libraries
 # here that are not available in stdlib! You should not import 
common_prek_utils.py here because
 # They are importing the rich library which is not available in the node 
environment.
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import AIRFLOW_CORE_SOURCES_PATH, AIRFLOW_ROOT_PATH
 
 MAIN_UI_DIRECTORY = AIRFLOW_CORE_SOURCES_PATH / "airflow" / "ui"
diff --git a/scripts/ci/prek/compile_ui_assets_dev.py 
b/scripts/ci/prek/compile_ui_assets_dev.py
index 903e7f5e98d..8f7760f0bc8 100755
--- a/scripts/ci/prek/compile_ui_assets_dev.py
+++ b/scripts/ci/prek/compile_ui_assets_dev.py
@@ -20,10 +20,7 @@ from __future__ import annotations
 import os
 import signal
 import subprocess
-import sys
-from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import AIRFLOW_CORE_SOURCES_PATH, AIRFLOW_ROOT_PATH
 
 # NOTE!. This script is executed from a node environment created by a prek 
hook, and this environment
diff --git a/scripts/ci/prek/download_k8s_schemas.py 
b/scripts/ci/prek/download_k8s_schemas.py
index 60069ba9e65..2576d5d0754 100755
--- a/scripts/ci/prek/download_k8s_schemas.py
+++ b/scripts/ci/prek/download_k8s_schemas.py
@@ -38,14 +38,11 @@ from __future__ import annotations
 import argparse
 import json
 import subprocess
-import sys
 from pathlib import Path
 from tempfile import NamedTemporaryFile
 
 import requests
 import yaml
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import AIRFLOW_ROOT_PATH, console, 
read_allowed_kubernetes_versions
 
 KUBERNETES_VERSIONS = read_allowed_kubernetes_versions()
diff --git a/scripts/ci/prek/generate_airflow_diagrams.py 
b/scripts/ci/prek/generate_airflow_diagrams.py
index 87c69839e67..cbdecb5a9bf 100755
--- a/scripts/ci/prek/generate_airflow_diagrams.py
+++ b/scripts/ci/prek/generate_airflow_diagrams.py
@@ -30,7 +30,6 @@ import subprocess
 import sys
 from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import console
 
 
diff --git a/scripts/ci/prek/generate_openapi_spec.py 
b/scripts/ci/prek/generate_openapi_spec.py
index be28facec19..b6ddcb64aed 100755
--- a/scripts/ci/prek/generate_openapi_spec.py
+++ b/scripts/ci/prek/generate_openapi_spec.py
@@ -23,10 +23,6 @@
 # ///
 from __future__ import annotations
 
-import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import (
     initialize_breeze_prek,
     run_command_via_breeze_shell,
diff --git a/scripts/ci/prek/generate_openapi_spec_providers.py 
b/scripts/ci/prek/generate_openapi_spec_providers.py
index 4cd8382fd9e..05424307edd 100755
--- a/scripts/ci/prek/generate_openapi_spec_providers.py
+++ b/scripts/ci/prek/generate_openapi_spec_providers.py
@@ -24,9 +24,7 @@
 from __future__ import annotations
 
 import sys
-from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import (
     initialize_breeze_prek,
     run_command_via_breeze_shell,
diff --git a/scripts/ci/prek/generate_volumes_for_sources.py 
b/scripts/ci/prek/generate_volumes_for_sources.py
index c1921fa8488..1c4bb2daa9b 100755
--- a/scripts/ci/prek/generate_volumes_for_sources.py
+++ b/scripts/ci/prek/generate_volumes_for_sources.py
@@ -24,10 +24,6 @@
 
 from __future__ import annotations
 
-import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import AIRFLOW_ROOT_PATH, get_all_provider_ids, 
insert_documentation
 
 START_MARKER = "      # START automatically generated volumes by 
generate-volumes-for-sources prek hook"
diff --git a/scripts/ci/prek/lint_helm.py b/scripts/ci/prek/lint_helm.py
index 789e04cccf9..4235398a503 100755
--- a/scripts/ci/prek/lint_helm.py
+++ b/scripts/ci/prek/lint_helm.py
@@ -28,9 +28,7 @@ from __future__ import annotations
 import os
 import subprocess
 import sys
-from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import AIRFLOW_ROOT_PATH, console, 
initialize_breeze_prek
 
 initialize_breeze_prek(__name__, __file__)
diff --git a/scripts/ci/prek/lint_json_schema.py 
b/scripts/ci/prek/lint_json_schema.py
index 43b6c7e2f19..ab6dc8bff37 100755
--- a/scripts/ci/prek/lint_json_schema.py
+++ b/scripts/ci/prek/lint_json_schema.py
@@ -32,7 +32,6 @@ import json
 import os
 import re
 import sys
-from pathlib import Path
 
 import requests
 import yaml
@@ -45,7 +44,6 @@ if __name__ != "__main__":
         "To run this script, run the ./build_docs.py command"
     )
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import AIRFLOW_ROOT_PATH
 
 
diff --git a/scripts/ci/prek/local_yml_mounts.py 
b/scripts/ci/prek/local_yml_mounts.py
index dcbca62b0e8..5f699e56599 100755
--- a/scripts/ci/prek/local_yml_mounts.py
+++ b/scripts/ci/prek/local_yml_mounts.py
@@ -18,10 +18,7 @@
 from __future__ import annotations
 
 import subprocess
-import sys
-from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import console, initialize_breeze_prek
 
 initialize_breeze_prek(__name__, __file__)
diff --git a/scripts/ci/prek/migration_reference.py 
b/scripts/ci/prek/migration_reference.py
index 27a1a130b3d..c4dfe5e0c85 100755
--- a/scripts/ci/prek/migration_reference.py
+++ b/scripts/ci/prek/migration_reference.py
@@ -23,10 +23,6 @@
 # ///
 from __future__ import annotations
 
-import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import (
     initialize_breeze_prek,
     run_command_via_breeze_shell,
diff --git a/scripts/ci/prek/mypy.py b/scripts/ci/prek/mypy.py
index 8405840eaff..98fe89c0ea2 100755
--- a/scripts/ci/prek/mypy.py
+++ b/scripts/ci/prek/mypy.py
@@ -28,19 +28,17 @@ import shlex
 import sys
 from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
 from common_prek_utils import (
     AIRFLOW_ROOT_PATH,
     console,
     initialize_breeze_prek,
-    pre_process_files,
+    pre_process_mypy_files,
     run_command_via_breeze_shell,
 )
 
 initialize_breeze_prek(__name__, __file__)
 
-files_to_test = pre_process_files(sys.argv[1:])
+files_to_test = pre_process_mypy_files(sys.argv[1:])
 if not files_to_test:
     print("No files to tests. Quitting")
     sys.exit(0)
diff --git a/scripts/ci/prek/mypy_folder.py b/scripts/ci/prek/mypy_folder.py
index a84139dfedb..c4e422450c0 100755
--- a/scripts/ci/prek/mypy_folder.py
+++ b/scripts/ci/prek/mypy_folder.py
@@ -27,9 +27,6 @@ import os
 import re
 import shlex
 import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 
 from common_prek_utils import (
     AIRFLOW_ROOT_PATH,
diff --git a/scripts/ci/prek/newsfragments.py b/scripts/ci/prek/newsfragments.py
index ecd03a6d8e3..cb994b44c13 100755
--- a/scripts/ci/prek/newsfragments.py
+++ b/scripts/ci/prek/newsfragments.py
@@ -28,42 +28,53 @@ from pathlib import Path
 
 VALID_CHANGE_TYPES = {"significant", "feature", "improvement", "bugfix", 
"doc", "misc"}
 
-files = sys.argv[1:]
 
-failed = False
-for filename in files:
-    with open(filename) as f:
-        lines = [line.strip() for line in f.readlines()]
+def validate_newsfragment(filename: str, lines: list[str]) -> list[str]:
+    """Validate a single newsfragment file. Returns a list of error 
messages."""
+    errors: list[str] = []
     num_lines = len(lines)
 
     name_parts = Path(filename).name.split(".")
     if len(name_parts) != 3:
-        print(f"Newsfragment {filename} has an unexpected filename. Should be 
{{pr_number}}.{{type}}.rst.")
-        failed = True
-        continue
+        errors.append(
+            f"Newsfragment {filename} has an unexpected filename. Should be 
{{pr_number}}.{{type}}.rst."
+        )
+        return errors
 
     change_type = name_parts[1]
     if change_type not in VALID_CHANGE_TYPES:
-        print(f"Newsfragment {filename} has an unexpected type. Should be one 
of {VALID_CHANGE_TYPES}.")
-        failed = True
-        continue
+        errors.append(
+            f"Newsfragment {filename} has an unexpected type. Should be one of 
{VALID_CHANGE_TYPES}."
+        )
+        return errors
 
     if change_type != "significant":
         if num_lines != 1:
-            print(f"Newsfragment {filename} can only have a single line.")
-            failed = True
+            errors.append(f"Newsfragment {filename} can only have a single 
line.")
     else:
         # significant newsfragment
         if num_lines == 1:
-            continue
-        if num_lines == 2:
-            print(f"Newsfragment {filename} can have 1, or 3+ lines.")
-            failed = True
-            continue
-        if lines[1] != "":
-            print(f"Newsfragment {filename} must have an empty second line.")
+            pass  # OK
+        elif num_lines == 2:
+            errors.append(f"Newsfragment {filename} can have 1, or 3+ lines.")
+        elif lines[1] != "":
+            errors.append(f"Newsfragment {filename} must have an empty second 
line.")
+
+    return errors
+
+
+def main(filenames: list[str]) -> int:
+    failed = False
+    for filename in filenames:
+        with open(filename) as f:
+            lines = [line.strip() for line in f.readlines()]
+        errors = validate_newsfragment(filename, lines)
+        for error in errors:
+            print(error)
+        if errors:
             failed = True
-            continue
+    return 1 if failed else 0
+
 
-if failed:
-    sys.exit(1)
+if __name__ == "__main__":
+    sys.exit(main(sys.argv[1:]))
diff --git a/scripts/ci/prek/sync_translation_namespaces.py 
b/scripts/ci/prek/sync_translation_namespaces.py
index 192aab49fa2..1dd9df6eb61 100755
--- a/scripts/ci/prek/sync_translation_namespaces.py
+++ b/scripts/ci/prek/sync_translation_namespaces.py
@@ -20,9 +20,7 @@
 from __future__ import annotations
 
 import sys
-from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import AIRFLOW_ROOT_PATH, insert_documentation
 
 EN_LOCALE_DIR = (
diff --git a/scripts/ci/prek/ts_compile_lint_common_ai.py 
b/scripts/ci/prek/ts_compile_lint_common_ai.py
index e47632428b9..ae0032feb31 100755
--- a/scripts/ci/prek/ts_compile_lint_common_ai.py
+++ b/scripts/ci/prek/ts_compile_lint_common_ai.py
@@ -20,7 +20,6 @@ from __future__ import annotations
 import sys
 from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import (
     AIRFLOW_PROVIDERS_ROOT_PATH,
     AIRFLOW_ROOT_PATH,
diff --git a/scripts/ci/prek/ts_compile_lint_edge.py 
b/scripts/ci/prek/ts_compile_lint_edge.py
index 8dfd847128f..229b890da18 100755
--- a/scripts/ci/prek/ts_compile_lint_edge.py
+++ b/scripts/ci/prek/ts_compile_lint_edge.py
@@ -20,7 +20,6 @@ from __future__ import annotations
 import sys
 from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import (
     AIRFLOW_PROVIDERS_ROOT_PATH,
     AIRFLOW_ROOT_PATH,
diff --git a/scripts/ci/prek/ts_compile_lint_simple_auth_manager_ui.py 
b/scripts/ci/prek/ts_compile_lint_simple_auth_manager_ui.py
index 697d6675225..1a3727bc910 100755
--- a/scripts/ci/prek/ts_compile_lint_simple_auth_manager_ui.py
+++ b/scripts/ci/prek/ts_compile_lint_simple_auth_manager_ui.py
@@ -20,7 +20,6 @@ from __future__ import annotations
 import sys
 from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import (
     AIRFLOW_CORE_SOURCES_PATH,
     AIRFLOW_ROOT_PATH,
diff --git a/scripts/ci/prek/ts_compile_lint_ui.py 
b/scripts/ci/prek/ts_compile_lint_ui.py
index b6fdd783788..17514f9be22 100755
--- a/scripts/ci/prek/ts_compile_lint_ui.py
+++ b/scripts/ci/prek/ts_compile_lint_ui.py
@@ -20,7 +20,6 @@ from __future__ import annotations
 import sys
 from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import (
     AIRFLOW_CORE_ROOT_PATH,
     AIRFLOW_CORE_SOURCES_PATH,
diff --git a/scripts/ci/prek/update_airflow_pyproject_toml.py 
b/scripts/ci/prek/update_airflow_pyproject_toml.py
index 924b183f2e9..cccae46867e 100755
--- a/scripts/ci/prek/update_airflow_pyproject_toml.py
+++ b/scripts/ci/prek/update_airflow_pyproject_toml.py
@@ -37,10 +37,8 @@ from datetime import datetime, timedelta, timezone
 from pathlib import Path
 from typing import Any
 
-from packaging.version import Version, parse as parse_version
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import AIRFLOW_ROOT_PATH, console, 
get_all_provider_ids, insert_documentation
+from packaging.version import Version, parse as parse_version
 
 AIRFLOW_PYPROJECT_TOML_FILE = AIRFLOW_ROOT_PATH / "pyproject.toml"
 AIRFLOW_CORE_ROOT_PATH = AIRFLOW_ROOT_PATH / "airflow-core"
diff --git a/scripts/ci/prek/update_chart_dependencies.py 
b/scripts/ci/prek/update_chart_dependencies.py
index daf01404002..d65642bbac2 100755
--- a/scripts/ci/prek/update_chart_dependencies.py
+++ b/scripts/ci/prek/update_chart_dependencies.py
@@ -27,12 +27,9 @@ from __future__ import annotations
 
 import json
 import sys
-from pathlib import Path
 
 import requests
 import yaml
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import AIRFLOW_ROOT_PATH, console
 
 VALUES_YAML_FILE = AIRFLOW_ROOT_PATH / "chart" / "values.yaml"
diff --git a/scripts/ci/prek/update_example_dags_paths.py 
b/scripts/ci/prek/update_example_dags_paths.py
index fe22ca9889a..6f4e5bd1dcc 100755
--- a/scripts/ci/prek/update_example_dags_paths.py
+++ b/scripts/ci/prek/update_example_dags_paths.py
@@ -37,7 +37,6 @@ if __name__ not in ("__main__", "__mp_main__"):
     )
 
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import AIRFLOW_PROVIDERS_ROOT_PATH, console
 
 EXAMPLE_DAGS_URL_MATCHER = re.compile(
diff --git a/scripts/ci/prek/update_providers_build_files.py 
b/scripts/ci/prek/update_providers_build_files.py
index cc0b42a668a..37159909917 100755
--- a/scripts/ci/prek/update_providers_build_files.py
+++ b/scripts/ci/prek/update_providers_build_files.py
@@ -30,7 +30,6 @@ from pathlib import Path
 
 AIRFLOW_ROOT_PATH = Path(__file__).parents[3].resolve()
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import console, initialize_breeze_prek
 
 initialize_breeze_prek(__name__, __file__)
diff --git a/scripts/ci/prek/update_providers_dependencies.py 
b/scripts/ci/prek/update_providers_dependencies.py
index af52f2975f0..9e89084f7b0 100755
--- a/scripts/ci/prek/update_providers_dependencies.py
+++ b/scripts/ci/prek/update_providers_dependencies.py
@@ -33,8 +33,6 @@ from pathlib import Path
 from typing import Any
 
 import yaml
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import (
     AIRFLOW_CORE_SOURCES_PATH,
     AIRFLOW_PROVIDERS_ROOT_PATH,
diff --git a/scripts/ci/prek/update_source_date_epoch.py 
b/scripts/ci/prek/update_source_date_epoch.py
index cbb040bb403..97bb1c74821 100755
--- a/scripts/ci/prek/update_source_date_epoch.py
+++ b/scripts/ci/prek/update_source_date_epoch.py
@@ -24,15 +24,11 @@
 # ///
 from __future__ import annotations
 
-import sys
 from hashlib import md5
 from pathlib import Path
 from time import time
 
 import yaml
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is importable
-
 from common_prek_utils import AIRFLOW_ROOT_PATH
 
 CHART_DIR = AIRFLOW_ROOT_PATH / "chart"
diff --git a/scripts/ci/prek/update_versions.py 
b/scripts/ci/prek/update_versions.py
index 4207f0f66d5..fe2c2fd30aa 100755
--- a/scripts/ci/prek/update_versions.py
+++ b/scripts/ci/prek/update_versions.py
@@ -24,11 +24,8 @@
 from __future__ import annotations
 
 import re
-import sys
 from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is importable
-
 from common_prek_utils import AIRFLOW_ROOT_PATH, read_airflow_version
 
 
diff --git a/scripts/ci/prek/upgrade_important_versions.py 
b/scripts/ci/prek/upgrade_important_versions.py
index 4a90c3c3d1b..8041de4c891 100755
--- a/scripts/ci/prek/upgrade_important_versions.py
+++ b/scripts/ci/prek/upgrade_important_versions.py
@@ -41,10 +41,8 @@ from enum import Enum
 from pathlib import Path
 
 import requests
-from packaging.version import Version
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 from common_prek_utils import AIRFLOW_CORE_ROOT_PATH, AIRFLOW_ROOT_PATH, 
console, retrieve_gh_token
+from packaging.version import Version
 
 DOCKER_IMAGES_EXAMPLE_DIR_PATH = AIRFLOW_ROOT_PATH / "docker-stack-docs" / 
"docker-examples"
 
diff --git a/scripts/ci/prek/validate_chart_annotations.py 
b/scripts/ci/prek/validate_chart_annotations.py
index 20d16474ed7..f7671cac267 100755
--- a/scripts/ci/prek/validate_chart_annotations.py
+++ b/scripts/ci/prek/validate_chart_annotations.py
@@ -26,11 +26,8 @@
 from __future__ import annotations
 
 import sys
-from pathlib import Path
 
 import yaml
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from common_prek_utils import AIRFLOW_ROOT_PATH, console
 
 CHART_YAML_FILE = AIRFLOW_ROOT_PATH / "chart" / "Chart.yaml"
diff --git a/scripts/cov/cli_coverage.py b/scripts/cov/cli_coverage.py
index 6c235245806..0dbaa9495b9 100644
--- a/scripts/cov/cli_coverage.py
+++ b/scripts/cov/cli_coverage.py
@@ -16,13 +16,8 @@
 # under the License.
 from __future__ import annotations
 
-import sys
-from pathlib import Path
-
 from cov_runner import run_tests
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
 source_files = ["airflow-core/src/airflow/cli"]
 
 cli_files = ["airflow-core/tests/unit/cli"]
diff --git a/scripts/cov/core_coverage.py b/scripts/cov/core_coverage.py
index 2b5aff9fcf9..2d812fef533 100644
--- a/scripts/cov/core_coverage.py
+++ b/scripts/cov/core_coverage.py
@@ -16,13 +16,8 @@
 # under the License.
 from __future__ import annotations
 
-import sys
-from pathlib import Path
-
 from cov_runner import run_tests
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
 source_files = [
     "airflow-core/src/airflow/executors",
     "airflow-core/src/airflow/jobs",
diff --git a/scripts/cov/other_coverage.py b/scripts/cov/other_coverage.py
index 914f3b047d8..4b648a2e1a8 100644
--- a/scripts/cov/other_coverage.py
+++ b/scripts/cov/other_coverage.py
@@ -16,13 +16,8 @@
 # under the License.
 from __future__ import annotations
 
-import sys
-from pathlib import Path
-
 from cov_runner import run_tests
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
 source_files = [
     "airflow-core/src/airflow/dag_processing",
     "airflow-core/src/airflow/triggers",
diff --git a/scripts/cov/restapi_coverage.py b/scripts/cov/restapi_coverage.py
index cc80db9241d..1478c066436 100644
--- a/scripts/cov/restapi_coverage.py
+++ b/scripts/cov/restapi_coverage.py
@@ -16,13 +16,8 @@
 # under the License.
 from __future__ import annotations
 
-import sys
-from pathlib import Path
-
 from cov_runner import run_tests
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
 source_files = ["airflow-core/tests/unit/api_fastapi"]
 
 files_not_fully_covered: list[str] = []
diff --git a/scripts/in_container/install_airflow_and_providers.py 
b/scripts/in_container/install_airflow_and_providers.py
index 5337643f152..df7443271af 100755
--- a/scripts/in_container/install_airflow_and_providers.py
+++ b/scripts/in_container/install_airflow_and_providers.py
@@ -27,7 +27,6 @@ from functools import cache
 from pathlib import Path
 from typing import NamedTuple
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from in_container_utils import (
     AIRFLOW_CORE_SOURCES_PATH,
     AIRFLOW_DIST_PATH,
diff --git a/scripts/in_container/install_airflow_python_client.py 
b/scripts/in_container/install_airflow_python_client.py
index 36657f8768a..c528b772067 100644
--- a/scripts/in_container/install_airflow_python_client.py
+++ b/scripts/in_container/install_airflow_python_client.py
@@ -19,9 +19,7 @@
 from __future__ import annotations
 
 import sys
-from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from in_container_utils import AIRFLOW_DIST_PATH, click, console, run_command
 
 ALLOWED_DISTRIBUTION_FORMAT = ["wheel", "sdist", "both"]
diff --git a/scripts/in_container/install_development_dependencies.py 
b/scripts/in_container/install_development_dependencies.py
index 6e14c60995d..ee5831b0199 100755
--- a/scripts/in_container/install_development_dependencies.py
+++ b/scripts/in_container/install_development_dependencies.py
@@ -30,9 +30,7 @@ from __future__ import annotations
 
 import json
 import sys
-from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from in_container_utils import AIRFLOW_ROOT_PATH, click, console, run_command
 from packaging.requirements import Requirement
 
diff --git a/scripts/in_container/run_capture_airflowctl_help.py 
b/scripts/in_container/run_capture_airflowctl_help.py
index b35472bffca..3fac46ebd56 100644
--- a/scripts/in_container/run_capture_airflowctl_help.py
+++ b/scripts/in_container/run_capture_airflowctl_help.py
@@ -28,11 +28,9 @@ from pathlib import Path
 from airflowctl import __file__ as AIRFLOW_CTL_SRC_PATH
 from rich.console import Console
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 AIRFLOW_CTL_ROOT_PATH = Path(AIRFLOW_CTL_SRC_PATH).parents[2]
 AIRFLOW_CTL_SOURCES_PATH = AIRFLOW_CTL_ROOT_PATH / "src"
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))  # make sure 
common_prek_utils is imported
 AIRFLOWCTL_IMAGES_PATH = AIRFLOW_CTL_ROOT_PATH / "docs" / "images"
 HASH_FILE = AIRFLOW_CTL_ROOT_PATH / "docs" / "images" / "command_hashes.txt"
 COMMANDS = [
diff --git a/scripts/in_container/run_check_imports_in_providers.py 
b/scripts/in_container/run_check_imports_in_providers.py
index a946bba5c85..3e6d467a151 100755
--- a/scripts/in_container/run_check_imports_in_providers.py
+++ b/scripts/in_container/run_check_imports_in_providers.py
@@ -23,7 +23,6 @@ import subprocess
 import sys
 from pathlib import Path
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from in_container_utils import console, get_provider_base_dir_from_path, 
get_provider_id_from_path
 
 
diff --git a/scripts/in_container/run_generate_constraints.py 
b/scripts/in_container/run_generate_constraints.py
index dc32ff03dc5..b22ade34305 100755
--- a/scripts/in_container/run_generate_constraints.py
+++ b/scripts/in_container/run_generate_constraints.py
@@ -28,8 +28,6 @@ from typing import TextIO
 
 import requests
 from click import Choice
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 from in_container_utils import AIRFLOW_DIST_PATH, AIRFLOW_ROOT_PATH, click, 
console, run_command
 
 DEFAULT_BRANCH = os.environ.get("DEFAULT_BRANCH", "main")
diff --git a/scripts/in_container/run_generate_openapi_spec.py 
b/scripts/in_container/run_generate_openapi_spec.py
index cd8c0d5d4f4..1bc436e7002 100755
--- a/scripts/in_container/run_generate_openapi_spec.py
+++ b/scripts/in_container/run_generate_openapi_spec.py
@@ -21,14 +21,13 @@ import os
 import sys
 from pathlib import Path
 
+from in_container_utils import console, generate_openapi_file, 
validate_openapi_file
+
 from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX, create_app
 from airflow.api_fastapi.auth.managers.simple import __file__ as 
SIMPLE_AUTH_MANAGER_PATH
 from airflow.api_fastapi.auth.managers.simple.simple_auth_manager import 
SimpleAuthManager
 from airflow.api_fastapi.core_api import __file__ as CORE_API_PATH
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-from in_container_utils import console, generate_openapi_file, 
validate_openapi_file
-
 OPENAPI_SPEC_FILE = Path(CORE_API_PATH).parent / "openapi" / 
"v2-rest-api-generated.yaml"
 # We need a "combined" spec file to generate the UI code with, but we don't 
want to include this in the repo
 # nor in the rendered docs, so we make this a separate file which is gitignored
diff --git a/scripts/in_container/run_generate_openapi_spec_providers.py 
b/scripts/in_container/run_generate_openapi_spec_providers.py
index 4461d6ebf73..f7ae0095eb5 100755
--- a/scripts/in_container/run_generate_openapi_spec_providers.py
+++ b/scripts/in_container/run_generate_openapi_spec_providers.py
@@ -36,7 +36,6 @@ class ProviderDef(NamedTuple):
     prefix: str
 
 
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
 ProvidersManager().initialize_providers_configuration()
 
 
diff --git a/scripts/in_container/run_provider_yaml_files_check.py 
b/scripts/in_container/run_provider_yaml_files_check.py
index 138d640fd1d..7dbb485ca48 100755
--- a/scripts/in_container/run_provider_yaml_files_check.py
+++ b/scripts/in_container/run_provider_yaml_files_check.py
@@ -37,6 +37,11 @@ from typing import Any
 
 import jsonschema
 import yaml
+from in_container_utils import (
+    AIRFLOW_CORE_SOURCES_PATH,
+    AIRFLOW_PROVIDERS_PATH,
+    AIRFLOW_ROOT_PATH,
+)
 from jsonpath_ng.ext import parse
 from rich.console import Console
 from tabulate import tabulate
@@ -45,13 +50,6 @@ from airflow.cli.commands.info_command import Architecture
 from airflow.exceptions import AirflowOptionalProviderFeatureException, 
AirflowProviderDeprecationWarning
 from airflow.providers_manager import ProvidersManager
 
-sys.path.insert(0, str(pathlib.Path(__file__).parent.resolve()))
-from in_container_utils import (
-    AIRFLOW_CORE_SOURCES_PATH,
-    AIRFLOW_PROVIDERS_PATH,
-    AIRFLOW_ROOT_PATH,
-)
-
 # Those are deprecated modules that contain removed Hooks/Sensors/Operators 
that we left in the code
 # so that users can get a very specific error message when they try to use 
them.
 
diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml
new file mode 100644
index 00000000000..bbcc8749bd2
--- /dev/null
+++ b/scripts/pyproject.toml
@@ -0,0 +1,76 @@
+
+# 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.
+
+[build-system]
+requires = [
+    "hatchling==1.29.0",
+    "packaging==26.0",
+    "pathspec==1.0.4",
+    "pluggy==1.6.0",
+    "tomli==2.4.0; python_version < '3.11'",
+    "trove-classifiers==2026.1.14.14",
+]
+build-backend = "hatchling.build"
+
+[project]
+name = "apache-airflow-scripts"
+description = "Scripts and utilities for Apache Airflow CI, Docker, and 
development"
+classifiers = [
+    "Private :: Do Not Upload",
+]
+requires-python = ">=3.10,!=3.14"
+authors = [
+    { name = "Apache Software Foundation", email = "[email protected]" },
+]
+maintainers = [
+    { name = "Apache Software Foundation", email = "[email protected]" },
+]
+version = "0.0.1"
+
+dependencies = [
+    "astor>=0.8.1",
+    "jsonschema>=4.19.1",
+    "libcst>=1.1.0",
+    "packaging>=25.0",
+    "python-dateutil>=2.8.2",
+    "pyyaml>=6.0.3",
+    "requests>=2.31.0",
+    "rich>=13.6.0",
+    "tabulate>=0.9.0",
+    "termcolor>=2.3.0",
+]
+
+[dependency-groups]
+dev = [
+    "apache-airflow-devel-common",
+]
+
+[tool.uv.sources]
+apache-airflow-devel-common = {workspace = true}
+
+[tool.hatch.build.targets.sdist]
+exclude = ["*"]
+
+[tool.hatch.build.targets.wheel]
+packages = ["ci", "cov", "docker", "in_container", "tools"]
+
+[tool.pytest.ini_options]
+# "." makes ci.prek.* importable as packages; "ci/prek" makes bare
+# "from common_prek_utils import ..." inside those modules resolve correctly
+# (mirroring what Python does automatically when scripts are run directly).
+pythonpath = [".", "ci/prek"]
diff --git a/scripts/cov/restapi_coverage.py b/scripts/tests/__init__.py
similarity index 67%
copy from scripts/cov/restapi_coverage.py
copy to scripts/tests/__init__.py
index cc80db9241d..13a83393a91 100644
--- a/scripts/cov/restapi_coverage.py
+++ b/scripts/tests/__init__.py
@@ -14,19 +14,3 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-from __future__ import annotations
-
-import sys
-from pathlib import Path
-
-from cov_runner import run_tests
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
-source_files = ["airflow-core/tests/unit/api_fastapi"]
-
-files_not_fully_covered: list[str] = []
-
-if __name__ == "__main__":
-    args = ["-qq"] + source_files
-    run_tests(args, source_files, files_not_fully_covered)
diff --git a/scripts/cov/restapi_coverage.py b/scripts/tests/ci/__init__.py
similarity index 67%
copy from scripts/cov/restapi_coverage.py
copy to scripts/tests/ci/__init__.py
index cc80db9241d..13a83393a91 100644
--- a/scripts/cov/restapi_coverage.py
+++ b/scripts/tests/ci/__init__.py
@@ -14,19 +14,3 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-from __future__ import annotations
-
-import sys
-from pathlib import Path
-
-from cov_runner import run_tests
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
-source_files = ["airflow-core/tests/unit/api_fastapi"]
-
-files_not_fully_covered: list[str] = []
-
-if __name__ == "__main__":
-    args = ["-qq"] + source_files
-    run_tests(args, source_files, files_not_fully_covered)
diff --git a/scripts/cov/restapi_coverage.py b/scripts/tests/ci/prek/__init__.py
similarity index 67%
copy from scripts/cov/restapi_coverage.py
copy to scripts/tests/ci/prek/__init__.py
index cc80db9241d..13a83393a91 100644
--- a/scripts/cov/restapi_coverage.py
+++ b/scripts/tests/ci/prek/__init__.py
@@ -14,19 +14,3 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-from __future__ import annotations
-
-import sys
-from pathlib import Path
-
-from cov_runner import run_tests
-
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-
-source_files = ["airflow-core/tests/unit/api_fastapi"]
-
-files_not_fully_covered: list[str] = []
-
-if __name__ == "__main__":
-    args = ["-qq"] + source_files
-    run_tests(args, source_files, files_not_fully_covered)
diff --git a/scripts/tests/ci/prek/conftest.py 
b/scripts/tests/ci/prek/conftest.py
new file mode 100644
index 00000000000..6ef372f3481
--- /dev/null
+++ b/scripts/tests/ci/prek/conftest.py
@@ -0,0 +1,76 @@
+# 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 textwrap
+from pathlib import Path
+
+import pytest
+import yaml
+
+
[email protected]
+def write_python_file(tmp_path):
+    """Factory fixture: write dedented Python code to a temp .py file and 
return its Path."""
+
+    def _write(code: str) -> Path:
+        path = tmp_path / "code.py"
+        path.write_text(textwrap.dedent(code))
+        return path
+
+    return _write
+
+
[email protected]
+def write_text_file(tmp_path):
+    """Factory fixture: write text content to a temp file and return its 
Path."""
+
+    def _write(content: str) -> Path:
+        path = tmp_path / "content.txt"
+        path.write_text(content)
+        return path
+
+    return _write
+
+
[email protected]
+def write_workflow_file(tmp_path):
+    """Factory fixture: write a workflow dict as YAML to a temp file and 
return its Path."""
+
+    def _write(content: dict) -> Path:
+        path = tmp_path / "workflow.yml"
+        path.write_text(yaml.dump(content))
+        return path
+
+    return _write
+
+
[email protected]
+def create_provider_tree(tmp_path):
+    """Factory fixture: create a directory tree with provider.yaml and return 
a file path inside it."""
+
+    def _create(relative_path: str) -> Path:
+        provider_dir = tmp_path / relative_path
+        provider_dir.mkdir(parents=True, exist_ok=True)
+        (provider_dir / "provider.yaml").touch()
+        hooks_dir = provider_dir / "hooks"
+        hooks_dir.mkdir(exist_ok=True)
+        test_file = hooks_dir / "hook.py"
+        test_file.touch()
+        return test_file
+
+    return _create
diff --git a/scripts/tests/ci/prek/test_changelog_duplicates.py 
b/scripts/tests/ci/prek/test_changelog_duplicates.py
new file mode 100644
index 00000000000..8cb097e3b28
--- /dev/null
+++ b/scripts/tests/ci/prek/test_changelog_duplicates.py
@@ -0,0 +1,101 @@
+# 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 ci.prek.changelog_duplicates import find_duplicates, known_exceptions, 
pr_number_re
+
+
+class TestPrNumberRegex:
+    @pytest.mark.parametrize(
+        "line, expected_pr",
+        [
+            ("* Fix something (#12345)", "12345"),
+            ("* Fix something (#1)", "1"),
+            ("* Fix something (#123456)", "123456"),
+            ("Some change (#99999)`", "99999"),
+            ("Some change (#99999)``", "99999"),
+        ],
+    )
+    def test_matches_valid_pr_numbers(self, line, expected_pr):
+        match = pr_number_re.search(line)
+        assert match is not None
+        assert match.group(1) == expected_pr
+
+    @pytest.mark.parametrize(
+        "line",
+        [
+            "* Fix something without PR number",
+            "* Fix something (#1234567)",  # 7 digits, too many
+            "* Fix something (#abc)",
+            "",
+            "Just some text",
+        ],
+    )
+    def test_no_match(self, line):
+        assert pr_number_re.search(line) is None
+
+
+class TestFindDuplicates:
+    def test_no_duplicates(self):
+        lines = [
+            "* Fix A (#1001)",
+            "* Fix B (#1002)",
+            "* Fix C (#1003)",
+        ]
+        assert find_duplicates(lines) == []
+
+    def test_with_duplicate(self):
+        lines = [
+            "* Fix A (#1001)",
+            "* Fix B (#1001)",
+        ]
+        assert find_duplicates(lines) == ["1001"]
+
+    def test_known_exception_not_reported(self):
+        lines = [
+            "* Fix A (#14738)",
+            "* Fix B (#14738)",
+        ]
+        assert find_duplicates(lines) == []
+
+    def test_mixed_lines(self):
+        lines = [
+            "# Changelog",
+            "",
+            "* Fix A (#1001)",
+            "Some description",
+            "* Fix B (#1002)",
+        ]
+        assert find_duplicates(lines) == []
+
+    def test_multiple_duplicates(self):
+        lines = [
+            "* Fix A (#1001)",
+            "* Fix B (#1002)",
+            "* Fix C (#1001)",
+            "* Fix D (#1002)",
+        ]
+        assert find_duplicates(lines) == ["1001", "1002"]
+
+    def test_empty_input(self):
+        assert find_duplicates([]) == []
+
+    def test_all_known_exceptions_are_strings(self):
+        for exc in known_exceptions:
+            assert isinstance(exc, str)
+            assert exc.isdigit()
diff --git a/scripts/tests/ci/prek/test_check_deprecations.py 
b/scripts/tests/ci/prek/test_check_deprecations.py
new file mode 100644
index 00000000000..e94e3c9854c
--- /dev/null
+++ b/scripts/tests/ci/prek/test_check_deprecations.py
@@ -0,0 +1,206 @@
+# 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 ast
+
+from ci.prek.check_deprecations import (
+    built_import,
+    built_import_from,
+    found_compatible_decorators,
+    get_decorator_argument,
+    is_file_under_eol_deprecation,
+    resolve_decorator_name,
+    resolve_name,
+)
+
+GOOGLE_BIGQUERY_HOOK_PATH = "airflow/providers/google/cloud/hooks/bigquery.py"
+AIRFLOW_PROVIDER_DEPRECATION_WARNING = "AirflowProviderDeprecationWarning"
+
+
+class TestResolveName:
+    def test_simple_name(self):
+        expr = ast.parse("foo", mode="eval").body
+        assert resolve_name(expr) == "foo"
+
+    def test_attribute(self):
+        expr = ast.parse("foo.bar", mode="eval").body
+        assert resolve_name(expr) == "foo.bar"
+
+    def test_nested_attribute(self):
+        expr = ast.parse("foo.bar.baz", mode="eval").body
+        assert resolve_name(expr) == "foo.bar.baz"
+
+
+class TestResolveDecoratorName:
+    def test_name_decorator(self):
+        func = ast.parse("@deprecated\ndef foo(): pass").body[0]
+        assert resolve_decorator_name(func.decorator_list[0]) == "deprecated"
+
+    def test_call_decorator(self):
+        func = ast.parse("@deprecated(category=X)\ndef foo(): pass").body[0]
+        assert resolve_decorator_name(func.decorator_list[0]) == "deprecated"
+
+    def test_attribute_decorator(self):
+        func = ast.parse("@warnings.deprecated\ndef foo(): pass").body[0]
+        assert resolve_decorator_name(func.decorator_list[0]) == 
"warnings.deprecated"
+
+    def test_attribute_call_decorator(self):
+        func = ast.parse("@warnings.deprecated(category=X)\ndef foo(): 
pass").body[0]
+        assert resolve_decorator_name(func.decorator_list[0]) == 
"warnings.deprecated"
+
+
+class TestBuiltImportFrom:
+    def test_from_warnings_import_deprecated(self):
+        node = ast.parse("from warnings import deprecated").body[0]
+        result = built_import_from(node)
+        assert "deprecated" in result
+
+    def test_from_typing_extensions_import_deprecated(self):
+        node = ast.parse("from typing_extensions import deprecated").body[0]
+        result = built_import_from(node)
+        assert "deprecated" in result
+
+    def test_from_deprecated_import_deprecated(self):
+        node = ast.parse("from deprecated import deprecated").body[0]
+        result = built_import_from(node)
+        assert "deprecated" in result
+
+    def test_from_deprecated_classic_import_deprecated(self):
+        node = ast.parse("from deprecated.classic import deprecated").body[0]
+        result = built_import_from(node)
+        assert "deprecated" in result
+
+    def test_unrelated_import(self):
+        node = ast.parse("from os import path").body[0]
+        result = built_import_from(node)
+        assert result == []
+
+    def test_aliased_import(self):
+        node = ast.parse("from warnings import deprecated as dep").body[0]
+        result = built_import_from(node)
+        assert "dep" in result
+
+    def test_no_module_name(self):
+        # relative import with no module
+        node = ast.parse("from . import something").body[0]
+        result = built_import_from(node)
+        assert result == []
+
+    def test_import_parent_module(self):
+        node = ast.parse("from deprecated import classic").body[0]
+        result = built_import_from(node)
+        assert "classic.deprecated" in result
+
+
+class TestBuiltImport:
+    def test_import_warnings(self):
+        node = ast.parse("import warnings").body[0]
+        result = built_import(node)
+        assert "warnings.deprecated" in result
+
+    def test_import_typing_extensions(self):
+        node = ast.parse("import typing_extensions").body[0]
+        result = built_import(node)
+        assert "typing_extensions.deprecated" in result
+
+    def test_import_deprecated(self):
+        node = ast.parse("import deprecated").body[0]
+        result = built_import(node)
+        assert "deprecated.deprecated" in result
+
+    def test_import_unrelated(self):
+        node = ast.parse("import os").body[0]
+        result = built_import(node)
+        assert result == []
+
+    def test_import_with_alias(self):
+        node = ast.parse("import warnings as w").body[0]
+        result = built_import(node)
+        assert "w.deprecated" in result
+
+
+class TestFoundCompatibleDecorators:
+    def test_no_imports(self):
+        mod = ast.parse("x = 1")
+        assert found_compatible_decorators(mod) == ()
+
+    def test_with_warnings_import(self):
+        mod = ast.parse("from warnings import deprecated")
+        result = found_compatible_decorators(mod)
+        assert "deprecated" in result
+
+    def test_with_multiple_imports(self):
+        code = "from warnings import deprecated\nfrom deprecated import 
deprecated as dep"
+        mod = ast.parse(code)
+        result = found_compatible_decorators(mod)
+        assert "dep" in result
+        assert "deprecated" in result
+
+    def test_deduplication(self):
+        code = "from warnings import deprecated\nfrom typing_extensions import 
deprecated"
+        mod = ast.parse(code)
+        result = found_compatible_decorators(mod)
+        assert result.count("deprecated") == 1
+
+
+class TestGetDecoratorArgument:
+    def test_finds_keyword(self):
+        func = ast.parse("@deprecated(category=DeprecationWarning)\ndef foo(): 
pass").body[0]
+        decorator = func.decorator_list[0]
+        result = get_decorator_argument(decorator, "category")
+        assert result is not None
+        assert result.arg == "category"
+
+    def test_missing_keyword(self):
+        func = ast.parse("@deprecated(message='old')\ndef foo(): pass").body[0]
+        decorator = func.decorator_list[0]
+        result = get_decorator_argument(decorator, "category")
+        assert result is None
+
+    def test_multiple_keywords(self):
+        code = "@deprecated(message='old', category=DeprecationWarning)\ndef 
foo(): pass"
+        func = ast.parse(code).body[0]
+        decorator = func.decorator_list[0]
+        result = get_decorator_argument(decorator, "category")
+        assert result is not None
+
+
+class TestIsFileUnderEolDeprecation:
+    def test_google_provider_with_matching_warning(self):
+        assert is_file_under_eol_deprecation(
+            GOOGLE_BIGQUERY_HOOK_PATH,
+            AIRFLOW_PROVIDER_DEPRECATION_WARNING,
+        )
+
+    def test_google_provider_with_non_matching_warning(self):
+        assert not is_file_under_eol_deprecation(
+            GOOGLE_BIGQUERY_HOOK_PATH,
+            "DeprecationWarning",
+        )
+
+    def test_non_google_provider(self):
+        assert not is_file_under_eol_deprecation(
+            "airflow/providers/amazon/aws/hooks/s3.py",
+            AIRFLOW_PROVIDER_DEPRECATION_WARNING,
+        )
+
+    def test_core_airflow_file(self):
+        assert not is_file_under_eol_deprecation(
+            "airflow/models/dag.py",
+            AIRFLOW_PROVIDER_DEPRECATION_WARNING,
+        )
diff --git a/scripts/tests/ci/prek/test_check_order_dockerfile_extras.py 
b/scripts/tests/ci/prek/test_check_order_dockerfile_extras.py
new file mode 100644
index 00000000000..8cbe556a31b
--- /dev/null
+++ b/scripts/tests/ci/prek/test_check_order_dockerfile_extras.py
@@ -0,0 +1,118 @@
+# 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
+
+from ci.prek.check_order_dockerfile_extras import get_replaced_content
+
+
+class TestGetReplacedContent:
+    def test_replaces_between_markers(self):
+        content = [
+            "before\n",
+            "# START\n",
+            "old_item_1\n",
+            "old_item_2\n",
+            "# END\n",
+            "after\n",
+        ]
+        result = get_replaced_content(
+            content,
+            ["new_a", "new_b"],
+            "# START",
+            "# END",
+            prefix='"',
+            suffix='",',
+            add_empty_lines=False,
+        )
+        assert result == [
+            "before\n",
+            "# START\n",
+            '"new_a",\n',
+            '"new_b",\n',
+            "# END\n",
+            "after\n",
+        ]
+
+    def test_replaces_with_empty_lines(self):
+        content = [
+            "before\n",
+            ".. START\n",
+            "old\n",
+            ".. END\n",
+            "after\n",
+        ]
+        result = get_replaced_content(
+            content,
+            ["item1", "item2"],
+            ".. START",
+            ".. END",
+            prefix="* ",
+            suffix="",
+            add_empty_lines=True,
+        )
+        assert result == [
+            "before\n",
+            ".. START\n",
+            "\n",
+            "* item1\n",
+            "* item2\n",
+            "\n",
+            ".. END\n",
+            "after\n",
+        ]
+
+    def test_preserves_content_outside_markers(self):
+        content = [
+            "line1\n",
+            "line2\n",
+            "# START\n",
+            "old\n",
+            "# END\n",
+            "line3\n",
+            "line4\n",
+        ]
+        result = get_replaced_content(
+            content, ["new"], "# START", "# END", prefix="", suffix="", 
add_empty_lines=False
+        )
+        assert result[0] == "line1\n"
+        assert result[1] == "line2\n"
+        assert result[-2] == "line3\n"
+        assert result[-1] == "line4\n"
+
+    def test_empty_extras_list(self):
+        content = [
+            "# START\n",
+            "old\n",
+            "# END\n",
+        ]
+        result = get_replaced_content(
+            content, [], "# START", "# END", prefix="", suffix="", 
add_empty_lines=False
+        )
+        assert result == ["# START\n", "# END\n"]
+
+    def test_no_markers_returns_content_unchanged(self):
+        content = ["line1\n", "line2\n", "line3\n"]
+        result = get_replaced_content(
+            content,
+            ["new"],
+            "# NONEXISTENT START",
+            "# NONEXISTENT END",
+            prefix="",
+            suffix="",
+            add_empty_lines=False,
+        )
+        assert result == content
diff --git a/scripts/tests/ci/prek/test_checkout_no_credentials.py 
b/scripts/tests/ci/prek/test_checkout_no_credentials.py
new file mode 100644
index 00000000000..bd94d6f9fd6
--- /dev/null
+++ b/scripts/tests/ci/prek/test_checkout_no_credentials.py
@@ -0,0 +1,251 @@
+# 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.
+"""Tests for checkout_no_credentials.py workflow validation logic.
+
+The script has a module-level guard preventing import, so we replicate
+the core check_file logic here and test it against the same rules.
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+import yaml
+
+ACTIONS_CHECKOUT_V4 = "actions/checkout@v4"
+
+
+def check_file(the_file: Path) -> int:
+    """Replicate the check_file logic from checkout_no_credentials.py."""
+    error_num = 0
+    res = yaml.safe_load(the_file.read_text())
+    for job in res["jobs"].values():
+        if job.get("steps") is None:
+            continue
+        for step in job["steps"]:
+            uses = step.get("uses")
+            if uses is not None and uses.startswith("actions/checkout"):
+                with_clause = step.get("with")
+                if with_clause is None:
+                    error_num += 1
+                    continue
+                path = with_clause.get("path")
+                if path == "constraints":
+                    continue
+                if step.get("id") == "checkout-for-backport":
+                    continue
+                persist_credentials = with_clause.get("persist-credentials")
+                if persist_credentials is None:
+                    error_num += 1
+                    continue
+                if persist_credentials:
+                    error_num += 1
+                    continue
+    return error_num
+
+
+class TestCheckFile:
+    def test_checkout_with_persist_credentials_false(self, 
write_workflow_file):
+        workflow = {
+            "jobs": {
+                "build": {
+                    "steps": [
+                        {
+                            "name": "Checkout",
+                            "uses": ACTIONS_CHECKOUT_V4,
+                            "with": {"persist-credentials": False},
+                        }
+                    ]
+                }
+            }
+        }
+        path = write_workflow_file(workflow)
+        assert check_file(path) == 0
+
+    def test_checkout_without_with_clause(self, write_workflow_file):
+        workflow = {
+            "jobs": {
+                "build": {
+                    "steps": [
+                        {
+                            "name": "Checkout",
+                            "uses": ACTIONS_CHECKOUT_V4,
+                        }
+                    ]
+                }
+            }
+        }
+        path = write_workflow_file(workflow)
+        assert check_file(path) == 1
+
+    def test_checkout_without_persist_credentials(self, write_workflow_file):
+        workflow = {
+            "jobs": {
+                "build": {
+                    "steps": [
+                        {
+                            "name": "Checkout",
+                            "uses": ACTIONS_CHECKOUT_V4,
+                            "with": {"fetch-depth": 0},
+                        }
+                    ]
+                }
+            }
+        }
+        path = write_workflow_file(workflow)
+        assert check_file(path) == 1
+
+    def test_checkout_with_persist_credentials_true(self, write_workflow_file):
+        workflow = {
+            "jobs": {
+                "build": {
+                    "steps": [
+                        {
+                            "name": "Checkout",
+                            "uses": ACTIONS_CHECKOUT_V4,
+                            "with": {"persist-credentials": True},
+                        }
+                    ]
+                }
+            }
+        }
+        path = write_workflow_file(workflow)
+        assert check_file(path) == 1
+
+    def test_constraints_path_exception(self, write_workflow_file):
+        workflow = {
+            "jobs": {
+                "build": {
+                    "steps": [
+                        {
+                            "name": "Checkout constraints",
+                            "uses": ACTIONS_CHECKOUT_V4,
+                            "with": {"path": "constraints"},
+                        }
+                    ]
+                }
+            }
+        }
+        path = write_workflow_file(workflow)
+        assert check_file(path) == 0
+
+    def test_backport_id_exception(self, write_workflow_file):
+        workflow = {
+            "jobs": {
+                "build": {
+                    "steps": [
+                        {
+                            "name": "Checkout for backport",
+                            "id": "checkout-for-backport",
+                            "uses": ACTIONS_CHECKOUT_V4,
+                            "with": {"fetch-depth": 0},
+                        }
+                    ]
+                }
+            }
+        }
+        path = write_workflow_file(workflow)
+        assert check_file(path) == 0
+
+    def test_non_checkout_step_ignored(self, write_workflow_file):
+        workflow = {
+            "jobs": {
+                "build": {
+                    "steps": [
+                        {
+                            "name": "Setup Python",
+                            "uses": "actions/setup-python@v5",
+                        }
+                    ]
+                }
+            }
+        }
+        path = write_workflow_file(workflow)
+        assert check_file(path) == 0
+
+    def test_job_without_steps(self, write_workflow_file):
+        workflow = {
+            "jobs": {
+                "build": {
+                    "uses": "./.github/workflows/reusable.yml",
+                }
+            }
+        }
+        path = write_workflow_file(workflow)
+        assert check_file(path) == 0
+
+    def test_multiple_errors(self, write_workflow_file):
+        workflow = {
+            "jobs": {
+                "build": {
+                    "steps": [
+                        {
+                            "name": "Checkout 1",
+                            "uses": ACTIONS_CHECKOUT_V4,
+                        },
+                        {
+                            "name": "Checkout 2",
+                            "uses": ACTIONS_CHECKOUT_V4,
+                            "with": {"persist-credentials": True},
+                        },
+                    ]
+                }
+            }
+        }
+        path = write_workflow_file(workflow)
+        assert check_file(path) == 2
+
+    def test_multiple_jobs(self, write_workflow_file):
+        workflow = {
+            "jobs": {
+                "build": {
+                    "steps": [
+                        {
+                            "name": "Checkout",
+                            "uses": ACTIONS_CHECKOUT_V4,
+                            "with": {"persist-credentials": False},
+                        }
+                    ]
+                },
+                "test": {
+                    "steps": [
+                        {
+                            "name": "Checkout",
+                            "uses": ACTIONS_CHECKOUT_V4,
+                        }
+                    ]
+                },
+            }
+        }
+        path = write_workflow_file(workflow)
+        assert check_file(path) == 1
+
+    def test_run_step_without_uses(self, write_workflow_file):
+        workflow = {
+            "jobs": {
+                "build": {
+                    "steps": [
+                        {
+                            "name": "Run tests",
+                            "run": "pytest",
+                        }
+                    ]
+                }
+            }
+        }
+        path = write_workflow_file(workflow)
+        assert check_file(path) == 0
diff --git a/scripts/tests/ci/prek/test_common_prek_utils.py 
b/scripts/tests/ci/prek/test_common_prek_utils.py
new file mode 100644
index 00000000000..e6da2f74be0
--- /dev/null
+++ b/scripts/tests/ci/prek/test_common_prek_utils.py
@@ -0,0 +1,425 @@
+# 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
+
+from pathlib import Path
+
+import pytest
+from ci.prek.common_prek_utils import (
+    ConsoleDiff,
+    check_list_sorted,
+    get_imports_from_file,
+    get_provider_base_dir_from_path,
+    get_provider_id_from_path,
+    initialize_breeze_prek,
+    insert_documentation,
+    pre_process_mypy_files,
+    read_airflow_version,
+    read_allowed_kubernetes_versions,
+    temporary_tsc_project,
+)
+
+PROVIDERS_AMAZON_S3_PATH = "providers/amazon/hooks/s3.py"
+AIRFLOW_MODELS_DAG_PATH = "airflow/models/dag.py"
+
+
+class TestPreProcessMypyFiles:
+    def test_excludes_conftest(self):
+        files = ["tests/conftest.py", "tests/test_foo.py"]
+        result = pre_process_mypy_files(files)
+        assert "tests/conftest.py" not in result
+        assert "tests/test_foo.py" in result
+
+    def test_excludes_init(self):
+        files = ["airflow/__init__.py", AIRFLOW_MODELS_DAG_PATH]
+        result = pre_process_mypy_files(files)
+        assert "airflow/__init__.py" not in result
+        assert AIRFLOW_MODELS_DAG_PATH in result
+
+    def test_excludes_both(self):
+        files = ["conftest.py", "__init__.py", "test_foo.py"]
+        result = pre_process_mypy_files(files)
+        assert result == ["test_foo.py"]
+
+    def test_empty_list(self):
+        assert pre_process_mypy_files([]) == []
+
+    def test_on_non_main_branch_excludes_providers(self, monkeypatch):
+        monkeypatch.setenv("DEFAULT_BRANCH", "v2-10-stable")
+        files = [PROVIDERS_AMAZON_S3_PATH, AIRFLOW_MODELS_DAG_PATH]
+        result = pre_process_mypy_files(files)
+        assert PROVIDERS_AMAZON_S3_PATH not in result
+        assert AIRFLOW_MODELS_DAG_PATH in result
+
+    def test_on_main_branch_keeps_providers(self, monkeypatch):
+        monkeypatch.setenv("DEFAULT_BRANCH", "main")
+        files = [PROVIDERS_AMAZON_S3_PATH, AIRFLOW_MODELS_DAG_PATH]
+        result = pre_process_mypy_files(files)
+        assert PROVIDERS_AMAZON_S3_PATH in result
+        assert AIRFLOW_MODELS_DAG_PATH in result
+
+    def test_no_default_branch_keeps_providers(self, monkeypatch):
+        monkeypatch.delenv("DEFAULT_BRANCH", raising=False)
+        files = [PROVIDERS_AMAZON_S3_PATH]
+        result = pre_process_mypy_files(files)
+        assert PROVIDERS_AMAZON_S3_PATH in result
+
+
+class TestGetImportsFromFile:
+    def test_simple_import(self, write_python_file):
+        path = write_python_file("import os\nimport sys\n")
+        result = get_imports_from_file(path, only_top_level=True)
+        assert "os" in result
+        assert "sys" in result
+
+    def test_from_import(self, write_python_file):
+        path = write_python_file("from collections import defaultdict\n")
+        result = get_imports_from_file(path, only_top_level=True)
+        assert "collections.defaultdict" in result
+
+    def test_skips_future_imports(self, write_python_file):
+        path = write_python_file("from __future__ import annotations\nimport 
os\n")
+        result = get_imports_from_file(path, only_top_level=True)
+        assert not any("__future__" in imp for imp in result)
+        assert "os" in result
+
+    def test_top_level_only_excludes_nested(self, write_python_file):
+        code = """\
+        import os
+
+        def inner():
+            import json
+        """
+        path = write_python_file(code)
+        top_level = get_imports_from_file(path, only_top_level=True)
+        assert "os" in top_level
+        assert "json" not in top_level
+
+    def test_all_levels_includes_nested(self, write_python_file):
+        code = """\
+        import os
+
+        def inner():
+            import json
+        """
+        path = write_python_file(code)
+        all_level = get_imports_from_file(path, only_top_level=False)
+        assert "os" in all_level
+        assert "json" in all_level
+
+    def test_multiple_from_imports(self, write_python_file):
+        path = write_python_file("from pathlib import Path, PurePath\n")
+        result = get_imports_from_file(path, only_top_level=True)
+        assert "pathlib.Path" in result
+        assert "pathlib.PurePath" in result
+
+    def test_empty_file(self, write_python_file):
+        path = write_python_file("")
+        result = get_imports_from_file(path, only_top_level=True)
+        assert result == []
+
+
+class TestInsertDocumentation:
+    def test_replaces_content_between_header_and_footer(self, write_text_file):
+        path = write_text_file("before\n<!-- START -->\nold content\n<!-- END 
-->\nafter\n")
+        result = insert_documentation(
+            path,
+            content=["new line 1\n", "new line 2\n"],
+            header="<!-- START -->",
+            footer="<!-- END -->",
+        )
+        assert result is True
+        text = path.read_text()
+        assert "new line 1" in text
+        assert "new line 2" in text
+        assert "old content" not in text
+        assert "before" in text
+        assert "after" in text
+
+    def test_returns_false_when_content_unchanged(self, write_text_file):
+        path = write_text_file("before\n<!-- START -->\nkept\n<!-- END 
-->\nafter\n")
+        result = insert_documentation(
+            path,
+            content=["kept\n"],
+            header="<!-- START -->",
+            footer="<!-- END -->",
+        )
+        assert result is False
+
+    def test_exits_when_header_not_found(self, write_text_file):
+        path = write_text_file("no markers here\n")
+        with pytest.raises(SystemExit):
+            insert_documentation(
+                path,
+                content=["anything\n"],
+                header="<!-- MISSING -->",
+                footer="<!-- END -->",
+            )
+
+    def test_add_comment_prefixes_lines(self, write_text_file):
+        path = write_text_file("before\n# START\nold\n# END\nafter\n")
+        result = insert_documentation(
+            path,
+            content=["line one\n", "line two\n"],
+            header="# START",
+            footer="# END",
+            add_comment=True,
+        )
+        assert result is True
+        text = path.read_text()
+        assert "# line one\n" in text
+        assert "# line two\n" in text
+
+    def test_add_comment_handles_blank_lines(self, write_text_file):
+        path = write_text_file("# START\nold\n# END\n")
+        result = insert_documentation(
+            path,
+            content=["\n"],
+            header="# START",
+            footer="# END",
+            add_comment=True,
+        )
+        assert result is True
+        text = path.read_text()
+        assert "#\n" in text
+
+    def test_preserves_header_and_footer_lines(self, write_text_file):
+        path = write_text_file("<!-- START -->\nold\n<!-- END -->\n")
+        insert_documentation(
+            path,
+            content=["new\n"],
+            header="<!-- START -->",
+            footer="<!-- END -->",
+        )
+        text = path.read_text()
+        assert "<!-- START -->" in text
+        assert "<!-- END -->" in text
+
+    def test_header_with_leading_whitespace(self, write_text_file):
+        path = write_text_file("  <!-- START -->\nold\n  <!-- END -->\n")
+        result = insert_documentation(
+            path,
+            content=["new\n"],
+            header="<!-- START -->",
+            footer="<!-- END -->",
+        )
+        assert result is True
+        assert "new" in path.read_text()
+
+    def test_multiple_content_lines(self, write_text_file):
+        path = write_text_file("header line\n## BEGIN\nreplaced\n## 
FINISH\nfooter line\n")
+        insert_documentation(
+            path,
+            content=["a\n", "b\n", "c\n"],
+            header="## BEGIN",
+            footer="## FINISH",
+        )
+        text = path.read_text()
+        assert "a\nb\nc\n" in text
+        assert "replaced" not in text
+        assert "header line" in text
+        assert "footer line" in text
+
+
+class TestReadAirflowVersion:
+    def test_returns_version_string(self):
+        version = read_airflow_version()
+        assert isinstance(version, str)
+        # Airflow version should look like X.Y.Z or X.Y.Z.devN
+        parts = version.split(".")
+        assert len(parts) >= 3
+        assert parts[0].isdigit()
+        assert parts[1].isdigit()
+
+
+class TestReadAllowedKubernetesVersions:
+    def test_returns_list_of_versions(self):
+        versions = read_allowed_kubernetes_versions()
+        assert isinstance(versions, list)
+        assert len(versions) > 0
+
+    def test_versions_have_no_v_prefix(self):
+        versions = read_allowed_kubernetes_versions()
+        for v in versions:
+            assert not v.startswith("v"), f"Version {v!r} should not have 'v' 
prefix"
+
+    def test_versions_look_like_semver(self):
+        versions = read_allowed_kubernetes_versions()
+        for v in versions:
+            parts = v.split(".")
+            assert len(parts) >= 2, f"Version {v!r} should have at least 
major.minor"
+            assert parts[0].isdigit()
+            assert parts[1].isdigit()
+
+
+class TestConsoleDiff:
+    def test_dump_added_lines(self):
+        diff = ConsoleDiff()
+        lines = list(diff._dump("+", ["line1", "line2"], 0, 2))
+        assert lines == ["[green]+ line1[/]", "[green]+ line2[/]"]
+
+    def test_dump_removed_lines(self):
+        diff = ConsoleDiff()
+        lines = list(diff._dump("-", ["line1"], 0, 1))
+        assert lines == ["[red]- line1[/]"]
+
+    def test_dump_unchanged_lines(self):
+        diff = ConsoleDiff()
+        lines = list(diff._dump(" ", ["line1", "line2"], 0, 2))
+        assert lines == ["  line1", "  line2"]
+
+    def test_dump_range(self):
+        diff = ConsoleDiff()
+        lines = list(diff._dump("+", ["a", "b", "c", "d"], 1, 3))
+        assert lines == ["[green]+ b[/]", "[green]+ c[/]"]
+
+
+class TestCheckListSorted:
+    def test_sorted_list_returns_true(self):
+        errors: list[str] = []
+        result = check_list_sorted(["a", "b", "c"], "test list", errors)
+        assert result is True
+        assert errors == []
+
+    def test_unsorted_list_returns_false(self):
+        errors: list[str] = []
+        result = check_list_sorted(["c", "a", "b"], "test list", errors)
+        assert result is False
+        assert len(errors) == 1
+        assert "not sorted" in errors[0]
+
+    def test_duplicates_returns_false(self):
+        errors: list[str] = []
+        result = check_list_sorted(["a", "a", "b"], "test list", errors)
+        assert result is False
+        assert len(errors) == 1
+
+    def test_empty_list_returns_true(self):
+        errors: list[str] = []
+        result = check_list_sorted([], "empty", errors)
+        assert result is True
+        assert errors == []
+
+    def test_single_element_returns_true(self):
+        errors: list[str] = []
+        result = check_list_sorted(["only"], "single", errors)
+        assert result is True
+        assert errors == []
+
+
+class TestGetProviderIdFromPath:
+    def test_simple_provider(self, create_provider_tree):
+        file_path = create_provider_tree("providers/amazon")
+        result = get_provider_id_from_path(file_path)
+        assert result == "amazon"
+
+    def test_nested_provider(self, create_provider_tree):
+        file_path = create_provider_tree("providers/apache/hive")
+        result = get_provider_id_from_path(file_path)
+        assert result == "apache.hive"
+
+    def test_no_provider_yaml(self, tmp_path):
+        some_dir = tmp_path / "no_provider"
+        some_dir.mkdir()
+        test_file = some_dir / "file.py"
+        test_file.touch()
+        result = get_provider_id_from_path(test_file)
+        assert result is None
+
+    def test_no_providers_parent(self, tmp_path):
+        # provider.yaml exists but no "providers" parent directory
+        some_dir = tmp_path / "something" / "else"
+        some_dir.mkdir(parents=True)
+        (some_dir / "provider.yaml").touch()
+        test_file = some_dir / "file.py"
+        test_file.touch()
+        result = get_provider_id_from_path(test_file)
+        assert result is None
+
+
+class TestGetProviderBaseDirFromPath:
+    def test_finds_provider_dir(self, tmp_path):
+        provider_dir = tmp_path / "providers" / "amazon"
+        provider_dir.mkdir(parents=True)
+        (provider_dir / "provider.yaml").touch()
+        sub_file = provider_dir / "hooks" / "s3.py"
+        sub_file.parent.mkdir()
+        sub_file.touch()
+        result = get_provider_base_dir_from_path(sub_file)
+        assert result == provider_dir
+
+    def test_returns_none_without_provider_yaml(self, tmp_path):
+        some_dir = tmp_path / "no_provider"
+        some_dir.mkdir()
+        test_file = some_dir / "file.py"
+        test_file.touch()
+        result = get_provider_base_dir_from_path(test_file)
+        assert result is None
+
+    def test_finds_nearest_provider_yaml(self, tmp_path):
+        outer = tmp_path / "providers" / "google"
+        inner = outer / "cloud"
+        inner.mkdir(parents=True)
+        (outer / "provider.yaml").touch()
+        test_file = inner / "hooks.py"
+        test_file.touch()
+        result = get_provider_base_dir_from_path(test_file)
+        assert result == outer
+
+
+class TestInitializeBreezePrek:
+    def test_raises_when_not_main(self):
+        with pytest.raises(SystemExit, match="intended to be executed"):
+            initialize_breeze_prek("some_module", "script.py")
+
+    def test_exits_when_skip_env_set(self, monkeypatch):
+        monkeypatch.setenv("SKIP_BREEZE_PREK_HOOKS", "1")
+        with pytest.raises(SystemExit) as exc_info:
+            initialize_breeze_prek("__main__", "script.py")
+        assert exc_info.value.code == 0
+
+    def test_exits_when_breeze_not_found(self, monkeypatch):
+        monkeypatch.delenv("SKIP_BREEZE_PREK_HOOKS", raising=False)
+        monkeypatch.setattr("shutil.which", lambda _: None)
+        with pytest.raises(SystemExit) as exc_info:
+            initialize_breeze_prek("__main__", "script.py")
+        assert exc_info.value.code == 1
+
+
+class TestTemporaryTscProject:
+    def test_creates_temp_tsconfig(self, tmp_path):
+        tsconfig = tmp_path / "tsconfig.json"
+        tsconfig.write_text("{}")
+        with temporary_tsc_project(tsconfig, ["src/app.ts", "src/main.ts"]) as 
temp:
+            content = Path(temp.name).read_text()
+            assert '"src/app.ts"' in content
+            assert '"src/main.ts"' in content
+            assert f"./{tsconfig.name}" in content
+
+    def test_raises_when_tsconfig_missing(self, tmp_path):
+        missing = tmp_path / "nonexistent.json"
+        with pytest.raises(RuntimeError, match="Cannot find"):
+            with temporary_tsc_project(missing, []):
+                pass
+
+    def test_extends_original_tsconfig(self, tmp_path):
+        tsconfig = tmp_path / "tsconfig.base.json"
+        tsconfig.write_text("{}")
+        with temporary_tsc_project(tsconfig, ["file.ts"]) as temp:
+            content = Path(temp.name).read_text()
+            assert f'"extends": "./{tsconfig.name}"' in content
+            assert '"include": ["file.ts"]' in content
diff --git a/scripts/tests/ci/prek/test_new_session_in_provide_session.py 
b/scripts/tests/ci/prek/test_new_session_in_provide_session.py
new file mode 100644
index 00000000000..e59c200e5a8
--- /dev/null
+++ b/scripts/tests/ci/prek/test_new_session_in_provide_session.py
@@ -0,0 +1,243 @@
+# 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 ast
+import textwrap
+
+import pytest
+from ci.prek.new_session_in_provide_session import (
+    _annotation_has_none,
+    _get_session_arg_and_default,
+    _is_decorated_correctly,
+    _is_new_session_or_none,
+    _iter_incorrect_new_session_usages,
+    _SessionDefault,
+)
+
+
+def _parse_func_args(code: str) -> ast.arguments:
+    """Parse a function definition and return its arguments node."""
+    node = ast.parse(textwrap.dedent(code)).body[0]
+    assert isinstance(node, ast.FunctionDef)
+    return node.args
+
+
+def _parse_expr(code: str) -> ast.expr:
+    """Parse a single expression."""
+    node = ast.parse(code, mode="eval").body
+    return node
+
+
[email protected]
+def check_session_code(write_python_file):
+    """Factory fixture: write code to a temp file and check for incorrect 
NEW_SESSION usages."""
+
+    def _check(code: str) -> list[ast.FunctionDef]:
+        path = write_python_file(code)
+        return list(_iter_incorrect_new_session_usages(path))
+
+    return _check
+
+
+class TestGetSessionArgAndDefault:
+    def test_no_session_arg(self):
+        args = _parse_func_args("def foo(x, y): pass")
+        assert _get_session_arg_and_default(args) is None
+
+    def test_session_positional_no_default(self):
+        args = _parse_func_args("def foo(session): pass")
+        result = _get_session_arg_and_default(args)
+        assert result is not None
+        assert result.argument.arg == "session"
+        assert result.default is None
+
+    def test_session_positional_with_default_none(self):
+        args = _parse_func_args("def foo(session=None): pass")
+        result = _get_session_arg_and_default(args)
+        assert result is not None
+        assert result.argument.arg == "session"
+        assert isinstance(result.default, ast.Constant)
+        assert result.default.value is None
+
+    def test_session_kwonly_with_default(self):
+        args = _parse_func_args("def foo(*, session=NEW_SESSION): pass")
+        result = _get_session_arg_and_default(args)
+        assert result is not None
+        assert result.argument.arg == "session"
+        assert isinstance(result.default, ast.Name)
+
+    def test_session_among_other_args(self):
+        args = _parse_func_args("def foo(x, y, session=None, z=5): pass")
+        result = _get_session_arg_and_default(args)
+        assert result is not None
+        assert result.argument.arg == "session"
+
+    def test_kwonly_session_among_other_kwargs(self):
+        args = _parse_func_args("def foo(x, *, timeout=30, session=None): 
pass")
+        result = _get_session_arg_and_default(args)
+        assert result is not None
+        assert result.argument.arg == "session"
+
+
+class TestIsNewSessionOrNone:
+    def test_none_constant(self):
+        expr = _parse_expr("None")
+        assert _is_new_session_or_none(expr) == _SessionDefault.none
+
+    def test_new_session_name(self):
+        expr = _parse_expr("NEW_SESSION")
+        assert _is_new_session_or_none(expr) == _SessionDefault.new_session
+
+    def test_other_name(self):
+        expr = _parse_expr("SOMETHING_ELSE")
+        assert _is_new_session_or_none(expr) is None
+
+    def test_integer_constant(self):
+        expr = _parse_expr("42")
+        assert _is_new_session_or_none(expr) is None
+
+    def test_string_constant(self):
+        expr = _parse_expr("'hello'")
+        assert _is_new_session_or_none(expr) is None
+
+
+class TestIsDecoratedCorrectly:
+    def test_provide_session_decorator(self):
+        func = ast.parse("@provide_session\ndef foo(): pass").body[0]
+        assert _is_decorated_correctly(func.decorator_list) is True
+
+    def test_overload_decorator(self):
+        func = ast.parse("@overload\ndef foo(): pass").body[0]
+        assert _is_decorated_correctly(func.decorator_list) is True
+
+    def test_abstractmethod_decorator(self):
+        func = ast.parse("@abstractmethod\ndef foo(): pass").body[0]
+        assert _is_decorated_correctly(func.decorator_list) is True
+
+    def test_no_decorator(self):
+        func = ast.parse("def foo(): pass").body[0]
+        assert _is_decorated_correctly(func.decorator_list) is False
+
+    def test_unrelated_decorator(self):
+        func = ast.parse("@staticmethod\ndef foo(): pass").body[0]
+        assert _is_decorated_correctly(func.decorator_list) is False
+
+    def test_multiple_decorators_with_provide_session(self):
+        code = "@staticmethod\n@provide_session\ndef foo(): pass"
+        func = ast.parse(code).body[0]
+        assert _is_decorated_correctly(func.decorator_list) is True
+
+
+class TestAnnotationHasNone:
+    def test_none_value(self):
+        assert _annotation_has_none(None) is False
+
+    def test_none_constant(self):
+        expr = _parse_expr("None")
+        assert _annotation_has_none(expr) is True
+
+    def test_non_none_constant(self):
+        expr = _parse_expr("42")
+        assert _annotation_has_none(expr) is False
+
+    def test_union_with_none(self):
+        expr = _parse_expr("int | None")
+        assert _annotation_has_none(expr) is True
+
+    def test_union_without_none(self):
+        expr = _parse_expr("int | str")
+        assert _annotation_has_none(expr) is False
+
+    def test_nested_union_with_none(self):
+        expr = _parse_expr("int | str | None")
+        assert _annotation_has_none(expr) is True
+
+    def test_name_type(self):
+        expr = _parse_expr("Session")
+        assert _annotation_has_none(expr) is False
+
+
+class TestIterIncorrectNewSessionUsages:
+    def test_correct_provide_session(self, check_session_code):
+        code = """\
+        @provide_session
+        def foo(session=NEW_SESSION):
+            pass
+        """
+        assert check_session_code(code) == []
+
+    def test_incorrect_new_session_without_decorator(self, check_session_code):
+        code = """\
+        def foo(session=NEW_SESSION):
+            pass
+        """
+        errors = check_session_code(code)
+        assert len(errors) == 1
+        assert errors[0].name == "foo"
+
+    def test_no_session_arg(self, check_session_code):
+        code = """\
+        def foo(x, y):
+            pass
+        """
+        assert check_session_code(code) == []
+
+    def test_session_no_default(self, check_session_code):
+        code = """\
+        def foo(session):
+            pass
+        """
+        assert check_session_code(code) == []
+
+    def test_none_default_with_none_annotation(self, check_session_code):
+        code = """\
+        def foo(session: Session | None = None):
+            pass
+        """
+        assert check_session_code(code) == []
+
+    def test_none_default_without_none_annotation(self, check_session_code):
+        code = """\
+        def foo(session: Session = None):
+            pass
+        """
+        errors = check_session_code(code)
+        assert len(errors) == 1
+
+    def test_overload_allows_new_session(self, check_session_code):
+        code = """\
+        @overload
+        def foo(session=NEW_SESSION):
+            pass
+        """
+        assert check_session_code(code) == []
+
+    def test_abstractmethod_allows_new_session(self, check_session_code):
+        code = """\
+        @abstractmethod
+        def foo(session=NEW_SESSION):
+            pass
+        """
+        assert check_session_code(code) == []
+
+    def test_other_default_value_is_ignored(self, check_session_code):
+        code = """\
+        def foo(session="default"):
+            pass
+        """
+        assert check_session_code(code) == []
diff --git a/scripts/tests/ci/prek/test_newsfragments.py 
b/scripts/tests/ci/prek/test_newsfragments.py
new file mode 100644
index 00000000000..bd249e45405
--- /dev/null
+++ b/scripts/tests/ci/prek/test_newsfragments.py
@@ -0,0 +1,81 @@
+# 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 ci.prek.newsfragments import VALID_CHANGE_TYPES, validate_newsfragment
+
+ALL_CHANGE_TYPES = sorted(VALID_CHANGE_TYPES)
+NON_SIGNIFICANT_CHANGE_TYPES = sorted(VALID_CHANGE_TYPES - {"significant"})
+
+
+class TestNewsfragmentFilenameValidation:
+    @pytest.mark.parametrize("change_type", ALL_CHANGE_TYPES)
+    def test_valid_filename_all_types(self, change_type):
+        errors = validate_newsfragment(f"12345.{change_type}.rst", ["A 
change"])
+        assert errors == []
+
+    def test_too_few_parts(self):
+        errors = validate_newsfragment("12345.rst", ["A change"])
+        assert len(errors) == 1
+        assert "unexpected filename" in errors[0]
+
+    def test_too_many_parts(self):
+        errors = validate_newsfragment("12345.bugfix.extra.rst", ["A change"])
+        assert len(errors) == 1
+        assert "unexpected filename" in errors[0]
+
+    def test_invalid_change_type(self):
+        errors = validate_newsfragment("12345.invalid.rst", ["A change"])
+        assert len(errors) == 1
+        assert "unexpected type" in errors[0]
+
+
+class TestNewsfragmentContentValidation:
+    @pytest.mark.parametrize("change_type", NON_SIGNIFICANT_CHANGE_TYPES)
+    def test_non_significant_single_line_ok(self, change_type):
+        errors = validate_newsfragment(f"123.{change_type}.rst", ["Fix 
something"])
+        assert errors == []
+
+    @pytest.mark.parametrize("change_type", NON_SIGNIFICANT_CHANGE_TYPES)
+    def test_non_significant_multi_line_fails(self, change_type):
+        errors = validate_newsfragment(f"123.{change_type}.rst", ["Fix 
something", "More details"])
+        assert len(errors) == 1
+        assert "single line" in errors[0]
+
+    def test_significant_single_line_ok(self):
+        errors = validate_newsfragment("123.significant.rst", ["Big change"])
+        assert errors == []
+
+    def test_significant_two_lines_fails(self):
+        errors = validate_newsfragment("123.significant.rst", ["Big change", 
"Second line"])
+        assert len(errors) == 1
+        assert "1, or 3+ lines" in errors[0]
+
+    def test_significant_three_lines_with_blank_second_ok(self):
+        errors = validate_newsfragment("123.significant.rst", ["Big change", 
"", "Details here"])
+        assert errors == []
+
+    def test_significant_three_lines_without_blank_second_fails(self):
+        errors = validate_newsfragment("123.significant.rst", ["Big change", 
"Not blank", "Details"])
+        assert len(errors) == 1
+        assert "empty second line" in errors[0]
+
+    def test_significant_many_lines_ok(self):
+        lines = ["Big change", "", "Details here", "More details", "Even more"]
+        errors = validate_newsfragment("123.significant.rst", lines)
+        assert errors == []
diff --git a/scripts/tests/ci/prek/test_unittest_testcase.py 
b/scripts/tests/ci/prek/test_unittest_testcase.py
new file mode 100644
index 00000000000..dfc03f29dc1
--- /dev/null
+++ b/scripts/tests/ci/prek/test_unittest_testcase.py
@@ -0,0 +1,93 @@
+# 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
+
+from ci.prek.unittest_testcase import check_test_file
+
+
+class TestCheckTestFile:
+    def test_no_testcase_inheritance(self, write_python_file):
+        path = write_python_file("""\
+        class TestFoo:
+            def test_something(self):
+                pass
+        """)
+        assert check_test_file(str(path)) == 0
+
+    def test_direct_testcase_inheritance(self, write_python_file):
+        path = write_python_file("""\
+        from unittest import TestCase
+
+        class TestFoo(TestCase):
+            def test_something(self):
+                pass
+        """)
+        assert check_test_file(str(path)) == 1
+
+    def test_attribute_testcase_inheritance(self, write_python_file):
+        path = write_python_file("""\
+        import unittest
+
+        class TestFoo(unittest.TestCase):
+            def test_something(self):
+                pass
+        """)
+        assert check_test_file(str(path)) == 1
+
+    def test_multiple_testcase_classes(self, write_python_file):
+        path = write_python_file("""\
+        from unittest import TestCase
+
+        class TestFoo(TestCase):
+            pass
+
+        class TestBar(TestCase):
+            pass
+        """)
+        assert check_test_file(str(path)) == 2
+
+    def test_inherited_from_local_testcase_class(self, write_python_file):
+        path = write_python_file("""\
+        from unittest import TestCase
+
+        class TestBase(TestCase):
+            pass
+
+        class TestChild(TestBase):
+            pass
+        """)
+        # TestBase is detected first, then TestChild inherits from known class
+        assert check_test_file(str(path)) == 2
+
+    def test_no_classes(self, write_python_file):
+        path = write_python_file("""\
+        def test_something():
+            pass
+        """)
+        assert check_test_file(str(path)) == 0
+
+    def test_class_with_other_base(self, write_python_file):
+        path = write_python_file("""\
+        class TestFoo(SomeOtherBase):
+            def test_something(self):
+                pass
+        """)
+        assert check_test_file(str(path)) == 0
+
+    def test_empty_file(self, write_python_file):
+        path = write_python_file("")
+        assert check_test_file(str(path)) == 0

Reply via email to