Skip to content

Commit 8c6dbb6

Browse files
[releaser] Automate branch selection for porting changelog #646
Implemented automated branch detection to avoid choosing between master and main when only one exists. Fixes #646
1 parent fe1cc60 commit 8c6dbb6

File tree

3 files changed

+86
-7
lines changed

3 files changed

+86
-7
lines changed

openwisp_utils/releaser/release.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from openwisp_utils.releaser.utils import (
2121
SkipSignal,
2222
adjust_markdown_headings,
23+
branch_exists,
2324
demote_markdown_headings,
2425
format_file_with_docstrfmt,
2526
get_current_branch,
@@ -176,10 +177,17 @@ def port_changelog_to_main(gh, config, version, changelog_body, original_branch)
176177
full_block_to_port = f"{version_header}\n{underline}\n\n{changelog_body}"
177178

178179
try:
179-
main_branch = questionary.select(
180-
"Which branch should the changelog be ported to?",
181-
choices=MAIN_BRANCHES,
182-
).ask()
180+
main_exists = branch_exists("main")
181+
master_exists = branch_exists("master")
182+
if main_exists and master_exists:
183+
main_branch = questionary.select(
184+
"Which branch should the changelog be ported to?",
185+
choices=MAIN_BRANCHES,
186+
).ask()
187+
elif main_exists:
188+
main_branch = "main"
189+
else:
190+
main_branch = "master"
183191

184192
if not main_branch:
185193
print("Porting cancelled.")

openwisp_utils/releaser/tests/test_release.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from unittest.mock import MagicMock, patch
1+
from unittest.mock import MagicMock, call, patch
22

33
import pytest
44
from openwisp_utils.releaser.release import check_prerequisites
@@ -155,26 +155,86 @@ def test_main_flow_pr_merge_wait(mock_all):
155155
@patch("openwisp_utils.releaser.release.update_changelog_file")
156156
@patch("openwisp_utils.releaser.release.format_file_with_docstrfmt")
157157
@patch("openwisp_utils.releaser.release.subprocess.run")
158+
@patch("openwisp_utils.releaser.release.branch_exists")
158159
@patch("openwisp_utils.releaser.release.questionary")
159160
def test_port_changelog_to_main_flow(
160-
mock_questionary, mock_subprocess, mock_format_file, mock_update_changelog
161+
mock_questionary,
162+
mock_branch_exists,
163+
mock_subprocess,
164+
mock_format_file,
165+
mock_update_changelog,
161166
):
162167
"""Tests the changelog porting process for both RST and MD files, and the cancellation path."""
163168
mock_gh = MagicMock()
164169
mock_config_rst = {"changelog_path": "CHANGES.rst"}
170+
# Both branches exist: user is asked
171+
mock_branch_exists.return_value = True
165172
mock_questionary.select.return_value.ask.return_value = "main"
166173
port_changelog_to_main(mock_gh, mock_config_rst, "1.1.1", "- fix", "1.1.x")
167174
mock_gh.create_pr.assert_called_once()
168175
mock_format_file.assert_called_once_with("CHANGES.rst")
169176

170177
mock_gh.reset_mock()
171178

172-
# Test Cancellation path
179+
# Test Cancellation path (when both branches exist and user cancels)
173180
mock_questionary.select.return_value.ask.return_value = None
174181
port_changelog_to_main(mock_gh, mock_config_rst, "1.1.1", "- fix", "1.1.x")
175182
mock_gh.create_pr.assert_not_called()
176183

177184

185+
@patch("openwisp_utils.releaser.release.update_changelog_file")
186+
@patch("openwisp_utils.releaser.release.subprocess.run")
187+
@patch("openwisp_utils.releaser.release.branch_exists")
188+
def test_port_changelog_only_master_exists(
189+
mock_branch_exists, mock_subprocess, mock_update_changelog
190+
):
191+
"""`master` is auto-selected when `main` does not exist locally."""
192+
mock_gh = MagicMock()
193+
mock_config = {"changelog_path": "CHANGES.rst"}
194+
# Simulate: main=False, master=True
195+
mock_branch_exists.side_effect = lambda name: name == "master"
196+
port_changelog_to_main(mock_gh, mock_config, "1.1.1", "- fix", "1.1.x")
197+
mock_gh.create_pr.assert_called_once()
198+
# Verify PR was opened against master
199+
assert mock_gh.create_pr.call_args[0][1] == "master"
200+
201+
202+
@patch("openwisp_utils.releaser.release.update_changelog_file")
203+
@patch("openwisp_utils.releaser.release.subprocess.run")
204+
@patch("openwisp_utils.releaser.release.branch_exists")
205+
def test_port_changelog_only_main_exists(
206+
mock_branch_exists, mock_subprocess, mock_update_changelog
207+
):
208+
"""`main` is auto-selected when it exists and `master` does not."""
209+
mock_gh = MagicMock()
210+
mock_config = {"changelog_path": "CHANGES.rst"}
211+
# Simulate: main=True, master=False
212+
mock_branch_exists.side_effect = lambda name: name == "main"
213+
port_changelog_to_main(mock_gh, mock_config, "1.1.1", "- fix", "1.1.x")
214+
mock_gh.create_pr.assert_called_once()
215+
# Verify PR was opened against main
216+
assert mock_gh.create_pr.call_args[0][1] == "main"
217+
218+
219+
@patch("openwisp_utils.releaser.release.update_changelog_file")
220+
@patch("openwisp_utils.releaser.release.subprocess.run")
221+
@patch("openwisp_utils.releaser.release.branch_exists")
222+
@patch("openwisp_utils.releaser.release.questionary")
223+
def test_port_changelog_both_branches_prompts_user(
224+
mock_questionary, mock_branch_exists, mock_subprocess, mock_update_changelog
225+
):
226+
"""User is prompted to choose when both `main` and `master` exist."""
227+
mock_gh = MagicMock()
228+
mock_config = {"changelog_path": "CHANGES.rst"}
229+
# Both branches exist
230+
mock_branch_exists.return_value = True
231+
mock_questionary.select.return_value.ask.return_value = "master"
232+
port_changelog_to_main(mock_gh, mock_config, "1.1.1", "- fix", "1.1.x")
233+
mock_questionary.select.assert_called_once()
234+
mock_gh.create_pr.assert_called_once()
235+
assert mock_gh.create_pr.call_args[0][1] == "master"
236+
237+
178238
def test_main_bugfix_flow_with_porting(mock_all, mocker):
179239
"""Tests the main release flow for a bugfix, including accepting the changelog port."""
180240
mock_all["_git_command_map"][

openwisp_utils/releaser/utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,17 @@ def get_current_branch():
8585
return result.stdout.strip()
8686

8787

88+
def branch_exists(branch_name):
89+
"""Check if a Git branch exists locally."""
90+
result = subprocess.run(
91+
["git", "show-ref", "--verify", "--quiet", f"refs/heads/{branch_name}"],
92+
capture_output=True,
93+
text=True,
94+
encoding="utf-8",
95+
)
96+
return result.returncode == 0
97+
98+
8899
def rst_to_markdown(text):
89100
"""Convert reStructuredText to Markdown using pypandoc."""
90101
escaped_text = re.sub(r"(?<!`)_", r"\\_", text)

0 commit comments

Comments
 (0)