Skip to content

Commit 70ad1d5

Browse files
authored
[ci] Add workflow to cc teams (#10322)
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: #10317 Co-authored-by: driazati <[email protected]>
1 parent 8f46d12 commit 70ad1d5

File tree

4 files changed

+584
-3
lines changed

4 files changed

+584
-3
lines changed

.github/workflows/tag_teams.yml

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
python tests/scripts/github_tag_teams.py || echo failed

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
@@ -406,5 +407,240 @@ def all_time_keys(time):
406407
)
407408

408409

410+
def assert_in(needle: str, haystack: str):
411+
if needle not in haystack:
412+
raise AssertionError(f"item not found:\n{needle}\nin:\n{haystack}")
413+
414+
415+
def test_github_tag_teams(tmpdir_factory):
416+
tag_script = REPO_ROOT / "tests" / "scripts" / "github_tag_teams.py"
417+
418+
def run(type, data, check):
419+
git = TempGit(tmpdir_factory.mktemp("tmp_git_dir"))
420+
git.run("init")
421+
git.run("checkout", "-b", "main")
422+
git.run("remote", "add", "origin", "https://github.com/apache/tvm.git")
423+
424+
issue_body = """
425+
some text
426+
[temporary] opt-in: @person5
427+
428+
- something: @person1 @person2
429+
- something else @person1 @person2
430+
- something else2: @person1 @person2
431+
- something-else @person1 @person2
432+
"""
433+
comment1 = """
434+
another thing: @person3
435+
another-thing @person3
436+
"""
437+
comment2 = """
438+
something @person4
439+
"""
440+
teams = {
441+
"data": {
442+
"repository": {
443+
"issue": {
444+
"body": issue_body,
445+
"comments": {"nodes": [{"body": comment1}, {"body": comment2}]},
446+
}
447+
}
448+
}
449+
}
450+
env = {
451+
type: json.dumps(data),
452+
}
453+
proc = subprocess.run(
454+
[
455+
str(tag_script),
456+
"--dry-run",
457+
"--team-issue-json",
458+
json.dumps(teams),
459+
],
460+
stdout=subprocess.PIPE,
461+
stderr=subprocess.PIPE,
462+
encoding="utf-8",
463+
cwd=git.cwd,
464+
env=env,
465+
)
466+
if proc.returncode != 0:
467+
raise RuntimeError(f"Process failed:\nstdout:\n{proc.stdout}\n\nstderr:\n{proc.stderr}")
468+
469+
assert_in(check, proc.stdout)
470+
471+
run(
472+
"ISSUE",
473+
{
474+
"title": "A title",
475+
"number": 1234,
476+
"user": {
477+
"login": "person5",
478+
},
479+
"labels": [{"name": "abc"}],
480+
"body": textwrap.dedent(
481+
"""
482+
hello
483+
""".strip()
484+
),
485+
},
486+
"No one to cc, exiting",
487+
)
488+
489+
run(
490+
"ISSUE",
491+
{
492+
"title": "A title",
493+
"number": 1234,
494+
"user": {
495+
"login": "person5",
496+
},
497+
"labels": [{"name": "abc"}],
498+
"body": textwrap.dedent(
499+
"""
500+
hello
501+
502+
cc @test
503+
""".strip()
504+
),
505+
},
506+
"No one to cc, exiting",
507+
)
508+
509+
run(
510+
type="ISSUE",
511+
data={
512+
"title": "A title",
513+
"number": 1234,
514+
"user": {
515+
"login": "person5",
516+
},
517+
"labels": [{"name": "something"}],
518+
"body": textwrap.dedent(
519+
"""
520+
hello
521+
522+
something"""
523+
),
524+
},
525+
check="would have updated issues/1234 with {'body': '\\nhello\\n\\nsomething\\n\\ncc @person1 @person2 @person4'}",
526+
)
527+
528+
run(
529+
type="ISSUE",
530+
data={
531+
"title": "A title",
532+
"number": 1234,
533+
"user": {
534+
"login": "person6",
535+
},
536+
"labels": [{"name": "something"}],
537+
"body": textwrap.dedent(
538+
"""
539+
hello
540+
541+
something"""
542+
),
543+
},
544+
check="Author person6 is not opted in, quitting",
545+
)
546+
547+
run(
548+
type="ISSUE",
549+
data={
550+
"title": "A title",
551+
"number": 1234,
552+
"user": {
553+
"login": "person5",
554+
},
555+
"labels": [{"name": "something"}],
556+
"body": textwrap.dedent(
557+
"""
558+
hello
559+
560+
cc @person1 @person2 @person4"""
561+
),
562+
},
563+
check="Everyone to cc is already cc'ed, no update needed",
564+
)
565+
566+
run(
567+
type="ISSUE",
568+
data={
569+
"title": "[something] A title",
570+
"number": 1234,
571+
"user": {
572+
"login": "person5",
573+
},
574+
"labels": [{"name": "something2"}],
575+
"body": textwrap.dedent(
576+
"""
577+
hello
578+
579+
something"""
580+
),
581+
},
582+
check="would have updated issues/1234 with {'body': '\\nhello\\n\\nsomething\\n\\ncc @person1 @person2 @person4'}",
583+
)
584+
585+
run(
586+
type="ISSUE",
587+
data={
588+
"title": "[something] A title",
589+
"number": 1234,
590+
"user": {
591+
"login": "person5",
592+
},
593+
"labels": [{"name": "something2"}],
594+
"body": textwrap.dedent(
595+
"""
596+
hello
597+
598+
cc @person1 @person2 @person4"""
599+
),
600+
},
601+
check="Everyone to cc is already cc'ed, no update needed",
602+
)
603+
604+
run(
605+
type="PR",
606+
data={
607+
"title": "[something] A title",
608+
"number": 1234,
609+
"draft": False,
610+
"user": {
611+
"login": "person5",
612+
},
613+
"labels": [{"name": "something2"}],
614+
"body": textwrap.dedent(
615+
"""
616+
hello
617+
618+
cc @person1 @person2 @person4"""
619+
),
620+
},
621+
check="Everyone to cc is already cc'ed, no update needed",
622+
)
623+
624+
run(
625+
type="PR",
626+
data={
627+
"title": "[something] A title",
628+
"number": 1234,
629+
"draft": True,
630+
"user": {
631+
"login": "person5",
632+
},
633+
"labels": [{"name": "something2"}],
634+
"body": textwrap.dedent(
635+
"""
636+
hello
637+
638+
cc @person1 @person2 @person4"""
639+
),
640+
},
641+
check="Terminating since 1234 is a draft",
642+
)
643+
644+
409645
if __name__ == "__main__":
410646
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)
@@ -95,3 +103,18 @@ def git(command, **kwargs):
95103
if proc.returncode != 0:
96104
raise RuntimeError(f"Command failed {command}:\nstdout:\n{proc.stdout}")
97105
return proc.stdout.strip()
106+
107+
108+
def find_ccs(body: str) -> List[str]:
109+
matches = re.findall(r"(cc( @[-A-Za-z0-9]+)+)", body, flags=re.MULTILINE)
110+
matches = [full for full, last in matches]
111+
112+
reviewers = []
113+
for match in matches:
114+
if match.startswith("cc "):
115+
match = match.replace("cc ", "")
116+
users = [x.strip() for x in match.split("@")]
117+
reviewers += users
118+
119+
reviewers = set(x for x in reviewers if x != "")
120+
return list(reviewers)

0 commit comments

Comments
 (0)