From 32ef67c67cd7b7e3db6762aa96fa3fb6acee5db0 Mon Sep 17 00:00:00 2001 From: Dev-iL <6509619+Dev-iL@users.noreply.github.com> Date: Sat, 14 Mar 2026 14:11:15 +0200 Subject: [PATCH] Add helper to tie Airflow version to the min supported Python version --- .github/actions/migration_tests/action.yml | 20 +++++- pyproject.toml | 1 + .../get_min_airflow_version_for_python.py | 60 +++++++++++++++++ scripts/tests/ci/__init__.py | 16 +++++ scripts/tests/ci/testing/__init__.py | 16 +++++ ...test_get_min_airflow_version_for_python.py | 64 +++++++++++++++++++ 6 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 scripts/ci/testing/get_min_airflow_version_for_python.py create mode 100644 scripts/tests/ci/__init__.py create mode 100644 scripts/tests/ci/testing/__init__.py create mode 100644 scripts/tests/ci/testing/test_get_min_airflow_version_for_python.py diff --git a/.github/actions/migration_tests/action.yml b/.github/actions/migration_tests/action.yml index c4fcd4853f152..ae05e647899d7 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/pyproject.toml b/pyproject.toml index 878533fff2fc4..4235824e8c8a1 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'. 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 0000000000000..ae404935c7d97 --- /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/__init__.py b/scripts/tests/ci/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/scripts/tests/ci/__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/__init__.py b/scripts/tests/ci/testing/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /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 0000000000000..b6f9bf239ddb8 --- /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" +) + + +@pytest.fixture +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 + + +@pytest.mark.parametrize( + ("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")