-
-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add(bot): auto merge by dependabot version (#607)
Add configuration option to auto merge PRs opened by Dependabot (and others) based on the upgrade type (major, minor, patch). Snyk creates batch PRs, but Dependabot doesn't. I think for a first pass we can just support single dependency pull requests. ```toml [merge] automerge_label = "ship it!" [merge.automerge_dependencies] versions = ["minor","patch"] usernames = ["dependabot"] [update] ignored_usernames = ["dependabot"] ``` related #606
- Loading branch information
Showing
6 changed files
with
305 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import re | ||
from typing import List, Optional, Sequence, Tuple, TypeVar | ||
|
||
from typing_extensions import Literal | ||
|
||
title_regex = re.compile(r"from (?P<old_version>\S+) to (?P<new_version>\S+)") | ||
|
||
|
||
def _extract_versions(x: str) -> Optional[Tuple[str, str]]: | ||
""" | ||
Find old and new version from PR title | ||
Example: | ||
title: "Bump jackson-databind from 2.9.10.1 to 2.10.0.pr1 in /LiveIngest/LiveEventWithDVR" | ||
result: "2.9.10.1", "2.10.0.pr1" | ||
""" | ||
match = title_regex.search(x) | ||
if match is None: | ||
return None | ||
group = match.groupdict() | ||
if "old_version" not in group or "new_version" not in group: | ||
return None | ||
return group["old_version"], group["new_version"] | ||
|
||
|
||
# regex to split version string. Versions aren't necessarily semver. | ||
# | ||
# from dependabot: https://github.com/dependabot/dependabot-core/blob/998de3be7811956354aea077ecb180831e24012c/common/lib/dependabot/pull_request_creator/labeler.rb#L95-L115 | ||
version_regex = re.compile(r"[.+]") | ||
|
||
|
||
def _parse_version_simple(x: str) -> List[str]: | ||
""" | ||
Split version string into pieces. | ||
""" | ||
return version_regex.split(x) | ||
|
||
|
||
T = TypeVar("T") | ||
|
||
|
||
def _get_or_none(arr: Sequence[T], index: int) -> Optional[T]: | ||
try: | ||
return arr[index] | ||
except IndexError: | ||
return None | ||
|
||
|
||
def _compare_versions( | ||
old_version: str, new_version: str | ||
) -> Optional[Literal["major", "minor", "patch"]]: | ||
""" | ||
Determine patch, like Dependabot. | ||
https://github.com/dependabot/dependabot-core/blob/998de3be7811956354aea077ecb180831e24012c/common/lib/dependabot/pull_request_creator/labeler.rb#L92-L114 | ||
""" | ||
old_version_parts = _parse_version_simple(old_version) | ||
new_version_parts = _parse_version_simple(new_version) | ||
|
||
for part in new_version_parts[:3] + old_version_parts[:3]: | ||
try: | ||
int(part) | ||
except ValueError: | ||
return None | ||
|
||
if _get_or_none(new_version_parts, 0) != _get_or_none(old_version_parts, 0): | ||
return "major" | ||
if _get_or_none(new_version_parts, 1) != _get_or_none(old_version_parts, 1): | ||
return "minor" | ||
return "patch" | ||
|
||
|
||
def dep_version_from_title(x: str) -> Optional[Literal["major", "minor", "patch"]]: | ||
""" | ||
Try to determine the semver upgrade type from string. | ||
For example, 'Bump lodash from 4.17.15 to 4.17.19', would be "patch", since the "patch" field is upgraded from 15 to 19. | ||
""" | ||
res = _extract_versions(x) | ||
if res is None: | ||
return None | ||
old_version, new_version = res | ||
return _compare_versions(old_version, new_version) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
from kodiak.dependencies import ( | ||
_compare_versions, | ||
_extract_versions, | ||
dep_version_from_title, | ||
) | ||
|
||
|
||
def test_extract_versions() -> None: | ||
for title, version, upgrade in [ | ||
( | ||
"Bump pip from 20.2.4 to 20.3 in /.github/workflows", | ||
("20.2.4", "20.3"), | ||
"minor", | ||
), | ||
("Bump lodash from 4.17.15 to 4.17.19", ("4.17.15", "4.17.19"), "patch"), | ||
("Update tokio requirement from 0.2 to 0.3", ("0.2", "0.3"), "minor"), | ||
( | ||
"Bump jackson-databind from 2.9.10.1 to 2.10.0.pr1 in /LiveIngest/LiveEventWithDVR", | ||
("2.9.10.1", "2.10.0.pr1"), | ||
"minor", | ||
), | ||
( | ||
"Bump commons-collections from 4.0 to 4.1 in /eosio-explorer/Quantum", | ||
("4.0", "4.1"), | ||
"minor", | ||
), | ||
( | ||
"[Snyk] Security upgrade engine.io from 3.5.0 to 4.0.0", | ||
("3.5.0", "4.0.0"), | ||
"major", | ||
), | ||
("Bump lodash", None, None), | ||
("Bump lodash to 4.17.19", None, None), | ||
("Bump lodash from 4.17.15 to", None, None), | ||
]: | ||
assert _extract_versions(title) == version | ||
assert dep_version_from_title(title) == upgrade | ||
|
||
|
||
def test_compare_versions() -> None: | ||
for old_version, new_version, change in [ | ||
("20.2.4", "20.3", "minor"), | ||
("4.17.15", "4.17.19", "patch"), | ||
("0.2", "0.3", "minor"), | ||
("2.9.10.1", "2.10.0.pr1", "minor"), | ||
("4.0", "4.1", "minor"), | ||
("1.5", "2.0", "major"), | ||
("feb", "may", None), | ||
]: | ||
assert ( | ||
_compare_versions(old_version=old_version, new_version=new_version) | ||
== change | ||
) |
98 changes: 98 additions & 0 deletions
98
bot/kodiak/tests/evaluation/test_automerge_dependencies.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import pytest | ||
|
||
from kodiak.test_evaluation import ( | ||
create_api, | ||
create_config, | ||
create_mergeable, | ||
create_pull_request, | ||
) | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_merge_okay() -> None: | ||
""" | ||
Happy case. | ||
The upgrade type (patch) is specified in "versions" and the PR author | ||
"my-custom-dependabot" is specified in "usernames". | ||
""" | ||
mergeable = create_mergeable() | ||
config = create_config() | ||
config.merge.automerge_dependencies.versions = ["minor", "patch"] | ||
config.merge.automerge_dependencies.usernames = ["my-custom-dependabot"] | ||
pull_request = create_pull_request() | ||
pull_request.labels = [] | ||
pull_request.author.login = "my-custom-dependabot" | ||
pull_request.title = "Bump lodash from 4.17.15 to 4.17.19" | ||
api = create_api() | ||
await mergeable(api=api, pull_request=pull_request, config=config) | ||
assert api.set_status.call_count == 1 | ||
assert "enqueued" in api.set_status.calls[0]["msg"] | ||
assert api.queue_for_merge.call_count == 1 | ||
assert api.dequeue.call_count == 0 | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_merge_mismatch_username() -> None: | ||
""" | ||
We should only merge the pull request if the userrname is specified within | ||
"usernames" and the version type is in the "versions" field. | ||
""" | ||
mergeable = create_mergeable() | ||
config = create_config() | ||
config.merge.automerge_dependencies.versions = ["minor", "patch"] | ||
config.merge.automerge_dependencies.usernames = ["dependabot"] | ||
pull_request = create_pull_request() | ||
pull_request.labels = [] | ||
pull_request.author.login = "my-custom-dependabot" | ||
pull_request.title = "Bump lodash from 4.17.15 to 4.17.19" | ||
api = create_api() | ||
await mergeable(api=api, pull_request=pull_request, config=config) | ||
assert api.queue_for_merge.call_count == 0 | ||
assert api.dequeue.call_count == 1 | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_merge_no_version_found() -> None: | ||
""" | ||
If we can't find a version from the PR title, we shouldn't merge. | ||
Packages don't necessarily use semver. | ||
""" | ||
mergeable = create_mergeable() | ||
config = create_config() | ||
config.merge.automerge_dependencies.versions = ["minor", "patch"] | ||
config.merge.automerge_dependencies.usernames = ["my-custom-dependabot"] | ||
pull_request = create_pull_request() | ||
pull_request.labels = [] | ||
pull_request.author.login = "my-custom-dependabot" | ||
|
||
for title in ("Bump lodash from 4.17.15 to", "Bump lodash from griffin to phoenix"): | ||
pull_request.title = title | ||
api = create_api() | ||
await mergeable(api=api, pull_request=pull_request, config=config) | ||
assert api.queue_for_merge.call_count == 0 | ||
assert api.dequeue.call_count == 1 | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_merge_disallowed_version() -> None: | ||
""" | ||
We should only auto merge if the upgrade type is specified in "versions". | ||
So if a PR is a major upgrade, we should only auto merge if "major" is in | ||
the "versions" configuration. | ||
""" | ||
mergeable = create_mergeable() | ||
config = create_config() | ||
config.merge.automerge_dependencies.versions = ["minor", "patch"] | ||
config.merge.automerge_dependencies.usernames = ["my-custom-dependabot"] | ||
pull_request = create_pull_request() | ||
pull_request.labels = [] | ||
pull_request.author.login = "my-custom-dependabot" | ||
pull_request.title = "Bump lodash from 4.17.15 to 5.0.1" | ||
api = create_api() | ||
await mergeable(api=api, pull_request=pull_request, config=config) | ||
assert api.queue_for_merge.call_count == 0 | ||
assert api.dequeue.call_count == 1 |