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 dbf8a81dce0 [v3-1-test] Fix Old RC removal logic and add test for the
function (#59438) (#59456)
dbf8a81dce0 is described below
commit dbf8a81dce0d5c6ccd27536dd5f42ba1cb7c56ef
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Dec 16 09:19:28 2025 +0100
[v3-1-test] Fix Old RC removal logic and add test for the function (#59438)
(#59456)
* Fix Old RC removal logic and add test for the function
Old RC releases should be removed when a new RC is built. This helps
us avoid the situation we had with 3.1.4. The step to remove the old RC
wasn't working before
because we were checking old releases against Airflow 2 instead of 3
using if name.startswith("2"). The fix here is to use pattern to select
all old RCs whether they are version 2 or 3.
* fixup! Update issue templates regarding Airflow 2 and misc. (#59384)
(cherry picked from commit 737b3bafcd1bce6f1be7336adcce397deaadf98b)
Co-authored-by: Ephraim Anierobi <[email protected]>
---
.../commands/release_candidate_command.py | 10 +-
dev/breeze/tests/test_release_candidate_command.py | 159 +++++++++++++++++++++
2 files changed, 165 insertions(+), 4 deletions(-)
diff --git
a/dev/breeze/src/airflow_breeze/commands/release_candidate_command.py
b/dev/breeze/src/airflow_breeze/commands/release_candidate_command.py
index e993209fe3b..37c67da5d55 100644
--- a/dev/breeze/src/airflow_breeze/commands/release_candidate_command.py
+++ b/dev/breeze/src/airflow_breeze/commands/release_candidate_command.py
@@ -17,6 +17,7 @@
from __future__ import annotations
import os
+import re
import shutil
import sys
from datetime import date
@@ -49,6 +50,8 @@ from airflow_breeze.utils.reproducible import
get_source_date_epoch, repack_dete
from airflow_breeze.utils.run_utils import run_command
from airflow_breeze.utils.shared_options import get_dry_run
+RC_PATTERN =
re.compile(r"^(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)rc(?P<rc>\d+)$")
+
def validate_remote_tracks_apache_airflow(remote_name):
"""Validate that the specified remote tracks the apache/airflow
repository."""
@@ -586,8 +589,6 @@ def push_release_candidate_tag_to_github(version,
remote_name):
def remove_old_releases(version, repo_root):
- if confirm_action("In beta release we do not remove old RCs. Is this a
beta release?"):
- return
if not confirm_action("Do you want to look for old RCs to remove?"):
return
@@ -598,11 +599,12 @@ def remove_old_releases(version, repo_root):
if entry.name == version:
# Don't remove the current RC
continue
- if entry.is_dir() and entry.name.startswith("2."):
+ if entry.is_dir() and RC_PATTERN.match(entry.name):
old_releases.append(entry.name)
old_releases.sort()
-
+ console_print(f"The following old releases should be removed:
{old_releases}")
for old_release in old_releases:
+ console_print(f"Removing old release {old_release}")
if confirm_action(f"Remove old RC {old_release}?"):
run_command(["svn", "rm", old_release], check=True)
run_command(
diff --git a/dev/breeze/tests/test_release_candidate_command.py
b/dev/breeze/tests/test_release_candidate_command.py
new file mode 100644
index 00000000000..3070f9420a4
--- /dev/null
+++ b/dev/breeze/tests/test_release_candidate_command.py
@@ -0,0 +1,159 @@
+# 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
+
+import pytest
+
+
+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 rc_cmd():
+ """Lazy import the rc command module."""
+ import airflow_breeze.commands.release_candidate_command as module
+
+ return module
+
+
+def test_remove_old_releases_only_collects_rc_directories(monkeypatch, rc_cmd):
+ version = "2.10.0rc3"
+ repo_root = "/repo/root"
+
+ # Arrange: entries include current RC, old RC directories, a matching
"file", and non-RC directory.
+ entries = [
+ FakeDirEntry(version, is_dir=True), # current RC: should be skipped
+ FakeDirEntry("2.10.0rc2", is_dir=True), # old RC dir: should be
included
+ FakeDirEntry("2.10.0rc1", is_dir=True), # old RC dir: should be
included
+ FakeDirEntry("2.10.0rc0", is_dir=False), # matches pattern but not a
directory: excluded
+ FakeDirEntry("not-a-rc", is_dir=True), # directory but not matching
pattern: excluded
+ ]
+
+ chdir_calls: list[str] = []
+ console_messages: list[str] = []
+ run_command_calls: list[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 RCs to remove?":
+ return True
+ # For each candidate, we decline removal to avoid running svn commands.
+ if prompt.startswith("Remove old RC "):
+ return False
+ raise AssertionError(f"Unexpected confirm prompt: {prompt}")
+
+ monkeypatch.setattr(rc_cmd.os, "chdir", lambda path:
chdir_calls.append(path))
+ monkeypatch.setattr(rc_cmd.os, "scandir", lambda: iter(entries))
+ monkeypatch.setattr(rc_cmd, "confirm_action", fake_confirm_action)
+ monkeypatch.setattr(rc_cmd, "console_print", lambda msg="":
console_messages.append(str(msg)))
+ monkeypatch.setattr(rc_cmd, "run_command", lambda cmd, **_kwargs:
run_command_calls.append(cmd))
+
+ # Act
+ rc_cmd.remove_old_releases(version=version, repo_root=repo_root)
+
+ # Assert: only directory entries matching RC_PATTERN, excluding current
version, and sorted.
+ assert f"{repo_root}/asf-dist/dev/airflow" in chdir_calls
+ assert repo_root in chdir_calls
+ assert "The following old releases should be removed: ['2.10.0rc1',
'2.10.0rc2']" in console_messages
+ assert run_command_calls == []
+
+
+def test_remove_old_releases_returns_early_when_user_declines(monkeypatch,
rc_cmd):
+ version = "2.10.0rc3"
+ repo_root = "/repo/root"
+
+ 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(rc_cmd, "confirm_action", fake_confirm_action)
+ monkeypatch.setattr(rc_cmd.os, "chdir", should_not_be_called)
+ monkeypatch.setattr(rc_cmd.os, "scandir", should_not_be_called)
+ monkeypatch.setattr(rc_cmd, "console_print", should_not_be_called)
+ monkeypatch.setattr(rc_cmd, "run_command", should_not_be_called)
+
+ rc_cmd.remove_old_releases(version=version, repo_root=repo_root)
+
+ assert confirm_prompts == ["Do you want to look for old RCs to remove?"]
+
+
+def test_remove_old_releases_removes_confirmed_old_releases(monkeypatch,
rc_cmd):
+ version = "3.1.5rc3"
+ repo_root = "/repo/root"
+
+ # Unsorted on purpose to verify sorting before prompting/removing.
+ entries = [
+ FakeDirEntry("3.1.5rc2", is_dir=True),
+ FakeDirEntry(version, is_dir=True),
+ FakeDirEntry("3.1.0rc1", is_dir=True),
+ ]
+
+ 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 RCs to remove?":
+ return True
+ if prompt == "Remove old RC 3.1.0rc1?":
+ return True
+ if prompt == "Remove old RC 3.1.5rc2?":
+ return False
+ raise AssertionError(f"Unexpected confirm prompt: {prompt}")
+
+ monkeypatch.setattr(rc_cmd.os, "chdir", lambda path:
chdir_calls.append(path))
+ monkeypatch.setattr(rc_cmd.os, "scandir", lambda: iter(entries))
+ monkeypatch.setattr(rc_cmd, "confirm_action", fake_confirm_action)
+ monkeypatch.setattr(rc_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(rc_cmd, "run_command", fake_run_command)
+
+ rc_cmd.remove_old_releases(version=version, repo_root=repo_root)
+
+ assert chdir_calls == [f"{repo_root}/asf-dist/dev/airflow", repo_root]
+ assert confirm_prompts == [
+ "Do you want to look for old RCs to remove?",
+ "Remove old RC 3.1.0rc1?",
+ "Remove old RC 3.1.5rc2?",
+ ]
+ assert "The following old releases should be removed: ['3.1.0rc1',
'3.1.5rc2']" in console_messages
+ assert "Removing old release 3.1.0rc1" in console_messages
+ assert "Removing old release 3.1.5rc2" in console_messages
+ assert "[success]Old releases removed" in console_messages
+
+ # Only rc1 was confirmed, so we should run rm+commit for rc1 only.
+ assert run_command_calls == [
+ (["svn", "rm", "3.1.0rc1"], {"check": True}),
+ (["svn", "commit", "-m", "Remove old release: 3.1.0rc1"], {"check":
True}),
+ ]