Skip to content

Commit d3a3d77

Browse files
committed
[ci] Add workflow to cc teams
As discussed in https://discuss.tvm.apache.org/t/rfc-remove-codeowners/12095/2?u=driazati, this adds a mechanism to auto-tag people based on PR/issue titles and labels. This should improve visibility across the project and make it easy for interested people to subscribe to various topics. Details on usage will be posted in the relevant issue: apache#10317
1 parent f3ea291 commit d3a3d77

File tree

4 files changed

+585
-3
lines changed

4 files changed

+585
-3
lines changed

.github/workflows/tag_teams.yml

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
# GH actions.
19+
# We use it to cover windows and mac builds
20+
# Jenkins is still the primary CI
21+
22+
name: Teams
23+
24+
on:
25+
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
26+
pull_request_target:
27+
types: [opened, reopened, edited, ready_for_review, labeled]
28+
issues:
29+
types: [opened, edited, reopened, labeled]
30+
31+
concurrency:
32+
group: Teams-${{ github.event.pull_request.number }}-${{ github.event.issue.number }}
33+
cancel-in-progress: true
34+
35+
jobs:
36+
tag-teams:
37+
runs-on: ubuntu-latest
38+
steps:
39+
- uses: actions/checkout@v2
40+
- name: Tag people from relevant teams
41+
env:
42+
PR: ${{ toJson(github.event.pull_request) }}
43+
ISSUE: ${{ toJson(github.event.issue) }}
44+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45+
run: |
46+
set -eux
47+
# TODO: Use actual tracking issue
48+
python tests/scripts/github_tag_teams.py --team-issue 5

tests/python/unittest/test_ci.py

+236
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import subprocess
2020
import sys
2121
import json
22+
import textwrap
2223
import tempfile
2324

2425
import pytest
@@ -293,5 +294,240 @@ def run(pr, check):
293294
)
294295

295296

297+
def assert_in(needle: str, haystack: str):
298+
if needle not in haystack:
299+
raise AssertionError(f"item not found:\n{needle}\nin:\n{haystack}")
300+
301+
302+
def test_github_tag_teams(tmpdir_factory):
303+
tag_script = REPO_ROOT / "tests" / "scripts" / "github_tag_teams.py"
304+
305+
def run(type, data, check):
306+
git = TempGit(tmpdir_factory.mktemp("tmp_git_dir"))
307+
git.run("init")
308+
git.run("checkout", "-b", "main")
309+
git.run("remote", "add", "origin", "https://github.com/apache/tvm.git")
310+
311+
issue_body = """
312+
some text
313+
[temporary] opt-in: @person5
314+
315+
- something: @person1 @person2
316+
- something else @person1 @person2
317+
- something else2: @person1 @person2
318+
- something-else @person1 @person2
319+
"""
320+
comment1 = """
321+
another thing: @person3
322+
another-thing @person3
323+
"""
324+
comment2 = """
325+
something @person4
326+
"""
327+
teams = {
328+
"data": {
329+
"repository": {
330+
"issue": {
331+
"body": issue_body,
332+
"comments": {"nodes": [{"body": comment1}, {"body": comment2}]},
333+
}
334+
}
335+
}
336+
}
337+
env = {
338+
type: json.dumps(data),
339+
}
340+
proc = subprocess.run(
341+
[
342+
str(tag_script),
343+
"--dry-run",
344+
"--team-issue-json",
345+
json.dumps(teams),
346+
],
347+
stdout=subprocess.PIPE,
348+
stderr=subprocess.PIPE,
349+
encoding="utf-8",
350+
cwd=git.cwd,
351+
env=env,
352+
)
353+
if proc.returncode != 0:
354+
raise RuntimeError(f"Process failed:\nstdout:\n{proc.stdout}\n\nstderr:\n{proc.stderr}")
355+
356+
assert_in(check, proc.stdout)
357+
358+
run(
359+
"ISSUE",
360+
{
361+
"title": "A title",
362+
"number": 1234,
363+
"user": {
364+
"login": "person5",
365+
},
366+
"labels": [{"name": "abc"}],
367+
"body": textwrap.dedent(
368+
"""
369+
hello
370+
""".strip()
371+
),
372+
},
373+
"No one to cc, exiting",
374+
)
375+
376+
run(
377+
"ISSUE",
378+
{
379+
"title": "A title",
380+
"number": 1234,
381+
"user": {
382+
"login": "person5",
383+
},
384+
"labels": [{"name": "abc"}],
385+
"body": textwrap.dedent(
386+
"""
387+
hello
388+
389+
cc @test
390+
""".strip()
391+
),
392+
},
393+
"No one to cc, exiting",
394+
)
395+
396+
run(
397+
type="ISSUE",
398+
data={
399+
"title": "A title",
400+
"number": 1234,
401+
"user": {
402+
"login": "person5",
403+
},
404+
"labels": [{"name": "something"}],
405+
"body": textwrap.dedent(
406+
"""
407+
hello
408+
409+
something"""
410+
),
411+
},
412+
check="would have updated issues/1234 with {'body': '\\nhello\\n\\nsomething\\n\\ncc @person1 @person2 @person4'}",
413+
)
414+
415+
run(
416+
type="ISSUE",
417+
data={
418+
"title": "A title",
419+
"number": 1234,
420+
"user": {
421+
"login": "person6",
422+
},
423+
"labels": [{"name": "something"}],
424+
"body": textwrap.dedent(
425+
"""
426+
hello
427+
428+
something"""
429+
),
430+
},
431+
check="Author person6 is not opted in, quitting",
432+
)
433+
434+
run(
435+
type="ISSUE",
436+
data={
437+
"title": "A title",
438+
"number": 1234,
439+
"user": {
440+
"login": "person5",
441+
},
442+
"labels": [{"name": "something"}],
443+
"body": textwrap.dedent(
444+
"""
445+
hello
446+
447+
cc @person1 @person2 @person4"""
448+
),
449+
},
450+
check="Everyone to cc is already cc'ed, no update needed",
451+
)
452+
453+
run(
454+
type="ISSUE",
455+
data={
456+
"title": "[something] A title",
457+
"number": 1234,
458+
"user": {
459+
"login": "person5",
460+
},
461+
"labels": [{"name": "something2"}],
462+
"body": textwrap.dedent(
463+
"""
464+
hello
465+
466+
something"""
467+
),
468+
},
469+
check="would have updated issues/1234 with {'body': '\\nhello\\n\\nsomething\\n\\ncc @person1 @person2 @person4'}",
470+
)
471+
472+
run(
473+
type="ISSUE",
474+
data={
475+
"title": "[something] A title",
476+
"number": 1234,
477+
"user": {
478+
"login": "person5",
479+
},
480+
"labels": [{"name": "something2"}],
481+
"body": textwrap.dedent(
482+
"""
483+
hello
484+
485+
cc @person1 @person2 @person4"""
486+
),
487+
},
488+
check="Everyone to cc is already cc'ed, no update needed",
489+
)
490+
491+
run(
492+
type="PR",
493+
data={
494+
"title": "[something] A title",
495+
"number": 1234,
496+
"isDraft": False,
497+
"user": {
498+
"login": "person5",
499+
},
500+
"labels": [{"name": "something2"}],
501+
"body": textwrap.dedent(
502+
"""
503+
hello
504+
505+
cc @person1 @person2 @person4"""
506+
),
507+
},
508+
check="Everyone to cc is already cc'ed, no update needed",
509+
)
510+
511+
run(
512+
type="PR",
513+
data={
514+
"title": "[something] A title",
515+
"number": 1234,
516+
"isDraft": True,
517+
"user": {
518+
"login": "person5",
519+
},
520+
"labels": [{"name": "something2"}],
521+
"body": textwrap.dedent(
522+
"""
523+
hello
524+
525+
cc @person1 @person2 @person4"""
526+
),
527+
},
528+
check="Terminating since 1234 is a draft",
529+
)
530+
531+
296532
if __name__ == "__main__":
297533
sys.exit(pytest.main([__file__] + sys.argv[1:]))

tests/scripts/git_utils.py

+26-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import subprocess
2121
import re
2222
from urllib import request
23-
from typing import Dict, Tuple, Any
23+
from typing import Dict, Tuple, Any, Optional, List
2424

2525

2626
class GitHubRepo:
@@ -35,8 +35,16 @@ def headers(self):
3535
"Authorization": f"Bearer {self.token}",
3636
}
3737

38-
def graphql(self, query: str) -> Dict[str, Any]:
39-
return self._post("https://api.github.com/graphql", {"query": query})
38+
def graphql(self, query: str, variables: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
39+
if variables is None:
40+
variables = {}
41+
response = self._post(
42+
"https://api.github.com/graphql", {"query": query, "variables": variables}
43+
)
44+
if "data" not in response:
45+
msg = f"Error fetching data with query:\n{query}\n\nvariables:\n{variables}\n\nerror:\n{json.dumps(response, indent=2)}"
46+
raise RuntimeError(msg)
47+
return response
4048

4149
def _post(self, full_url: str, body: Dict[str, Any]) -> Dict[str, Any]:
4250
print("Requesting POST to", full_url, "with", body)
@@ -88,6 +96,21 @@ def parse_remote(remote: str) -> Tuple[str, str]:
8896
return m.groups()
8997

9098

99+
def find_ccs(body: str) -> List[str]:
100+
matches = re.findall(r"(cc( @[-A-Za-z0-9]+)+)", body, flags=re.MULTILINE)
101+
matches = [full for full, last in matches]
102+
103+
reviewers = []
104+
for match in matches:
105+
if match.startswith("cc "):
106+
match = match.replace("cc ", "")
107+
users = [x.strip() for x in match.split("@")]
108+
reviewers += users
109+
110+
reviewers = set(x for x in reviewers if x != "")
111+
return list(reviewers)
112+
113+
91114
def git(command):
92115
command = ["git"] + command
93116
print("Running", command)

0 commit comments

Comments
 (0)