Skip to content

Commit a32bf26

Browse files
committed
scripts/set_assignees.py: set assignee on manifest changes
Parse manifest for changes and set assignees for any manifest entries that has 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 <[email protected]>
1 parent 58b7b86 commit a32bf26

File tree

2 files changed

+163
-59
lines changed

2 files changed

+163
-59
lines changed

.github/workflows/assigner.yml

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,41 +24,66 @@ 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:
31-
- name: Check out source code
32-
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
30+
- name: Check out source code
31+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
32+
with:
33+
fetch-depth: 0
34+
persist-credentials: false
3335

34-
- name: Set up Python
35-
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
36-
with:
37-
python-version: 3.12
38-
cache: pip
39-
cache-dependency-path: scripts/requirements-actions.txt
36+
- name: Set up Python
37+
uses: zephyrproject-rtos/action-python-env@ace91a63fd503cd618ff1eb83fbcf302dabd7d44 # main
38+
with:
39+
python-version: 3.12
4040

41-
- name: Install Python packages
42-
run: |
43-
pip install -r scripts/requirements-actions.txt --require-hashes
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
4445
45-
- name: Run assignment script
46-
env:
47-
GITHUB_TOKEN: ${{ secrets.ZB_PR_ASSIGNER_GITHUB_TOKEN }}
48-
run: |
49-
FLAGS="-v"
50-
FLAGS+=" -o ${{ github.event.repository.owner.login }}"
51-
FLAGS+=" -r ${{ github.event.repository.name }}"
52-
FLAGS+=" -M MAINTAINERS.yml"
53-
if [ "${{ github.event_name }}" = "pull_request_target" ]; then
54-
FLAGS+=" -P ${{ github.event.pull_request.number }}"
55-
elif [ "${{ github.event_name }}" = "issues" ]; then
46+
- name: west setup
47+
if: >
48+
github.event_name == 'pull_request_target'
49+
run: |
50+
git config --global user.email "[email protected]"
51+
git config --global user.name "Your Name"
52+
west init -l . || true
53+
54+
- name: Run assignment script
55+
env:
56+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
57+
run: |
58+
FLAGS="-v"
59+
FLAGS+=" -o ${{ github.event.repository.owner.login }}"
60+
FLAGS+=" -r ${{ github.event.repository.name }}"
61+
FLAGS+=" -M MAINTAINERS.yml"
62+
if [ "${{ github.event_name }}" = "pull_request_target" ]; then
63+
FLAGS+=" -P ${{ github.event.pull_request.number }} --old-manifest pr_west.yml"
64+
python3 scripts/set_assignees.py $FLAGS
65+
cp -f manifest_areas.json ./pr/
66+
elif [ "${{ github.event_name }}" = "issues" ]; then
5667
FLAGS+=" -I ${{ github.event.issue.number }}"
57-
elif [ "${{ github.event_name }}" = "schedule" ]; then
68+
python3 scripts/set_assignees.py $FLAGS
69+
elif [ "${{ github.event_name }}" = "schedule" ]; then
5870
FLAGS+=" --modules"
59-
else
60-
echo "Unknown event: ${{ github.event_name }}"
61-
exit 1
62-
fi
71+
python3 scripts/set_assignees.py $FLAGS
72+
else
73+
echo "Unknown event: ${{ github.event_name }}"
74+
exit 1
75+
fi
76+
77+
78+
- name: Save PR number
79+
if: >
80+
github.event_name == 'pull_request'
81+
run: |
82+
echo ${{ github.event.number }} > ./pr/NR
83+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
84+
if: >
85+
github.event_name == 'pull_request'
86+
with:
87+
name: pr
88+
path: pr/
6389

64-
python3 scripts/set_assignees.py $FLAGS

scripts/set_assignees.py

Lines changed: 109 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,21 @@
88
import os
99
import time
1010
import datetime
11+
import json
1112
from github import Github, GithubException
1213
from github.GithubException import UnknownObjectException
1314
from collections import defaultdict
1415
from west.manifest import Manifest
1516
from west.manifest import ManifestProject
17+
from git import Repo
18+
from pathlib import Path
1619

1720
TOP_DIR = os.path.join(os.path.dirname(__file__))
1821
sys.path.insert(0, os.path.join(TOP_DIR, "scripts"))
1922
from get_maintainer import Maintainers
2023

24+
zephyr_base = os.getenv('ZEPHYR_BASE', os.path.join(TOP_DIR, '..'))
25+
2126
def log(s):
2227
if args.verbose > 0:
2328
print(s, file=sys.stdout)
@@ -50,11 +55,45 @@ def parse_args():
5055
parser.add_argument("-r", "--repo", default="zephyr",
5156
help="Github repository")
5257

58+
parser.add_argument( "--updated-manifest", default=None,
59+
help="Updated manifest file to compare against current west.yml")
60+
5361
parser.add_argument("-v", "--verbose", action="count", default=0,
5462
help="Verbose Output")
5563

5664
args = parser.parse_args()
5765

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

6099
gh_repo = gh.get_repo(f"{args.org}/{args.repo}")
@@ -67,35 +106,59 @@ def process_pr(gh, maintainer_file, number):
67106
found_maintainers = defaultdict(int)
68107

69108
num_files = 0
70-
all_areas = set()
71109
fn = list(pr.get_files())
72110

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

80114
if len(fn) > 500:
81115
log(f"Too many files changed ({len(fn)}), skipping....")
82116
return
83117

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

89145
if not areas:
90146
continue
91147

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

98160
area_counter[area] += c
161+
print(f"area counter: {area_counter}")
99162
labels.update(area.labels)
100163
# FIXME: Here we count the same file multiple times if it exists in
101164
# multiple areas with same maintainer
@@ -122,22 +185,26 @@ def process_pr(gh, maintainer_file, number):
122185
log(f"Submitted by: {pr.user.login}")
123186
log(f"candidate maintainers: {_all_maintainers}")
124187

125-
assignees = []
126-
tmp_assignees = []
188+
ranked_assignees = []
189+
assignees = None
127190

128191
# we start with areas with most files changed and pick the maintainer from the first one.
129192
# if the first area is an implementation, i.e. driver or platform, we
130193
# continue searching for any other areas involved
131194
for area, count in area_counter.items():
132-
if count == 0:
195+
# if only meta area is affected, assign one of the maintainers of that area
196+
if area.name in meta_areas and len(area_counter) == 1:
197+
assignees = area.maintainers
198+
break
199+
# if no maintainers, skip
200+
if count == 0 or len(area.maintainers) == 0:
133201
continue
202+
# if there are maintainers, but no assignees yet, set them
134203
if len(area.maintainers) > 0:
135-
tmp_assignees = area.maintainers
136204
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
205+
# If submitter = assignee, try to pick next area and assign
206+
# someone else other than the submitter, otherwise when there
207+
# are other maintainers for the area, assign them.
141208
if len(area.maintainers) > 1:
142209
assignees = area.maintainers.copy()
143210
assignees.remove(pr.user.login)
@@ -146,16 +213,25 @@ def process_pr(gh, maintainer_file, number):
146213
else:
147214
assignees = area.maintainers
148215

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

152-
if tmp_assignees and not assignees:
153-
assignees = tmp_assignees
224+
if ranked_assignees:
225+
assignees = ranked_assignees[0]
154226

155227
if assignees:
156228
prop = (found_maintainers[assignees[0]] / num_files) * 100
157229
log(f"Picked assignees: {assignees} ({prop:.2f}% ownership)")
158230
log("+++++++++++++++++++++++++")
231+
elif len(_all_maintainers) > 0:
232+
# if we have maintainers found, but could not pick one based on area,
233+
# then pick the one with most changes
234+
assignees = [next(iter(_all_maintainers))]
159235

160236
# Set labels
161237
if labels:
@@ -206,21 +282,24 @@ def process_pr(gh, maintainer_file, number):
206282
if len(existing_reviewers) < 15:
207283
reviewer_vacancy = 15 - len(existing_reviewers)
208284
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")
217285
else:
218286
log("not adding reviewers because the existing reviewer count is greater than or "
219-
"equal to 15")
287+
"equal to 15. Adding maintainers of all areas as reviewers instead.")
288+
# FIXME: Here we could also add collaborators of the areas most
289+
# affected, i.e. the one with the final assigne.
290+
reviewers = list(_all_maintainers.keys())
291+
292+
if reviewers:
293+
try:
294+
log(f"adding reviewers {reviewers}...")
295+
if not args.dry_run:
296+
pr.create_review_request(reviewers=reviewers)
297+
except GithubException:
298+
log("can't add reviewer")
220299

221300
ms = []
222301
# assignees
223-
if assignees and not pr.assignee:
302+
if assignees and (not pr.assignee or args.dry_run):
224303
try:
225304
for assignee in assignees:
226305
u = gh.get_user(assignee)

0 commit comments

Comments
 (0)