This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch v3-2-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-2-test by this push:
new 577ef832825 [v3-2-test] breeze: prune unreleased versions from
provider.yaml during metadata generation (#65986) (#66005)
577ef832825 is described below
commit 577ef832825d0a3d93a5c779fe129d0b32f0569c
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri May 1 17:22:02 2026 +0200
[v3-2-test] breeze: prune unreleased versions from provider.yaml during
metadata generation (#65986) (#66005)
Adds a hygiene pre-pass to ``breeze release-management
generate-providers-metadata``:
for each provider, every entry in ``provider.yaml``'s ``versions:`` list
except the
first is checked against PyPI. Entries PyPI doesn't recognise are stale
(e.g. a
release that was prepared but never published) and get removed from the
file in
place, so generated metadata reflects only versions a user can actually
install.
The first entry is intentionally skipped — it is the in-progress next
release and
is allowed to predate publication. PyPI lookups run in parallel; on network
failure the file is left untouched. The version-line removal is regex-based
on
the exact shape produced by ``_update_version_in_provider_yaml`` in
``provider_documentation.py``, so comments and surrounding entries are
preserved.
(cherry picked from commit b28a45e7b5eb33126f112dfc58d3721881364427)
Co-authored-by: Jarek Potiuk <[email protected]>
---
.../commands/release_management_commands.py | 33 +++++++++-
.../airflow_breeze/utils/provider_dependencies.py | 73 ++++++++++++++++++++++
2 files changed, 105 insertions(+), 1 deletion(-)
diff --git
a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py
b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py
index fe663a2925e..1cde739ce2a 100644
--- a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py
@@ -3102,6 +3102,38 @@ def generate_providers_metadata(
console_print(metadata_dict)
return
+ package_ids = list(get_provider_dependencies().keys())
+
+ # Hygiene pass: every entry in `provider.yaml`'s `versions:` list except
+ # the first is supposed to be a published PyPI release. The first entry
+ # is the in-progress next release and is allowed to predate publication;
+ # the rest must be reachable on PyPI. Any older entry that PyPI does not
+ # know about is stale (e.g. a release that was prepared but never
+ # published) — drop it from `provider.yaml` so the metadata generated
+ # below reflects only versions a user can actually install.
+ from airflow_breeze.utils.packages import
get_provider_distributions_metadata
+ from airflow_breeze.utils.provider_dependencies import (
+ prune_unreleased_versions_from_provider_yaml,
+ )
+
+ console_print("\n[info]Checking provider.yaml versions[1:] against PyPI
for stale entries...[/]\n")
+ with Pool() as pypi_pool:
+ pruned_per_provider =
pypi_pool.map(prune_unreleased_versions_from_provider_yaml, package_ids)
+ total_pruned = 0
+ for pid, pruned in zip(package_ids, pruned_per_provider):
+ if pruned:
+ console_print(f"[warning]{pid}: removed unreleased versions from
provider.yaml: {pruned}[/]")
+ total_pruned += len(pruned)
+ if total_pruned:
+ console_print(
+ f"\n[warning]Removed {total_pruned} unreleased version entr"
+ f"{'y' if total_pruned == 1 else 'ies'} from provider.yaml files. "
+ "Re-reading provider metadata.[/]\n"
+ )
+ get_provider_distributions_metadata.cache_clear()
+ else:
+ console_print("[info]All provider.yaml versions[1:] are present on
PyPI.[/]\n")
+
partial_generate_providers_metadata = partial(
generate_providers_metadata_for_provider,
provider_version=None,
@@ -3110,7 +3142,6 @@ def generate_providers_metadata(
airflow_release_dates=airflow_release_dates,
current_metadata=current_metadata,
)
- package_ids = get_provider_dependencies().keys()
with Pool() as pool:
results = pool.map(
partial_generate_providers_metadata,
diff --git a/dev/breeze/src/airflow_breeze/utils/provider_dependencies.py
b/dev/breeze/src/airflow_breeze/utils/provider_dependencies.py
index ecfaaf2b89c..38e19cbf32d 100644
--- a/dev/breeze/src/airflow_breeze/utils/provider_dependencies.py
+++ b/dev/breeze/src/airflow_breeze/utils/provider_dependencies.py
@@ -22,6 +22,8 @@ import re
import shutil
import subprocess
import sys
+import urllib.error
+import urllib.request
from collections.abc import Generator
from functools import cache, partial
from multiprocessing import Pool
@@ -88,6 +90,77 @@ def regenerate_provider_dependencies_once() -> None:
)
+def _fetch_pypi_released_versions(package_name: str) -> set[str] | None:
+ """
+ Fetch the set of versions of ``package_name`` released on PyPI.
+
+ Returns ``None`` on network/HTTP error so the caller can skip pruning
+ rather than crash a long batch run.
+ """
+ pypi_url = f"https://pypi.org/pypi/{package_name}/json"
+ try:
+ with urllib.request.urlopen(pypi_url, timeout=30) as resp:
+ data = json.loads(resp.read().decode("utf-8"))
+ except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError) as e:
+ console_print(f"[warning]Could not fetch PyPI data for {package_name}:
{e}")
+ return None
+ return set((data.get("releases") or {}).keys())
+
+
+def prune_unreleased_versions_from_provider_yaml(provider_id: str) ->
list[str]:
+ """
+ Check ``versions[1:]`` in a provider's ``provider.yaml`` against PyPI.
+
+ The first entry is intentionally skipped — it is the in-progress next
+ release, which is allowed to predate its PyPI publication. For every
+ other entry, if PyPI does not list that exact version, the entry is
+ removed from the file in place.
+
+ Returns the list of versions that were removed, in original order.
+ Returns ``[]`` if nothing was pruned (or PyPI lookup failed).
+ """
+ import yaml
+
+ # Local import to avoid a circular import: packages.py already depends on
+ # provider_dependencies.py via get_provider_dependencies() lookups.
+ from airflow_breeze.utils.packages import get_provider_yaml
+
+ provider_yaml_path = get_provider_yaml(provider_id)
+ provider_yaml_text = provider_yaml_path.read_text()
+ provider_yaml_dict = yaml.safe_load(provider_yaml_text)
+ versions: list[str] = provider_yaml_dict.get("versions") or []
+ if len(versions) <= 1:
+ return []
+
+ package_name = "apache-airflow-providers-" + provider_id.replace(".", "-")
+ pypi_released = _fetch_pypi_released_versions(package_name)
+ if pypi_released is None:
+ # On network failure, leave the file alone — better than deleting
+ # entries based on incomplete data.
+ return []
+
+ to_prune = [v for v in versions[1:] if v not in pypi_released]
+ if not to_prune:
+ return []
+
+ new_text = provider_yaml_text
+ for v in to_prune:
+ # Match the exact line shape produced by
_update_version_in_provider_yaml
+ # in provider_documentation.py: two-space indent, "- ", version, EOL.
+ # `[ \t]*\n` (not `\s*\n`) avoids consuming the next line's newline.
+ new_text = re.sub(
+ rf"^[ \t]+-[ \t]+{re.escape(v)}[ \t]*\n",
+ "",
+ new_text,
+ count=1,
+ flags=re.MULTILINE,
+ )
+
+ if new_text != provider_yaml_text:
+ provider_yaml_path.write_text(new_text)
+ return to_prune
+
+
def _calculate_provider_deps_hash():
import hashlib