This is an automated email from the ASF dual-hosted git repository.
ephraimanierobi 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 bef558dfb20 Update the remove old releases function to include
task-sdk (#59468)
bef558dfb20 is described below
commit bef558dfb20104683fc76f86ba78c7851c405947
Author: Ephraim Anierobi <[email protected]>
AuthorDate: Tue Dec 16 03:30:41 2025 +0100
Update the remove old releases function to include task-sdk (#59468)
For the releases, we also want to have only the current release in
the svn. This PR updates the current function to include task-sdk
---
.../output_release-management_start-release.svg | 26 +-
.../output_release-management_start-release.txt | 2 +-
.../src/airflow_breeze/commands/release_command.py | 100 ++++--
.../commands/release_management_commands_config.py | 2 +-
dev/breeze/tests/test_release_command.py | 377 +++++++++++++++++++++
5 files changed, 471 insertions(+), 36 deletions(-)
diff --git a/dev/breeze/doc/images/output_release-management_start-release.svg
b/dev/breeze/doc/images/output_release-management_start-release.svg
index 22acf18ad1b..301fcefa2d6 100644
--- a/dev/breeze/doc/images/output_release-management_start-release.svg
+++ b/dev/breeze/doc/images/output_release-management_start-release.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 464.79999999999995"
xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 1482 440.4"
xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -45,7 +45,7 @@
<defs>
<clipPath id="breeze-release-management-start-release-clip-terminal">
- <rect x="0" y="0" width="1463.0" height="413.79999999999995" />
+ <rect x="0" y="0" width="1463.0" height="389.4" />
</clipPath>
<clipPath id="breeze-release-management-start-release-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -92,12 +92,9 @@
<clipPath id="breeze-release-management-start-release-line-14">
<rect x="0" y="343.1" width="1464" height="24.65"/>
</clipPath>
-<clipPath id="breeze-release-management-start-release-line-15">
- <rect x="0" y="367.5" width="1464" height="24.65"/>
- </clipPath>
</defs>
- <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="1480" height="462.8" rx="8"/><text
class="breeze-release-management-start-release-title" fill="#c5c8c6"
text-anchor="middle" x="740"
y="27">Command: release-management start-release</text>
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="1480" height="438.4" rx="8"/><text
class="breeze-release-management-start-release-title" fill="#c5c8c6"
text-anchor="middle" x="740"
y="27">Command: release-management start-release</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -115,15 +112,14 @@
</text><text class="breeze-release-management-start-release-r1" x="1464"
y="142" textLength="12.2"
clip-path="url(#breeze-release-management-start-release-line-5)">
</text><text class="breeze-release-management-start-release-r5" x="0"
y="166.4" textLength="24.4"
clip-path="url(#breeze-release-management-start-release-line-6)">╭─</text><text
class="breeze-release-management-start-release-r5" x="24.4" y="166.4"
textLength="256.2"
clip-path="url(#breeze-release-management-start-release-line-6)"> Start release flags </text><text
class="breeze-release-management-start-release-r5" x="280.6" y="166.4"
textLength="1159" clip-path="url(#b [...]
</text><text class="breeze-release-management-start-release-r5" x="0"
y="190.8" textLength="12.2"
clip-path="url(#breeze-release-management-start-release-line-7)">│</text><text
class="breeze-release-management-start-release-r6" x="24.4" y="190.8"
textLength="12.2"
clip-path="url(#breeze-release-management-start-release-line-7)">*</text><text
class="breeze-release-management-start-release-r4" x="61" y="190.8"
textLength="109.8"
clip-path="url(#breeze-release-management-start-release-line- [...]
-</text><text class="breeze-release-management-start-release-r5" x="0"
y="215.2" textLength="12.2"
clip-path="url(#breeze-release-management-start-release-line-8)">│</text><text
class="breeze-release-management-start-release-r6" x="24.4" y="215.2"
textLength="12.2"
clip-path="url(#breeze-release-management-start-release-line-8)">*</text><text
class="breeze-release-management-start-release-r4" x="61" y="215.2"
textLength="219.6"
clip-path="url(#breeze-release-management-start-release-line- [...]
-</text><text class="breeze-release-management-start-release-r5" x="0"
y="239.6" textLength="12.2"
clip-path="url(#breeze-release-management-start-release-line-9)">│</text><text
class="breeze-release-management-start-release-r4" x="61" y="239.6"
textLength="219.6"
clip-path="url(#breeze-release-management-start-release-line-9)">--task-sdk-version</text><text
class="breeze-release-management-start-release-r1" x="329.4" y="239.6"
textLength="427" clip-path="url(#breeze-release-management-st [...]
-</text><text class="breeze-release-management-start-release-r5" x="0" y="264"
textLength="1464"
clip-path="url(#breeze-release-management-start-release-line-10)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-release-management-start-release-r1" x="1464" y="264"
textLength="12.2"
clip-path="url(#breeze-release-management-start-release-line-10)">
-</text><text class="breeze-release-management-start-release-r5" x="0"
y="288.4" textLength="24.4"
clip-path="url(#breeze-release-management-start-release-line-11)">╭─</text><text
class="breeze-release-management-start-release-r5" x="24.4" y="288.4"
textLength="195.2"
clip-path="url(#breeze-release-management-start-release-line-11)"> Common options </text><text
class="breeze-release-management-start-release-r5" x="219.6" y="288.4"
textLength="1220" clip-path="url(#breeze-re [...]
-</text><text class="breeze-release-management-start-release-r5" x="0"
y="312.8" textLength="12.2"
clip-path="url(#breeze-release-management-start-release-line-12)">│</text><text
class="breeze-release-management-start-release-r4" x="24.4" y="312.8"
textLength="97.6"
clip-path="url(#breeze-release-management-start-release-line-12)">--answer</text><text
class="breeze-release-management-start-release-r9" x="158.6" y="312.8"
textLength="24.4" clip-path="url(#breeze-release-management-start-re [...]
-</text><text class="breeze-release-management-start-release-r5" x="0"
y="337.2" textLength="12.2"
clip-path="url(#breeze-release-management-start-release-line-13)">│</text><text
class="breeze-release-management-start-release-r4" x="24.4" y="337.2"
textLength="109.8"
clip-path="url(#breeze-release-management-start-release-line-13)">--dry-run</text><text
class="breeze-release-management-start-release-r9" x="158.6" y="337.2"
textLength="24.4" clip-path="url(#breeze-release-management-start- [...]
-</text><text class="breeze-release-management-start-release-r5" x="0"
y="361.6" textLength="12.2"
clip-path="url(#breeze-release-management-start-release-line-14)">│</text><text
class="breeze-release-management-start-release-r4" x="24.4" y="361.6"
textLength="109.8"
clip-path="url(#breeze-release-management-start-release-line-14)">--verbose</text><text
class="breeze-release-management-start-release-r9" x="158.6" y="361.6"
textLength="24.4" clip-path="url(#breeze-release-management-start- [...]
-</text><text class="breeze-release-management-start-release-r5" x="0" y="386"
textLength="12.2"
clip-path="url(#breeze-release-management-start-release-line-15)">│</text><text
class="breeze-release-management-start-release-r4" x="24.4" y="386"
textLength="73.2"
clip-path="url(#breeze-release-management-start-release-line-15)">--help</text><text
class="breeze-release-management-start-release-r9" x="158.6" y="386"
textLength="24.4" clip-path="url(#breeze-release-management-start-release-li
[...]
-</text><text class="breeze-release-management-start-release-r5" x="0"
y="410.4" textLength="1464"
clip-path="url(#breeze-release-management-start-release-line-16)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-release-management-start-release-r1" x="1464" y="410.4"
textLength="12.2"
clip-path="url(#breeze-release-management-start-release-line-16)">
+</text><text class="breeze-release-management-start-release-r5" x="0"
y="215.2" textLength="12.2"
clip-path="url(#breeze-release-management-start-release-line-8)">│</text><text
class="breeze-release-management-start-release-r4" x="61" y="215.2"
textLength="219.6"
clip-path="url(#breeze-release-management-start-release-line-8)">--task-sdk-version</text><text
class="breeze-release-management-start-release-r1" x="329.4" y="215.2"
textLength="427" clip-path="url(#breeze-release-management-st [...]
+</text><text class="breeze-release-management-start-release-r5" x="0"
y="239.6" textLength="1464"
clip-path="url(#breeze-release-management-start-release-line-9)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-release-management-start-release-r1" x="1464" y="239.6"
textLength="12.2"
clip-path="url(#breeze-release-management-start-release-line-9)">
+</text><text class="breeze-release-management-start-release-r5" x="0" y="264"
textLength="24.4"
clip-path="url(#breeze-release-management-start-release-line-10)">╭─</text><text
class="breeze-release-management-start-release-r5" x="24.4" y="264"
textLength="195.2"
clip-path="url(#breeze-release-management-start-release-line-10)"> Common options </text><text
class="breeze-release-management-start-release-r5" x="219.6" y="264"
textLength="1220" clip-path="url(#breeze-release- [...]
+</text><text class="breeze-release-management-start-release-r5" x="0"
y="288.4" textLength="12.2"
clip-path="url(#breeze-release-management-start-release-line-11)">│</text><text
class="breeze-release-management-start-release-r4" x="24.4" y="288.4"
textLength="97.6"
clip-path="url(#breeze-release-management-start-release-line-11)">--answer</text><text
class="breeze-release-management-start-release-r9" x="158.6" y="288.4"
textLength="24.4" clip-path="url(#breeze-release-management-start-re [...]
+</text><text class="breeze-release-management-start-release-r5" x="0"
y="312.8" textLength="12.2"
clip-path="url(#breeze-release-management-start-release-line-12)">│</text><text
class="breeze-release-management-start-release-r4" x="24.4" y="312.8"
textLength="109.8"
clip-path="url(#breeze-release-management-start-release-line-12)">--dry-run</text><text
class="breeze-release-management-start-release-r9" x="158.6" y="312.8"
textLength="24.4" clip-path="url(#breeze-release-management-start- [...]
+</text><text class="breeze-release-management-start-release-r5" x="0"
y="337.2" textLength="12.2"
clip-path="url(#breeze-release-management-start-release-line-13)">│</text><text
class="breeze-release-management-start-release-r4" x="24.4" y="337.2"
textLength="109.8"
clip-path="url(#breeze-release-management-start-release-line-13)">--verbose</text><text
class="breeze-release-management-start-release-r9" x="158.6" y="337.2"
textLength="24.4" clip-path="url(#breeze-release-management-start- [...]
+</text><text class="breeze-release-management-start-release-r5" x="0"
y="361.6" textLength="12.2"
clip-path="url(#breeze-release-management-start-release-line-14)">│</text><text
class="breeze-release-management-start-release-r4" x="24.4" y="361.6"
textLength="73.2"
clip-path="url(#breeze-release-management-start-release-line-14)">--help</text><text
class="breeze-release-management-start-release-r9" x="158.6" y="361.6"
textLength="24.4" clip-path="url(#breeze-release-management-start-rele [...]
+</text><text class="breeze-release-management-start-release-r5" x="0" y="386"
textLength="1464"
clip-path="url(#breeze-release-management-start-release-line-15)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-release-management-start-release-r1" x="1464" y="386"
textLength="12.2"
clip-path="url(#breeze-release-management-start-release-line-15)">
</text>
</g>
</g>
diff --git a/dev/breeze/doc/images/output_release-management_start-release.txt
b/dev/breeze/doc/images/output_release-management_start-release.txt
index 9576601cabf..2aa75ecda76 100644
--- a/dev/breeze/doc/images/output_release-management_start-release.txt
+++ b/dev/breeze/doc/images/output_release-management_start-release.txt
@@ -1 +1 @@
-481eebefa96f86a5a385c295c2cbbdd0
+8817e1cc91ae9a6e67e45f2582b19b5c
diff --git a/dev/breeze/src/airflow_breeze/commands/release_command.py
b/dev/breeze/src/airflow_breeze/commands/release_command.py
index 9115c40f21c..a66dd38c52f 100644
--- a/dev/breeze/src/airflow_breeze/commands/release_command.py
+++ b/dev/breeze/src/airflow_breeze/commands/release_command.py
@@ -28,6 +28,9 @@ from airflow_breeze.utils.console import console_print
from airflow_breeze.utils.path_utils import AIRFLOW_ROOT_PATH
from airflow_breeze.utils.run_utils import run_command
+# Pattern to match Airflow release versions (e.g., "3.0.5")
+RELEASE_PATTERN =
re.compile(r"^(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)$")
+
def clone_asf_repo(working_dir):
if confirm_action("Clone ASF repo?"):
@@ -159,18 +162,75 @@ def commit_release(version, task_sdk_version, rc,
task_sdk_rc, svn_release_repo)
os.chdir(current_dir)
-def remove_old_release(previous_release):
- if confirm_action(f"Remove old release {previous_release}?"):
- run_command(["svn", "rm", f"{previous_release}"], check=True)
- run_command(
- ["svn", "commit", "-m", f"Remove old release: {previous_release}"],
- check=True,
- )
- confirm_action(
- "Verify that the packages appear in "
- "[airflow](https://dist.apache.org/repos/dist/release/airflow/).
Continue?",
- abort=True,
- )
+def remove_old_release(version, task_sdk_version, svn_release_repo):
+ """
+ Remove all old Airflow and Task SDK releases from SVN except the current
versions.
+
+ :param version: Current Airflow release version to keep
+ :param task_sdk_version: Current Task SDK release version to keep (if any)
+ :param svn_release_repo: Path to the SVN release repository
+ """
+ if not confirm_action("Do you want to look for old releases to remove?"):
+ return
+
+ # Save current directory
+ current_dir = os.getcwd()
+ os.chdir(svn_release_repo)
+
+ # Initialize lists for old releases
+ old_airflow_releases = []
+ old_task_sdk_releases = []
+
+ # Remove old Airflow releases
+ for entry in os.scandir():
+ if entry.name == version:
+ # Don't remove the current release
+ continue
+ if entry.is_dir() and RELEASE_PATTERN.match(entry.name):
+ old_airflow_releases.append(entry.name)
+ old_airflow_releases.sort()
+
+ if old_airflow_releases:
+ console_print(f"The following old Airflow releases should be removed:
{old_airflow_releases}")
+ for old_release in old_airflow_releases:
+ console_print(f"Removing old release {old_release}")
+ if confirm_action(f"Remove old release {old_release}?"):
+ run_command(["svn", "rm", old_release], check=True)
+ run_command(
+ ["svn", "commit", "-m", f"Remove old release:
{old_release}"],
+ check=True,
+ )
+
+ # Remove old Task SDK releases
+ if task_sdk_version:
+ task_sdk_dir = os.path.join(svn_release_repo, "task-sdk")
+ if os.path.exists(task_sdk_dir):
+ for entry in os.scandir(task_sdk_dir):
+ if entry.name == task_sdk_version:
+ # Don't remove the current Task SDK release
+ continue
+ if entry.is_dir() and RELEASE_PATTERN.match(entry.name):
+ old_task_sdk_releases.append(f"task-sdk/{entry.name}")
+ old_task_sdk_releases.sort()
+
+ if old_task_sdk_releases:
+ console_print(
+ f"The following old Task SDK releases should be removed:
{old_task_sdk_releases}"
+ )
+ for old_release in old_task_sdk_releases:
+ console_print(f"Removing old release {old_release}")
+ if confirm_action(f"Remove old release {old_release}?"):
+ run_command(["svn", "rm", old_release], check=True)
+ run_command(
+ ["svn", "commit", "-m", f"Remove old release:
{old_release}"],
+ check=True,
+ )
+
+ if not old_airflow_releases and not old_task_sdk_releases:
+ console_print("No old releases to remove.")
+ if old_airflow_releases or old_task_sdk_releases:
+ console_print("[success]Old releases removed")
+ os.chdir(current_dir)
def verify_pypi_package(version):
@@ -315,22 +375,18 @@ def push_tag_for_final_version(version,
release_candidate, task_sdk_version=None
"The latest release candidate for the given version will be automatically
found from SVN dev directory.",
)
@click.option("--version", required=True, help="Airflow release version e.g.
3.0.5")
[email protected]("--previous-release", required=True, help="Previous Airflow
release e.g. 3.0.4")
@click.option("--task-sdk-version", required=False, help="Task SDK release
version e.g. 1.0.5")
@option_answer
@option_dry_run
@option_verbose
-def airflow_release(version, previous_release, task_sdk_version):
+def airflow_release(version, task_sdk_version):
if "rc" in version:
exit("Version must not contain 'rc' - use the final version (e.g.,
3.0.5)")
- if "rc" in previous_release:
- exit("Previous release must not contain 'rc'")
os.chdir(AIRFLOW_ROOT_PATH)
airflow_repo_root = os.getcwd()
console_print()
console_print("Airflow Release Version:", version)
- console_print("Previous Airflow release:", previous_release)
if task_sdk_version:
console_print("Task SDK Release Version:", task_sdk_version)
console_print("Airflow repo root:", airflow_repo_root)
@@ -402,10 +458,16 @@ def airflow_release(version, previous_release,
task_sdk_version):
"Verify that the artifacts appear in
https://dist.apache.org/repos/dist/release/airflow/", abort=True
)
- # Remove old release
+ # Remove old releases
if os.path.exists(svn_release_version_dir):
os.chdir("..")
- remove_old_release(previous_release)
+ remove_old_release(version, task_sdk_version, svn_release_repo)
+ confirm_action(
+ "Verify that the packages appear in "
+ "[airflow](https://dist.apache.org/repos/dist/release/airflow/)"
+ "and
[airflow](https://dist.apache.org/repos/dist/release/airflow/task-sdk/).
Continue?",
+ abort=True,
+ )
# Verify pypi package
if os.path.exists(svn_release_version_dir):
diff --git
a/dev/breeze/src/airflow_breeze/commands/release_management_commands_config.py
b/dev/breeze/src/airflow_breeze/commands/release_management_commands_config.py
index 9ce0b13224a..4400442a6bb 100644
---
a/dev/breeze/src/airflow_breeze/commands/release_management_commands_config.py
+++
b/dev/breeze/src/airflow_breeze/commands/release_management_commands_config.py
@@ -457,7 +457,7 @@ RELEASE_MANAGEMENT_PARAMETERS: dict[str, list[dict[str, str
| list[str]]]] = {
"breeze release-management start-release": [
{
"name": "Start release flags",
- "options": ["--version", "--previous-release",
"--task-sdk-version"],
+ "options": ["--version", "--task-sdk-version"],
}
],
"breeze release-management update-constraints": [
diff --git a/dev/breeze/tests/test_release_command.py
b/dev/breeze/tests/test_release_command.py
index 686e3df1ea5..f10c79655fa 100644
--- a/dev/breeze/tests/test_release_command.py
+++ b/dev/breeze/tests/test_release_command.py
@@ -18,6 +18,8 @@ from __future__ import annotations
from unittest.mock import patch
+import pytest
+
from airflow_breeze.commands.release_command import
find_latest_release_candidate
@@ -140,3 +142,378 @@ class TestFindLatestReleaseCandidate:
with patch("os.listdir", side_effect=OSError("Permission denied")):
result = find_latest_release_candidate("3.0.5", str(svn_dev_repo),
component="airflow")
assert result is None
+
+
+class FakeDirEntry:
+ def __init__(self, name: str, *, is_dir: bool):
+ self.name = name
+ self._is_dir = is_dir
+
+ def is_dir(self) -> bool:
+ return self._is_dir
+
+
[email protected]
+def release_cmd():
+ """Lazy import the release command module."""
+ import airflow_breeze.commands.release_command as module
+
+ return module
+
+
+def test_remove_old_release_only_collects_release_directories(monkeypatch,
release_cmd):
+ version = "3.0.5"
+ task_sdk_version = "1.0.5"
+ svn_release_repo = "/svn/release/repo"
+
+ # Arrange: entries include current release, old release directories, a
matching "file", and non-release directory.
+ entries = [
+ FakeDirEntry(version, is_dir=True), # current release: should be
skipped
+ FakeDirEntry("3.0.4", is_dir=True), # old release dir: should be
included
+ FakeDirEntry("3.0.3", is_dir=True), # old release dir: should be
included
+ FakeDirEntry("3.0.2", is_dir=False), # matches pattern but not a
directory: excluded
+ FakeDirEntry("task-sdk", is_dir=True), # task-sdk directory: will be
scanned separately
+ FakeDirEntry("not-a-release", is_dir=True), # directory but not
matching pattern: excluded
+ ]
+
+ # Task SDK directory entries
+ task_sdk_entries = [
+ FakeDirEntry(task_sdk_version, is_dir=True), # current task-sdk
release: should be skipped
+ FakeDirEntry("1.0.4", is_dir=True), # old task-sdk release: should be
included
+ FakeDirEntry("1.0.3", is_dir=True), # old task-sdk release: should be
included
+ ]
+
+ chdir_calls: list[str] = []
+ console_messages: list[str] = []
+ run_command_calls: list[list[str]] = []
+ getcwd_calls: list[int] = []
+ path_exists_calls: list[str] = []
+
+ def fake_confirm_action(prompt: str, **_kwargs) -> bool:
+ # First prompt decides whether we scan. We want to.
+ if prompt == "Do you want to look for old releases to remove?":
+ return True
+ # For each candidate, we decline removal to avoid running svn commands.
+ if prompt.startswith("Remove old release "):
+ return False
+ raise AssertionError(f"Unexpected confirm prompt: {prompt}")
+
+ def fake_getcwd() -> str:
+ getcwd_calls.append(1)
+ return "/original/dir"
+
+ def fake_path_exists(path: str) -> bool:
+ path_exists_calls.append(path)
+ return path == "/svn/release/repo/task-sdk"
+
+ def fake_scandir(path=None):
+ if path == "/svn/release/repo/task-sdk":
+ return iter(task_sdk_entries)
+ return iter(entries)
+
+ monkeypatch.setattr(release_cmd.os, "getcwd", fake_getcwd)
+ monkeypatch.setattr(release_cmd.os, "chdir", lambda p:
chdir_calls.append(p))
+ monkeypatch.setattr(release_cmd.os, "scandir", fake_scandir)
+ monkeypatch.setattr(release_cmd.os.path, "exists", fake_path_exists)
+ monkeypatch.setattr(release_cmd.os.path, "join", lambda *args:
"/".join(args))
+ monkeypatch.setattr(release_cmd, "confirm_action", fake_confirm_action)
+ monkeypatch.setattr(release_cmd, "console_print", lambda msg="":
console_messages.append(str(msg)))
+ monkeypatch.setattr(release_cmd, "run_command", lambda cmd, **_kwargs:
run_command_calls.append(cmd))
+
+ # Act
+ release_cmd.remove_old_release(
+ version=version, task_sdk_version=task_sdk_version,
svn_release_repo=svn_release_repo
+ )
+
+ # Assert: only directory entries matching RELEASE_PATTERN, excluding
current version, and sorted.
+ assert svn_release_repo in chdir_calls
+ assert "/original/dir" in chdir_calls
+ assert "The following old Airflow releases should be removed: ['3.0.3',
'3.0.4']" in console_messages
+ assert (
+ "The following old Task SDK releases should be removed:
['task-sdk/1.0.3', 'task-sdk/1.0.4']"
+ in console_messages
+ )
+ assert run_command_calls == []
+
+
+def test_remove_old_release_returns_early_when_user_declines(monkeypatch,
release_cmd):
+ version = "3.0.5"
+ task_sdk_version = "1.0.5"
+ svn_release_repo = "/svn/release/repo"
+
+ confirm_prompts: list[str] = []
+
+ def fake_confirm_action(prompt: str, **_kwargs) -> bool:
+ confirm_prompts.append(prompt)
+ return False
+
+ def should_not_be_called(*_args, **_kwargs):
+ raise AssertionError("This should not have been called when user
declines the initial prompt.")
+
+ monkeypatch.setattr(release_cmd, "confirm_action", fake_confirm_action)
+ monkeypatch.setattr(release_cmd.os, "getcwd", should_not_be_called)
+ monkeypatch.setattr(release_cmd.os, "chdir", should_not_be_called)
+ monkeypatch.setattr(release_cmd.os, "scandir", should_not_be_called)
+ monkeypatch.setattr(release_cmd, "console_print", should_not_be_called)
+ monkeypatch.setattr(release_cmd, "run_command", should_not_be_called)
+
+ release_cmd.remove_old_release(
+ version=version, task_sdk_version=task_sdk_version,
svn_release_repo=svn_release_repo
+ )
+
+ assert confirm_prompts == ["Do you want to look for old releases to
remove?"]
+
+
+def test_remove_old_release_removes_confirmed_old_releases(monkeypatch,
release_cmd):
+ version = "3.1.5"
+ task_sdk_version = "1.1.5"
+ svn_release_repo = "/svn/release/repo"
+
+ entries = [
+ FakeDirEntry("3.1.4", is_dir=True),
+ FakeDirEntry(version, is_dir=True),
+ FakeDirEntry("3.1.0", is_dir=True),
+ ]
+
+ task_sdk_entries = [
+ FakeDirEntry("1.1.4", is_dir=True),
+ FakeDirEntry(task_sdk_version, is_dir=True),
+ FakeDirEntry("1.1.0", is_dir=True),
+ ]
+
+ chdir_calls: list[str] = []
+ console_messages: list[str] = []
+ run_command_calls: list[tuple[list[str], dict]] = []
+ confirm_prompts: list[str] = []
+ getcwd_calls: list[int] = []
+
+ def fake_confirm_action(prompt: str, **_kwargs) -> bool:
+ confirm_prompts.append(prompt)
+ if prompt == "Do you want to look for old releases to remove?":
+ return True
+ if prompt == "Remove old release 3.1.0?":
+ return True
+ if prompt == "Remove old release 3.1.4?":
+ return False
+ if prompt.startswith("Remove old release task-sdk/"):
+ return False
+ raise AssertionError(f"Unexpected confirm prompt: {prompt}")
+
+ def fake_getcwd() -> str:
+ getcwd_calls.append(1)
+ return "/original/dir"
+
+ def fake_path_exists(path: str) -> bool:
+ return path == "/svn/release/repo/task-sdk"
+
+ def fake_scandir(path=None):
+ if path == "/svn/release/repo/task-sdk":
+ return iter(task_sdk_entries)
+ return iter(entries)
+
+ monkeypatch.setattr(release_cmd.os, "getcwd", fake_getcwd)
+ monkeypatch.setattr(release_cmd.os, "chdir", lambda path:
chdir_calls.append(path))
+ monkeypatch.setattr(release_cmd.os, "scandir", fake_scandir)
+ monkeypatch.setattr(release_cmd.os.path, "exists", fake_path_exists)
+ monkeypatch.setattr(release_cmd.os.path, "join", lambda *args:
"/".join(args))
+ monkeypatch.setattr(release_cmd, "confirm_action", fake_confirm_action)
+ monkeypatch.setattr(release_cmd, "console_print", lambda msg="":
console_messages.append(str(msg)))
+
+ def fake_run_command(cmd: list[str], **kwargs):
+ run_command_calls.append((cmd, kwargs))
+
+ monkeypatch.setattr(release_cmd, "run_command", fake_run_command)
+
+ release_cmd.remove_old_release(
+ version=version, task_sdk_version=task_sdk_version,
svn_release_repo=svn_release_repo
+ )
+
+ assert chdir_calls == [svn_release_repo, "/original/dir"]
+ assert confirm_prompts == [
+ "Do you want to look for old releases to remove?",
+ "Remove old release 3.1.0?",
+ "Remove old release 3.1.4?",
+ "Remove old release task-sdk/1.1.0?",
+ "Remove old release task-sdk/1.1.4?",
+ ]
+ assert "The following old Airflow releases should be removed: ['3.1.0',
'3.1.4']" in console_messages
+ assert (
+ "The following old Task SDK releases should be removed:
['task-sdk/1.1.0', 'task-sdk/1.1.4']"
+ in console_messages
+ )
+ assert "Removing old release 3.1.0" in console_messages
+ assert "Removing old release 3.1.4" in console_messages
+ assert "[success]Old releases removed" in console_messages
+
+ # Only 3.1.0 was confirmed, so we should run rm+commit for 3.1.0 only.
+ assert run_command_calls == [
+ (["svn", "rm", "3.1.0"], {"check": True}),
+ (["svn", "commit", "-m", "Remove old release: 3.1.0"], {"check":
True}),
+ ]
+
+
+def test_remove_old_release_no_old_releases(monkeypatch, release_cmd):
+ version = "3.0.5"
+ task_sdk_version = "1.0.5"
+ svn_release_repo = "/svn/release/repo"
+
+ # Only current release exists
+ entries = [
+ FakeDirEntry(version, is_dir=True),
+ FakeDirEntry("task-sdk", is_dir=True), # task-sdk directory exists
+ ]
+
+ task_sdk_entries = [
+ FakeDirEntry(task_sdk_version, is_dir=True), # Only current task-sdk
release
+ ]
+
+ chdir_calls: list[str] = []
+ console_messages: list[str] = []
+ run_command_calls: list[list[str]] = []
+
+ def fake_confirm_action(prompt: str, **_kwargs) -> bool:
+ if prompt == "Do you want to look for old releases to remove?":
+ return True
+ raise AssertionError(f"Unexpected confirm prompt: {prompt}")
+
+ def fake_getcwd() -> str:
+ return "/original/dir"
+
+ def fake_path_exists(path: str) -> bool:
+ return path == "/svn/release/repo/task-sdk"
+
+ def fake_scandir(path=None):
+ if path == "/svn/release/repo/task-sdk":
+ return iter(task_sdk_entries)
+ return iter(entries)
+
+ monkeypatch.setattr(release_cmd.os, "getcwd", fake_getcwd)
+ monkeypatch.setattr(release_cmd.os, "chdir", lambda path:
chdir_calls.append(path))
+ monkeypatch.setattr(release_cmd.os, "scandir", fake_scandir)
+ monkeypatch.setattr(release_cmd.os.path, "exists", fake_path_exists)
+ monkeypatch.setattr(release_cmd.os.path, "join", lambda *args:
"/".join(args))
+ monkeypatch.setattr(release_cmd, "confirm_action", fake_confirm_action)
+ monkeypatch.setattr(release_cmd, "console_print", lambda msg="":
console_messages.append(str(msg)))
+ monkeypatch.setattr(release_cmd, "run_command", lambda cmd, **_kwargs:
run_command_calls.append(cmd))
+
+ release_cmd.remove_old_release(
+ version=version, task_sdk_version=task_sdk_version,
svn_release_repo=svn_release_repo
+ )
+
+ assert "No old releases to remove." in console_messages
+ assert run_command_calls == []
+ assert chdir_calls == [svn_release_repo, "/original/dir"]
+
+
+def test_remove_old_release_task_sdk_only(monkeypatch, release_cmd):
+ version = "3.0.5"
+ task_sdk_version = "1.0.5"
+ svn_release_repo = "/svn/release/repo"
+
+ # Only current Airflow release exists, but old Task SDK releases exist
+ entries = [
+ FakeDirEntry(version, is_dir=True),
+ ]
+
+ task_sdk_entries = [
+ FakeDirEntry(task_sdk_version, is_dir=True), # current task-sdk
release
+ FakeDirEntry("1.0.4", is_dir=True), # old task-sdk release
+ FakeDirEntry("1.0.3", is_dir=True), # old task-sdk release
+ ]
+
+ chdir_calls: list[str] = []
+ console_messages: list[str] = []
+ run_command_calls: list[tuple[list[str], dict]] = []
+ confirm_prompts: list[str] = []
+
+ def fake_confirm_action(prompt: str, **_kwargs) -> bool:
+ confirm_prompts.append(prompt)
+ if prompt == "Do you want to look for old releases to remove?":
+ return True
+ if prompt == "Remove old release task-sdk/1.0.3?":
+ return True
+ if prompt == "Remove old release task-sdk/1.0.4?":
+ return False
+ raise AssertionError(f"Unexpected confirm prompt: {prompt}")
+
+ def fake_getcwd() -> str:
+ return "/original/dir"
+
+ def fake_path_exists(path: str) -> bool:
+ return path == "/svn/release/repo/task-sdk"
+
+ def fake_scandir(path=None):
+ if path == "/svn/release/repo/task-sdk":
+ return iter(task_sdk_entries)
+ return iter(entries)
+
+ monkeypatch.setattr(release_cmd.os, "getcwd", fake_getcwd)
+ monkeypatch.setattr(release_cmd.os, "chdir", lambda path:
chdir_calls.append(path))
+ monkeypatch.setattr(release_cmd.os, "scandir", fake_scandir)
+ monkeypatch.setattr(release_cmd.os.path, "exists", fake_path_exists)
+ monkeypatch.setattr(release_cmd.os.path, "join", lambda *args:
"/".join(args))
+ monkeypatch.setattr(release_cmd, "confirm_action", fake_confirm_action)
+ monkeypatch.setattr(release_cmd, "console_print", lambda msg="":
console_messages.append(str(msg)))
+
+ def fake_run_command(cmd: list[str], **kwargs):
+ run_command_calls.append((cmd, kwargs))
+
+ monkeypatch.setattr(release_cmd, "run_command", fake_run_command)
+
+ release_cmd.remove_old_release(
+ version=version, task_sdk_version=task_sdk_version,
svn_release_repo=svn_release_repo
+ )
+
+ assert chdir_calls == [svn_release_repo, "/original/dir"]
+ assert (
+ "The following old Task SDK releases should be removed:
['task-sdk/1.0.3', 'task-sdk/1.0.4']"
+ in console_messages
+ )
+ assert "Removing old release task-sdk/1.0.3" in console_messages
+ assert "Removing old release task-sdk/1.0.4" in console_messages
+ assert "[success]Old releases removed" in console_messages
+ assert run_command_calls == [
+ (["svn", "rm", "task-sdk/1.0.3"], {"check": True}),
+ (["svn", "commit", "-m", "Remove old release: task-sdk/1.0.3"],
{"check": True}),
+ ]
+
+
+def test_remove_old_release_no_task_sdk_version(monkeypatch, release_cmd):
+ version = "3.0.5"
+ task_sdk_version = None
+ svn_release_repo = "/svn/release/repo"
+
+ entries = [
+ FakeDirEntry(version, is_dir=True),
+ FakeDirEntry("3.0.4", is_dir=True), # old release
+ ]
+
+ chdir_calls: list[str] = []
+ console_messages: list[str] = []
+ run_command_calls: list[list[str]] = []
+
+ def fake_confirm_action(prompt: str, **_kwargs) -> bool:
+ if prompt == "Do you want to look for old releases to remove?":
+ return True
+ if prompt.startswith("Remove old release "):
+ return False
+ raise AssertionError(f"Unexpected confirm prompt: {prompt}")
+
+ def fake_getcwd() -> str:
+ return "/original/dir"
+
+ monkeypatch.setattr(release_cmd.os, "getcwd", fake_getcwd)
+ monkeypatch.setattr(release_cmd.os, "chdir", lambda path:
chdir_calls.append(path))
+ monkeypatch.setattr(release_cmd.os, "scandir", lambda: iter(entries))
+ monkeypatch.setattr(release_cmd, "confirm_action", fake_confirm_action)
+ monkeypatch.setattr(release_cmd, "console_print", lambda msg="":
console_messages.append(str(msg)))
+ monkeypatch.setattr(release_cmd, "run_command", lambda cmd, **_kwargs:
run_command_calls.append(cmd))
+
+ release_cmd.remove_old_release(
+ version=version, task_sdk_version=task_sdk_version,
svn_release_repo=svn_release_repo
+ )
+
+ assert "The following old Airflow releases should be removed: ['3.0.4']"
in console_messages
+ assert "task-sdk" not in " ".join(console_messages).lower()
+ assert run_command_calls == []
+ assert chdir_calls == [svn_release_repo, "/original/dir"]