1+ import shlex
2+ from functools import partial
3+
4+ import click
5+ import github
6+
7+
8+ class EventError (Exception ):
9+ pass
10+
11+
12+ class CommandError (Exception ):
13+
14+ def __init__ (self , message ):
15+ self .message = message
16+
17+
18+ class _CommandMixin :
19+
20+ def get_help_option (self , ctx ):
21+ def show_help (ctx , param , value ):
22+ if value and not ctx .resilient_parsing :
23+ raise click .UsageError (ctx .get_help ())
24+ option = super ().get_help_option (ctx )
25+ option .callback = show_help
26+ return option
27+
28+ def __call__ (self , message , ** kwargs ):
29+ args = shlex .split (message )
30+ try :
31+ with self .make_context (self .name , args = args , obj = kwargs ) as ctx :
32+ return self .invoke (ctx )
33+ except click .ClickException as e :
34+ raise CommandError (e .format_message ())
35+
36+
37+ class Command (_CommandMixin , click .Command ):
38+ pass
39+
40+
41+ class Group (_CommandMixin , click .Group ):
42+
43+ def command (self , * args , ** kwargs ):
44+ kwargs .setdefault ('cls' , Command )
45+ return super ().command (* args , ** kwargs )
46+
47+ def group (self , * args , ** kwargs ):
48+ kwargs .setdefault ('cls' , Group )
49+ return super ().group (* args , ** kwargs )
50+
51+ def parse_args (self , ctx , args ):
52+ if not args and self .no_args_is_help and not ctx .resilient_parsing :
53+ raise click .UsageError (ctx .get_help ())
54+ return super ().parse_args (ctx , args )
55+
56+
57+ class CommentBot :
58+
59+ def __init__ (self , name , handler , token = None ):
60+ # TODO(kszucs): validate
61+ self .name = name
62+ self .handler = handler
63+ self .github = github .Github (token )
64+
65+ def parse_command (self , payload ):
66+ # only allow users of apache org to submit commands, for more see
67+ # https://developer.github.com/v4/enum/commentauthorassociation/
68+ allowed_roles = {'OWNER' , 'MEMBER' , 'CONTRIBUTOR' }
69+ mention = f'@{ self .name } '
70+ comment = payload ['comment' ]
71+
72+ if payload ['sender' ]['login' ] == self .name :
73+ raise EventError ("Don't respond to itself" )
74+ elif payload ['action' ] not in {'created' , 'edited' }:
75+ raise EventError ("Don't respond to comment deletion" )
76+ elif comment ['author_association' ] not in allowed_roles :
77+ raise EventError (
78+ "Don't respond to comments from non-authorized users"
79+ )
80+ elif not comment ['body' ].lstrip ().startswith (mention ):
81+ raise EventError ("The bot is not mentioned" )
82+
83+ return payload ['comment' ]['body' ].split (mention )[- 1 ].strip ()
84+
85+ def handle (self , event , payload ):
86+ try :
87+ command = self .parse_command (payload )
88+ except EventError :
89+ # see the possible reasons in the validate method
90+ return
91+
92+ if event == 'issue_comment' :
93+ return self .handle_issue_comment (command , payload )
94+ elif event == 'pull_request_review_comment' :
95+ return self .handle_review_comment (command , payload )
96+ else :
97+ raise ValueError ("Unexpected event type {}" .format (event ))
98+
99+ def handle_issue_comment (self , command , payload ):
100+ repo = self .github .get_repo (payload ['repository' ]['id' ], lazy = True )
101+ issue = repo .get_issue (payload ['issue' ]['number' ])
102+
103+ try :
104+ pull = issue .as_pull_request ()
105+ except github .GithubException :
106+ return issue .create_comment (
107+ "The comment bot only listens to pull request comments!"
108+ )
109+
110+ comment = pull .get_issue_comment (payload ['comment' ]['id' ])
111+ try :
112+ self .handler (command , issue = issue , pull = pull , comment = comment )
113+ except CommandError as e :
114+ pull .create_issue_comment ("```\n {}\n ```" .format (e .message ))
115+ except Exception :
116+ comment .create_reaction ('-1' )
117+ else :
118+ comment .create_reaction ('+1' )
119+
120+ def handle_review_comment (self , payload ):
121+ raise NotImplementedError ()
122+
123+
124+ command = partial (click .command , cls = Command )
125+ group = partial (click .group , cls = Group )
126+
127+
128+ @group (name = '@ursabot' )
129+ @click .pass_context
130+ def bot (ctx ):
131+ """Ursabot"""
132+ ctx .ensure_object (dict )
133+
134+
135+ # @ursabot.command()
136+ # @click.argument('baseline', type=str, metavar='[<baseline>]', default=None,
137+ # required=False)
138+ # @click.option('--suite-filter', metavar='<regex>', show_default=True,
139+ # type=str, default=None, help='Regex filtering benchmark suites.')
140+ # @click.option('--benchmark-filter', metavar='<regex>', show_default=True,
141+ # type=str, default=None,
142+ # help='Regex filtering benchmarks.')
143+ # def benchmark(baseline, suite_filter, benchmark_filter):
144+ # """Run the benchmark suite in comparison mode.
145+
146+ # This command will run the benchmark suite for tip of the branch commit
147+ # against `<baseline>` (or master if not provided).
148+
149+ # Examples:
150+
151+ # \b
152+ # # Run the all the benchmarks
153+ # @ursabot benchmark
154+
155+ # \b
156+ # # Compare only benchmarks where the name matches the /^Sum/ regex
157+ # @ursabot benchmark --benchmark-filter=^Sum
158+
159+ # \b
160+ # # Compare only benchmarks where the suite matches the /compute-/ regex.
161+ # # A suite is the C++ binary.
162+ # @ursabot benchmark --suite-filter=compute-
163+
164+ # \b
165+ # # Sometimes a new optimization requires the addition of new benchmarks to
166+ # # quantify the performance increase. When doing this be sure to add the
167+ # # benchmark in a separate commit before introducing the optimization.
168+ # #
169+ # # Note that specifying the baseline is the only way to compare using a new
170+ # # benchmark, since master does not contain the new benchmark and no
171+ # # comparison is possible.
172+ # #
173+ # # The following command compares the results of matching benchmarks,
174+ # # compiling against HEAD and the provided baseline commit, e.g. eaf8302.
175+ # # You can use this to quantify the performance improvement of new
176+ # # optimizations or to check for regressions.
177+ # @ursabot benchmark --benchmark-filter=MyBenchmark eaf8302
178+ # """
179+ # # each command must return a dictionary which are set as build properties
180+ # props = {'command': 'benchmark'}
181+
182+ # if baseline:
183+ # props['benchmark_baseline'] = baseline
184+
185+ # opts = []
186+ # if suite_filter:
187+ # suite_filter = shlex.quote(suite_filter)
188+ # opts.append(f'--suite-filter={suite_filter}')
189+ # if benchmark_filter:
190+ # benchmark_filter = shlex.quote(benchmark_filter)
191+ # opts.append(f'--benchmark-filter={benchmark_filter}')
192+
193+ # if opts:
194+ # props['benchmark_options'] = opts
195+
196+ # return props
197+
198+
199+ @bot .group ()
200+ @click .option ('--repo' , '-r' , default = 'ursa-labs/crossbow' ,
201+ help = 'Crossbow repository on github to use' )
202+ @click .pass_obj
203+ def crossbow (props , repo ):
204+ """Trigger crossbow builds for this pull request"""
205+ # TODO(kszucs): validate the repo format
206+ props ['command' ] = 'crossbow'
207+ props ['crossbow_repo' ] = repo # github user/repo
208+ props ['crossbow_repository' ] = f'https://github.com/{ repo } ' # git url
209+
210+
211+ @crossbow .command ()
212+ @click .argument ('task' , nargs = - 1 , required = False )
213+ @click .option ('--group' , '-g' , multiple = True ,
214+ help = 'Submit task groups as defined in tests.yml' )
215+ @click .pass_obj
216+ def submit (props , task , group ):
217+ """Submit crossbow testing tasks.
218+
219+ See groups defined in arrow/dev/tasks/tests.yml
220+ """
221+ args = ['-c' , 'tasks.yml' ]
222+ for g in group :
223+ args .extend (['-g' , g ])
224+ for t in task :
225+ args .append (t )
226+
227+ return {'crossbow_args' : args , ** props }
0 commit comments