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:&#160;release-management&#160;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:&#160;release-management&#160;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)">&#160;Start&#160;release&#160;flags&#160;</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)">&#160;Common&#160;options&#160;</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)">&#160;Common&#160;options&#160;</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"]


Reply via email to