Skip to content

Commit 7a36221

Browse files
committed
scripts/set_assignees.py: set assignee on manifest changes
Parse manifest for changes and set assignees for any manifest entries that have changed. Other changes: - Do not assign to meta area when additional areas are being changed - Cleanup of unused code - Comment where comments are needed. Signed-off-by: Anas Nashif <anas.nashif@intel.com>
1 parent 0af05c8 commit 7a36221

File tree

2 files changed

+133
-49
lines changed

2 files changed

+133
-49
lines changed

.github/workflows/assigner.yml

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,32 @@ jobs:
2424
if: github.event.pull_request.draft == false
2525
runs-on: ubuntu-24.04
2626
permissions:
27-
pull-requests: write # to add assignees to pull requests
2827
issues: write # to add assignees to issues
2928

3029
steps:
3130
- name: Check out source code
3231
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
32+
with:
33+
fetch-depth: 0
34+
persist-credentials: false
3335

3436
- name: Set up Python
35-
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
37+
uses: zephyrproject-rtos/action-python-env@ace91a63fd503cd618ff1eb83fbcf302dabd7d44 # main
3638
with:
3739
python-version: 3.12
38-
cache: pip
39-
cache-dependency-path: scripts/requirements-actions.txt
4040

41-
- name: Install Python packages
41+
- name: Fetch west.yml from pull request
42+
run: |
43+
git fetch origin pull/${{ github.event.pull_request.number }}/head
44+
git show FETCH_HEAD:west.yml > pr_west.yml
45+
46+
- name: west setup
47+
if: >
48+
github.event_name == 'pull_request_target'
4249
run: |
43-
pip install -r scripts/requirements-actions.txt --require-hashes
50+
git config --global user.email "you@example.com"
51+
git config --global user.name "Your Name"
52+
west init -l . || true
4453
4554
- name: Run assignment script
4655
env:
@@ -51,14 +60,13 @@ jobs:
5160
FLAGS+=" -r ${{ github.event.repository.name }}"
5261
FLAGS+=" -M MAINTAINERS.yml"
5362
if [ "${{ github.event_name }}" = "pull_request_target" ]; then
54-
FLAGS+=" -P ${{ github.event.pull_request.number }}"
63+
FLAGS+=" -P ${{ github.event.pull_request.number }} --updated-manifest pr_west.yml"
5564
elif [ "${{ github.event_name }}" = "issues" ]; then
56-
FLAGS+=" -I ${{ github.event.issue.number }}"
65+
FLAGS+=" -I ${{ github.event.issue.number }}"
5766
elif [ "${{ github.event_name }}" = "schedule" ]; then
58-
FLAGS+=" --modules"
67+
FLAGS+=" --modules"
5968
else
6069
echo "Unknown event: ${{ github.event_name }}"
6170
exit 1
6271
fi
63-
6472
python3 scripts/set_assignees.py $FLAGS

scripts/set_assignees.py

Lines changed: 115 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,22 @@
44
# SPDX-License-Identifier: Apache-2.0
55

66
import argparse
7-
import sys
7+
import datetime
88
import os
9+
import sys
910
import time
10-
import datetime
11+
from collections import defaultdict
12+
1113
from github import Github, GithubException
1214
from github.GithubException import UnknownObjectException
13-
from collections import defaultdict
14-
from west.manifest import Manifest
15-
from west.manifest import ManifestProject
15+
from west.manifest import Manifest, ManifestProject
1616

1717
TOP_DIR = os.path.join(os.path.dirname(__file__))
1818
sys.path.insert(0, os.path.join(TOP_DIR, "scripts"))
1919
from get_maintainer import Maintainers
2020

21+
zephyr_base = os.getenv('ZEPHYR_BASE', os.path.join(TOP_DIR, '..'))
22+
2123
def log(s):
2224
if args.verbose > 0:
2325
print(s, file=sys.stdout)
@@ -50,11 +52,45 @@ def parse_args():
5052
parser.add_argument("-r", "--repo", default="zephyr",
5153
help="Github repository")
5254

55+
parser.add_argument( "--updated-manifest", default=None,
56+
help="Updated manifest file to compare against current west.yml")
57+
5358
parser.add_argument("-v", "--verbose", action="count", default=0,
5459
help="Verbose Output")
5560

5661
args = parser.parse_args()
5762

63+
64+
def process_manifest(old_manifest_file):
65+
log("Processing manifest changes")
66+
if not os.path.isfile("west.yml") or not os.path.isfile(old_manifest_file):
67+
log("No west.yml found, skipping...")
68+
return []
69+
old_manifest = Manifest.from_file(old_manifest_file)
70+
new_manifest = Manifest.from_file("west.yml")
71+
old_projs = set((p.name, p.revision) for p in old_manifest.projects)
72+
new_projs = set((p.name, p.revision) for p in new_manifest.projects)
73+
# Removed projects
74+
rprojs = set(filter(lambda p: p[0] not in list(p[0] for p in new_projs),
75+
old_projs - new_projs))
76+
# Updated projects
77+
uprojs = set(filter(lambda p: p[0] in list(p[0] for p in old_projs),
78+
new_projs - old_projs))
79+
# Added projects
80+
aprojs = new_projs - old_projs - uprojs
81+
82+
# All projs
83+
projs = rprojs | uprojs | aprojs
84+
projs_names = [name for name, rev in projs]
85+
86+
log(f"found modified projects: {projs_names}")
87+
areas = []
88+
for p in projs_names:
89+
areas.append(f'West project: {p}')
90+
91+
log(f'manifest areas: {areas}')
92+
return areas
93+
5894
def process_pr(gh, maintainer_file, number):
5995

6096
gh_repo = gh.get_repo(f"{args.org}/{args.repo}")
@@ -67,35 +103,59 @@ def process_pr(gh, maintainer_file, number):
67103
found_maintainers = defaultdict(int)
68104

69105
num_files = 0
70-
all_areas = set()
71106
fn = list(pr.get_files())
72107

73-
for changed_file in fn:
74-
if changed_file.filename in ['west.yml','submanifests/optional.yaml']:
75-
break
76-
77108
if pr.commits == 1 and (pr.additions <= 1 and pr.deletions <= 1):
78109
labels = {'size: XS'}
79110

80111
if len(fn) > 500:
81112
log(f"Too many files changed ({len(fn)}), skipping....")
82113
return
83114

115+
# areas where assignment happens if only area is affected
116+
meta_areas = [
117+
'Release Notes',
118+
'Documentation',
119+
'Samples'
120+
]
121+
84122
for changed_file in fn:
123+
85124
num_files += 1
86125
log(f"file: {changed_file.filename}")
87-
areas = maintainer_file.path2areas(changed_file.filename)
126+
127+
areas = []
128+
if changed_file.filename in ['west.yml','submanifests/optional.yaml']:
129+
if not args.updated_manifest:
130+
log("No updated manifest file provided, cannot process west.yml changes, skipping...")
131+
continue
132+
parsed_areas = process_manifest(old_manifest_file=args.updated_manifest)
133+
for _area in parsed_areas:
134+
area_match = maintainer_file.name2areas(_area)
135+
if area_match:
136+
areas.extend(area_match)
137+
else:
138+
areas = maintainer_file.path2areas(changed_file.filename)
139+
140+
log(f"areas for {changed_file}: {areas}")
88141

89142
if not areas:
90143
continue
91144

92-
all_areas.update(areas)
145+
# instance of an area, for example a driver or a board, not APIs or subsys code.
93146
is_instance = False
94147
sorted_areas = sorted(areas, key=lambda x: 'Platform' in x.name, reverse=True)
95148
for area in sorted_areas:
96-
c = 1 if not is_instance else 0
149+
# do not count cmake file changes, i.e. when there are changes to
150+
# instances of an area listed in both the subsystem and the
151+
# platform implementing it
152+
if 'CMakeLists.txt' in changed_file.filename or area.name in meta_areas:
153+
c = 0
154+
else:
155+
c = 1 if not is_instance else 0
97156

98157
area_counter[area] += c
158+
log(f"area counter: {area_counter}")
99159
labels.update(area.labels)
100160
# FIXME: Here we count the same file multiple times if it exists in
101161
# multiple areas with same maintainer
@@ -122,22 +182,26 @@ def process_pr(gh, maintainer_file, number):
122182
log(f"Submitted by: {pr.user.login}")
123183
log(f"candidate maintainers: {_all_maintainers}")
124184

125-
assignees = []
126-
tmp_assignees = []
185+
ranked_assignees = []
186+
assignees = None
127187

128188
# we start with areas with most files changed and pick the maintainer from the first one.
129189
# if the first area is an implementation, i.e. driver or platform, we
130190
# continue searching for any other areas involved
131191
for area, count in area_counter.items():
132-
if count == 0:
192+
# if only meta area is affected, assign one of the maintainers of that area
193+
if area.name in meta_areas and len(area_counter) == 1:
194+
assignees = area.maintainers
195+
break
196+
# if no maintainers, skip
197+
if count == 0 or len(area.maintainers) == 0:
133198
continue
199+
# if there are maintainers, but no assignees yet, set them
134200
if len(area.maintainers) > 0:
135-
tmp_assignees = area.maintainers
136201
if pr.user.login in area.maintainers:
137-
# submitter = assignee, try to pick next area and
138-
# assign someone else other than the submitter
139-
# when there also other maintainers for the area
140-
# assign them
202+
# If submitter = assignee, try to pick next area and assign
203+
# someone else other than the submitter, otherwise when there
204+
# are other maintainers for the area, assign them.
141205
if len(area.maintainers) > 1:
142206
assignees = area.maintainers.copy()
143207
assignees.remove(pr.user.login)
@@ -146,16 +210,25 @@ def process_pr(gh, maintainer_file, number):
146210
else:
147211
assignees = area.maintainers
148212

149-
if 'Platform' not in area.name:
150-
break
213+
# found a non-platform area that was changed, pick assignee from this
214+
# area and put them on top of the list, otherwise just append.
215+
if 'Platform' not in area.name:
216+
ranked_assignees.insert(0, area.maintainers)
217+
break
218+
else:
219+
ranked_assignees.append(area.maintainers)
151220

152-
if tmp_assignees and not assignees:
153-
assignees = tmp_assignees
221+
if ranked_assignees:
222+
assignees = ranked_assignees[0]
154223

155224
if assignees:
156225
prop = (found_maintainers[assignees[0]] / num_files) * 100
157226
log(f"Picked assignees: {assignees} ({prop:.2f}% ownership)")
158227
log("+++++++++++++++++++++++++")
228+
elif len(_all_maintainers) > 0:
229+
# if we have maintainers found, but could not pick one based on area,
230+
# then pick the one with most changes
231+
assignees = [next(iter(_all_maintainers))]
159232

160233
# Set labels
161234
if labels:
@@ -165,7 +238,7 @@ def process_pr(gh, maintainer_file, number):
165238
if not args.dry_run:
166239
pr.add_to_labels(l)
167240
else:
168-
log(f"Too many labels to be applied")
241+
log("Too many labels to be applied")
169242

170243
if collab:
171244
reviewers = []
@@ -206,27 +279,30 @@ def process_pr(gh, maintainer_file, number):
206279
if len(existing_reviewers) < 15:
207280
reviewer_vacancy = 15 - len(existing_reviewers)
208281
reviewers = reviewers[:reviewer_vacancy]
209-
210-
if reviewers:
211-
try:
212-
log(f"adding reviewers {reviewers}...")
213-
if not args.dry_run:
214-
pr.create_review_request(reviewers=reviewers)
215-
except GithubException:
216-
log("cant add reviewer")
217282
else:
218283
log("not adding reviewers because the existing reviewer count is greater than or "
219-
"equal to 15")
284+
"equal to 15. Adding maintainers of all areas as reviewers instead.")
285+
# FIXME: Here we could also add collaborators of the areas most
286+
# affected, i.e. the one with the final assigne.
287+
reviewers = list(_all_maintainers.keys())
288+
289+
if reviewers:
290+
try:
291+
log(f"adding reviewers {reviewers}...")
292+
if not args.dry_run:
293+
pr.create_review_request(reviewers=reviewers)
294+
except GithubException:
295+
log("can't add reviewer")
220296

221297
ms = []
222298
# assignees
223-
if assignees and not pr.assignee:
299+
if assignees and (not pr.assignee or args.dry_run):
224300
try:
225301
for assignee in assignees:
226302
u = gh.get_user(assignee)
227303
ms.append(u)
228304
except GithubException:
229-
log(f"Error: Unknown user")
305+
log("Error: Unknown user")
230306

231307
for mm in ms:
232308
log(f"Adding assignee {mm}...")
@@ -280,7 +356,7 @@ def process_issue(gh, maintainer_file, number):
280356
print(f"Using labels: {issue_labels}")
281357

282358
if issue_labels not in label_to_maintainer:
283-
print(f"no match for the label set, not assigning")
359+
print("no match for the label set, not assigning")
284360
return
285361

286362
for maintainer in label_to_maintainer[issue_labels]:
@@ -317,7 +393,7 @@ def process_modules(gh, maintainers_file):
317393
repo_name = f"{args.org}/{project.name}"
318394
repos[repo_name] = maintainers_file.areas[area]
319395

320-
query = f"is:open is:pr no:assignee"
396+
query = "is:open is:pr no:assignee"
321397
for repo in repos:
322398
query += f" repo:{repo}"
323399

0 commit comments

Comments
 (0)