This is an automated email from the ASF dual-hosted git repository. ephraimanierobi pushed a commit to branch backport-a9f095e-v3-1-test in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 065cd6079016bf2640556111e8f9c6ab90f1289d Author: Ephraim Anierobi <[email protected]> AuthorDate: Mon Dec 15 13:45:12 2025 +0100 Infer the RC from the target version during release. (#59455) 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: 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="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> <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 release-management 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 the process of releasing an Airflow version. This command will guide you through the release 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)"> Start release flags </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)"> 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><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 the process of releasing an Airflow version. This command will guide you through the release process. The 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 candidate for the given version will be automatically found from SVN dev 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)"> 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> </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
