Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 53 additions & 29 deletions .github/workflows/assigner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,41 +24,65 @@ jobs:
if: github.event.pull_request.draft == false
runs-on: ubuntu-24.04
permissions:
pull-requests: write # to add assignees to pull requests
issues: write # to add assignees to issues

steps:
- name: Check out source code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Check out source code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
persist-credentials: false

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

- name: Install Python packages
run: |
pip install -r scripts/requirements-actions.txt --require-hashes
- name: Fetch west.yml from pull request
run: |
git fetch origin pull/${{ github.event.pull_request.number }}/head
git show FETCH_HEAD:west.yml > pr_west.yml

- name: Run assignment script
env:
GITHUB_TOKEN: ${{ secrets.ZB_PR_ASSIGNER_GITHUB_TOKEN }}
run: |
FLAGS="-v"
FLAGS+=" -o ${{ github.event.repository.owner.login }}"
FLAGS+=" -r ${{ github.event.repository.name }}"
FLAGS+=" -M MAINTAINERS.yml"
if [ "${{ github.event_name }}" = "pull_request_target" ]; then
FLAGS+=" -P ${{ github.event.pull_request.number }}"
elif [ "${{ github.event_name }}" = "issues" ]; then
- name: west setup
if: >
github.event_name == 'pull_request_target'
run: |
git config --global user.email "[email protected]"
git config --global user.name "Your Name"
west init -l . || true

- name: Run assignment script
env:
GITHUB_TOKEN: ${{ secrets.ZB_PR_ASSIGNER_GITHUB_TOKEN }}
run: |
FLAGS="-v"
FLAGS+=" -o ${{ github.event.repository.owner.login }}"
FLAGS+=" -r ${{ github.event.repository.name }}"
FLAGS+=" -M MAINTAINERS.yml"
if [ "${{ github.event_name }}" = "pull_request_target" ]; then
FLAGS+=" -P ${{ github.event.pull_request.number }} --updated-manifest pr_west.yml"
python3 scripts/set_assignees.py $FLAGS
elif [ "${{ github.event_name }}" = "issues" ]; then
FLAGS+=" -I ${{ github.event.issue.number }}"
elif [ "${{ github.event_name }}" = "schedule" ]; then
python3 scripts/set_assignees.py $FLAGS
elif [ "${{ github.event_name }}" = "schedule" ]; then
FLAGS+=" --modules"
else
echo "Unknown event: ${{ github.event_name }}"
exit 1
fi
python3 scripts/set_assignees.py $FLAGS
else
echo "Unknown event: ${{ github.event_name }}"
exit 1
fi


- name: Save PR number
if: >
github.event_name == 'pull_request'
run: |
echo ${{ github.event.number }} > ./pr/NR
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: >
github.event_name == 'pull_request'
with:
name: pr
path: pr/

python3 scripts/set_assignees.py $FLAGS
1 change: 1 addition & 0 deletions kernel/init.c
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

/*
* Copyright (c) 2010-2014 Wind River Systems, Inc.
*
Expand Down
26 changes: 26 additions & 0 deletions scripts/get_maintainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ def _parse_args():
nargs="?",
help="List all areas maintained by maintainer.")


area_parser = subparsers.add_parser(
"area",
help="List area(s) by name")
area_parser.add_argument(
"name",
metavar="AREA",
nargs="?",
help="List all areas with the given name.")

area_parser.set_defaults(cmd_fn=Maintainers._area_cmd)

# New arguments for filtering
areas_parser.add_argument(
"--without-maintainers",
Expand Down Expand Up @@ -220,6 +232,12 @@ def __init__(self, filename=None):

self.areas[area_name] = area

def name2areas(self, name):
"""
Returns a list of Area instances for the areas that match 'name'.
"""
return [area for area in self.areas.values() if area.name == name]

def path2areas(self, path):
"""
Returns a list of Area instances for the areas that contain 'path',
Expand Down Expand Up @@ -262,6 +280,14 @@ def __repr__(self):
# Command-line subcommands
#

def _area_cmd(self, args):
# 'area' subcommand implementation

res = set()
areas = self.name2areas(args.name)
res.update(areas)
_print_areas(res)

def _path_cmd(self, args):
# 'path' subcommand implementation

Expand Down
139 changes: 109 additions & 30 deletions scripts/set_assignees.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@
import os
import time
import datetime
import json
from github import Github, GithubException
from github.GithubException import UnknownObjectException
from collections import defaultdict
from west.manifest import Manifest
from west.manifest import ManifestProject
from git import Repo
from pathlib import Path

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

zephyr_base = os.getenv('ZEPHYR_BASE', os.path.join(TOP_DIR, '..'))

def log(s):
if args.verbose > 0:
print(s, file=sys.stdout)
Expand Down Expand Up @@ -50,11 +55,45 @@ def parse_args():
parser.add_argument("-r", "--repo", default="zephyr",
help="Github repository")

parser.add_argument( "--updated-manifest", default=None,
help="Updated manifest file to compare against current west.yml")

parser.add_argument("-v", "--verbose", action="count", default=0,
help="Verbose Output")

args = parser.parse_args()


def process_manifest(old_manifest_file):
log("Processing manifest changes")
if not os.path.isfile("west.yml") or not os.path.isfile(old_manifest_file):
log("No west.yml found, skipping...")
return []
old_manifest = Manifest.from_file(old_manifest_file)
new_manifest = Manifest.from_file("west.yml")
old_projs = set((p.name, p.revision) for p in old_manifest.projects)
new_projs = set((p.name, p.revision) for p in new_manifest.projects)
# Removed projects
rprojs = set(filter(lambda p: p[0] not in list(p[0] for p in new_projs),
old_projs - new_projs))
# Updated projects
uprojs = set(filter(lambda p: p[0] in list(p[0] for p in old_projs),
new_projs - old_projs))
# Added projects
aprojs = new_projs - old_projs - uprojs

# All projs
projs = rprojs | uprojs | aprojs
projs_names = [name for name, rev in projs]

log(f"found modified projects: {projs_names}")
areas = []
for p in projs_names:
areas.append(f'West project: {p}')

log(f'manifest areas: {areas}')
return areas

def process_pr(gh, maintainer_file, number):

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

num_files = 0
all_areas = set()
fn = list(pr.get_files())

for changed_file in fn:
if changed_file.filename in ['west.yml','submanifests/optional.yaml']:
break

if pr.commits == 1 and (pr.additions <= 1 and pr.deletions <= 1):
labels = {'size: XS'}

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

# areas where assignment happens if only area is affected
meta_areas = [
'Release Notes',
'Documentation',
'Samples'
]

for changed_file in fn:

num_files += 1
log(f"file: {changed_file.filename}")
areas = maintainer_file.path2areas(changed_file.filename)

areas = []
if changed_file.filename in ['west.yml','submanifests/optional.yaml']:
if not args.updated_manifest:
log("No updated manifest file provided, cannot process west.yml changes, skipping...")
continue
parsed_areas = process_manifest(old_manifest_file=args.updated_manifest)
for _area in parsed_areas:
area_match = maintainer_file.name2areas(_area)
if area_match:
areas.extend(area_match)
else:
areas = maintainer_file.path2areas(changed_file.filename)

print(f"areas for {changed_file}: {areas}")

if not areas:
continue

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

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

assignees = []
tmp_assignees = []
ranked_assignees = []
assignees = None

# we start with areas with most files changed and pick the maintainer from the first one.
# if the first area is an implementation, i.e. driver or platform, we
# continue searching for any other areas involved
for area, count in area_counter.items():
if count == 0:
# if only meta area is affected, assign one of the maintainers of that area
if area.name in meta_areas and len(area_counter) == 1:
assignees = area.maintainers
break
# if no maintainers, skip
if count == 0 or len(area.maintainers) == 0:
continue
# if there are maintainers, but no assignees yet, set them
if len(area.maintainers) > 0:
tmp_assignees = area.maintainers
if pr.user.login in area.maintainers:
# submitter = assignee, try to pick next area and
# assign someone else other than the submitter
# when there also other maintainers for the area
# assign them
# If submitter = assignee, try to pick next area and assign
# someone else other than the submitter, otherwise when there
# are other maintainers for the area, assign them.
if len(area.maintainers) > 1:
assignees = area.maintainers.copy()
assignees.remove(pr.user.login)
Expand All @@ -146,16 +213,25 @@ def process_pr(gh, maintainer_file, number):
else:
assignees = area.maintainers

if 'Platform' not in area.name:
break
# found a non-platform area that was changed, pick assignee from this
# area and put them on top of the list, otherwise just append.
if 'Platform' not in area.name:
ranked_assignees.insert(0, area.maintainers)
break
else:
ranked_assignees.append(area.maintainers)

if tmp_assignees and not assignees:
assignees = tmp_assignees
if ranked_assignees:
assignees = ranked_assignees[0]

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

# Set labels
if labels:
Expand Down Expand Up @@ -206,21 +282,24 @@ def process_pr(gh, maintainer_file, number):
if len(existing_reviewers) < 15:
reviewer_vacancy = 15 - len(existing_reviewers)
reviewers = reviewers[:reviewer_vacancy]

if reviewers:
try:
log(f"adding reviewers {reviewers}...")
if not args.dry_run:
pr.create_review_request(reviewers=reviewers)
except GithubException:
log("cant add reviewer")
else:
log("not adding reviewers because the existing reviewer count is greater than or "
"equal to 15")
"equal to 15. Adding maintainers of all areas as reviewers instead.")
# FIXME: Here we could also add collaborators of the areas most
# affected, i.e. the one with the final assigne.
reviewers = list(_all_maintainers.keys())

if reviewers:
try:
log(f"adding reviewers {reviewers}...")
if not args.dry_run:
pr.create_review_request(reviewers=reviewers)
except GithubException:
log("can't add reviewer")

ms = []
# assignees
if assignees and not pr.assignee:
if assignees and (not pr.assignee or args.dry_run):
try:
for assignee in assignees:
u = gh.get_user(assignee)
Expand Down
2 changes: 1 addition & 1 deletion west.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ manifest:
# zephyr-keep-sorted-start re(^\s+\- name:)
projects:
- name: acpica
revision: 8d24867bc9c9d81c81eeac59391cda59333affd4
revision: fd24867bc9c9d81c81eeac59391cda59333affd4
path: modules/lib/acpica
- name: babblesim_base
remote: babblesim
Expand Down
Loading