Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions dev/README_RELEASE_AIRFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -1146,25 +1146,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``
Expand Down
2 changes: 1 addition & 1 deletion dev/breeze/doc/images/output_release-management.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
071f14a0452cdd65380c61487aab1cd0
0fba653b39033f9d1a33d39096ae0985
36 changes: 20 additions & 16 deletions dev/breeze/doc/images/output_release-management_start-release.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1 +1 @@
a860ddc98626bc82dc298e2bd0081f97
481eebefa96f86a5a385c295c2cbbdd0
89 changes: 73 additions & 16 deletions dev/breeze/src/airflow_breeze/commands/release_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from __future__ import annotations

import os
import re

import click

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.",
)
@click.option("--release-candidate", required=True, help="Airflow release candidate e.g. 3.0.5rc1")
@click.option("--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")
@click.option("--task-sdk-release-candidate", required=False, help="Task SDK release candidate e.g. 1.0.5rc1")
@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(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()
Expand All @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@
"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": [
Expand Down
142 changes: 142 additions & 0 deletions dev/breeze/tests/test_release_command.py
Original file line number Diff line number Diff line change
@@ -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
Loading