diff --git a/tagbot/action/repo.py b/tagbot/action/repo.py index 63449ac..30fe80e 100644 --- a/tagbot/action/repo.py +++ b/tagbot/action/repo.py @@ -329,9 +329,23 @@ def _registry_url(self) -> Optional[str]: raise InvalidProject("Package.toml is missing the 'repo' key") return self.__registry_url - @property - def _release_branch(self) -> str: - """Get the name of the release branch.""" + def _release_branch(self, version: str) -> str: + """Get the name of the release branch for a specific version. + + Priority: + 1. Branch specified by Registrator invocation (from PR body) + 2. Release branch specified in TagBot config + 3. Default branch + """ + # First check if Registrator specified a branch for this version + try: + pr_branch = self._branch_from_registry_pr(version) + except Exception as e: + logger.debug(f"Skipping registry PR branch lookup: {e}") + pr_branch = None + if pr_branch: + return pr_branch + # Fall back to config branch or default return self.__release_branch or self._repo.default_branch def _only(self, val: Union[T, List[T]]) -> T: @@ -498,6 +512,25 @@ def _registry_pr(self, version: str) -> Optional[PullRequest]: logger.debug(f"Did not find registry PR for branch {head}") return None + def _branch_from_registry_pr(self, version: str) -> Optional[str]: + """Extract release branch name from registry PR body. + + Registrator includes branch info in PR body like: + - Branch: my-branch + """ + pr = self._registry_pr(version) + if not pr: + return None + if not pr.body: + return None + # Look for "- Branch: " or "Branch: " in PR body + m = re.search(r"^-?\s*Branch:\s*(.+)$", pr.body, re.MULTILINE) + if m: + branch = m[1].strip() + logger.debug(f"Found branch '{branch}' in registry PR for {version}") + return branch + return None + def _commit_sha_from_registry_pr(self, version: str, tree: str) -> Optional[str]: """Look up the commit SHA of version from its registry PR.""" pr = self._registry_pr(version) @@ -669,9 +702,9 @@ def _commit_sha_of_tag(self, version_tag: str) -> Optional[str]: return resolved_sha return sha - def _commit_sha_of_release_branch(self) -> str: - """Get the latest commit SHA of the release branch.""" - branch = self._repo.get_branch(self._release_branch) + def _commit_sha_of_release_branch(self, version: str) -> str: + """Get the latest commit SHA of the release branch for a specific version.""" + branch = self._repo.get_branch(self._release_branch(version)) return cast(str, branch.commit.sha) def _highest_existing_version(self) -> Optional[VersionInfo]: @@ -1429,10 +1462,10 @@ def create_release(self, version: str, sha: str, is_latest: bool = True) -> None them as latest. """ target = sha - if self._commit_sha_of_release_branch() == sha: + if self._commit_sha_of_release_branch(version) == sha: # If we use as the target, GitHub will show # " commits to since this release" on the release page. - target = self._release_branch + target = self._release_branch(version) version_tag = self._get_version_tag(version) logger.debug(f"Release {version_tag} target: {target}") # Check if a release for this tag already exists before doing work diff --git a/test/action/test_repo.py b/test/action/test_repo.py index 91b4b03..8ff1da4 100644 --- a/test/action/test_repo.py +++ b/test/action/test_repo.py @@ -281,9 +281,25 @@ def test_registry_url_missing_repo_key(): def test_release_branch(): r = _repo() r._repo = Mock(default_branch="a") - assert r._release_branch == "a" + r._registry_pr = Mock(return_value=None) + assert r._release_branch("v1.0.0") == "a" + r = _repo(branch="b") - assert r._release_branch == "b" + r._registry_pr = Mock(return_value=None) + assert r._release_branch("v1.0.0") == "b" + + # Test PR branch has highest priority + r = _repo(branch="config-branch") + r._repo = Mock(default_branch="default-branch") + pr_body = "foo\n- Branch: pr-branch\nbar" + r._registry_pr = Mock(return_value=Mock(body=pr_body)) + assert r._release_branch("v1.0.0") == "pr-branch" + + # Test that missing branch in PR falls back to config + r = _repo(branch="config-branch") + r._repo = Mock(default_branch="default-branch") + r._registry_pr = Mock(return_value=Mock(body="no branch here")) + assert r._release_branch("v1.0.0") == "config-branch" def test_only(): @@ -477,6 +493,39 @@ def test_commit_sha_from_registry_pr(logger): assert r._commit_sha_from_registry_pr("v4.5.6", "def") == "sha" +@patch("tagbot.action.repo.logger") +def test_branch_from_registry_pr(logger): + """Test extracting branch from registry PR body.""" + r = _repo() + + # No PR found + r._registry_pr = Mock(return_value=None) + assert r._branch_from_registry_pr("v1.0.0") is None + + # PR body without branch info + r._registry_pr.return_value = Mock(body="foo\nbar\nbaz") + assert r._branch_from_registry_pr("v1.0.0") is None + + # PR body is None + r._registry_pr.return_value.body = None + assert r._branch_from_registry_pr("v1.0.0") is None + + # PR body with "- Branch: " format + r._registry_pr.return_value.body = "foo\n- Branch: my-release-branch\nbar" + assert r._branch_from_registry_pr("v1.0.0") == "my-release-branch" + logger.debug.assert_called_with( + "Found branch 'my-release-branch' in registry PR for v1.0.0" + ) + + # PR body with "Branch: " format (without dash) + r._registry_pr.return_value.body = "foo\nBranch: another-branch\nbar" + assert r._branch_from_registry_pr("v2.0.0") == "another-branch" + + # PR body with extra whitespace + r._registry_pr.return_value.body = "foo\n- Branch: spaced-branch \nbar" + assert r._branch_from_registry_pr("v3.0.0") == "spaced-branch" + + def test_commit_sha_of_tree(): """Test tree→commit lookup using git log cache.""" r = _repo() @@ -673,8 +722,9 @@ def test_version_with_latest_commit_marks_latest_when_newer(logger): def test_commit_sha_of_release_branch(): r = _repo() r._repo = Mock(default_branch="a") + r._registry_pr = Mock(return_value=None) r._repo.get_branch.return_value.commit.sha = "sha" - assert r._commit_sha_of_release_branch() == "sha" + assert r._commit_sha_of_release_branch("v1.0.0") == "sha" r._repo.get_branch.assert_called_with("a") @@ -1020,6 +1070,7 @@ def test_handle_release_branch_subdir(): def test_create_release(): r = _repo(user="user", email="email") r._commit_sha_of_release_branch = Mock(return_value="a") + r._registry_pr = Mock(return_value=None) r._git.create_tag = Mock() r._repo = Mock(default_branch="default") r._repo.create_git_tag.return_value.sha = "t" @@ -1118,6 +1169,7 @@ def test_create_release_handles_existing_release_error(): def test_create_release_subdir(): r = _repo(user="user", email="email", subdir="path/to/Foo.jl") r._commit_sha_of_release_branch = Mock(return_value="a") + r._registry_pr = Mock(return_value=None) r._repo.get_contents = Mock( return_value=Mock(decoded_content=b"""name = "Foo"\nuuid="abc-def"\n""") )