This is an automated email from the ASF dual-hosted git repository.
eladkal 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 ece27bba4a5 Regenerate stale provider dependencies instead of silently
using them (#70955)
ece27bba4a5 is described below
commit ece27bba4a5cffeb96aa6a8aad5faf3e63f7288c
Author: Jarek Potiuk <[email protected]>
AuthorDate: Mon Aug 3 09:27:57 2026 +0200
Regenerate stale provider dependencies instead of silently using them
(#70955)
Release commands enumerate providers from
generated/provider_dependencies.json,
filtering on each provider's state. The freshness check runs on every breeze
invocation, but when the json was present and its sha256sum sidecar was
not, it
delegated to a helper that only regenerates when the json is missing. The
stale
file was therefore returned as-is and no sidecar was ever written, leaving
the
check permanently disabled for that checkout.
A provider whose state changed then stays invisible to the release:
common.ai
was flipped to ready in 682cf06525 but a checkout carrying an older json
still
called it not-ready, so it was dropped from the 2026-08-01 wave with no
warning
and was only caught by an unrelated package count mismatch.
---
.../airflow_breeze/utils/provider_dependencies.py | 20 ++++--
dev/breeze/tests/test_provider_dependencies.py | 81 ++++++++++++++++++++++
2 files changed, 94 insertions(+), 7 deletions(-)
diff --git a/dev/breeze/src/airflow_breeze/utils/provider_dependencies.py
b/dev/breeze/src/airflow_breeze/utils/provider_dependencies.py
index d67e0ec9bfc..02aa337de37 100644
--- a/dev/breeze/src/airflow_breeze/utils/provider_dependencies.py
+++ b/dev/breeze/src/airflow_breeze/utils/provider_dependencies.py
@@ -179,24 +179,30 @@ def _calculate_provider_deps_hash():
def get_provider_dependencies() -> dict:
if not PROVIDER_DEPENDENCIES_JSON_PATH.exists():
calculated_hash = _calculate_provider_deps_hash()
- PROVIDER_DEPENDENCIES_JSON_HASH_PATH.write_text(calculated_hash + "\n")
# We use regular print there as rich console might not be initialized
yet here
print("Regenerating provider dependencies file")
regenerate_provider_dependencies_once()
+ # Only record the hash once regeneration succeeded, otherwise a failed
run would
+ # leave a sidecar claiming that a missing/stale file is up to date.
+ PROVIDER_DEPENDENCIES_JSON_HASH_PATH.write_text(calculated_hash + "\n")
return json.loads(PROVIDER_DEPENDENCIES_JSON_PATH.read_text())
+def _force_regenerate_provider_dependencies() -> None:
+ # get_provider_dependencies() only regenerates when the JSON is absent, so
the file has
+ # to be removed for it to pick up changed provider.yaml/pyproject.toml
contents.
+ PROVIDER_DEPENDENCIES_JSON_PATH.unlink(missing_ok=True)
+ get_provider_dependencies.cache_clear()
+ get_provider_dependencies()
+
+
def generate_provider_dependencies_if_needed():
if not PROVIDER_DEPENDENCIES_JSON_PATH.exists() or not
PROVIDER_DEPENDENCIES_JSON_HASH_PATH.exists():
- get_provider_dependencies.cache_clear()
- get_provider_dependencies()
+ _force_regenerate_provider_dependencies()
else:
calculated_hash = _calculate_provider_deps_hash()
if calculated_hash.strip() !=
PROVIDER_DEPENDENCIES_JSON_HASH_PATH.read_text().strip():
- # Force re-generation
- PROVIDER_DEPENDENCIES_JSON_PATH.unlink(missing_ok=True)
- get_provider_dependencies.cache_clear()
- get_provider_dependencies()
+ _force_regenerate_provider_dependencies()
def get_related_providers(
diff --git a/dev/breeze/tests/test_provider_dependencies.py
b/dev/breeze/tests/test_provider_dependencies.py
index 4ddc16a60cf..bf77f6dd29d 100644
--- a/dev/breeze/tests/test_provider_dependencies.py
+++ b/dev/breeze/tests/test_provider_dependencies.py
@@ -16,9 +16,13 @@
# under the License.
from __future__ import annotations
+from unittest import mock
+
import pytest
+from airflow_breeze.utils import provider_dependencies as
provider_dependencies_module
from airflow_breeze.utils.provider_dependencies import (
+ generate_provider_dependencies_if_needed,
get_related_providers,
)
@@ -47,3 +51,80 @@ def test_both():
def test_none():
with pytest.raises(ValueError, match=r".*must be.*"):
get_related_providers("trino", upstream_dependencies=False,
downstream_dependencies=False)
+
+
[email protected]
+def provider_deps_files(tmp_path):
+ """Point the module at a throwaway json/sha256sum pair and clear the
lru_cache around it."""
+ json_path = tmp_path / "provider_dependencies.json"
+ hash_path = tmp_path / "provider_dependencies.json.sha256sum"
+ provider_dependencies_module.get_provider_dependencies.cache_clear()
+ with mock.patch.multiple(
+ provider_dependencies_module,
+ PROVIDER_DEPENDENCIES_JSON_PATH=json_path,
+ PROVIDER_DEPENDENCIES_JSON_HASH_PATH=hash_path,
+ ):
+ yield json_path, hash_path
+ provider_dependencies_module.get_provider_dependencies.cache_clear()
+
+
[email protected](
+ "hash_sidecar_present",
+ [
+ pytest.param(False, id="missing-sidecar"),
+ pytest.param(True, id="stale-sidecar"),
+ ],
+)
[email protected](provider_dependencies_module,
"regenerate_provider_dependencies_once")
[email protected](provider_dependencies_module,
"_calculate_provider_deps_hash")
+def test_stale_dependencies_are_regenerated(
+ mock_hash, mock_regenerate, provider_deps_files, hash_sidecar_present
+):
+ """A present-but-outdated json must be regenerated, and the sidecar
refreshed.
+
+ Without this, breeze silently enumerates providers from stale state -
which dropped
+ common.ai from the 2026-08-01 provider release wave.
+ """
+ json_path, hash_path = provider_deps_files
+ json_path.write_text('{"common.ai": {"state": "not-ready"}}')
+ if hash_sidecar_present:
+ hash_path.write_text("stale-hash\n")
+ mock_hash.return_value = "fresh-hash"
+
+ def regenerate():
+ json_path.write_text('{"common.ai": {"state": "ready"}}')
+
+ mock_regenerate.side_effect = regenerate
+
+ generate_provider_dependencies_if_needed()
+
+ mock_regenerate.assert_called_once()
+ assert provider_dependencies_module.get_provider_dependencies() ==
{"common.ai": {"state": "ready"}}
+ assert hash_path.read_text().strip() == "fresh-hash"
+
+
[email protected](provider_dependencies_module,
"regenerate_provider_dependencies_once")
[email protected](provider_dependencies_module,
"_calculate_provider_deps_hash")
+def test_up_to_date_dependencies_are_not_regenerated(mock_hash,
mock_regenerate, provider_deps_files):
+ json_path, hash_path = provider_deps_files
+ json_path.write_text('{"common.ai": {"state": "ready"}}')
+ hash_path.write_text("fresh-hash\n")
+ mock_hash.return_value = "fresh-hash"
+
+ generate_provider_dependencies_if_needed()
+
+ mock_regenerate.assert_not_called()
+
+
[email protected](provider_dependencies_module,
"regenerate_provider_dependencies_once")
[email protected](provider_dependencies_module,
"_calculate_provider_deps_hash")
+def test_hash_not_written_when_regeneration_fails(mock_hash, mock_regenerate,
provider_deps_files):
+ """A failed regeneration must not leave a sidecar vouching for stale
content."""
+ _, hash_path = provider_deps_files
+ mock_hash.return_value = "fresh-hash"
+ mock_regenerate.side_effect = RuntimeError("boom")
+
+ with pytest.raises(RuntimeError, match="boom"):
+ generate_provider_dependencies_if_needed()
+
+ assert not hash_path.exists()