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
4 changes: 3 additions & 1 deletion scripts/automerge_dependabot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Comment thread
toufali marked this conversation as resolved.


# --- Main ---
Expand Down
37 changes: 34 additions & 3 deletions scripts/github_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Comment thread
toufali marked this conversation as resolved.
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.

Expand All @@ -96,18 +118,27 @@ 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 } }
}
}
"""
_, 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:
Expand Down
56 changes: 55 additions & 1 deletion tests/scripts/test_automerge_dependabot.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,20 @@
SkipPR,
_normalize_pep440_range,
_post_dependabot_recreate,
approve_and_merge,
extract_metadata,
gate_advisories,
gate_compatibility,
gate_versions,
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 ---
Expand Down Expand Up @@ -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()