This is an automated email from the ASF dual-hosted git repository.

ephraimanierobi 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 c2f349d9695 Infer the RC from the target version during release. 
(#59455) (#59469)
c2f349d9695 is described below

commit c2f349d9695c787dae3359dc5826faafa7d94dcb
Author: Ephraim Anierobi <[email protected]>
AuthorDate: Mon Dec 15 18:07:08 2025 +0100

    Infer the RC from the target version during release. (#59455) (#59469)
    
    This reduces the risk of error by ensuring a release is only
    performed when a corresponding RC exists and that the latest
    RC is selected automatically, rather than relying on manually supplied 
input.
    
    (cherry picked from commit a9f095e6fc4a81bc259eaab44ae87105e4a43812)
---
 dev/README_RELEASE_AIRFLOW.md                      |  12 +-
 .../doc/images/output_release-management.txt       |   2 +-
 .../output_release-management_start-release.svg    |  36 +++---
 .../output_release-management_start-release.txt    |   2 +-
 .../src/airflow_breeze/commands/release_command.py |  89 ++++++++++---
 .../commands/release_management_commands_config.py |   2 +-
 dev/breeze/tests/test_release_command.py           | 142 +++++++++++++++++++++
 7 files changed, 241 insertions(+), 44 deletions(-)

diff --git a/dev/README_RELEASE_AIRFLOW.md b/dev/README_RELEASE_AIRFLOW.md
index 598cc4e9096..3005b9d2c12 100644
--- a/dev/README_RELEASE_AIRFLOW.md
+++ b/dev/README_RELEASE_AIRFLOW.md
@@ -1003,25 +1003,19 @@ The best way of doing this is to svn cp between the two 
repos (this avoids havin
 
 ```shell script
 export VERSION=3.1.3
-export VERSION_SUFFIX=rc1
-export VERSION_RC=${VERSION}${VERSION_SUFFIX}
 export TASK_SDK_VERSION=1.1.3
-export TASK_SDK_VERSION_RC=${TASK_SDK_VERSION}${VERSION_SUFFIX}
 export PREVIOUS_RELEASE=3.1.2
 # cd to the airflow repo directory and set the environment variable below
 export AIRFLOW_REPO_ROOT=$(pwd)
 # start the release process by running the below command
 breeze release-management start-release \
-    --release-candidate ${VERSION_RC} \
+    --version ${VERSION} \
     --previous-release ${PREVIOUS_RELEASE} \
-    --task-sdk-release-candidate ${TASK_SDK_VERSION_RC}
+    --task-sdk-version ${TASK_SDK_VERSION}
 ```
 
-Note: The `--task-sdk-release-candidate` parameter is optional. If you are 
releasing Airflow without a corresponding Task SDK release, you can omit this 
parameter.
+Note: The `--task-sdk-version` parameter is optional. If you are releasing 
Airflow without a corresponding Task SDK release, you can omit this parameter.
 
-```Dockerfile
-ARG AIRFLOW_EXTRAS=".....,<provider>,...."
-```
 
 4. Make sure to update Airflow version in ``v3-*-test`` branch after 
cherry-picking to X.Y.1 in
    ``airflow/__init__.py``
diff --git a/dev/breeze/doc/images/output_release-management.txt 
b/dev/breeze/doc/images/output_release-management.txt
index 74e81545457..0543e36d195 100644
--- a/dev/breeze/doc/images/output_release-management.txt
+++ b/dev/breeze/doc/images/output_release-management.txt
@@ -1 +1 @@
-adf95164020fa37164c778e10dcdaa36
+718c25395e194f1839a38424f73838f5
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 41227a891b6..22acf18ad1b 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 440.4" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 1482 464.79999999999995" 
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="389.4" />
+      <rect x="0" y="0" width="1463.0" height="413.79999999999995" />
     </clipPath>
     <clipPath id="breeze-release-management-start-release-line-0">
     <rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -92,9 +92,12 @@
 <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="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>
+    <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>
             <g transform="translate(26,22)">
             <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
             <circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -107,19 +110,20 @@
     <text class="breeze-release-management-start-release-r1" x="1464" y="20" 
textLength="12.2" 
clip-path="url(#breeze-release-management-start-release-line-0)">
 </text><text class="breeze-release-management-start-release-r2" x="12.2" 
y="44.4" textLength="73.2" 
clip-path="url(#breeze-release-management-start-release-line-1)">Usage:</text><text
 class="breeze-release-management-start-release-r3" x="97.6" y="44.4" 
textLength="475.8" 
clip-path="url(#breeze-release-management-start-release-line-1)">breeze&#160;release-management&#160;start-release</text><text
 class="breeze-release-management-start-release-r1" x="585.6" y="44.4" 
textLength="12.2" clip- [...]
 </text><text class="breeze-release-management-start-release-r1" x="1464" 
y="68.8" textLength="12.2" 
clip-path="url(#breeze-release-management-start-release-line-2)">
-</text><text class="breeze-release-management-start-release-r1" x="12.2" 
y="93.2" textLength="1305.4" 
clip-path="url(#breeze-release-management-start-release-line-3)">Start&#160;the&#160;process&#160;of&#160;releasing&#160;an&#160;Airflow&#160;version.&#160;This&#160;command&#160;will&#160;guide&#160;you&#160;through&#160;the&#160;release&#160;process.</text><text
 class="breeze-release-management-start-release-r1" x="1464" y="93.2" 
textLength="12.2" clip-path="url(#breeze-release-managem [...]
-</text><text class="breeze-release-management-start-release-r1" x="1464" 
y="117.6" textLength="12.2" 
clip-path="url(#breeze-release-management-start-release-line-4)">
-</text><text class="breeze-release-management-start-release-r5" x="0" y="142" 
textLength="24.4" 
clip-path="url(#breeze-release-management-start-release-line-5)">╭─</text><text 
class="breeze-release-management-start-release-r5" x="24.4" y="142" 
textLength="256.2" 
clip-path="url(#breeze-release-management-start-release-line-5)">&#160;Start&#160;release&#160;flags&#160;</text><text
 class="breeze-release-management-start-release-r5" x="280.6" y="142" 
textLength="1159" clip-path="url(#breeze- [...]
-</text><text class="breeze-release-management-start-release-r5" x="0" 
y="166.4" textLength="12.2" 
clip-path="url(#breeze-release-management-start-release-line-6)">│</text><text 
class="breeze-release-management-start-release-r6" x="24.4" y="166.4" 
textLength="12.2" 
clip-path="url(#breeze-release-management-start-release-line-6)">*</text><text 
class="breeze-release-management-start-release-r4" x="61" y="166.4" 
textLength="231.8" 
clip-path="url(#breeze-release-management-start-release-line- [...]
-</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="219.6" 
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-r4" x="61" y="215.2" 
textLength="341.6" 
clip-path="url(#breeze-release-management-start-release-line-8)">--task-sdk-release-candidate</text><text
 class="breeze-release-management-start-release-r1" x="451.4" y="215.2" 
textLength="488" clip-path="url(#breeze-release-man [...]
-</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><text class="breeze-release-management-start-release-r1" x="12.2" 
y="93.2" textLength="1439.6" 
clip-path="url(#breeze-release-management-start-release-line-3)">Start&#160;the&#160;process&#160;of&#160;releasing&#160;an&#160;Airflow&#160;version.&#160;This&#160;command&#160;will&#160;guide&#160;you&#160;through&#160;the&#160;release&#160;process.&#160;The&#160;latest</text><text
 class="breeze-release-management-start-release-r1" x="1464" y="93.2" 
textLength="12.2" clip-path="url(#b [...]
+</text><text class="breeze-release-management-start-release-r1" x="12.2" 
y="117.6" textLength="1110.2" 
clip-path="url(#breeze-release-management-start-release-line-4)">release&#160;candidate&#160;for&#160;the&#160;given&#160;version&#160;will&#160;be&#160;automatically&#160;found&#160;from&#160;SVN&#160;dev&#160;directory.</text><text
 class="breeze-release-management-start-release-r1" x="1464" y="117.6" 
textLength="12.2" 
clip-path="url(#breeze-release-management-start-release-line-4)">
+</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>
     </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 5920f348902..9576601cabf 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 @@
-a860ddc98626bc82dc298e2bd0081f97
+481eebefa96f86a5a385c295c2cbbdd0
diff --git a/dev/breeze/src/airflow_breeze/commands/release_command.py 
b/dev/breeze/src/airflow_breeze/commands/release_command.py
index b05ddf92a6d..9115c40f21c 100644
--- a/dev/breeze/src/airflow_breeze/commands/release_command.py
+++ b/dev/breeze/src/airflow_breeze/commands/release_command.py
@@ -17,6 +17,7 @@
 from __future__ import annotations
 
 import os
+import re
 
 import click
 
@@ -44,6 +45,47 @@ def clone_asf_repo(working_dir):
         )
 
 
+def find_latest_release_candidate(version, svn_dev_repo, component="airflow"):
+    """
+    Find the latest release candidate for a given version from SVN dev 
directory.
+
+    :param version: The base version (e.g., "3.0.5")
+    :param svn_dev_repo: Path to the SVN dev repository
+    :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 component == "task-sdk":
+        search_dir = f"{svn_dev_repo}/task-sdk"
+    else:
+        search_dir = svn_dev_repo
+
+    if not os.path.exists(search_dir):
+        return None
+
+    # Pattern to match release candidates for this version (e.g., "3.0.5rc1", 
"3.0.5rc2")
+    pattern = re.compile(rf"^{re.escape(version)}rc(\d+)$")
+    matching_rcs = []
+
+    try:
+        entries = os.listdir(search_dir)
+        for entry in entries:
+            match = pattern.match(entry)
+            if match:
+                rc_number = int(match.group(1))
+                matching_rcs.append((rc_number, entry))
+
+        if not matching_rcs:
+            return None
+
+        # Sort by RC number and return the latest
+        matching_rcs.sort(key=lambda x: x[0], reverse=True)
+        latest_rc = matching_rcs[0][1]
+        console_print(f"Found latest {component} release candidate: 
{latest_rc}")
+        return latest_rc
+    except OSError:
+        return None
+
+
 def create_version_dir(version, task_sdk_version=None):
     if confirm_action(f"Create SVN version directory for Airflow {version}?"):
         run_command(["svn", "mkdir", f"{version}"], check=True)
@@ -269,35 +311,27 @@ def push_tag_for_final_version(version, 
release_candidate, task_sdk_version=None
     name="start-release",
     short_help="Start Airflow release process",
     help="Start the process of releasing an Airflow version. "
-    "This command will guide you through the release process. ",
+    "This command will guide you through the release process. "
+    "The latest release candidate for the given version will be automatically 
found from SVN dev directory.",
 )
[email protected]("--release-candidate", required=True, help="Airflow release 
candidate e.g. 3.0.5rc1")
[email protected]("--version", required=True, help="Airflow release version e.g. 
3.0.5")
 @click.option("--previous-release", required=True, help="Previous Airflow 
release e.g. 3.0.4")
[email protected]("--task-sdk-release-candidate", required=False, help="Task SDK 
release candidate e.g. 1.0.5rc1")
[email protected]("--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(release_candidate, previous_release, 
task_sdk_release_candidate):
-    if "rc" not in release_candidate:
-        exit("Release candidate must contain 'rc'")
+def airflow_release(version, previous_release, 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'")
 
-    version = release_candidate[:-3]
-    task_sdk_version = None
-    if task_sdk_release_candidate:
-        if "rc" not in task_sdk_release_candidate:
-            exit("Task SDK release candidate must contain 'rc'")
-        task_sdk_version = task_sdk_release_candidate[:-3]
-
     os.chdir(AIRFLOW_ROOT_PATH)
     airflow_repo_root = os.getcwd()
     console_print()
-    console_print("Airflow Release candidate:", release_candidate)
     console_print("Airflow Release Version:", version)
     console_print("Previous Airflow release:", previous_release)
-    if task_sdk_release_candidate:
-        console_print("Task SDK Release candidate:", 
task_sdk_release_candidate)
+    if task_sdk_version:
         console_print("Task SDK Release Version:", task_sdk_version)
     console_print("Airflow repo root:", airflow_repo_root)
     console_print()
@@ -317,6 +351,29 @@ def airflow_release(release_candidate, previous_release, 
task_sdk_release_candid
     console_print("SVN dev repo root:", svn_dev_repo)
     console_print("SVN release repo root:", svn_release_repo)
 
+    # 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")
+    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"
+        )
+        if not task_sdk_release_candidate:
+            exit(f"No Task SDK release candidate found for version 
{task_sdk_version} in SVN dev directory")
+
+    console_print()
+    console_print("Airflow Release candidate:", release_candidate)
+    console_print("Airflow Release Version:", version)
+    if task_sdk_release_candidate:
+        console_print("Task SDK Release candidate:", 
task_sdk_release_candidate)
+        console_print("Task SDK Release Version:", task_sdk_version)
+    console_print()
+
     # Create the version directory
     confirm_action("Confirm that the above repo exists. Continue?", abort=True)
 
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 5e00c23b866..9ce0b13224a 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": ["--release-candidate", "--previous-release", 
"--task-sdk-release-candidate"],
+            "options": ["--version", "--previous-release", 
"--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
new file mode 100644
index 00000000000..686e3df1ea5
--- /dev/null
+++ b/dev/breeze/tests/test_release_command.py
@@ -0,0 +1,142 @@
+# 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
+
+from unittest.mock import patch
+
+from airflow_breeze.commands.release_command import 
find_latest_release_candidate
+
+
+class TestFindLatestReleaseCandidate:
+    """Test the find_latest_release_candidate function."""
+
+    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()
+
+        result = find_latest_release_candidate("3.0.5", str(svn_dev_repo), 
component="airflow")
+        assert result == "3.0.5rc1"
+
+    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
+
+        result = find_latest_release_candidate("3.0.5", str(svn_dev_repo), 
component="airflow")
+        assert result == "3.0.5rc10"
+
+    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"
+        svn_dev_repo.mkdir(parents=True)
+
+        # 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.0.6rc1").mkdir()
+
+        result = find_latest_release_candidate("3.0.5", str(svn_dev_repo), 
component="airflow")
+        assert result == "3.0.5rc2"
+
+    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 / "some-other-dir").mkdir()
+
+        result = find_latest_release_candidate("3.0.5", str(svn_dev_repo), 
component="airflow")
+        assert result == "3.0.5rc1"
+
+    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"
+        svn_dev_repo.mkdir(parents=True)
+
+        # 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")
+        assert result is None
+
+    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")
+        assert result is None
+
+    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")
+        assert result is None
+
+    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"
+        task_sdk_dir = svn_dev_repo / "task-sdk"
+        task_sdk_dir.mkdir(parents=True)
+
+        # Create multiple Task SDK RC directories
+        (task_sdk_dir / "1.0.5rc1").mkdir()
+        (task_sdk_dir / "1.0.5rc2").mkdir()
+        (task_sdk_dir / "1.0.5rc3").mkdir()
+
+        result = find_latest_release_candidate("1.0.5", str(svn_dev_repo), 
component="task-sdk")
+        assert result == "1.0.5rc3"
+
+    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"
+        svn_dev_repo.mkdir(parents=True)
+        task_sdk_dir = svn_dev_repo / "task-sdk"
+        task_sdk_dir.mkdir()
+
+        # Create airflow RC (should be ignored)
+        (svn_dev_repo / "3.0.5rc1").mkdir()
+        # Create task-sdk RC
+        (task_sdk_dir / "1.0.5rc1").mkdir()
+
+        result = find_latest_release_candidate("1.0.5", str(svn_dev_repo), 
component="task-sdk")
+        assert result == "1.0.5rc1"
+
+    def test_find_latest_rc_handles_oserror(self, tmp_path):
+        """Test that function handles OSError gracefully."""
+        svn_dev_repo = tmp_path / "dev" / "airflow"
+        svn_dev_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")
+            assert result is None

Reply via email to