This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch v3-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-1-test by this push:
new 43bd8f3eea9 [v3-1-test] Add pre-commit hook to verify version
consistency (#59509) (#59517)
43bd8f3eea9 is described below
commit 43bd8f3eea95a7b7f98fd754357d1764e17466e0
Author: Ephraim Anierobi <[email protected]>
AuthorDate: Thu Dec 18 18:49:57 2025 +0100
[v3-1-test] Add pre-commit hook to verify version consistency (#59509)
(#59517)
* [v3-1-test] Add pre-commit hook to verify version consistency (#59509)
Ensures Airflow and Task SDK versions remain consistent across all
configuration files. The hook validates:
This prevents version mismatches that could cause installation failures
or dependency resolution issues after releases.
(cherry picked from commit fbbce53abd89ca7e892f5c1035740aa7e03857f9)
Co-authored-by: Ephraim Anierobi <[email protected]>
* Use exact match for this branch
---
.pre-commit-config.yaml | 12 ++
scripts/ci/prek/check_version_consistency.py | 302 +++++++++++++++++++++++++++
2 files changed, 314 insertions(+)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2efcf12fe47..4177c5ef68e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -180,6 +180,18 @@ repos:
entry: ./scripts/ci/prek/check_min_python_version.py
language: python
require_serial: true
+ - id: check-version-consistency
+ name: Check version consistency
+ entry: ./scripts/ci/prek/check_version_consistency.py
+ language: python
+ files: >
+ (?x)
+ ^airflow-core/src/airflow/__init__\.py$|
+ ^airflow-core/pyproject\.toml$|
+ ^task-sdk/src/airflow/sdk/__init__\.py$|
+ ^pyproject\.toml$
+ pass_filenames: false
+ require_serial: true
- id: upgrade-important-versions
name: Upgrade important versions (manual)
entry: ./scripts/ci/prek/upgrade_important_versions.py
diff --git a/scripts/ci/prek/check_version_consistency.py
b/scripts/ci/prek/check_version_consistency.py
new file mode 100755
index 00000000000..b565787eff6
--- /dev/null
+++ b/scripts/ci/prek/check_version_consistency.py
@@ -0,0 +1,302 @@
+#!/usr/bin/env python
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# /// script
+# requires-python = ">=3.10,<3.11"
+# dependencies = [
+# "rich>=13.6.0",
+# "packaging>=25.0",
+# "tomli>=2.0.1",
+# ]
+# ///
+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,
+)
+
+
+def read_airflow_version() -> str:
+ """Read Airflow version from airflow-core/src/airflow/__init__.py"""
+ ast_obj = ast.parse((AIRFLOW_CORE_SOURCES_PATH / "airflow" /
"__init__.py").read_text())
+ for node in ast_obj.body:
+ if isinstance(node, ast.Assign):
+ if node.targets[0].id == "__version__": # type:
ignore[attr-defined]
+ return ast.literal_eval(node.value)
+
+ raise RuntimeError("Couldn't find __version__ in
airflow-core/src/airflow/__init__.py")
+
+
+def read_task_sdk_version() -> str:
+ """Read Task SDK version from task-sdk/src/airflow/sdk/__init__.py"""
+ ast_obj = ast.parse((AIRFLOW_TASK_SDK_SOURCES_PATH / "airflow" / "sdk" /
"__init__.py").read_text())
+ for node in ast_obj.body:
+ if isinstance(node, ast.Assign):
+ if node.targets[0].id == "__version__": # type:
ignore[attr-defined]
+ return ast.literal_eval(node.value)
+
+ raise RuntimeError("Couldn't find __version__ in
task-sdk/src/airflow/sdk/__init__.py")
+
+
+def read_airflow_version_from_pyproject() -> str:
+ """Read Airflow version from airflow-core/pyproject.toml"""
+ pyproject_path = AIRFLOW_ROOT_PATH / "airflow-core" / "pyproject.toml"
+ with pyproject_path.open("rb") as f:
+ data = tomllib.load(f)
+ version = data.get("project", {}).get("version")
+ if not version:
+ raise RuntimeError("Couldn't find version in
airflow-core/pyproject.toml")
+ return str(version)
+
+
+def read_task_sdk_dependency_constraint() -> str:
+ """Read Task SDK dependency constraint from airflow-core/pyproject.toml"""
+ pyproject_path = AIRFLOW_ROOT_PATH / "airflow-core" / "pyproject.toml"
+ with pyproject_path.open("rb") as f:
+ data = tomllib.load(f)
+ dependencies = data.get("project", {}).get("dependencies", [])
+ for dep in dependencies:
+ # Extract package name (everything before >=, >, <, ==, etc.)
+ package_name = re.split(r"[<>=!]", dep)[0].strip().strip("\"'")
+ if package_name == "apache-airflow-task-sdk":
+ # Extract the version constraint part
+ constraint_match = re.search(r"apache-airflow-task-sdk\s*(.*)",
dep)
+ if constraint_match:
+ return constraint_match.group(1).strip().strip("\"'")
+ return dep
+ raise RuntimeError("Couldn't find apache-airflow-task-sdk dependency in
airflow-core/pyproject.toml")
+
+
+def read_root_pyproject_version() -> str:
+ """Read version from root pyproject.toml"""
+ pyproject_path = AIRFLOW_ROOT_PATH / "pyproject.toml"
+ with pyproject_path.open("rb") as f:
+ data = tomllib.load(f)
+ version = data.get("project", {}).get("version")
+ if not version:
+ raise RuntimeError("Couldn't find version in pyproject.toml")
+ return str(version)
+
+
+def read_root_airflow_core_dependency() -> str:
+ """Read apache-airflow-core dependency from root pyproject.toml"""
+ pyproject_path = AIRFLOW_ROOT_PATH / "pyproject.toml"
+ with pyproject_path.open("rb") as f:
+ data = tomllib.load(f)
+ dependencies = data.get("project", {}).get("dependencies", [])
+ for dep in dependencies:
+ # Extract package name (everything before >=, >, <, ==, etc.)
+ package_name = re.split(r"[<>=!]", dep)[0].strip().strip("\"'")
+ if package_name == "apache-airflow-core":
+ # Extract the version constraint part
+ constraint_match = re.search(r"apache-airflow-core\s*(.*)", dep)
+ if constraint_match:
+ return constraint_match.group(1).strip().strip("\"'")
+ return dep
+ raise RuntimeError("Couldn't find apache-airflow-core dependency in
pyproject.toml")
+
+
+def read_root_task_sdk_dependency_constraint() -> str:
+ """Read Task SDK dependency constraint from root pyproject.toml"""
+ pyproject_path = AIRFLOW_ROOT_PATH / "pyproject.toml"
+ with pyproject_path.open("rb") as f:
+ data = tomllib.load(f)
+ dependencies = data.get("project", {}).get("dependencies", [])
+ for dep in dependencies:
+ # Extract package name (everything before >=, >, <, ==, etc.)
+ package_name = re.split(r"[<>=!]", dep)[0].strip().strip("\"'")
+ if package_name == "apache-airflow-task-sdk":
+ # Extract the version constraint part
+ constraint_match = re.search(r"apache-airflow-task-sdk\s*(.*)",
dep)
+ if constraint_match:
+ return constraint_match.group(1).strip().strip("\"'")
+ return dep
+ raise RuntimeError("Couldn't find apache-airflow-task-sdk dependency in
pyproject.toml")
+
+
+def check_version_in_constraint(version: str, constraint: str) -> bool:
+ """Check if version satisfies the constraint"""
+ try:
+ spec = SpecifierSet(constraint)
+ return Version(version) in spec
+ except Exception as e:
+ console.print(f"[red]Error parsing constraint '{constraint}':
{e}[/red]")
+ return False
+
+
+def get_exact_version_from_constraint(constraint: str) -> str | None:
+ """
+ Extract the exact version from a constraint string (== operator).
+ Returns the version if found, or None if not found.
+ """
+ try:
+ spec = SpecifierSet(constraint)
+ exact_version = None
+
+ for specifier in spec:
+ if specifier.operator == "==":
+ exact_version = specifier.version
+ break
+
+ return exact_version
+ except Exception:
+ return None
+
+
+def check_constraint_matches_version(version: str, constraint: str) ->
tuple[bool, str | None]:
+ """
+ Check if the constraint has an exact match (==) that matches the actual
version.
+ Returns (is_match, exact_version_from_constraint)
+ """
+ exact_version = get_exact_version_from_constraint(constraint)
+ if exact_version is None:
+ return (False, None)
+
+ # Check if the exact version in the constraint matches the actual version
+ return (Version(exact_version) == Version(version), exact_version)
+
+
+def main():
+ errors: list[str] = []
+
+ # Read versions
+ try:
+ airflow_version_init = read_airflow_version()
+ airflow_version_pyproject = read_airflow_version_from_pyproject()
+ root_pyproject_version = read_root_pyproject_version()
+ root_airflow_core_dep = read_root_airflow_core_dependency()
+ task_sdk_version_init = read_task_sdk_version()
+ task_sdk_constraint = read_task_sdk_dependency_constraint()
+ root_task_sdk_constraint = read_root_task_sdk_dependency_constraint()
+ except Exception as e:
+ console.print(f"[red]Error reading versions: {e}[/red]")
+ sys.exit(1)
+
+ # Check Airflow version consistency
+ if airflow_version_init != airflow_version_pyproject:
+ errors.append(
+ f"Airflow version mismatch:\n"
+ f" airflow-core/src/airflow/__init__.py: {airflow_version_init}\n"
+ f" airflow-core/pyproject.toml: {airflow_version_pyproject}"
+ )
+
+ # Check root pyproject.toml version matches Airflow version
+ if airflow_version_init != root_pyproject_version:
+ errors.append(
+ f"Root pyproject.toml version mismatch:\n"
+ f" airflow-core/src/airflow/__init__.py: {airflow_version_init}\n"
+ f" pyproject.toml: {root_pyproject_version}"
+ )
+
+ # Check root pyproject.toml apache-airflow-core dependency matches Airflow
version exactly
+ expected_core_dep = f"=={airflow_version_init}"
+ if root_airflow_core_dep != expected_core_dep:
+ errors.append(
+ f"Root pyproject.toml apache-airflow-core dependency mismatch:\n"
+ f" Expected: apache-airflow-core=={airflow_version_init}\n"
+ f" Found: apache-airflow-core{root_airflow_core_dep}"
+ )
+
+ # Check Task SDK version is within constraint in
airflow-core/pyproject.toml
+ if not check_version_in_constraint(task_sdk_version_init,
task_sdk_constraint):
+ errors.append(
+ f"Task SDK version does not satisfy constraint in
airflow-core/pyproject.toml:\n"
+ f" task-sdk/src/airflow/sdk/__init__.py:
{task_sdk_version_init}\n"
+ f" airflow-core/pyproject.toml constraint:
apache-airflow-task-sdk{task_sdk_constraint}"
+ )
+
+ # Check Task SDK constraint exact version matches actual version in
airflow-core/pyproject.toml
+ constraint_matches, exact_version = check_constraint_matches_version(
+ task_sdk_version_init, task_sdk_constraint
+ )
+ if not constraint_matches:
+ errors.append(
+ f"Task SDK constraint exact version does not match actual version
in airflow-core/pyproject.toml:\n"
+ f" task-sdk/src/airflow/sdk/__init__.py:
{task_sdk_version_init}\n"
+ f" airflow-core/pyproject.toml constraint exact:
{exact_version}\n"
+ f" Expected constraint to have exact version: ==
{task_sdk_version_init}"
+ )
+
+ # Check Task SDK version is within constraint in root pyproject.toml
+ if not check_version_in_constraint(task_sdk_version_init,
root_task_sdk_constraint):
+ errors.append(
+ f"Task SDK version does not satisfy constraint in
pyproject.toml:\n"
+ f" task-sdk/src/airflow/sdk/__init__.py:
{task_sdk_version_init}\n"
+ f" pyproject.toml constraint:
apache-airflow-task-sdk{root_task_sdk_constraint}"
+ )
+
+ # Check Task SDK constraint exact version matches actual version in root
pyproject.toml
+ root_constraint_matches, root_exact_version =
check_constraint_matches_version(
+ task_sdk_version_init, root_task_sdk_constraint
+ )
+ if not root_constraint_matches:
+ errors.append(
+ f"Task SDK constraint exact version does not match actual version
in pyproject.toml:\n"
+ f" task-sdk/src/airflow/sdk/__init__.py:
{task_sdk_version_init}\n"
+ f" pyproject.toml constraint exact: {root_exact_version}\n"
+ f" Expected constraint to have exact version: ==
{task_sdk_version_init}"
+ )
+
+ # Verify constraints match between airflow-core and root pyproject.toml
+ # Compare the exact versions extracted from constraints rather than raw
strings
+ airflow_core_exact = get_exact_version_from_constraint(task_sdk_constraint)
+ root_exact = get_exact_version_from_constraint(root_task_sdk_constraint)
+ if airflow_core_exact != root_exact:
+ errors.append(
+ f"Task SDK constraint exact version mismatch between
pyproject.toml files:\n"
+ f" airflow-core/pyproject.toml:
apache-airflow-task-sdk{task_sdk_constraint} (exact: {airflow_core_exact})\n"
+ f" pyproject.toml:
apache-airflow-task-sdk{root_task_sdk_constraint} (exact: {root_exact})"
+ )
+
+ # Report results
+ if errors:
+ console.print("[red]Version consistency check failed:[/red]\n")
+ for error in errors:
+ console.print(f"[red]{error}[/red]\n")
+ console.print(
+ "[yellow]Please ensure versions are consistent:\n"
+ " 1. Set the Airflow version in
airflow-core/src/airflow/__init__.py\n"
+ " 2. Set the Airflow version in airflow-core/pyproject.toml\n"
+ " 3. Set the Airflow version in pyproject.toml\n"
+ " 4. Set apache-airflow-core==<version> in pyproject.toml
dependencies\n"
+ " 5. Set the Task SDK version in
task-sdk/src/airflow/sdk/__init__.py\n"
+ " 6. Update the Task SDK version constraint in
airflow-core/pyproject.toml to include the Task SDK version\n"
+ " 7. Update the Task SDK version constraint in pyproject.toml to
include the Task SDK version[/yellow]"
+ )
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()