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
24 changes: 20 additions & 4 deletions dev/release/add_backports.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
add_backports_to_body,
add_rc_task_to_body,
add_sync_changelog_task_to_body,
is_release_complete,
load_release_tracking_template,
parse_checklist_state,
)
Expand All @@ -33,16 +34,31 @@ def run(self) -> int:
)
try:
open_issues = self.gh.get_open_tracking_issues()
if len(open_issues) > 1:
# An open issue whose release has already been tagged is
# finished; it just hasn't been closed. Don't add work to it.
active_issues = []
for issue in open_issues:
body = self.gh.get_issue_body(issue["number"])
if is_release_complete(body):
print(
"::warning::Ignoring open release tracking issue"
f" #{issue['number']} ({issue['title']}): its"
" 'Tag Final' task is done, so the release is"
" complete. Consider closing it."
)
continue
active_issues.append(issue)

if len(active_issues) > 1:
print(
"::error::Multiple open release tracking issues found."
" Cannot determine active one:"
)
for issue in open_issues:
for issue in active_issues:
print(f"- #{issue['number']}: {issue['title']}")
return 1
elif len(open_issues) == 1:
issue_num = open_issues[0]["number"]
elif len(active_issues) == 1:
issue_num = active_issues[0]["number"]
print(
f"Auto-discovered active release tracking issue: #{issue_num}"
)
Expand Down
19 changes: 19 additions & 0 deletions dev/release/release_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,25 @@ def parse_checklist_state(body):
return state


def is_release_complete(body: str) -> bool:
"""Returns whether the release tracked by an issue body has been completed.

A release is complete once its "Tag Final" task is done: the checklist
item is checked, or it carries `status=done` metadata (as written by the
promote step). An open tracking issue in this state should not receive
further work, such as new backports.

Args:
body: The release tracking issue body.

Returns:
True if the "Tag Final" task is done, False otherwise (including when
the task is absent).
"""
tag_final = parse_checklist_state(body)["tag_final"]
return tag_final.checked or tag_final.status == "done"


def parse_backports(body):
"""Parses the ## Backports checklist section."""
body = body.replace("\r\n", "\n")
Expand Down
78 changes: 78 additions & 0 deletions tests/tools/private/release/add_backports_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,84 @@ def test_add_backports_auto_discover_no_issues_creates_patch_release(
assert "- [ ] Tag Final" in body


def test_add_backports_auto_discover_skips_complete_release_creates_patch_release(
mock_gh, mock_git, release_tool_env
):
# 1.2.1 has already been tagged (Tag Final done) but the issue was never
# closed. It must be left alone and a new 1.2.2 issue created instead.
mock_git.get_tags.return_value = ["1.2.0", "1.2.1"]
mock_git.get_current_branch.return_value = "main"
complete_body = """
## Checklist
- [x] Sync Changelog #100 | status=done
- [x] Tag Final | status=done tag=1.2.1 commit= abcdef12

## Backports
- [x] #100 | status=done
"""
complete_issue_num = mock_gh.create_issue(
title="Release 1.2.1",
body=complete_body,
labels=["type: release"],
)

args = argparse.Namespace(issue=None, prs=["124"])

result = AddBackports(args, mock_gh, mock_git).run()

assert result == 0
# The completed issue is untouched.
assert mock_gh.get_issue_body(complete_issue_num) == complete_body

open_issues = mock_gh.get_open_tracking_issues()
assert len(open_issues) == 2
new_issue = next(i for i in open_issues if i["number"] != complete_issue_num)
assert new_issue["title"] == "Release 1.2.2"
body = new_issue["body"]
assert "- [ ] #124" in body
assert "- [ ] Sync Changelog #124" in body
assert "- [ ] Tag Final" in body


def test_add_backports_auto_discover_ignores_complete_release_among_multiple(
mock_gh,
):
# A stale-but-complete issue alongside an active one should not cause the
# "multiple open issues" error; the active one is used.
complete_issue_num = mock_gh.create_issue(
title="Release 1.2.1",
body="""
## Checklist
- [x] Tag Final | status=done tag=1.2.1 commit= abcdef12

## Backports
""",
labels=["type: release"],
)
active_issue_num = mock_gh.create_issue(
title="Release 1.3.0",
body="""
## Checklist
- [ ] Prepare Release
- [ ] Create Release branch
- [ ] Tag Final

## Backports
""",
labels=["type: release"],
)

args = argparse.Namespace(issue=None, prs=["124"])

result = AddBackports(args, mock_gh).run()

assert result == 0
assert "#124" not in mock_gh.get_issue_body(complete_issue_num)
active_body = mock_gh.get_issue_body(active_issue_num)
assert "- [ ] #124" in active_body
assert "- [ ] Sync Changelog #124" in active_body


def test_add_backports_patch_release_no_rc_added(mock_gh):
args = argparse.Namespace(issue=123, prs=["124"])
mock_gh.issues[123] = {
Expand Down
17 changes: 17 additions & 0 deletions tests/tools/private/release/release_issue_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
add_backports_to_body,
add_sync_changelog_task_to_body,
format_metadata_line,
is_release_complete,
load_release_tracking_template,
parse_backports,
parse_checklist_state,
Expand Down Expand Up @@ -194,6 +195,22 @@ def test_parse_checklist_state_with_sync_changelogs():
assert task_126.pr is None


def test_is_release_complete():
assert not is_release_complete("- [ ] Tag Final")
assert is_release_complete("- [x] Tag Final")
assert is_release_complete(
"- [x] Tag Final | status=done tag=2.0.0 commit= abcdef12"
)
# status=done is authoritative even if the box is somehow unchecked.
assert is_release_complete("- [ ] Tag Final | status=done tag=2.0.0")
# A failed promote attempt leaves the release incomplete.
assert not is_release_complete(
"- [ ] Tag Final | status=error-rc-tag-not-branch-head rc=2.0.0rc1"
)
# No Tag Final task at all (e.g. malformed body) is not complete.
assert not is_release_complete("## Checklist\n- [x] Prepare Release\n")


def test_load_release_tracking_template(tmp_path):
template_file = tmp_path / "template.md"
template_file.write_text("""## Checklist
Expand Down
Loading