-
Notifications
You must be signed in to change notification settings - Fork 17.5k
Automate syncing documented tested versions with the test matrix #69124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| #!/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 = [] | ||
| # /// | ||
| """Keep the documented "tested with" versions in sync with global_constants.py. | ||
|
|
||
| The list of Python, database and Kubernetes versions Airflow is tested with lives | ||
| in a single source of truth (``dev/breeze/src/airflow_breeze/global_constants.py``) | ||
| but is also rendered for humans in two hand-maintained places: | ||
|
|
||
| * ``airflow-core/docs/installation/prerequisites.rst`` — the "Prerequisites" bullet list. | ||
| * ``README.md`` — the "Main version (dev)" column of the Requirements table. | ||
|
|
||
| These used to drift out of sync silently (e.g. PostgreSQL 18 was added to the test | ||
| matrix and the README but the prerequisites doc was forgotten). This hook regenerates | ||
| both from the constants so the drift becomes a (auto-fixable) prek failure instead of | ||
| a documentation bug nobody notices. | ||
|
|
||
| Only versions that have a clean single source in global_constants.py are generated: | ||
| Python, PostgreSQL, MySQL (numeric releases) and Kubernetes. SQLite has no constant | ||
| and the MySQL "Innovation" annotation is editorial, so both are defined here. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| from common_prek_utils import ( | ||
| AIRFLOW_ROOT_PATH, | ||
| read_allowed_kubernetes_versions, | ||
| read_allowed_python_major_minor_versions, | ||
| read_current_mysql_versions, | ||
| read_current_postgres_versions, | ||
| ) | ||
|
|
||
| PREREQUISITES_RST = AIRFLOW_ROOT_PATH / "airflow-core" / "docs" / "installation" / "prerequisites.rst" | ||
| README_MD = AIRFLOW_ROOT_PATH / "README.md" | ||
|
|
||
| PREREQUISITES_START = " .. Beginning of the auto-generated tested versions\n" | ||
| PREREQUISITES_END = " .. End of the auto-generated tested versions\n" | ||
|
|
||
| README_DEV_HEADER = "Main version (dev)" | ||
|
|
||
| # Editorial values without a single source of truth in global_constants.py. | ||
| SQLITE_VERSION = "3.15.0+" | ||
| MYSQL_INNOVATION_ANNOTATION = "Innovation" | ||
| MYSQL_INNOVATION_RST_LINK = ( | ||
| "`Innovation <https://dev.mysql.com/blog-archive/" | ||
| "introducing-mysql-innovation-and-long-term-support-lts-versions>`_" | ||
| ) | ||
|
|
||
|
|
||
| def kubernetes_major_minor_versions() -> list[str]: | ||
| """Kubernetes versions reduced to ``major.minor`` (e.g. ``1.30``) for display.""" | ||
| return [".".join(version.split(".")[:2]) for version in read_allowed_kubernetes_versions()] | ||
|
|
||
|
|
||
| def comma(values: list[str]) -> str: | ||
| return ", ".join(values) | ||
|
|
||
|
|
||
| def render_prerequisites_block() -> str: | ||
| """The bullet list rendered between the prerequisites.rst markers.""" | ||
| python_versions = comma(read_allowed_python_major_minor_versions()) | ||
| postgres_versions = comma(read_current_postgres_versions()) | ||
| mysql_versions = comma([*read_current_mysql_versions(), MYSQL_INNOVATION_RST_LINK]) | ||
| kubernetes_versions = comma(kubernetes_major_minor_versions()) | ||
| return ( | ||
| "\n" | ||
| f"* Python: {python_versions}\n" | ||
| "\n" | ||
| "* Databases:\n" | ||
| "\n" | ||
| f" * PostgreSQL: {postgres_versions}\n" | ||
| f" * MySQL: {mysql_versions}\n" | ||
| f" * SQLite: {SQLITE_VERSION}\n" | ||
| "\n" | ||
| f"* Kubernetes: {kubernetes_versions}\n" | ||
| "\n" | ||
| ) | ||
|
|
||
|
|
||
| def dev_version_values() -> dict[str, str]: | ||
| """Generated value for each README row label whose dev column we keep in sync.""" | ||
| return { | ||
| "Python": comma(read_allowed_python_major_minor_versions()), | ||
| "Kubernetes": comma(kubernetes_major_minor_versions()), | ||
| "PostgreSQL": comma(read_current_postgres_versions()), | ||
| "MySQL": comma([*read_current_mysql_versions(), MYSQL_INNOVATION_ANNOTATION]), | ||
| } | ||
|
|
||
|
|
||
| def replace_text_between(text: str, start: str, end: str, replacement: str) -> str: | ||
| if start not in text or end not in text: | ||
| raise RuntimeError(f"Could not find markers {start!r} / {end!r} in the file") | ||
| leading = text.split(start)[0] | ||
| trailing = text.split(end)[1] | ||
| return leading + start + replacement + end + trailing | ||
|
|
||
|
|
||
| def update_prerequisites() -> bool: | ||
| """Regenerate the tested-versions block in prerequisites.rst. Return True if changed.""" | ||
| original = PREREQUISITES_RST.read_text() | ||
| updated = replace_text_between( | ||
| original, PREREQUISITES_START, PREREQUISITES_END, render_prerequisites_block() | ||
| ) | ||
| if updated != original: | ||
| PREREQUISITES_RST.write_text(updated) | ||
| return True | ||
| return False | ||
|
|
||
|
|
||
| def update_readme() -> bool: | ||
| """Sync the "Main version (dev)" column of the README Requirements table. | ||
|
|
||
| Only the dev cell of the matched rows is rewritten; the label and the historical | ||
| "Stable version" columns are preserved byte-for-byte. The dev column is widened (and | ||
| only the dev column) if a generated value no longer fits, keeping the table aligned | ||
| without touching the stable columns. | ||
| """ | ||
| lines = README_MD.read_text().splitlines(keepends=True) | ||
| header_index = next( | ||
| (i for i, line in enumerate(lines) if README_DEV_HEADER in line and line.lstrip().startswith("|")), | ||
| None, | ||
| ) | ||
| if header_index is None: | ||
| raise RuntimeError( | ||
| f"Could not find the Requirements table header ({README_DEV_HEADER!r}) in README.md" | ||
| ) | ||
|
|
||
| # The table spans contiguous lines starting with '|', header first then separator. | ||
| table_end = header_index | ||
| while table_end < len(lines) and lines[table_end].lstrip().startswith("|"): | ||
| table_end += 1 | ||
| table_rows = list(range(header_index, table_end)) | ||
| separator_index = header_index + 1 | ||
|
|
||
| generated = dev_version_values() | ||
|
|
||
| def cells(line: str) -> list[str]: | ||
| # Drop the empty fragments before the first and after the last pipe. | ||
| return line.rstrip("\n").split("|")[1:-1] | ||
|
|
||
| # Determine the dev column width: max of the current width and every value we render. | ||
| current_width = len(cells(lines[header_index])[1]) - 2 | ||
| needed = [current_width] | ||
| for idx in table_rows: | ||
| if idx == separator_index: | ||
| continue | ||
| row = cells(lines[idx]) | ||
| label = row[0].strip() | ||
| value = generated.get(label, row[1].strip()) | ||
| needed.append(len(value)) | ||
| dev_width = max(needed) | ||
|
|
||
| changed = False | ||
| for idx in table_rows: | ||
| row = cells(lines[idx]) | ||
| if idx == separator_index: | ||
| new_dev = "-" * (dev_width + 2) | ||
| else: | ||
| label = row[0].strip() | ||
| value = generated.get(label, row[1].strip()) | ||
| new_dev = f" {value.ljust(dev_width)} " | ||
| if new_dev == row[1]: | ||
| continue | ||
| row[1] = new_dev | ||
| lines[idx] = "|" + "|".join(row) + "|\n" | ||
| changed = True | ||
|
|
||
| if changed: | ||
| README_MD.write_text("".join(lines)) | ||
| return changed | ||
|
|
||
|
|
||
| def main() -> int: | ||
| changed_files: list[Path] = [] | ||
| if update_prerequisites(): | ||
| changed_files.append(PREREQUISITES_RST) | ||
| if update_readme(): | ||
| changed_files.append(README_MD) | ||
| if changed_files: | ||
| for path in changed_files: | ||
| print(f"Updated tested versions in {path.relative_to(AIRFLOW_ROOT_PATH)}") | ||
| print("Files were re-generated from global_constants.py - please re-stage them.") | ||
| return 1 | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.