This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch v3-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-1-test by this push:
new d53ec592db4 [v3-1-test] Amend simulation in release command (#61787)
(#61792)
d53ec592db4 is described below
commit d53ec592db4517079271788f3a63c27633736c63
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Wed Feb 11 23:49:12 2026 +0100
[v3-1-test] Amend simulation in release command (#61787) (#61792)
* Add CI env var to ensure is_ci_environment() returns true and remove
canary to test
* Move environment check to central and add simulation to find latest
* Create directories for releases
* Try with base execution dir
* Create exact locations
* Pass svn location
* Fix tests and run certain ones not in CI to catch errors
* Fix static checks and mypy errors
* Revert github ci rule
* Update .github/workflows/basic-tests.yml
---------
(cherry picked from commit 6c04a4404ae2533c2159717ef79c884451b7c29a)
Co-authored-by: Bugra Ozturk <[email protected]>
Co-authored-by: Jarek Potiuk <[email protected]>
---
.../commands/release_candidate_command.py | 6 +-
.../src/airflow_breeze/commands/release_command.py | 51 ++++++++++++--
.../src/airflow_breeze/utils/environment_check.py | 24 +++++++
dev/breeze/tests/test_release_command.py | 81 ++++++++++++++--------
4 files changed, 122 insertions(+), 40 deletions(-)
diff --git
a/dev/breeze/src/airflow_breeze/commands/release_candidate_command.py
b/dev/breeze/src/airflow_breeze/commands/release_candidate_command.py
index b6fc228bd39..9df625b6e54 100644
--- a/dev/breeze/src/airflow_breeze/commands/release_candidate_command.py
+++ b/dev/breeze/src/airflow_breeze/commands/release_candidate_command.py
@@ -43,6 +43,7 @@ from airflow_breeze.global_constants import (
from airflow_breeze.utils.confirm import confirm_action
from airflow_breeze.utils.console import console_print
from airflow_breeze.utils.custom_param_types import BetterChoice
+from airflow_breeze.utils.environment_check import is_ci_environment
from airflow_breeze.utils.path_utils import (
AIRFLOW_DIST_PATH,
AIRFLOW_ROOT_PATH,
@@ -59,11 +60,6 @@ SVN_NUM_TRIES = 3
SVN_OPERATION_RETRY_DELAY = 5
-def is_ci_environment():
- """Check if running in CI environment."""
- return os.environ.get("CI", "").lower() in ("true", "1", "yes")
-
-
def validate_remote_tracks_apache_airflow(remote_name):
"""Validate that the specified remote tracks the apache/airflow
repository."""
console_print(f"[info]Validating remote '{remote_name}' tracks
apache/airflow...")
diff --git a/dev/breeze/src/airflow_breeze/commands/release_command.py
b/dev/breeze/src/airflow_breeze/commands/release_command.py
index 2b0f9c0446b..9579e2ec242 100644
--- a/dev/breeze/src/airflow_breeze/commands/release_command.py
+++ b/dev/breeze/src/airflow_breeze/commands/release_command.py
@@ -27,6 +27,7 @@ from airflow_breeze.commands.common_options import
option_answer, option_dry_run
from airflow_breeze.commands.release_management_group import
release_management_group
from airflow_breeze.utils.confirm import confirm_action
from airflow_breeze.utils.console import console_print
+from airflow_breeze.utils.environment_check import is_ci_environment
from airflow_breeze.utils.path_utils import AIRFLOW_ROOT_PATH
from airflow_breeze.utils.run_utils import run_command
@@ -37,7 +38,15 @@ RELEASE_PATTERN =
re.compile(r"^(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)$"
SVN_NUM_TRIES = 3
-def clone_asf_repo(working_dir):
+def clone_asf_repo(working_dir, svn_dev_repo):
+
+ if is_ci_environment():
+ console_print("[info]Running in CI environment - simulating SVN
checkout")
+ # Create empty directory structure to simulate svn checkout (override
dry-run if specified)
+ run_command(["mkdir", "-p", svn_dev_repo], check=True,
dry_run_override=False)
+ console_print("[success]Simulated ASF repo checkout in CI")
+ return
+
if confirm_action("Clone ASF repo?"):
run_command(["rm", "-rf", f"{working_dir}/asf-dist"], check=True)
@@ -69,7 +78,7 @@ def clone_asf_repo(working_dir):
run_command(["svn", "update", "--set-depth", "infinity", release_dir],
check=True)
-def find_latest_release_candidate(version, svn_dev_repo, component="airflow"):
+def find_latest_release_candidate(version, svn_dev_repo, component="airflow",
svn_simulation_repo=None):
"""
Find the latest release candidate for a given version from SVN dev
directory.
@@ -78,6 +87,36 @@ def find_latest_release_candidate(version, svn_dev_repo,
component="airflow"):
:param component: Component name ("airflow" or "task-sdk")
:return: The latest release candidate string (e.g., "3.0.5rc3") or None if
not found
"""
+ if is_ci_environment():
+
+ def create_simulation_dir(candidate_dir_to_create):
+ svn_dev_component_dir = (
+ f"{svn_simulation_repo}/{candidate_dir_to_create}"
+ if component == "airflow"
+ else
f"{svn_simulation_repo}/task-sdk/{candidate_dir_to_create}"
+ )
+ print(f"Creating simulation directory: {svn_dev_component_dir}")
+ print(f"candidate_dir_to_create: {candidate_dir_to_create}")
+ run_command(
+ ["mkdir", "-p",
f"{svn_dev_component_dir}/{candidate_dir_to_create}"],
+ check=True,
+ dry_run_override=False,
+ )
+
+ console_print("[info]Running in CI environment - simulating SVN find
latest release candidate")
+ airflow_simulate_release_candidate = ["3.1.7rc1", "3.1.7rc2",
"3.1.7rc3"]
+ task_sdk_simulate_release_candidate = ["1.1.7rc1", "1.1.7rc2",
"1.1.7rc3"]
+ for candidate in airflow_simulate_release_candidate:
+ create_simulation_dir(candidate)
+ for candidate in task_sdk_simulate_release_candidate:
+ create_simulation_dir(candidate)
+ console_print("[success]Simulated ASF repo checkout in CI")
+ return (
+ airflow_simulate_release_candidate
+ if component == "airflow"
+ else task_sdk_simulate_release_candidate
+ )
+
if component == "task-sdk":
search_dir = f"{svn_dev_repo}/task-sdk"
else:
@@ -425,8 +464,8 @@ def airflow_release(version, task_sdk_version):
# Clone the asf repo
os.chdir("..")
working_dir = os.getcwd()
- clone_asf_repo(working_dir)
svn_dev_repo = f"{working_dir}/asf-dist/dev/airflow"
+ clone_asf_repo(working_dir, svn_dev_repo=svn_dev_repo)
svn_release_repo = f"{working_dir}/asf-dist/release/airflow"
console_print("SVN dev repo root:", svn_dev_repo)
console_print("SVN release repo root:", svn_release_repo)
@@ -434,14 +473,16 @@ def airflow_release(version, task_sdk_version):
# Find the latest release candidate for the given version
console_print()
console_print("Finding latest release candidate from SVN dev directory...")
- release_candidate = find_latest_release_candidate(version, svn_dev_repo,
component="airflow")
+ release_candidate = find_latest_release_candidate(
+ version=version, svn_dev_repo=svn_dev_repo, component="airflow",
svn_simulation_repo=svn_release_repo
+ )
if not release_candidate:
exit(f"No release candidate found for version {version} in SVN dev
directory")
task_sdk_release_candidate = None
if task_sdk_version:
task_sdk_release_candidate = find_latest_release_candidate(
- task_sdk_version, svn_dev_repo, component="task-sdk"
+ task_sdk_version, svn_dev_repo, component="task-sdk",
svn_simulation_repo=svn_release_repo
)
if not task_sdk_release_candidate:
exit(f"No Task SDK release candidate found for version
{task_sdk_version} in SVN dev directory")
diff --git a/dev/breeze/src/airflow_breeze/utils/environment_check.py
b/dev/breeze/src/airflow_breeze/utils/environment_check.py
new file mode 100644
index 00000000000..3ffd870d67a
--- /dev/null
+++ b/dev/breeze/src/airflow_breeze/utils/environment_check.py
@@ -0,0 +1,24 @@
+# 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 os
+
+
+def is_ci_environment():
+ """Check if running in CI environment."""
+ return os.environ.get("CI", "").lower() in ("true", "1", "yes")
diff --git a/dev/breeze/tests/test_release_command.py
b/dev/breeze/tests/test_release_command.py
index f10c79655fa..2745a8387eb 100644
--- a/dev/breeze/tests/test_release_command.py
+++ b/dev/breeze/tests/test_release_command.py
@@ -16,6 +16,8 @@
# under the License.
from __future__ import annotations
+import os
+from unittest import mock
from unittest.mock import patch
import pytest
@@ -23,34 +25,42 @@ import pytest
from airflow_breeze.commands.release_command import
find_latest_release_candidate
+def is_ci_environment() -> bool:
+ """Check if running in CI environment by checking the CI environment
variable."""
+ return os.environ.get("CI", "").lower() in ("true", "1", "yes")
+
+
class TestFindLatestReleaseCandidate:
"""Test the find_latest_release_candidate function."""
+ @mock.patch.dict(os.environ, {"CI": "false"})
def test_find_latest_rc_single_candidate(self, tmp_path):
"""Test finding release candidate when only one exists."""
svn_dev_repo = tmp_path / "dev" / "airflow"
svn_dev_repo.mkdir(parents=True)
# Create a single RC directory
- (svn_dev_repo / "3.0.5rc1").mkdir()
+ (svn_dev_repo / "3.1.7rc1").mkdir()
- result = find_latest_release_candidate("3.0.5", str(svn_dev_repo),
component="airflow")
- assert result == "3.0.5rc1"
+ result = find_latest_release_candidate("3.1.7", str(svn_dev_repo),
component="airflow")
+ assert result == "3.1.7rc1"
+ @mock.patch.dict(os.environ, {"CI": "false"})
def test_find_latest_rc_multiple_candidates(self, tmp_path):
"""Test finding latest release candidate when multiple exist."""
svn_dev_repo = tmp_path / "dev" / "airflow"
svn_dev_repo.mkdir(parents=True)
# Create multiple RC directories
- (svn_dev_repo / "3.0.5rc1").mkdir()
- (svn_dev_repo / "3.0.5rc2").mkdir()
- (svn_dev_repo / "3.0.5rc3").mkdir()
- (svn_dev_repo / "3.0.5rc10").mkdir() # Test that rc10 > rc3
+ (svn_dev_repo / "3.1.7rc1").mkdir()
+ (svn_dev_repo / "3.1.7rc2").mkdir()
+ (svn_dev_repo / "3.1.7rc3").mkdir()
+ (svn_dev_repo / "3.1.7rc10").mkdir() # Test that rc10 > rc3
- result = find_latest_release_candidate("3.0.5", str(svn_dev_repo),
component="airflow")
- assert result == "3.0.5rc10"
+ result = find_latest_release_candidate("3.1.7", str(svn_dev_repo),
component="airflow")
+ assert result == "3.1.7rc10"
+ @mock.patch.dict(os.environ, {"CI": "false"})
def test_find_latest_rc_ignores_other_versions(self, tmp_path):
"""Test that function ignores RCs for other versions."""
svn_dev_repo = tmp_path / "dev" / "airflow"
@@ -58,26 +68,28 @@ class TestFindLatestReleaseCandidate:
# Create RCs for different versions
(svn_dev_repo / "3.0.4rc1").mkdir()
- (svn_dev_repo / "3.0.5rc1").mkdir()
- (svn_dev_repo / "3.0.5rc2").mkdir()
+ (svn_dev_repo / "3.1.7rc1").mkdir()
+ (svn_dev_repo / "3.1.7rc2").mkdir()
(svn_dev_repo / "3.0.6rc1").mkdir()
- result = find_latest_release_candidate("3.0.5", str(svn_dev_repo),
component="airflow")
- assert result == "3.0.5rc2"
+ result = find_latest_release_candidate("3.1.7", str(svn_dev_repo),
component="airflow")
+ assert result == "3.1.7rc2"
+ @mock.patch.dict(os.environ, {"CI": "false"})
def test_find_latest_rc_ignores_non_rc_directories(self, tmp_path):
"""Test that function ignores directories that don't match RC
pattern."""
svn_dev_repo = tmp_path / "dev" / "airflow"
svn_dev_repo.mkdir(parents=True)
# Create RC directory and non-RC directories
- (svn_dev_repo / "3.0.5rc1").mkdir()
- (svn_dev_repo / "3.0.5").mkdir() # Final release directory
+ (svn_dev_repo / "3.1.7rc1").mkdir()
+ (svn_dev_repo / "3.1.7").mkdir() # Final release directory
(svn_dev_repo / "some-other-dir").mkdir()
- result = find_latest_release_candidate("3.0.5", str(svn_dev_repo),
component="airflow")
- assert result == "3.0.5rc1"
+ result = find_latest_release_candidate("3.1.7", str(svn_dev_repo),
component="airflow")
+ assert result == "3.1.7rc1"
+ @mock.patch.dict(os.environ, {"CI": "false"})
def test_find_latest_rc_no_match(self, tmp_path):
"""Test that function returns None when no matching RC found."""
svn_dev_repo = tmp_path / "dev" / "airflow"
@@ -86,25 +98,28 @@ class TestFindLatestReleaseCandidate:
# Create RCs for different version
(svn_dev_repo / "3.0.4rc1").mkdir()
- result = find_latest_release_candidate("3.0.5", str(svn_dev_repo),
component="airflow")
+ result = find_latest_release_candidate("3.1.5", str(svn_dev_repo),
component="airflow")
assert result is None
+ @mock.patch.dict(os.environ, {"CI": "false"})
def test_find_latest_rc_directory_not_exists(self, tmp_path):
"""Test that function returns None when directory doesn't exist."""
svn_dev_repo = tmp_path / "dev" / "airflow"
# Don't create the directory
- result = find_latest_release_candidate("3.0.5", str(svn_dev_repo),
component="airflow")
+ result = find_latest_release_candidate("3.1.7", str(svn_dev_repo),
component="airflow")
assert result is None
+ @mock.patch.dict(os.environ, {"CI": "false"})
def test_find_latest_rc_empty_directory(self, tmp_path):
"""Test that function returns None when directory is empty."""
svn_dev_repo = tmp_path / "dev" / "airflow"
svn_dev_repo.mkdir(parents=True)
- result = find_latest_release_candidate("3.0.5", str(svn_dev_repo),
component="airflow")
+ result = find_latest_release_candidate("3.1.7", str(svn_dev_repo),
component="airflow")
assert result is None
+ @mock.patch.dict(os.environ, {"CI": "false"})
def test_find_latest_rc_task_sdk_component(self, tmp_path):
"""Test finding release candidate for task-sdk component."""
svn_dev_repo = tmp_path / "dev" / "airflow"
@@ -119,6 +134,7 @@ class TestFindLatestReleaseCandidate:
result = find_latest_release_candidate("1.0.5", str(svn_dev_repo),
component="task-sdk")
assert result == "1.0.5rc3"
+ @mock.patch.dict(os.environ, {"CI": "false"})
def test_find_latest_rc_task_sdk_ignores_airflow_rcs(self, tmp_path):
"""Test that task-sdk component ignores airflow RCs."""
svn_dev_repo = tmp_path / "dev" / "airflow"
@@ -127,20 +143,25 @@ class TestFindLatestReleaseCandidate:
task_sdk_dir.mkdir()
# Create airflow RC (should be ignored)
- (svn_dev_repo / "3.0.5rc1").mkdir()
+ (svn_dev_repo / "3.1.7rc1").mkdir()
# Create task-sdk RC
- (task_sdk_dir / "1.0.5rc1").mkdir()
+ (task_sdk_dir / "1.1.7rc1").mkdir()
- result = find_latest_release_candidate("1.0.5", str(svn_dev_repo),
component="task-sdk")
- assert result == "1.0.5rc1"
+ result = find_latest_release_candidate("1.1.7", str(svn_dev_repo),
component="task-sdk")
+ assert result == "1.1.7rc1"
+ @mock.patch.dict(os.environ, {"CI": "false"})
def test_find_latest_rc_handles_oserror(self, tmp_path):
"""Test that function handles OSError gracefully."""
svn_dev_repo = tmp_path / "dev" / "airflow"
+ svn_simulate_repo = tmp_path / "release" / "airflow"
svn_dev_repo.mkdir(parents=True)
+ svn_simulate_repo.mkdir(parents=True)
with patch("os.listdir", side_effect=OSError("Permission denied")):
- result = find_latest_release_candidate("3.0.5", str(svn_dev_repo),
component="airflow")
+ result = find_latest_release_candidate(
+ "3.1.5", str(svn_dev_repo), component="airflow",
svn_simulation_repo=svn_simulate_repo
+ )
assert result is None
@@ -162,7 +183,7 @@ def release_cmd():
def test_remove_old_release_only_collects_release_directories(monkeypatch,
release_cmd):
- version = "3.0.5"
+ version = "3.1.7"
task_sdk_version = "1.0.5"
svn_release_repo = "/svn/release/repo"
@@ -237,7 +258,7 @@ def
test_remove_old_release_only_collects_release_directories(monkeypatch, relea
def test_remove_old_release_returns_early_when_user_declines(monkeypatch,
release_cmd):
- version = "3.0.5"
+ version = "3.1.7"
task_sdk_version = "1.0.5"
svn_release_repo = "/svn/release/repo"
@@ -353,7 +374,7 @@ def
test_remove_old_release_removes_confirmed_old_releases(monkeypatch, release_
def test_remove_old_release_no_old_releases(monkeypatch, release_cmd):
- version = "3.0.5"
+ version = "3.1.7"
task_sdk_version = "1.0.5"
svn_release_repo = "/svn/release/repo"
@@ -406,7 +427,7 @@ def test_remove_old_release_no_old_releases(monkeypatch,
release_cmd):
def test_remove_old_release_task_sdk_only(monkeypatch, release_cmd):
- version = "3.0.5"
+ version = "3.1.7"
task_sdk_version = "1.0.5"
svn_release_repo = "/svn/release/repo"
@@ -479,7 +500,7 @@ def test_remove_old_release_task_sdk_only(monkeypatch,
release_cmd):
def test_remove_old_release_no_task_sdk_version(monkeypatch, release_cmd):
- version = "3.0.5"
+ version = "3.1.7"
task_sdk_version = None
svn_release_repo = "/svn/release/repo"