Skip to content

Commit

Permalink
Add create_pull_request_on_conflict option to automerge tasks (#3632)
Browse files Browse the repository at this point in the history
Co-authored-by: Ben French <[email protected]>
  • Loading branch information
davidmreed and BenjaminFrench authored Jul 31, 2023
1 parent c9215c9 commit a90d88c
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 21 deletions.
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ For example:

* Ed Rivas (jerivas)
* Gustavo Tandeciarz (dcinzona)
* Ben French (BenjaminFrench)
60 changes: 39 additions & 21 deletions cumulusci/tasks/github/merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ class MergeBranch(BaseGithubTask):
"update_future_releases": {
"description": "If true, then include release branches that are not the lowest release number even if they are not child branches. Defaults to False."
},
"create_pull_request_on_conflict": {
"description": "If true, then create a pull request when a merge conflict arises. Defaults to True."
},
}

def _init_options(self, kwargs):
Expand All @@ -58,6 +61,12 @@ def _init_options(self, kwargs):
self.options["update_future_releases"] = process_bool_arg(
self.options.get("update_future_releases") or False
)
if "create_pull_request_on_conflict" not in self.options:
self.options["create_pull_request_on_conflict"] = True
else:
self.options["create_pull_request_on_conflict"] = process_bool_arg(
self.options.get("create_pull_request_on_conflict")
)

def _init_task(self):
super()._init_task()
Expand Down Expand Up @@ -245,30 +254,39 @@ def _merge(self, branch_name, source, commit):
if e.code != http.client.CONFLICT:
raise

if branch_name in self._get_existing_prs(
self.options["source_branch"], self.options["branch_prefix"]
):
self.logger.info(
f"Merge conflict on branch {branch_name}: merge PR already exists"
)
return

try:
pull = self.repo.create_pull(
title=f"Merge {source} into {branch_name}",
base=branch_name,
head=source,
body="This pull request was automatically generated because "
"an automated merge hit a merge conflict",
)
if self.options["create_pull_request_on_conflict"]:
self._create_conflict_pull_request(branch_name, source)
else:
self.logger.info(
f"Merge conflict on branch {branch_name}: created pull request #{pull.number}"
)
except github3.exceptions.UnprocessableEntity as e:
self.logger.error(
f"Error creating merge conflict pull request to merge {source} into {branch_name}:\n{e.response.text}"
f"Merge conflict on branch {branch_name}: skipping pull request creation"
)

def _create_conflict_pull_request(self, branch_name, source):
"""Attempt to create a pull request from source into branch_name if merge operation encounters a conflict"""
if branch_name in self._get_existing_prs(
self.options["source_branch"], self.options["branch_prefix"]
):
self.logger.info(
f"Merge conflict on branch {branch_name}: merge PR already exists"
)
return

try:
pull = self.repo.create_pull(
title=f"Merge {source} into {branch_name}",
base=branch_name,
head=source,
body="This pull request was automatically generated because "
"an automated merge hit a merge conflict",
)
self.logger.info(
f"Merge conflict on branch {branch_name}: created pull request #{pull.number}"
)
except github3.exceptions.UnprocessableEntity as e:
self.logger.error(
f"Error creating merge conflict pull request to merge {source} into {branch_name}:\n{e.response.text}"
)

def _is_source_branch_direct_descendent(self, branch_name):
"""Returns True if branch is a direct descendent of the source branch"""
source_dunder_count = self.options["source_branch"].count("__")
Expand Down
92 changes: 92 additions & 0 deletions cumulusci/tasks/github/tests/test_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,45 @@ def test_task_output__feature_branch_merge_conflict(self):
assert expected_log == actual_log
assert 7 == len(responses.calls)

@responses.activate
def test_task_output__feature_branch_merge_conflict_skip_pull_requests(self):
self._mock_repo()
self._mock_branch(self.branch)
self.mock_pulls()
branch_name = "feature/a-test"
branches = []
branches.append(self._get_expected_branch("main"))
branches.append(self._get_expected_branch(branch_name))
branches = self._mock_branches(branches)
self._mock_compare(
base=branches[1]["name"],
head=self.project_config.repo_commit,
files=[{"filename": "test.txt"}],
)
self._mock_merge(http.client.CONFLICT)
self._mock_pull_create(1, 2)
with LogCapture() as log:
task = self._create_task(
task_config={
"options": {
"create_pull_request_on_conflict": "False",
}
}
)
task()
actual_log = self._get_log_lines(log)

expected_log = log_header() + [
("DEBUG", "Skipping branch main: is source branch"),
("DEBUG", "Found descendents of main to update: ['feature/a-test']"),
(
"INFO",
"Merge conflict on branch feature/a-test: skipping pull request creation",
),
]
assert expected_log == actual_log
assert 5 == len(responses.calls)

@responses.activate
def test_merge__error_on_merge_conflict_pr(self):
self._mock_repo()
Expand Down Expand Up @@ -402,6 +441,59 @@ def test_task_output__main_parent_with_child_pr(self):
assert expected_log == actual_log
assert 7 == len(responses.calls)

@responses.activate
def test_task_output__main_parent_with_child_skip_pull_requests(self):
self._mock_repo()
self._mock_branch(self.branch)
# branches
parent_branch_name = "feature/a-test"
child_branch_name = "feature/a-test__a-child"
branches = []
branches.append(self._get_expected_branch("main"))
branches.append(self._get_expected_branch(parent_branch_name))
branches.append(self._get_expected_branch(child_branch_name))
branches = self._mock_branches(branches)
# pull request
pull = self._get_expected_pull_request(1, 2)
pull["base"]["ref"] = parent_branch_name
pull["base"]["sha"] = branches[1]["commit"]["sha"]
pull["head"]["ref"] = child_branch_name
self.mock_pulls(pulls=[pull])
# compare
self._mock_compare(
base=parent_branch_name,
head=self.project_config.repo_commit,
files=[{"filename": "test.txt"}],
)
# merge
self._mock_merge(http.client.CONFLICT)
# create PR
self._mock_pull_create(1, 2)
with LogCapture() as log:
task = self._create_task(
task_config={
"options": {
"create_pull_request_on_conflict": "False",
}
}
)
task()
actual_log = self._get_log_lines(log)
expected_log = log_header() + [
("DEBUG", "Skipping branch main: is source branch"),
(
"DEBUG",
"Skipping branch feature/a-test__a-child: is not a direct descendent of main",
),
("DEBUG", "Found descendents of main to update: ['feature/a-test']"),
(
"INFO",
"Merge conflict on branch feature/a-test: skipping pull request creation",
),
]
assert expected_log == actual_log
assert 5 == len(responses.calls)

@responses.activate
def test_task_output__main_merge_to_feature(self):
"""Tests that commits to the main branch are merged to the expected feature branches"""
Expand Down

0 comments on commit a90d88c

Please sign in to comment.