From 5c8fbf38bcc2d3b2a1c6c78b0ba1f29bcf1378cd Mon Sep 17 00:00:00 2001 From: Amri Toufali Date: Wed, 29 Jul 2026 19:40:08 -0700 Subject: [PATCH 1/3] fix(automerge): pick a repo-allowed merge method for auto-merge Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/github_utils.py | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/scripts/github_utils.py b/scripts/github_utils.py index a1a8dcc..d6940dd 100644 --- a/scripts/github_utils.py +++ b/scripts/github_utils.py @@ -87,6 +87,28 @@ def has_codeowner_approval(pr: PullRequest) -> bool: return False +_MERGE_METHOD_PREFERENCE = ("SQUASH", "MERGE", "REBASE") + + +def _allowed_merge_method(pr: PullRequest) -> str: + """Return a merge method the target repo allows, preferring squash. + + Hardcoding SQUASH fails on repos that disable squash merges (e.g. + merge-commit-only repos like mozilla/fxa), so fall back to whatever + the repo actually permits. + """ + repo = pr.base.repo + allowed = { + "SQUASH": repo.allow_squash_merge, + "MERGE": repo.allow_merge_commit, + "REBASE": repo.allow_rebase_merge, + } + for method in _MERGE_METHOD_PREFERENCE: + if allowed.get(method): + return method + return "MERGE" + + def enable_auto_merge(pr: PullRequest) -> str | None: """Enable auto-merge on a PR via the GraphQL API. @@ -96,10 +118,16 @@ def enable_auto_merge(pr: PullRequest) -> str | None: GITHUB_TOKEN in Actions doesn't have with branch protection. The GraphQL enablePullRequestAutoMerge mutation works with the standard token and lets GitHub merge once protection rules pass. + + Uses a merge method the repo allows (see _allowed_merge_method); + hardcoding SQUASH fails on repos that only allow merge commits. """ + method = _allowed_merge_method(pr) query = """ - mutation EnableAutoMerge($prId: ID!) { - enablePullRequestAutoMerge(input: {pullRequestId: $prId, mergeMethod: SQUASH}) { + mutation EnableAutoMerge($prId: ID!, $method: PullRequestMergeMethod!) { + enablePullRequestAutoMerge( + input: {pullRequestId: $prId, mergeMethod: $method} + ) { pullRequest { autoMergeRequest { enabledAt } } } } @@ -107,7 +135,10 @@ def enable_auto_merge(pr: PullRequest) -> str | None: _, data = pr._requester.requestJsonAndCheck( "POST", "/graphql", - input={"query": query, "variables": {"prId": pr.node_id}}, + input={ + "query": query, + "variables": {"prId": pr.node_id, "method": method}, + }, ) errors = data.get("errors") if errors: From 72a1df50f15fc94c7eba3b775723721bc3dca49f Mon Sep 17 00:00:00 2001 From: Amri Toufali Date: Wed, 29 Jul 2026 19:40:10 -0700 Subject: [PATCH 2/3] fix(automerge): surface enable_auto_merge errors instead of logging false success Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/automerge_dependabot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/automerge_dependabot.py b/scripts/automerge_dependabot.py index 6ab9993..8bca304 100755 --- a/scripts/automerge_dependabot.py +++ b/scripts/automerge_dependabot.py @@ -602,7 +602,9 @@ def approve_and_merge(pr: PullRequest, compat_score: int | None) -> None: "no advisories)." ) pr.create_review(event="APPROVE", body=review_body) - enable_auto_merge(pr) + error = enable_auto_merge(pr) + if error: + raise SkipPR(f"could not enable auto-merge: {error}") # --- Main --- From 871b0f0e1504b5020eb1cfad11980a876b2d2c8c Mon Sep 17 00:00:00 2001 From: Amri Toufali Date: Thu, 30 Jul 2026 10:56:42 -0700 Subject: [PATCH 3/3] test(automerge): cover _allowed_merge_method fallback and enable_auto_merge error path Addresses review feedback on #110: unit tests for merge-method selection (squash disabled -> MERGE) and approve_and_merge raising SkipPR when enable_auto_merge returns an error. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/scripts/test_automerge_dependabot.py | 56 +++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/tests/scripts/test_automerge_dependabot.py b/tests/scripts/test_automerge_dependabot.py index 925a7b5..93035d7 100644 --- a/tests/scripts/test_automerge_dependabot.py +++ b/tests/scripts/test_automerge_dependabot.py @@ -19,6 +19,7 @@ SkipPR, _normalize_pep440_range, _post_dependabot_recreate, + approve_and_merge, extract_metadata, gate_advisories, gate_compatibility, @@ -26,7 +27,12 @@ main, version_in_range, ) -from scripts.github_utils import Verdict, has_blender_verdict, has_codeowner_approval +from scripts.github_utils import ( + Verdict, + _allowed_merge_method, + has_blender_verdict, + has_codeowner_approval, +) # --- _normalize_pep440_range --- @@ -630,3 +636,51 @@ def test_gate_compatibility_raises_when_dep_has_no_old_version(): with pytest.raises(RetryPR, match="no compatibility badge"): gate_compatibility(pr, meta) + + +# --- _allowed_merge_method --- + + +class TestAllowedMergeMethod: + """Pick a merge method the repo allows; prefer SQUASH, then MERGE, REBASE.""" + + @staticmethod + def _pr(squash: bool, merge: bool, rebase: bool) -> MagicMock: + pr = MagicMock() + pr.base.repo.allow_squash_merge = squash + pr.base.repo.allow_merge_commit = merge + pr.base.repo.allow_rebase_merge = rebase + return pr + + def test_prefers_squash_when_allowed(self): + assert _allowed_merge_method(self._pr(True, True, True)) == "SQUASH" + + def test_falls_back_to_merge_when_squash_disabled(self): + # e.g. mozilla/fxa: merge-commit-only. + assert _allowed_merge_method(self._pr(False, True, False)) == "MERGE" + + def test_falls_back_to_rebase_when_only_rebase(self): + assert _allowed_merge_method(self._pr(False, False, True)) == "REBASE" + + def test_defaults_to_merge_when_none_allowed(self): + assert _allowed_merge_method(self._pr(False, False, False)) == "MERGE" + + +# --- approve_and_merge surfaces enable_auto_merge errors --- + + +@patch("scripts.automerge_dependabot.enable_auto_merge") +def test_approve_and_merge_raises_skippr_on_enable_error(mock_enable): + mock_enable.return_value = "Pull request is in clean status" + pr = MagicMock() + with pytest.raises(SkipPR, match="could not enable auto-merge"): + approve_and_merge(pr, compat_score=90) + pr.create_review.assert_called_once() + + +@patch("scripts.automerge_dependabot.enable_auto_merge") +def test_approve_and_merge_succeeds_when_enable_returns_none(mock_enable): + mock_enable.return_value = None + pr = MagicMock() + approve_and_merge(pr, compat_score=90) + pr.create_review.assert_called_once()