From 81a1daa2f454dbe3e1e4156c78d763803aab35e3 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Thu, 2 Jul 2026 07:18:43 -0400 Subject: [PATCH 1/2] Fix missing merged PR lists for mainline releases (#574) When a release commit on the default branch was also reachable from other branches (e.g. feature branches created off main after the commit, or long-lived branches that had merged main), branches_of_commit returned those branches and the changelog took the backport path. PRs were then filtered to ones merged into those branches via _pulls_on_branches, which matches nothing since PRs merge into the default branch, so the "Merged pull requests" section came out empty while "Closed issues" (which use a date-only filter) still appeared. This is why the problem was intermittent: it depended on which branches happened to contain the release commit when TagBot ran. branches_of_commit now returns an empty list whenever the default branch contains the commit: a commit reachable from the default branch is a mainline release regardless of what other branches also contain it. Genuine backports (commit not on the default branch) are unaffected. Also document in DEVGUIDE.md that `poetry run pytest` can run against a stale installed copy of tagbot instead of the local sources; use `poetry run python -m pytest` or `make pytest`. Fixes #574 Co-Authored-By: Claude --- DEVGUIDE.md | 5 +++++ tagbot/action/repo.py | 17 ++++++++++++----- test/action/test_repo.py | 11 ++++++++--- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/DEVGUIDE.md b/DEVGUIDE.md index cf88719b..34c2ab00 100644 --- a/DEVGUIDE.md +++ b/DEVGUIDE.md @@ -248,6 +248,11 @@ make pytest black flake8 mypy **Test locations**: `test/action/`, `test/web/`, `test/test_tagbot.py` +> **Warning**: `poetry run pytest` can silently run against a stale copy of +> tagbot installed in the venv's site-packages instead of the local sources, +> making test results not reflect your edits. Use `poetry run python -m pytest` +> (or `make pytest`), which puts the repo root first on `sys.path`. + ### What NOT to Do - ❌ Print statements (use logger) diff --git a/tagbot/action/repo.py b/tagbot/action/repo.py index edbfdc81..325e6806 100644 --- a/tagbot/action/repo.py +++ b/tagbot/action/repo.py @@ -1749,17 +1749,24 @@ def commit_sha_of_version(self, version: str) -> Optional[str]: return self._commit_sha_of_tree(tree) def branches_of_commit(self, sha: str) -> List[str]: - """Return short names of non-default remote branches that contain sha.""" + """Return short names of non-default remote branches that contain sha. + + Returns an empty list if the default branch contains the commit: a + commit reachable from the default branch is a mainline release, even + if other branches (e.g. feature branches based on it) also contain it + (see issue #574). + """ try: output = self._git.command("branch", "-r", "--contains", sha) default = f"origin/{self._repo.default_branch}" prefix = "origin/" prefix_len = len(prefix) - return [ - b.strip()[prefix_len:] - for b in output.splitlines() - if b.strip() and " -> " not in b and b.strip() != default + branches = [ + b.strip() for b in output.splitlines() if b.strip() and " -> " not in b ] + if default in branches: + return [] + return [b[prefix_len:] for b in branches] except Abort: logger.debug("Failed to get branches for commit", exc_info=True) return [] diff --git a/test/action/test_repo.py b/test/action/test_repo.py index 2323fc41..4b84c128 100644 --- a/test/action/test_repo.py +++ b/test/action/test_repo.py @@ -1517,9 +1517,10 @@ def test_is_backport_commit(): r._git.command.return_value = " origin/main\n" assert not r.is_backport_commit("abc123") - # Commit on default branch and another branch + # Commit on default branch and another branch → mainline, not a backport + # (e.g. a feature branch based on the release commit; issue #574) r._git.command.return_value = " origin/main\n origin/release-1.0\n" - assert r.is_backport_commit("abc123") + assert not r.is_backport_commit("abc123") # Commit on non-default branch only r._git.command.return_value = " origin/release-1.0\n" @@ -1557,8 +1558,12 @@ def test_branches_of_commit(): r._git.command.return_value = " origin/main\n" assert r.branches_of_commit("abc123") == [] - # One non-default branch + # Default branch plus other branches → mainline, so empty (issue #574) r._git.command.return_value = " origin/main\n origin/release-1.0\n" + assert r.branches_of_commit("abc123") == [] + + # One non-default branch + r._git.command.return_value = " origin/release-1.0\n" assert r.branches_of_commit("abc123") == ["release-1.0"] # Multiple non-default branches, no default From d903026d35e3a0b8ab81d44635f54be0f430628a Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Wed, 8 Jul 2026 12:51:14 -0400 Subject: [PATCH 2/2] fix --- tagbot/action/repo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tagbot/action/repo.py b/tagbot/action/repo.py index 325e6806..81b9bf31 100644 --- a/tagbot/action/repo.py +++ b/tagbot/action/repo.py @@ -1772,5 +1772,5 @@ def branches_of_commit(self, sha: str) -> List[str]: return [] def is_backport_commit(self, sha: str) -> bool: - """Check if the commit is on a non-default branch.""" + """Check if the commit is only reachable from non-default branches.""" return bool(self.branches_of_commit(sha))