Skip to content

Commit a5ae786

Browse files
authored
[CI] Add bot to ping reviewers after no activity (#165)
1 parent 8ab5393 commit a5ae786

File tree

4 files changed

+451
-0
lines changed

4 files changed

+451
-0
lines changed

.github/workflows/cc_bot.yml

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
name: PR
19+
20+
on:
21+
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
22+
pull_request_target:
23+
types: [assigned, opened, synchronize, reopened, edited, ready_for_review]
24+
25+
concurrency:
26+
group: PR-${{ github.event.pull_request.number }}
27+
cancel-in-progress: true
28+
29+
jobs:
30+
cc-reviewers:
31+
if: github.repository == 'koalanet-project/koala'
32+
runs-on: ubuntu-22.04
33+
steps:
34+
- uses: actions/checkout@v2
35+
with:
36+
submodules: false
37+
- name: Add cc'ed reviewers
38+
env:
39+
PR: ${{ toJson(github.event.pull_request) }}
40+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41+
run: |
42+
set -eux
43+
python tests/scripts/github_cc_reviewers.py || echo step failed

.github/workflows/ping_reviewers.yml

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Ping Reviewers
2+
on:
3+
schedule:
4+
- cron: "0/15 * * * *"
5+
workflow_dispatch:
6+
7+
concurrency:
8+
group: ping
9+
cancel-in-progress: true
10+
11+
jobs:
12+
ping:
13+
if: github.repository == 'koalanet-project/koala'
14+
runs-on: ubuntu-22.04
15+
steps:
16+
- uses: actions/checkout@v2
17+
- name: Ping reviewers
18+
env:
19+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20+
run: |
21+
set -eux
22+
python scripts/ping_reviewers.py --wait-time-minutes 10080 || echo failed

scripts/github_cc_reviewers.py

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
#!/usr/bin/env python3
2+
# Licensed to the Apache Software Foundation (ASF) under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. The ASF licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing,
13+
# software distributed under the License is distributed on an
14+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
# KIND, either express or implied. See the License for the
16+
# specific language governing permissions and limitations
17+
# under the License.
18+
19+
import os
20+
import json
21+
import argparse
22+
import re
23+
from urllib import error
24+
from typing import Dict, Any, List
25+
26+
27+
from git_utils import git, GitHubRepo, parse_remote
28+
29+
30+
def find_reviewers(body: str) -> List[str]:
31+
print(f"Parsing body:\n{body}")
32+
matches = re.findall(r"(cc( @[-A-Za-z0-9]+)+)", body, flags=re.MULTILINE)
33+
matches = [full for full, last in matches]
34+
35+
print("Found matches:", matches)
36+
reviewers = []
37+
for match in matches:
38+
if match.startswith("cc "):
39+
match = match.replace("cc ", "")
40+
users = [x.strip() for x in match.split("@")]
41+
reviewers += users
42+
43+
reviewers = set(x for x in reviewers if x != "")
44+
return sorted(list(reviewers))
45+
46+
47+
if __name__ == "__main__":
48+
help = "Add @cc'ed people in a PR body as reviewers"
49+
parser = argparse.ArgumentParser(description=help)
50+
parser.add_argument("--remote", default="origin", help="ssh remote to parse")
51+
parser.add_argument("--testing-reviews-json", help="(testing only) reviews as JSON")
52+
parser.add_argument(
53+
"--dry-run",
54+
action="store_true",
55+
default=False,
56+
help="run but don't send any request to GitHub",
57+
)
58+
args = parser.parse_args()
59+
60+
remote = git(["config", "--get", f"remote.{args.remote}.url"])
61+
user, repo = parse_remote(remote)
62+
63+
pr = json.loads(os.environ["PR"])
64+
65+
number = pr["number"]
66+
body = pr["body"]
67+
if body is None:
68+
body = ""
69+
70+
new_reviewers = find_reviewers(body)
71+
print("Found these reviewers:", new_reviewers)
72+
73+
if args.testing_reviews_json:
74+
existing_reviews = json.loads(args.testing_reviews_json)
75+
else:
76+
github = GitHubRepo(token=os.environ["GITHUB_TOKEN"], user=user, repo=repo)
77+
existing_reviews = github.get(f"pulls/{number}/reviews")
78+
79+
existing_review_users = [review["user"]["login"] for review in existing_reviews]
80+
print("PR has reviews from these users:", existing_review_users)
81+
existing_review_users = set(r.lower() for r in existing_review_users)
82+
83+
existing_reviewers = [review["login"] for review in pr["requested_reviewers"]]
84+
print("PR already had these reviewers requested:", existing_reviewers)
85+
86+
existing_reviewers_lower = {
87+
existing_reviewer.lower() for existing_reviewer in existing_reviewers
88+
}
89+
to_add = []
90+
for new_reviewer in new_reviewers:
91+
if (
92+
new_reviewer.lower() in existing_reviewers_lower
93+
or new_reviewer.lower() in existing_review_users
94+
):
95+
print(f"{new_reviewer} is already review requested, skipping")
96+
else:
97+
to_add.append(new_reviewer)
98+
99+
print(f"After filtering existing reviewers, adding: {to_add}")
100+
101+
if not args.dry_run:
102+
github = GitHubRepo(token=os.environ["GITHUB_TOKEN"], user=user, repo=repo)
103+
104+
# Add reviewers 1 by 1 since GitHub will error out if any of the
105+
# requested reviewers aren't members / contributors
106+
for reviewer in to_add:
107+
try:
108+
github.post(f"pulls/{number}/requested_reviewers", {"reviewers": [reviewer]})
109+
except error.HTTPError as e:
110+
print(f"Failed to add reviewer {reviewer}: {e}")

0 commit comments

Comments
 (0)