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 58c768d05bc Add helper to tie Airflow version to the min supported
Python version (#63596)
58c768d05bc is described below
commit 58c768d05bcd41a851fb48fb7fd6b93adaadd808
Author: Dev-iL <[email protected]>
AuthorDate: Sat Mar 14 23:29:36 2026 +0200
Add helper to tie Airflow version to the min supported Python version
(#63596)
---
.github/actions/migration_tests/action.yml | 20 ++++++-
.../testing/get_min_airflow_version_for_python.py | 60 ++++++++++++++++++++
scripts/tests/ci/testing/__init__.py | 16 ++++++
.../test_get_min_airflow_version_for_python.py | 64 ++++++++++++++++++++++
4 files changed, 157 insertions(+), 3 deletions(-)
diff --git a/.github/actions/migration_tests/action.yml
b/.github/actions/migration_tests/action.yml
index c4fcd4853f1..ae05e647899 100644
--- a/.github/actions/migration_tests/action.yml
+++ b/.github/actions/migration_tests/action.yml
@@ -28,13 +28,20 @@ runs:
- name: "Test migration file 2 to 3 migration: ${{env.BACKEND}}"
shell: bash
run: |
- breeze shell "${AIRFLOW_2_CMD}" --use-airflow-version 2.11.0
--airflow-extras pydantic --answer y &&
+ MIN_AIRFLOW_VERSION="$(
+ python ./scripts/ci/testing/get_min_airflow_version_for_python.py
"${PYTHON_VERSION}"
+ )"
+ breeze shell "${AIRFLOW_2_CMD}" \
+ --use-airflow-version "${MIN_AIRFLOW_VERSION}" \
+ --airflow-extras pydantic \
+ --answer y &&
breeze shell "export
AIRFLOW__DATABASE__EXTERNAL_DB_MANAGERS=${DB_MANGERS}
${AIRFLOW_3_CMD}" --no-db-cleanup
env:
COMPOSE_PROJECT_NAME: "docker-compose"
DB_RESET: "false"
DB_MANAGERS:
"airflow.providers.fab.auth_manager.models.db.FABDBManager"
+ PYTHON_VERSION: "${{ inputs.python-version }}"
AIRFLOW_2_CMD: >-
airflow db reset --skip-init -y &&
airflow db migrate --to-revision heads
@@ -50,14 +57,21 @@ runs:
COMPOSE_PROJECT_NAME: "docker-compose"
- name: "Test ORM migration 2 to 3: ${{env.BACKEND}}"
shell: bash
- run: >
- breeze shell "${AIRFLOW_2_CMD}" --use-airflow-version 2.11.0
--airflow-extras pydantic --answer y &&
+ run: |
+ MIN_AIRFLOW_VERSION="$(
+ python ./scripts/ci/testing/get_min_airflow_version_for_python.py
"${PYTHON_VERSION}"
+ )"
+ breeze shell "${AIRFLOW_2_CMD}" \
+ --use-airflow-version "${MIN_AIRFLOW_VERSION}" \
+ --airflow-extras pydantic \
+ --answer y &&
breeze shell "export
AIRFLOW__DATABASE__EXTERNAL_DB_MANAGERS=${DB_MANGERS}
${AIRFLOW_3_CMD}" --no-db-cleanup
env:
COMPOSE_PROJECT_NAME: "docker-compose"
DB_RESET: "false"
DB_MANAGERS:
"airflow.providers.fab.auth_manager.models.db.FABDBManager"
+ PYTHON_VERSION: "${{ inputs.python-version }}"
AIRFLOW_2_CMD: >-
airflow db reset -y
AIRFLOW_3_CMD: >-
diff --git a/scripts/ci/testing/get_min_airflow_version_for_python.py
b/scripts/ci/testing/get_min_airflow_version_for_python.py
new file mode 100644
index 00000000000..ae404935c7d
--- /dev/null
+++ b/scripts/ci/testing/get_min_airflow_version_for_python.py
@@ -0,0 +1,60 @@
+# 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 argparse
+
+MIN_AIRFLOW_VERSION_BY_PYTHON = {
+ "3.10": "2.11.0",
+ "3.13": "3.1.0",
+ # "3.14": "3.2.0",
+}
+
+
+def _version_key(version: str) -> tuple[int, ...]:
+ return tuple(int(part) for part in version.split("."))
+
+
+def get_min_airflow_version_for_python(python_version: str) -> str:
+ """
+ Return the minimum supported Airflow version for the given Python version.
+
+ Unknown future Python versions inherit the latest known minimum Airflow
version so the
+ requirement never decreases when a new Python version is added to the DB
test matrix.
+ """
+
+ matching_versions = [
+ version
+ for version in MIN_AIRFLOW_VERSION_BY_PYTHON
+ if _version_key(version) <= _version_key(python_version)
+ ]
+ if not matching_versions:
+ raise ValueError(f"No minimum Airflow version defined for Python
{python_version}")
+ return MIN_AIRFLOW_VERSION_BY_PYTHON[max(matching_versions,
key=_version_key)]
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(
+ description="Return the minimum supported Airflow version for a Python
version."
+ )
+ parser.add_argument("python_version", help="Python major.minor version,
for example 3.14")
+ args = parser.parse_args()
+ print(get_min_airflow_version_for_python(args.python_version))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/tests/ci/testing/__init__.py
b/scripts/tests/ci/testing/__init__.py
new file mode 100644
index 00000000000..13a83393a91
--- /dev/null
+++ b/scripts/tests/ci/testing/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git
a/scripts/tests/ci/testing/test_get_min_airflow_version_for_python.py
b/scripts/tests/ci/testing/test_get_min_airflow_version_for_python.py
new file mode 100644
index 00000000000..b6f9bf239dd
--- /dev/null
+++ b/scripts/tests/ci/testing/test_get_min_airflow_version_for_python.py
@@ -0,0 +1,64 @@
+# 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 importlib.util
+import sys
+from pathlib import Path
+
+import pytest
+
+MODULE_PATH = (
+ Path(__file__).resolve().parents[4]
+ / "scripts"
+ / "ci"
+ / "testing"
+ / "get_min_airflow_version_for_python.py"
+)
+
+
[email protected]
+def min_airflow_module():
+ module_name = "test_get_min_airflow_version_for_python_module"
+ sys.modules.pop(module_name, None)
+ spec = importlib.util.spec_from_file_location(module_name, MODULE_PATH)
+ module = importlib.util.module_from_spec(spec)
+ assert spec.loader is not None
+ sys.modules[module_name] = module
+ spec.loader.exec_module(module)
+ return module
+
+
[email protected](
+ ("python_version", "expected_airflow_version"),
+ [
+ ("3.10", "2.11.0"),
+ ("3.12", "2.11.0"),
+ ("3.13", "3.1.0"),
+ ("3.14", "3.2.0"),
+ ("3.15", "3.2.0"),
+ ],
+)
+def test_get_min_airflow_version_for_python_is_monotonic(
+ min_airflow_module, python_version, expected_airflow_version
+):
+ assert
min_airflow_module.get_min_airflow_version_for_python(python_version) ==
expected_airflow_version
+
+
+def
test_get_min_airflow_version_for_python_raises_for_older_python(min_airflow_module):
+ with pytest.raises(ValueError, match="No minimum Airflow version defined
for Python 3.9"):
+ min_airflow_module.get_min_airflow_version_for_python("3.9")