Skip to content

Commit 0979fe7

Browse files
author
bors-servo
authored
Auto merge of #141 - kennytm:master, r=jdm
Support relabeling when some event happens Let Homu automatically relabel the issue when some predefined events happened. The labels can be configured through `cfg.toml` via the `[repo.NAME.labels.EVENT]` key, e.g. ```toml # when a merge conflict is detected... [repo.rust.labels.conflict] # remove the `S-waiting-on-bors` and `S-waiting-on-review` labels... remove = ['S-waiting-on-bors', 'S-waiting-on-review'] # add the `S-waiting-on-author` label... add = ['S-waiting-on-author'] # but don't do anything if the PR is already labeled with `S-blocked`, # `S-waiting-on-crater` or `S-waiting-on-team`. except = ['S-blocked', 'S-waiting-on-crater', 'S-waiting-on-team'] ``` See `cfg.sample.toml` for a list of supported events. <!-- Reviewable:start --> --- This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/homu/141) <!-- Reviewable:end -->
2 parents 11ef946 + 9527a80 commit 0979fe7

File tree

3 files changed

+82
-6
lines changed

3 files changed

+82
-6
lines changed

cfg.sample.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,28 @@ try_users = []
9696
# Arbitrary secret. You can generate one with: openssl rand -hex 20
9797
secret = ""
9898

99+
# Remove and add GitHub labels when some event happened.
100+
# See servo/homu#141 for detail.
101+
#
102+
#[repo.NAME.labels.approved] # after homu received `r+`
103+
#[repo.NAME.labels.rejected] # after homu received `r-`
104+
#[repo.NAME.labels.conflict] # a merge conflict is detected
105+
#[repo.NAME.labels.succeed] # test successful
106+
#[repo.NAME.labels.failed] # test failed
107+
#[repo.NAME.labels.exempted] # test exempted
108+
#[repo.NAME.labels.timed_out] # test timed out (after 10 hours)
109+
#[repo.NAME.labels.interrupted] # test interrupted (buildbot only)
110+
#[repo.NAME.labels.try] # after homu received `try`
111+
#[repo.NAME.labels.try_succeed] # try-build successful
112+
#[repo.NAME.labels.try_failed] # try-build failed
113+
#[repo.NAME.labels.pushed] # user pushed a commit after `r+`/`try`
114+
#remove = ['list', 'of', 'labels', 'to', 'remove']
115+
#add = ['list', 'of', 'labels', 'to', 'add']
116+
#unless = [
117+
# 'avoid', 'relabeling', 'if',
118+
# 'any', 'of', 'these', 'labels', 'are', 'present',
119+
#]
120+
99121
# Travis integration. Don't forget to allow Travis to test the `auto` branch!
100122
[repo.NAME.status.travis]
101123
# String label set by status updates. Don't touch this unless you really know

homu/main.py

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from queue import Queue
1818
import os
1919
import sys
20-
from enum import IntEnum
20+
from enum import IntEnum, Enum
2121
import subprocess
2222
from .git_helper import SSH_KEY_FILE
2323
import shlex
@@ -115,7 +115,7 @@ class PullReqState:
115115
delegate = ''
116116

117117
def __init__(self, num, head_sha, status, db, repo_label, mergeable_que,
118-
gh, owner, name, repos):
118+
gh, owner, name, label_events, repos):
119119
self.head_advanced('', use_db=False)
120120

121121
self.num = num
@@ -130,6 +130,7 @@ def __init__(self, num, head_sha, status, db, repo_label, mergeable_que,
130130
self.repos = repos
131131
self.timeout_timer = None
132132
self.test_started = time.time()
133+
self.label_events = label_events
133134

134135
def head_advanced(self, head_sha, *, use_db=True):
135136
self.head_sha = head_sha
@@ -178,6 +179,21 @@ def get_issue(self):
178179
def add_comment(self, text):
179180
self.get_issue().create_comment(text)
180181

182+
def change_labels(self, event):
183+
event = self.label_events.get(event.value, {})
184+
removes = event.get('remove', [])
185+
adds = event.get('add', [])
186+
unless = event.get('unless', [])
187+
if not removes and not adds:
188+
return
189+
190+
issue = self.get_issue()
191+
labels = {label.name for label in issue.iter_labels()}
192+
if labels.isdisjoint(unless):
193+
labels.difference_update(removes)
194+
labels.update(adds)
195+
issue.replace_labels(list(labels))
196+
181197
def set_status(self, status):
182198
self.status = status
183199
if self.timeout_timer:
@@ -347,6 +363,7 @@ def timed_out(self):
347363
desc,
348364
context='homu')
349365
self.add_comment(':boom: {}'.format(desc))
366+
self.change_labels(LabelEvent.TIMED_OUT)
350367

351368

352369
def sha_cmp(short, full):
@@ -364,6 +381,21 @@ class AuthState(IntEnum):
364381
NONE = 1
365382

366383

384+
class LabelEvent(Enum):
385+
APPROVED = 'approved'
386+
REJECTED = 'rejected'
387+
CONFLICT = 'conflict'
388+
SUCCEED = 'succeed'
389+
FAILED = 'failed'
390+
TRY = 'try'
391+
TRY_SUCCEED = 'try_succeed'
392+
TRY_FAILED = 'try_failed'
393+
EXEMPTED = 'exempted'
394+
TIMED_OUT = 'timed_out'
395+
INTERRUPTED = 'interrupted'
396+
PUSHED = 'pushed'
397+
398+
367399
def verify_auth(username, repo_cfg, state, auth, realtime, my_username):
368400
# In some cases (e.g. non-fully-qualified r+) we recursively talk to
369401
# ourself via a hidden markdown comment in the message. This is so that
@@ -534,15 +566,17 @@ def parse_commands(body, username, repo_cfg, state, my_username, db, states,
534566
':evergreen_tree: The tree is currently closed for pull requests below priority {}, this pull request will be tested once the tree is reopened' # noqa
535567
.format(treeclosed)
536568
)
569+
state.change_labels(LabelEvent.APPROVED)
537570

538571
elif word == 'r-':
539572
if not verify_auth(username, repo_cfg, state, AuthState.REVIEWER,
540573
realtime, my_username):
541574
continue
542575

543576
state.approved_by = ''
544-
545577
state.save()
578+
if realtime:
579+
state.change_labels(LabelEvent.REJECTED)
546580

547581
elif word.startswith('p='):
548582
if not verify_auth(username, repo_cfg, state, AuthState.TRY,
@@ -601,6 +635,9 @@ def parse_commands(body, username, repo_cfg, state, my_username, db, states,
601635
if not _try_auth_verified():
602636
continue
603637
state.set_status('')
638+
if realtime:
639+
event = LabelEvent.TRY if state.try_ else LabelEvent.APPROVED
640+
state.change_labels(event)
604641

605642
elif word in ['try', 'try-'] and realtime:
606643
if not _try_auth_verified():
@@ -611,6 +648,10 @@ def parse_commands(body, username, repo_cfg, state, my_username, db, states,
611648
state.init_build_res([])
612649

613650
state.save()
651+
if realtime and state.try_:
652+
# `try-` just resets the `try` bit and doesn't correspond to
653+
# any meaningful labeling events.
654+
state.change_labels(LabelEvent.TRY)
614655

615656
elif word in ['rollup', 'rollup-']:
616657
if not _try_auth_verified():
@@ -924,6 +965,7 @@ def create_merge(state, repo_cfg, branch, logger, git_cfg,
924965
context='homu')
925966

926967
state.add_comment(':lock: ' + desc)
968+
state.change_labels(LabelEvent.CONFLICT)
927969

928970
return ''
929971

@@ -979,6 +1021,7 @@ def do_exemption_merge(state, logger, repo_cfg, git_cfg, url, check_merge,
9791021
utils.github_create_status(state.get_repo(), state.head_sha, 'success',
9801022
url, desc, context='homu')
9811023
state.add_comment(':zap: {}: {}.'.format(desc, reason))
1024+
state.change_labels(LabelEvent.EXEMPTED)
9821025

9831026
state.merge_sha = merge_sha
9841027
state.save()
@@ -1343,6 +1386,7 @@ def fetch_mergeability(mergeable_que):
13431386
state.add_comment(':umbrella: The latest upstream changes{} made this pull request unmergeable. Please resolve the merge conflicts.'.format( # noqa
13441387
_blame
13451388
))
1389+
state.change_labels(LabelEvent.CONFLICT)
13461390

13471391
state.set_mergeable(mergeable, que=False)
13481392

@@ -1388,7 +1432,7 @@ def synchronize(repo_label, repo_cfg, logger, gh, states, repos, db, mergeable_q
13881432
status = info.state
13891433
break
13901434

1391-
state = PullReqState(pull.number, pull.head.sha, status, db, repo_label, mergeable_que, gh, repo_cfg['owner'], repo_cfg['name'], repos) # noqa
1435+
state = PullReqState(pull.number, pull.head.sha, status, db, repo_label, mergeable_que, gh, repo_cfg['owner'], repo_cfg['name'], repo_cfg.get('labels', {}), repos) # noqa
13921436
state.title = pull.title
13931437
state.body = pull.body
13941438
state.head_ref = pull.head.repo[0] + ':' + pull.head.ref
@@ -1557,7 +1601,7 @@ def main():
15571601
'SELECT num, head_sha, status, title, body, head_ref, base_ref, assignee, approved_by, priority, try_, rollup, delegate, merge_sha FROM pull WHERE repo = ?', # noqa
15581602
[repo_label])
15591603
for num, head_sha, status, title, body, head_ref, base_ref, assignee, approved_by, priority, try_, rollup, delegate, merge_sha in db.fetchall(): # noqa
1560-
state = PullReqState(num, head_sha, status, db, repo_label, mergeable_que, gh, repo_cfg['owner'], repo_cfg['name'], repos) # noqa
1604+
state = PullReqState(num, head_sha, status, db, repo_label, mergeable_que, gh, repo_cfg['owner'], repo_cfg['name'], repo_cfg.get('labels', {}), repos) # noqa
15611605
state.title = title
15621606
state.body = body
15631607
state.head_ref = head_ref

homu/server.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
db_query,
88
INTERRUPTED_BY_HOMU_RE,
99
synchronize,
10+
LabelEvent,
1011
)
1112
from . import utils
1213
from .utils import lazy_debug
@@ -328,7 +329,9 @@ def github():
328329
state = PullReqState(pull_num, head_sha, '', g.db, repo_label,
329330
g.mergeable_que, g.gh,
330331
info['repository']['owner']['login'],
331-
info['repository']['name'], g.repos)
332+
info['repository']['name'],
333+
repo_cfg.get('labels', {}),
334+
g.repos)
332335
state.title = info['pull_request']['title']
333336
state.body = info['pull_request']['body']
334337
state.head_ref = info['pull_request']['head']['repo']['owner']['login'] + ':' + info['pull_request']['head']['ref'] # noqa
@@ -419,6 +422,8 @@ def fail(err):
419422
})
420423

421424
if state.head_sha == info['before']:
425+
if state.status:
426+
state.change_labels(LabelEvent.PUSHED)
422427
state.head_advanced(info['after'])
423428

424429
state.save()
@@ -499,6 +504,7 @@ def report_build_res(succ, url, builder, state, logger, repo_cfg):
499504
).format(state.approved_by, state.merge_sha,
500505
state.base_ref)
501506
state.add_comment(comment)
507+
state.change_labels(LabelEvent.SUCCEED)
502508
try:
503509
try:
504510
utils.github_set_ref(state.get_repo(), 'heads/' +
@@ -529,6 +535,7 @@ def report_build_res(succ, url, builder, state, logger, repo_cfg):
529535
'State: approved={} try={}'
530536
).format(state.approved_by, state.try_)
531537
state.add_comment(comment)
538+
state.change_labels(LabelEvent.TRY_SUCCEED)
532539

533540
else:
534541
if state.status == 'pending':
@@ -540,6 +547,8 @@ def report_build_res(succ, url, builder, state, logger, repo_cfg):
540547
state.add_comment(':broken_heart: {} - [{}]({})'.format(desc,
541548
builder,
542549
url))
550+
event = LabelEvent.TRY_FAILED if state.try_ else LabelEvent.FAILED
551+
state.change_labels(event)
543552

544553
g.queue_handler()
545554

@@ -625,6 +634,7 @@ def buildbot():
625634
desc = (':snowman: The build was interrupted '
626635
'to prioritize another pull request.')
627636
state.add_comment(desc)
637+
state.change_labels(LabelEvent.INTERRUPTED)
628638
utils.github_create_status(state.get_repo(),
629639
state.head_sha,
630640
'error', url,

0 commit comments

Comments
 (0)