Skip to content

Commit 2bc54d3

Browse files
SirTakobiSirAionTech
authored andcommitted
[ADD] migration_pr_check: Remind migration guidelines
1 parent 5687d24 commit 2bc54d3

File tree

8 files changed

+288
-2
lines changed

8 files changed

+288
-2
lines changed

environment.sample

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,6 @@ OCABOT_TWINE_REPOSITORIES="[('https://pypi.org/simple','https://upload.pypi.org/
8484
# List of branches the bot will check to verify if user is the maintainer
8585
# of module(s)
8686
MAINTAINER_CHECK_ODOO_RELEASES=8.0,9.0,10.0,11.0,12.0,13.0,14.0,15.0
87+
88+
# Reminder of migration guidelines.
89+
#MIGRATION_GUIDELINES_REMINDER=

src/oca_github_bot/config.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,20 @@ def func_wrapper(*args, **kwargs):
144144
).split(",")
145145

146146
MAIN_BRANCH_BOT_MIN_VERSION = os.environ.get("MAIN_BRANCH_BOT_MIN_VERSION", "8.0")
147+
148+
MIGRATION_GUIDELINES_REMINDER = os.environ.get(
149+
"MIGRATION_GUIDELINES_REMINDER",
150+
"""Thanks for the contribution!
151+
This appears to be a migration, \
152+
so here you have a gentle reminder about the migration guidelines.
153+
154+
Please preserve commit history following technical method \
155+
explained in https://github.com/OCA/maintainer-tools/wiki/#migration.
156+
If the jump is between several versions, \
157+
you have to modify the source branch in the main command \
158+
to accommodate it to this circumstance.
159+
160+
You can also take a look on the project \
161+
https://github.com/OCA/odoo-module-migrator/ to make easier migration.
162+
""",
163+
)

src/oca_github_bot/tasks/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
main_branch_bot,
77
mention_maintainer,
88
migration_issue_bot,
9+
migration_pr_check,
910
tag_approved,
1011
tag_needs_review,
1112
tag_ready_to_merge,

src/oca_github_bot/tasks/migration_issue_bot.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,18 @@
1212
from ..utils import hide_secrets
1313

1414

15-
def _create_or_find_branch_milestone(gh_repo, branch):
15+
def _find_branch_milestone(gh_repo, branch):
1616
for milestone in gh_repo.milestones():
1717
if milestone.title == branch:
1818
return milestone
19-
return gh_repo.create_milestone(branch)
19+
return None
20+
21+
22+
def _create_or_find_branch_milestone(gh_repo, branch):
23+
branch_milestone = _find_branch_milestone(gh_repo, branch)
24+
if not branch_milestone:
25+
branch_milestone = gh_repo.create_milestone(branch)
26+
return branch_milestone
2027

2128

2229
def _find_issue(gh_repo, milestone, target_branch):
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright 2022 Simone Rubino - TAKOBI
2+
# Distributed under the MIT License (http://opensource.org/licenses/MIT).
3+
import os.path
4+
import re
5+
6+
from .. import config, github
7+
from ..config import switchable
8+
from ..manifest import addon_dirs_in
9+
from ..process import check_call
10+
from ..queue import getLogger, task
11+
from .migration_issue_bot import _find_branch_milestone, _find_issue
12+
13+
_logger = getLogger(__name__)
14+
15+
16+
def _get_added_modules(org, repo, gh_pr):
17+
target_branch = gh_pr.base.ref
18+
with github.temporary_clone(org, repo, target_branch) as clonedir:
19+
# We need a list now because otherwise modules
20+
# are yielded when the directory is already changed
21+
existing_addons_paths = list(addon_dirs_in(clonedir, installable_only=True))
22+
23+
pr_branch = f"tmp-pr-{gh_pr.number}"
24+
check_call(
25+
["git", "fetch", "origin", f"refs/pull/{gh_pr.number}/head:{pr_branch}"],
26+
cwd=clonedir,
27+
)
28+
check_call(["git", "checkout", pr_branch], cwd=clonedir)
29+
pr_addons_paths = addon_dirs_in(clonedir, installable_only=True)
30+
31+
new_addons_paths = {
32+
addon_path
33+
for addon_path in pr_addons_paths
34+
if addon_path not in existing_addons_paths
35+
}
36+
return new_addons_paths
37+
38+
39+
def is_migration_pr(org, repo, pr):
40+
"""
41+
Determine if the PR is a migration.
42+
"""
43+
with github.login() as gh:
44+
gh_repo = gh.repository(org, repo)
45+
gh_pr = gh_repo.pull_request(pr)
46+
target_branch = gh_pr.base.ref
47+
milestone = _find_branch_milestone(gh_repo, target_branch)
48+
gh_migration_issue = _find_issue(gh_repo, milestone, target_branch)
49+
50+
# The PR is mentioned in the migration issue
51+
pr_regex = re.compile(rf"#({gh_pr.number})")
52+
found_pr = pr_regex.findall(gh_migration_issue.body)
53+
if found_pr:
54+
return True
55+
56+
# The added module is mentioned in the migration issue
57+
new_addons_paths = _get_added_modules(org, repo, gh_pr)
58+
new_addons = map(os.path.basename, new_addons_paths)
59+
for addon in new_addons:
60+
module_regex = re.compile(rf"- \[[ x]] {addon}")
61+
found_module = module_regex.search(gh_migration_issue.body)
62+
if found_module:
63+
return True
64+
65+
# The Title of the PR contains [MIG]
66+
pr_title = gh_pr.title
67+
if "[MIG]" in pr_title:
68+
return True
69+
return False
70+
71+
72+
@task()
73+
@switchable("comment_migration_guidelines")
74+
def comment_migration_guidelines(org, repo, pr, dry_run=False):
75+
migration_reminder = config.MIGRATION_GUIDELINES_REMINDER
76+
with github.login() as gh:
77+
gh_pr = gh.pull_request(org, repo, pr)
78+
return github.gh_call(gh_pr.create_comment, migration_reminder)

src/oca_github_bot/webhooks/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
on_pr_green_label_needs_review,
88
on_pr_open_label_new_contributor,
99
on_pr_open_mention_maintainer,
10+
on_pr_open_migration_check,
1011
on_pr_review,
1112
on_push_to_main_branch,
1213
on_status_merge_bot,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright 2022 Simone Rubino - TAKOBI
2+
# Distributed under the MIT License (http://opensource.org/licenses/MIT).
3+
4+
import logging
5+
6+
from ..router import router
7+
from ..tasks.migration_pr_check import comment_migration_guidelines, is_migration_pr
8+
9+
_logger = logging.getLogger(__name__)
10+
11+
12+
@router.register("pull_request", action="opened")
13+
async def on_pr_open_migration_check(event, *args, **kwargs):
14+
"""
15+
Whenever a PR is opened, if it is a migration PR,
16+
remind the migration guidelines.
17+
"""
18+
org, repo = event.data["repository"]["full_name"].split("/")
19+
pr = event.data["pull_request"]["number"]
20+
if is_migration_pr(org, repo, pr):
21+
comment_migration_guidelines.delay(org, repo, pr)

tests/test_migration_pr_check.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Copyright 2022 Simone Rubino - TAKOBI
2+
# Distributed under the MIT License (http://opensource.org/licenses/MIT).
3+
4+
import pytest
5+
6+
from oca_github_bot.tasks.migration_pr_check import is_migration_pr
7+
8+
MIGRATION_PR_PATH = "oca_github_bot.tasks.migration_pr_check"
9+
10+
11+
def _get_addons_gen_mock(pr_new_modules=None):
12+
"""
13+
Return a callable that returns a list of modules.
14+
The list contains `pr_new_modules` only after first call.
15+
"""
16+
if pr_new_modules is None:
17+
pr_new_modules = list()
18+
19+
class AddonsGenMock:
20+
def __init__(self):
21+
self.addons_gen_calls = 0
22+
23+
def __call__(self, *args, **kwargs):
24+
# First time, only the existing addons are returned
25+
existing_addons = ["existing_addon"]
26+
if self.addons_gen_calls > 0:
27+
# After that, return also `pr_new_modules`
28+
if pr_new_modules:
29+
existing_addons.extend(pr_new_modules)
30+
self.addons_gen_calls += 1
31+
return existing_addons
32+
33+
return AddonsGenMock()
34+
35+
36+
@pytest.mark.vcr()
37+
def test_no_new_module(mocker):
38+
"""
39+
If a PR does not add a new module, then it is not a migration.
40+
"""
41+
mocker.patch("%s.github" % MIGRATION_PR_PATH)
42+
mocker.patch("%s.check_call" % MIGRATION_PR_PATH)
43+
44+
migration_issue = mocker.patch("%s._find_issue" % MIGRATION_PR_PATH)
45+
migration_issue.return_value.body = "Migration Issue Body"
46+
47+
addons_gen = mocker.patch("%s.addon_dirs_in" % MIGRATION_PR_PATH)
48+
addons_gen.side_effect = _get_addons_gen_mock()
49+
50+
is_migration = is_migration_pr("org", "repo", "pr")
51+
assert not is_migration
52+
53+
54+
@pytest.mark.vcr()
55+
def test_new_module_no_migration(mocker):
56+
"""
57+
If a PR adds a new module but the module is not in the migration issue,
58+
then it is not a migration.
59+
"""
60+
mocker.patch("%s.github" % MIGRATION_PR_PATH)
61+
mocker.patch("%s.check_call" % MIGRATION_PR_PATH)
62+
63+
migration_issue_body = """
64+
Modules to migrate:
65+
- [ ] a_module
66+
"""
67+
migration_issue = mocker.patch("%s._find_issue" % MIGRATION_PR_PATH)
68+
migration_issue.return_value.body = migration_issue_body
69+
70+
addons_gen = mocker.patch("%s.addon_dirs_in" % MIGRATION_PR_PATH)
71+
addons_gen.side_effect = _get_addons_gen_mock()
72+
73+
is_migration = is_migration_pr("org", "repo", "pr")
74+
assert not is_migration
75+
76+
77+
@pytest.mark.vcr()
78+
def test_new_module_migration(mocker):
79+
"""
80+
If a PR adds a new module and the module is in the migration issue,
81+
then it is a migration.
82+
"""
83+
mocker.patch("%s.github" % MIGRATION_PR_PATH)
84+
mocker.patch("%s.check_call" % MIGRATION_PR_PATH)
85+
86+
addon_name = "migrated_module"
87+
migration_issue_body = f"""
88+
Modules to migrate:
89+
- [ ] {addon_name}
90+
"""
91+
migration_issue = mocker.patch("%s._find_issue" % MIGRATION_PR_PATH)
92+
migration_issue.return_value.body = migration_issue_body
93+
94+
addons_gen = mocker.patch("%s.addon_dirs_in" % MIGRATION_PR_PATH)
95+
addons_gen.side_effect = _get_addons_gen_mock([addon_name])
96+
97+
is_migration = is_migration_pr("org", "repo", "pr")
98+
assert is_migration
99+
100+
101+
@pytest.mark.vcr()
102+
def test_migration_comment(mocker):
103+
"""
104+
If a PR adds a new module and it is in the migration issue,
105+
then it is a migration.
106+
"""
107+
github_mock = mocker.patch("%s.github" % MIGRATION_PR_PATH)
108+
mocker.patch("%s.check_call" % MIGRATION_PR_PATH)
109+
110+
addon_name = "migrated_module"
111+
migration_issue_body = f"""
112+
Modules to migrate:
113+
- [ ] {addon_name}
114+
"""
115+
migration_issue = mocker.patch("%s._find_issue" % MIGRATION_PR_PATH)
116+
migration_issue.return_value.body = migration_issue_body
117+
118+
addons_gen = mocker.patch("%s.addon_dirs_in" % MIGRATION_PR_PATH)
119+
addons_gen.side_effect = _get_addons_gen_mock([addon_name])
120+
121+
gh_context = mocker.MagicMock()
122+
github_mock_login_cm = github_mock.login.return_value
123+
github_mock_login_cm.__enter__.return_value = gh_context
124+
125+
is_migration = is_migration_pr("org", "repo", "pr")
126+
assert is_migration
127+
128+
129+
@pytest.mark.vcr()
130+
def test_pr_title(mocker):
131+
"""
132+
If a PR has [MIG] in its Title,
133+
then it is a migration.
134+
"""
135+
github_mock = mocker.patch("%s.github" % MIGRATION_PR_PATH)
136+
mocker.patch("%s.check_call" % MIGRATION_PR_PATH)
137+
138+
migration_issue_body = """
139+
Modules to migrate
140+
"""
141+
migration_issue = mocker.patch("%s._find_issue" % MIGRATION_PR_PATH)
142+
migration_issue.return_value.body = migration_issue_body
143+
144+
addons_gen = mocker.patch("%s.addon_dirs_in" % MIGRATION_PR_PATH)
145+
addons_gen.side_effect = _get_addons_gen_mock(["another_module"])
146+
147+
pr_title = "[MIG] migrated_module"
148+
gh_pr = mocker.MagicMock()
149+
gh_pr.title = pr_title
150+
gh_repo = mocker.MagicMock()
151+
gh_repo.pull_request.return_value = gh_pr
152+
gh_context = mocker.MagicMock()
153+
gh_context.repository.return_value = gh_repo
154+
github_mock_login_cm = github_mock.login.return_value
155+
github_mock_login_cm.__enter__.return_value = gh_context
156+
157+
is_migration = is_migration_pr("org", "repo", "pr")
158+
assert is_migration

0 commit comments

Comments
 (0)