Skip to content

Commit 84e0585

Browse files
authored
Merge pull request #3177 from migueldiascosta/review_pr_labels
advise PR labels in --review-pr and add support for --add-pr-labels
2 parents 3586b40 + 5c3a069 commit 84e0585

File tree

9 files changed

+287
-24
lines changed

9 files changed

+287
-24
lines changed

easybuild/framework/easyconfig/tools.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,18 @@
4747
from easybuild.base import fancylogger
4848
from easybuild.framework.easyconfig import EASYCONFIGS_PKG_SUBDIR
4949
from easybuild.framework.easyconfig.easyconfig import EASYCONFIGS_ARCHIVE_DIR, ActiveMNS, EasyConfig
50-
from easybuild.framework.easyconfig.easyconfig import create_paths, get_easyblock_class, process_easyconfig
50+
from easybuild.framework.easyconfig.easyconfig import create_paths, det_file_info, get_easyblock_class
51+
from easybuild.framework.easyconfig.easyconfig import process_easyconfig
5152
from easybuild.framework.easyconfig.format.yeb import quote_yaml_special_chars
5253
from easybuild.framework.easyconfig.style import cmdline_easyconfigs_style_check
5354
from easybuild.tools.build_log import EasyBuildError, print_msg, print_warning
5455
from easybuild.tools.config import build_option
5556
from easybuild.tools.environment import restore_env
5657
from easybuild.tools.filetools import find_easyconfigs, is_patch_file, locate_files
5758
from easybuild.tools.filetools import read_file, resolve_path, which, write_file
58-
from easybuild.tools.github import fetch_easyconfigs_from_pr, fetch_files_from_pr, download_repo
59+
from easybuild.tools.github import GITHUB_EASYCONFIGS_REPO
60+
from easybuild.tools.github import det_pr_labels, download_repo, fetch_easyconfigs_from_pr, fetch_pr_data
61+
from easybuild.tools.github import fetch_files_from_pr
5962
from easybuild.tools.multidiff import multidiff
6063
from easybuild.tools.py2vs3 import OrderedDict
6164
from easybuild.tools.toolchain.toolchain import is_system_toolchain
@@ -474,14 +477,19 @@ def find_related_easyconfigs(path, ec):
474477
return sorted(res)
475478

476479

477-
def review_pr(paths=None, pr=None, colored=True, branch='develop'):
480+
def review_pr(paths=None, pr=None, colored=True, branch='develop', testing=False):
478481
"""
479482
Print multi-diff overview between specified easyconfigs or PR and specified branch.
480483
:param pr: pull request number in easybuild-easyconfigs repo to review
481484
:param paths: path tuples (path, generated) of easyconfigs to review
482485
:param colored: boolean indicating whether a colored multi-diff should be generated
483486
:param branch: easybuild-easyconfigs branch to compare with
487+
:param testing: whether to ignore PR labels (used in test_review_pr)
484488
"""
489+
pr_target_repo = build_option('pr_target_repo') or GITHUB_EASYCONFIGS_REPO
490+
if pr_target_repo != GITHUB_EASYCONFIGS_REPO:
491+
raise EasyBuildError("Reviewing PRs for repositories other than easyconfigs hasn't been implemented yet")
492+
485493
tmpdir = tempfile.mkdtemp()
486494

487495
download_repo_path = download_repo(branch=branch, path=tmpdir)
@@ -508,6 +516,20 @@ def review_pr(paths=None, pr=None, colored=True, branch='develop'):
508516
else:
509517
lines.extend(['', "(no related easyconfigs found for %s)\n" % os.path.basename(ec['spec'])])
510518

519+
if pr:
520+
file_info = det_file_info(pr_files, download_repo_path)
521+
522+
pr_target_account = build_option('pr_target_account')
523+
github_user = build_option('github_user')
524+
pr_data, _ = fetch_pr_data(pr, pr_target_account, pr_target_repo, github_user)
525+
pr_labels = [label['name'] for label in pr_data['labels']] if not testing else []
526+
527+
expected_labels = det_pr_labels(file_info, pr_target_repo)
528+
missing_labels = [label for label in expected_labels if label not in pr_labels]
529+
530+
if missing_labels:
531+
lines.extend(['', "This PR should be labelled with %s" % ', '.join(["'%s'" % ml for ml in missing_labels])])
532+
511533
return '\n'.join(lines)
512534

513535

easybuild/main.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
from easybuild.tools.filetools import adjust_permissions, cleanup, copy_files, dump_index, load_index
6262
from easybuild.tools.filetools import locate_files, read_file, register_lock_cleanup_signal_handlers, write_file
6363
from easybuild.tools.github import check_github, close_pr, find_easybuild_easyconfig
64-
from easybuild.tools.github import install_github_token, list_prs, merge_pr, new_branch_github, new_pr
64+
from easybuild.tools.github import add_pr_labels, install_github_token, list_prs, merge_pr, new_branch_github, new_pr
6565
from easybuild.tools.github import new_pr_from_branch
6666
from easybuild.tools.github import sync_branch_with_develop, sync_pr_with_develop, update_branch, update_pr
6767
from easybuild.tools.hooks import START, END, load_hooks, run_hook
@@ -260,7 +260,10 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None):
260260
merge_pr(options.merge_pr)
261261

262262
elif options.review_pr:
263-
print(review_pr(pr=options.review_pr, colored=use_color(options.color)))
263+
print(review_pr(pr=options.review_pr, colored=use_color(options.color), testing=testing))
264+
265+
elif options.add_pr_labels:
266+
add_pr_labels(options.add_pr_labels)
264267

265268
elif options.list_installed_software:
266269
detailed = options.list_installed_software == 'detailed'
@@ -277,6 +280,7 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None):
277280

278281
# non-verbose cleanup after handling GitHub integration stuff or printing terse info
279282
early_stop_options = [
283+
options.add_pr_labels,
280284
options.check_github,
281285
options.create_index,
282286
options.install_github_token,

easybuild/tools/github.py

Lines changed: 93 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1359,6 +1359,95 @@ def merge_url(gh):
13591359
print_warning("Review indicates this PR should not be merged (use -f/--force to do so anyway)")
13601360

13611361

1362+
def det_pr_labels(file_info, pr_target_repo):
1363+
"""
1364+
Determine labels for a pull request based on provided information on files changed by that pull request.
1365+
"""
1366+
labels = []
1367+
if pr_target_repo == GITHUB_EASYCONFIGS_REPO:
1368+
if any(file_info['new_folder']):
1369+
labels.append('new')
1370+
if any(file_info['new_file_in_existing_folder']):
1371+
labels.append('update')
1372+
elif pr_target_repo == GITHUB_EASYBLOCKS_REPO:
1373+
if any(file_info['new']):
1374+
labels.append('new')
1375+
return labels
1376+
1377+
1378+
def post_pr_labels(pr, labels):
1379+
"""
1380+
Update PR labels
1381+
"""
1382+
pr_target_account = build_option('pr_target_account')
1383+
pr_target_repo = build_option('pr_target_repo') or GITHUB_EASYCONFIGS_REPO
1384+
1385+
# fetch GitHub token if available
1386+
github_user = build_option('github_user')
1387+
if github_user is None:
1388+
_log.info("GitHub user not specified, not adding labels to PR# %s" % pr)
1389+
return False
1390+
1391+
github_token = fetch_github_token(github_user)
1392+
if github_token is None:
1393+
_log.info("GitHub token for user '%s' not found, not adding labels to PR# %s" % (github_user, pr))
1394+
return False
1395+
1396+
dry_run = build_option('dry_run') or build_option('extended_dry_run')
1397+
1398+
if not dry_run:
1399+
g = RestClient(GITHUB_API_URL, username=github_user, token=github_token)
1400+
1401+
pr_url = g.repos[pr_target_account][pr_target_repo].issues[pr]
1402+
try:
1403+
status, data = pr_url.labels.post(body=labels)
1404+
if status == HTTP_STATUS_OK:
1405+
print_msg("Added labels %s to PR#%s" % (', '.join(labels), pr), log=_log, prefix=False)
1406+
return True
1407+
except HTTPError as err:
1408+
_log.info("Failed to add labels to PR# %s: %s." % (pr, err))
1409+
return False
1410+
else:
1411+
return True
1412+
1413+
1414+
def add_pr_labels(pr, branch='develop'):
1415+
"""
1416+
Try to determine and add labels to PR.
1417+
:param pr: pull request number in easybuild-easyconfigs repo
1418+
:param branch: easybuild-easyconfigs branch to compare with
1419+
"""
1420+
pr_target_repo = build_option('pr_target_repo') or GITHUB_EASYCONFIGS_REPO
1421+
if pr_target_repo != GITHUB_EASYCONFIGS_REPO:
1422+
raise EasyBuildError("Adding labels to PRs for repositories other than easyconfigs hasn't been implemented yet")
1423+
1424+
tmpdir = tempfile.mkdtemp()
1425+
1426+
download_repo_path = download_repo(branch=branch, path=tmpdir)
1427+
1428+
pr_files = [path for path in fetch_easyconfigs_from_pr(pr) if path.endswith('.eb')]
1429+
1430+
file_info = det_file_info(pr_files, download_repo_path)
1431+
1432+
pr_target_account = build_option('pr_target_account')
1433+
github_user = build_option('github_user')
1434+
pr_data, _ = fetch_pr_data(pr, pr_target_account, pr_target_repo, github_user)
1435+
pr_labels = [label['name'] for label in pr_data['labels']]
1436+
1437+
expected_labels = det_pr_labels(file_info, pr_target_repo)
1438+
missing_labels = [label for label in expected_labels if label not in pr_labels]
1439+
1440+
dry_run = build_option('dry_run') or build_option('extended_dry_run')
1441+
1442+
if missing_labels:
1443+
missing_labels_txt = ', '.join(["'%s'" % ml for ml in missing_labels])
1444+
print_msg("PR #%s should be labelled %s" % (pr, missing_labels_txt), log=_log, prefix=False)
1445+
if not dry_run and not post_pr_labels(pr, missing_labels):
1446+
print_msg("Could not add labels %s to PR #%s" % (missing_labels_txt, pr), log=_log, prefix=False)
1447+
else:
1448+
print_msg("Could not determine any missing labels for PR #%s" % pr, log=_log, prefix=False)
1449+
1450+
13621451
@only_if_module_is_available('git', pkgname='GitPython')
13631452
def new_branch_github(paths, ecs, commit_msg=None):
13641453
"""
@@ -1486,14 +1575,9 @@ def new_pr_from_branch(branch_name, title=None, descr=None, pr_target_repo=None,
14861575

14871576
file_info = det_file_info(ec_paths, target_dir)
14881577

1489-
labels = []
1490-
if pr_target_repo == GITHUB_EASYCONFIGS_REPO:
1491-
# label easyconfigs for new software and/or new easyconfigs for existing software
1492-
if any(file_info['new_folder']):
1493-
labels.append('new')
1494-
if any(file_info['new_file_in_existing_folder']):
1495-
labels.append('update')
1578+
labels = det_pr_labels(file_info, pr_target_repo)
14961579

1580+
if pr_target_repo == GITHUB_EASYCONFIGS_REPO:
14971581
# only use most common toolchain(s) in toolchain label of PR title
14981582
toolchains = ['%(name)s/%(version)s' % ec['toolchain'] for ec in file_info['ecs']]
14991583
toolchains_counted = sorted([(toolchains.count(tc), tc) for tc in nub(toolchains)])
@@ -1503,9 +1587,6 @@ def new_pr_from_branch(branch_name, title=None, descr=None, pr_target_repo=None,
15031587
classes = [ec['moduleclass'] for ec in file_info['ecs']]
15041588
classes_counted = sorted([(classes.count(c), c) for c in nub(classes)])
15051589
class_label = ','.join([tc for (cnt, tc) in classes_counted if cnt == classes_counted[-1][0]])
1506-
elif pr_target_repo == GITHUB_EASYBLOCKS_REPO:
1507-
if any(file_info['new']):
1508-
labels.append('new')
15091590

15101591
if title is None:
15111592
if pr_target_repo == GITHUB_EASYCONFIGS_REPO:
@@ -1581,15 +1662,9 @@ def new_pr_from_branch(branch_name, title=None, descr=None, pr_target_repo=None,
15811662
print_msg("Opened pull request: %s" % data['html_url'], log=_log, prefix=False)
15821663

15831664
if labels:
1584-
# post labels
15851665
pr = data['html_url'].split('/')[-1]
1586-
pr_url = g.repos[pr_target_account][pr_target_repo].issues[pr]
1587-
try:
1588-
status, data = pr_url.labels.post(body=labels)
1589-
if status == HTTP_STATUS_OK:
1590-
print_msg("Added labels %s to PR#%s" % (', '.join(labels), pr), log=_log, prefix=False)
1591-
except HTTPError as err:
1592-
_log.info("Failed to add labels to PR# %s: %s." % (pr, err))
1666+
if not post_pr_labels(pr, labels):
1667+
print_msg("This PR should be labelled %s" % ', '.join(labels), log=_log, prefix=False)
15931668

15941669

15951670
def new_pr(paths, ecs, title=None, descr=None, commit_msg=None):

easybuild/tools/options.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,7 @@ def github_options(self):
631631
descr = ("GitHub integration options", "Integration with GitHub")
632632

633633
opts = OrderedDict({
634+
'add-pr-labels': ("Try to add labels to PR based on files changed", int, 'store', None, {'metavar': 'PR#'}),
634635
'check-github': ("Check status of GitHub integration, and report back", None, 'store_true', False),
635636
'check-contrib': ("Runs checks to see whether the given easyconfigs are ready to be contributed back",
636637
None, 'store_true', False),

test/framework/github.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
from easybuild.tools.config import build_option, module_classes
4343
from easybuild.tools.configobj import ConfigObj
4444
from easybuild.tools.filetools import read_file, write_file
45-
from easybuild.tools.github import VALID_CLOSE_PR_REASONS
45+
from easybuild.tools.github import GITHUB_EASYCONFIGS_REPO, GITHUB_EASYBLOCKS_REPO, VALID_CLOSE_PR_REASONS
4646
from easybuild.tools.testing import post_pr_test_report, session_state
4747
from easybuild.tools.py2vs3 import HTTPError, URLError, ascii_letters
4848
import easybuild.tools.github as gh
@@ -120,6 +120,48 @@ def test_read(self):
120120
except (IOError, OSError):
121121
pass
122122

123+
def test_add_pr_labels(self):
124+
"""Test add_pr_labels function."""
125+
if self.skip_github_tests:
126+
print("Skipping test_add_pr_labels, no GitHub token available?")
127+
return
128+
129+
build_options = {
130+
'pr_target_account': GITHUB_USER,
131+
'pr_target_repo': GITHUB_EASYBLOCKS_REPO,
132+
'github_user': GITHUB_TEST_ACCOUNT,
133+
'dry_run': True,
134+
}
135+
init_config(build_options=build_options)
136+
137+
self.mock_stdout(True)
138+
error_pattern = "Adding labels to PRs for repositories other than easyconfigs hasn't been implemented yet"
139+
self.assertErrorRegex(EasyBuildError, error_pattern, gh.add_pr_labels, 1)
140+
self.mock_stdout(False)
141+
142+
build_options['pr_target_repo'] = GITHUB_EASYCONFIGS_REPO
143+
init_config(build_options=build_options)
144+
145+
# PR #11262 includes easyconfigs that use 'dummy' toolchain,
146+
# so we need to allow triggering deprecated behaviour
147+
self.allow_deprecated_behaviour()
148+
149+
self.mock_stdout(True)
150+
self.mock_stderr(True)
151+
gh.add_pr_labels(11262)
152+
stdout = self.get_stdout()
153+
self.mock_stdout(False)
154+
self.mock_stderr(False)
155+
self.assertTrue("Could not determine any missing labels for PR #11262" in stdout)
156+
157+
self.mock_stdout(True)
158+
self.mock_stderr(True)
159+
gh.add_pr_labels(8006) # closed, unmerged, unlabeled PR
160+
stdout = self.get_stdout()
161+
self.mock_stdout(False)
162+
self.mock_stderr(False)
163+
self.assertTrue("PR #8006 should be labelled 'update'" in stdout)
164+
123165
def test_fetch_pr_data(self):
124166
"""Test fetch_pr_data function."""
125167
if self.skip_github_tests:
@@ -681,6 +723,25 @@ def run_check(expected_result=False):
681723
expected_warning = ''
682724
self.assertEqual(run_check(True), '')
683725

726+
def test_det_pr_labels(self):
727+
"""Test for det_pr_labels function."""
728+
729+
file_info = {'new_folder': [False], 'new_file_in_existing_folder': [True]}
730+
res = gh.det_pr_labels(file_info, GITHUB_EASYCONFIGS_REPO)
731+
self.assertEqual(res, ['update'])
732+
733+
file_info = {'new_folder': [True], 'new_file_in_existing_folder': [False]}
734+
res = gh.det_pr_labels(file_info, GITHUB_EASYCONFIGS_REPO)
735+
self.assertEqual(res, ['new'])
736+
737+
file_info = {'new_folder': [True, False], 'new_file_in_existing_folder': [False, True]}
738+
res = gh.det_pr_labels(file_info, GITHUB_EASYCONFIGS_REPO)
739+
self.assertTrue(sorted(res), ['new', 'update'])
740+
741+
file_info = {'new': [True]}
742+
res = gh.det_pr_labels(file_info, GITHUB_EASYBLOCKS_REPO)
743+
self.assertEqual(res, ['new'])
744+
684745
def test_det_patch_specs(self):
685746
"""Test for det_patch_specs function."""
686747

test/framework/options.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,8 @@ def test_list_easyblocks(self):
754754
r'EasyBlock',
755755
r'\|-- bar',
756756
r'\|-- ConfigureMake',
757+
r'\| \|-- MakeCp',
758+
r'\|-- EB_EasyBuildMeta',
757759
r'\|-- EB_FFTW',
758760
r'\|-- EB_foo',
759761
r'\| \|-- EB_foofoo',
@@ -3582,6 +3584,21 @@ def test_review_pr(self):
35823584
regex = re.compile(r"^Comparing gzip-1.10-\S* with gzip-1.10-")
35833585
self.assertTrue(regex.search(txt), "Pattern '%s' not found in: %s" % (regex.pattern, txt))
35843586

3587+
self.mock_stdout(True)
3588+
self.mock_stderr(True)
3589+
# closed PR for gzip 1.2.8 easyconfig,
3590+
# see https://github.com/easybuilders/easybuild-easyconfigs/pull/5365
3591+
args = [
3592+
'--color=never',
3593+
'--github-user=%s' % GITHUB_TEST_ACCOUNT,
3594+
'--review-pr=5365',
3595+
]
3596+
self.eb_main(args, raise_error=True, testing=True)
3597+
txt = self.get_stdout()
3598+
self.mock_stdout(False)
3599+
self.mock_stderr(False)
3600+
self.assertTrue("This PR should be labelled with 'update'" in txt)
3601+
35853602
def test_set_tmpdir(self):
35863603
"""Test set_tmpdir config function."""
35873604
self.purge_environment()

test/framework/sandbox/easybuild/easyblocks/e/__init__.py

Whitespace-only changes.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
##
2+
# Copyright 2009-2020 Ghent University
3+
#
4+
# This file is part of EasyBuild,
5+
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
6+
# with support of Ghent University (http://ugent.be/hpc),
7+
# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
8+
# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
9+
# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
10+
#
11+
# https://github.com/easybuilders/easybuild
12+
#
13+
# EasyBuild is free software: you can redistribute it and/or modify
14+
# it under the terms of the GNU General Public License as published by
15+
# the Free Software Foundation v2.
16+
#
17+
# EasyBuild is distributed in the hope that it will be useful,
18+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
# GNU General Public License for more details.
21+
#
22+
# You should have received a copy of the GNU General Public License
23+
# along with EasyBuild. If not, see <http://www.gnu.org/licenses/>.
24+
##
25+
"""
26+
Dummy easyblock for EasyBuildMeta
27+
28+
@author: Miguel Dias Costa (National University of Singapore)
29+
"""
30+
from easybuild.framework.easyblock import EasyBlock
31+
32+
33+
class EB_EasyBuildMeta(EasyBlock):
34+
pass

0 commit comments

Comments
 (0)