From 7a70348e841d96a16d70d8224814a5566404a3dd Mon Sep 17 00:00:00 2001 From: Anas Nashif Date: Wed, 17 Sep 2025 15:02:49 -0400 Subject: [PATCH 01/10] scripts: get_maintainer: support file groups This new section allows defining a group of files in an area and makes it possible to assign collaborators to the file group being defined. The purpose of this new section is to allow fine tuning who is added as reviewer when files change in a group. It is especially useful in large areas with hundreds of files, for example platform areas. Signed-off-by: Anas Nashif --- scripts/get_maintainer.py | 178 +++++++++++++++++++++++++++++++++++++- 1 file changed, 175 insertions(+), 3 deletions(-) diff --git a/scripts/get_maintainer.py b/scripts/get_maintainer.py index 7ca14d9a773..afb287e8199 100755 --- a/scripts/get_maintainer.py +++ b/scripts/get_maintainer.py @@ -218,6 +218,26 @@ def __init__(self, filename=None): area.tags = area_dict.get("tags", []) area.description = area_dict.get("description") + # Initialize file groups if present + area.file_groups = [] + if "file-groups" in area_dict: + for group_dict in area_dict["file-groups"]: + file_group = FileGroup() + file_group.name = group_dict.get("name", "Unnamed Group") + file_group.description = group_dict.get("description") + file_group.collaborators = group_dict.get("collaborators", []) + + # Create match functions for this file group + file_group._match_fn = \ + _get_match_fn(group_dict.get("files"), + group_dict.get("files-regex")) + + file_group._exclude_match_fn = \ + _get_match_fn(group_dict.get("files-exclude"), + group_dict.get("files-regex-exclude")) + + area.file_groups.append(file_group) + # area._match_fn(path) tests if the path matches files and/or # files-regex area._match_fn = \ @@ -260,6 +280,32 @@ def path2areas(self, path): return [area for area in self.areas.values() if area._contains(path)] + def path2area_info(self, path): + """ + Returns a list of tuples (Area, FileGroup) for the areas that contain 'path'. + FileGroup will be None if the path matches the area's general files rather + than a specific file group. + """ + areas = self.path2areas(path) + result = [] + + # Make directory paths end in '/' so that foo/bar matches foo/bar/. + is_dir = os.path.isdir(path) + + # Make 'path' relative to the repository root and normalize it. + path = os.path.normpath(os.path.join( + os.path.relpath(os.getcwd(), self._toplevel), + path)) + + if is_dir: + path += "/" + + for area in areas: + file_group = area.get_file_group_for_path(path) + result.append((area, file_group)) + + return result + def commits2areas(self, commits): """ Returns a set() of Area instances for the areas that contain files that @@ -420,6 +466,30 @@ def _orphaned_cmd(self, args): print(path) # We get here if we never hit the 'break' +class FileGroup: + """ + Represents a file group within an area in MAINTAINERS.yml. + + These attributes are available: + + name: + The name of the file group, as specified in the 'name' key + + description: + Text from 'description' key, or None if the group has no 'description' + + collaborators: + List of collaborators specific to this file group + """ + def _contains(self, path): + # Returns True if the file group contains 'path', and False otherwise + return self._match_fn and self._match_fn(path) and not \ + (self._exclude_match_fn and self._exclude_match_fn(path)) + + def __repr__(self): + return "".format(self.name) + + class Area: """ Represents an entry for an area in MAINTAINERS.yml. @@ -447,13 +517,46 @@ class Area: description: Text from 'description' key, or None if the area has no 'description' key + + file_groups: + List of FileGroup instances for any file-groups defined in the area. + Empty if the area has no 'file-groups' key. """ def _contains(self, path): # Returns True if the area contains 'path', and False otherwise + # First check if path matches any file groups - they take precedence + for file_group in self.file_groups: + if file_group._contains(path): + return True + # If no file group matches, check area-level patterns return self._match_fn and self._match_fn(path) and not \ (self._exclude_match_fn and self._exclude_match_fn(path)) + def get_collaborators_for_path(self, path): + """ + Returns a list of collaborators for a specific path. + If the path matches a file group, returns the file group's collaborators. + Otherwise, returns the area's general collaborators. + """ + # Check file groups first + for file_group in self.file_groups: + if file_group._contains(path): + return file_group.collaborators + + # Return general area collaborators if no file group matches + return self.collaborators + + def get_file_group_for_path(self, path): + """ + Returns the FileGroup instance that contains the given path, + or None if the path doesn't match any file group. + """ + for file_group in self.file_groups: + if file_group._contains(path): + return file_group + return None + def __repr__(self): return "".format(self.name) @@ -484,6 +587,17 @@ def _print_areas(areas): ", ".join(area.tags), area.description or "")) + # Print file groups if any exist + if area.file_groups: + print("\tfile-groups:") + for file_group in area.file_groups: + print("\t\t{}: {}".format( + file_group.name, + ", ".join(file_group.collaborators) if file_group.collaborators else "no collaborators" + )) + if file_group.description: + print("\t\t description: {}".format(file_group.description)) + def _get_match_fn(globs, regexes): # Constructs a single regex that tests for matches against the globs in @@ -552,7 +666,7 @@ def ferr(msg): ok_keys = {"status", "maintainers", "collaborators", "inform", "files", "files-exclude", "files-regex", "files-regex-exclude", - "labels", "description", "tests", "tags"} + "labels", "description", "tests", "tags", "file-groups"} ok_status = {"maintained", "odd fixes", "unmaintained", "obsolete"} ok_status_s = ", ".join('"' + s + '"' for s in ok_status) # For messages @@ -572,8 +686,8 @@ def ferr(msg): ferr("bad 'status' key on area '{}', should be one of {}" .format(area_name, ok_status_s)) - if not area_dict.keys() & {"files", "files-regex"}: - ferr("either 'files' or 'files-regex' (or both) must be specified " + if not area_dict.keys() & {"files", "files-regex", "file-groups"}: + ferr("either 'files', 'files-regex', or 'file-groups' (or combinations) must be specified " "for area '{}'".format(area_name)) if not area_dict.get("maintainers") and area_dict.get("status") == "maintained": @@ -617,6 +731,64 @@ def ferr(msg): "'{}': {}".format(regex, files_regex_key, area_name, e.msg)) + # Validate file-groups structure + if "file-groups" in area_dict: + file_groups = area_dict["file-groups"] + if not isinstance(file_groups, list): + ferr("malformed 'file-groups' value for area '{}' -- should be a list" + .format(area_name)) + + ok_group_keys = {"name", "description", "collaborators", "files", + "files-exclude", "files-regex", "files-regex-exclude"} + + for i, group_dict in enumerate(file_groups): + if not isinstance(group_dict, dict): + ferr("malformed file group {} in area '{}' -- should be a dict" + .format(i, area_name)) + + for key in group_dict: + if key not in ok_group_keys: + ferr("unknown key '{}' in file group {} in area '{}'" + .format(key, i, area_name)) + + # Each file group must have either files or files-regex + if not group_dict.keys() & {"files", "files-regex"}: + ferr("file group {} in area '{}' must specify either 'files' or 'files-regex'" + .format(i, area_name)) + + # Validate string fields in file groups + for str_field in ["name", "description"]: + if str_field in group_dict and not isinstance(group_dict[str_field], str): + ferr("malformed '{}' in file group {} in area '{}' -- should be a string" + .format(str_field, i, area_name)) + + # Validate list fields in file groups + for list_field in ["collaborators", "files", "files-exclude", "files-regex", "files-regex-exclude"]: + if list_field in group_dict: + lst = group_dict[list_field] + if not (isinstance(lst, list) and all(isinstance(elm, str) for elm in lst)): + ferr("malformed '{}' in file group {} in area '{}' -- should be a list of strings" + .format(list_field, i, area_name)) + + # Validate file patterns in file groups + for files_key in "files", "files-exclude": + if files_key in group_dict: + for glob_pattern in group_dict[files_key]: + paths = tuple(root.glob(glob_pattern)) + if not paths: + ferr("glob pattern '{}' in '{}' in file group {} in area '{}' does not " + "match any files".format(glob_pattern, files_key, i, area_name)) + + # Validate regex patterns in file groups + for files_regex_key in "files-regex", "files-regex-exclude": + if files_regex_key in group_dict: + for regex in group_dict[files_regex_key]: + try: + re.compile(regex) + except re.error as e: + ferr("bad regular expression '{}' in '{}' in file group {} in area '{}': {}" + .format(regex, files_regex_key, i, area_name, e.msg)) + if "description" in area_dict and \ not isinstance(area_dict["description"], str): ferr("malformed 'description' value for area '{}' -- should be a " From 55c9c332792cb30cde3ab5d78edb5a9a766b913b Mon Sep 17 00:00:00 2001 From: Anas Nashif Date: Tue, 4 Nov 2025 07:18:35 -0500 Subject: [PATCH 02/10] scripts: set_assignee.py: Support file groups Deal with new section in the maintainer file defining file groups. Signed-off-by: Anas Nashif --- scripts/set_assignees.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scripts/set_assignees.py b/scripts/set_assignees.py index 1cf2a29867a..330b6811f7c 100755 --- a/scripts/set_assignees.py +++ b/scripts/set_assignees.py @@ -128,6 +128,7 @@ def process_pr(gh, maintainer_file, number): # areas where assignment happens if only said areas are affected meta_areas = ['Release Notes', 'Documentation', 'Samples', 'Tests'] + collab_per_path = [] for changed_file in fn: num_files += 1 log(f"file: {changed_file.filename}") @@ -139,11 +140,14 @@ def process_pr(gh, maintainer_file, number): continue parsed_areas = process_manifest(old_manifest_file=args.updated_manifest) for _area in parsed_areas: + collab_per_path.extend(_area.get_collaborators_for_path(changed_file)) area_match = maintainer_file.name2areas(_area) if area_match: areas.extend(area_match) else: areas = maintainer_file.path2areas(changed_file.filename) + for _area in areas: + collab_per_path.extend(_area.get_collaborators_for_path(changed_file)) log(f"areas for {changed_file}: {areas}") @@ -173,6 +177,9 @@ def process_pr(gh, maintainer_file, number): if 'Platform' in area.name: is_instance = True + for _area in sorted_areas: + collab_per_path.extend(_area.get_collaborators_for_path(changed_file)) + area_counter = dict(sorted(area_counter.items(), key=lambda item: item[1], reverse=True)) log(f"Area matches: {area_counter}") log(f"labels: {labels}") @@ -182,6 +189,8 @@ def process_pr(gh, maintainer_file, number): for area in area_counter: collab += maintainer_file.areas[area.name].maintainers collab += maintainer_file.areas[area.name].collaborators + collab += collab_per_path + collab = list(dict.fromkeys(collab)) log(f"collab: {collab}") From aa419aa199f0fbf2648b044083ecc740230a5bb8 Mon Sep 17 00:00:00 2001 From: Anas Nashif Date: Tue, 4 Nov 2025 07:59:16 -0500 Subject: [PATCH 03/10] MAINTAINERS file: add documentation for file groups Document file groups and how they should be used. Signed-off-by: Anas Nashif --- MAINTAINERS.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/MAINTAINERS.yml b/MAINTAINERS.yml index af9c6861f11..b56f02a13c2 100644 --- a/MAINTAINERS.yml +++ b/MAINTAINERS.yml @@ -60,6 +60,22 @@ # Like 'files-regex', but any matching files will be excluded from the # area. # +# file-groups: +# A list of groups of files that are treated as a single unit. +# This is useful for areas where different collaborators are responsible for +# different parts of the area. +# Each group should have the following structure: +# - name: +# collaborators: +# - +# - +# files: +# - +# - +# files-regex: +# - +# - +# # description: >- # Plain-English description. Describe what the system is about, from an # outsider's perspective. From 5834f3a03334a60a5fb29abeb40845333a1b65a4 Mon Sep 17 00:00:00 2001 From: Anas Nashif Date: Tue, 4 Nov 2025 08:01:25 -0500 Subject: [PATCH 04/10] MAINTAINERS file: add description for tests Add a brief description for the `tests` key used in areas. Signed-off-by: Anas Nashif --- MAINTAINERS.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MAINTAINERS.yml b/MAINTAINERS.yml index b56f02a13c2..186f6a28f31 100644 --- a/MAINTAINERS.yml +++ b/MAINTAINERS.yml @@ -32,6 +32,9 @@ # labels: # List of GitHub labels to add to pull requests that modify the area. # +# tests: +# List of test identifiers to be run when this area is affected by a PR. +# # files: # List of paths and/or glob patterns giving the files in the area, # relative to the root directory. From af7ccf8b6cdaa0d36bb360775475c1e59189e34d Mon Sep 17 00:00:00 2001 From: Anas Nashif Date: Fri, 10 Oct 2025 10:14:14 -0400 Subject: [PATCH 05/10] ci: assigner: merge maintainer check into assigner workflow Merge two workflows into one for code sharing an efficiency. Signed-off-by: Anas Nashif --- .github/workflows/assigner.yml | 12 ++++++- .github/workflows/maintainer_check.yml | 43 -------------------------- 2 files changed, 11 insertions(+), 44 deletions(-) delete mode 100644 .github/workflows/maintainer_check.yml diff --git a/.github/workflows/assigner.yml b/.github/workflows/assigner.yml index f5bb122ceb4..47be3e2b6ae 100644 --- a/.github/workflows/assigner.yml +++ b/.github/workflows/assigner.yml @@ -38,12 +38,13 @@ jobs: with: python-version: 3.12 - - name: Fetch west.yml from pull request + - name: Fetch west.yml/Maintainer.yml from pull request if: > github.event_name == 'pull_request_target' run: | git fetch origin pull/${{ github.event.pull_request.number }}/merge git show FETCH_HEAD:west.yml > pr_west.yml + git show FETCH_HEAD:MAINTAINERS.yml > pr_MAINTAINERS.yml - name: west setup if: > @@ -72,3 +73,12 @@ jobs: exit 1 fi python3 scripts/set_assignees.py $FLAGS + + - name: Check maintainer file changes + if: > + github.event_name == 'pull_request_target' + env: + GITHUB_TOKEN: ${{ secrets.ZB_PR_ASSIGNER_GITHUB_TOKEN }} + run: | + python ./scripts/ci/check_maintainer_changes.py \ + --repo zephyrproject-rtos/zephyr MAINTAINERS.yml pr_MAINTAINERS.yml diff --git a/.github/workflows/maintainer_check.yml b/.github/workflows/maintainer_check.yml deleted file mode 100644 index a44efeb1272..00000000000 --- a/.github/workflows/maintainer_check.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Maintainer file check - -on: - pull_request_target: - branches: - - main - paths: - - MAINTAINERS.yml - -permissions: - contents: read - -jobs: - assignment: - name: Check MAINTAINERS.yml changes - runs-on: ubuntu-24.04 - - steps: - - name: Check out source code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - - name: Set up Python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - with: - python-version: 3.12 - cache: pip - cache-dependency-path: scripts/requirements-actions.txt - - - name: Install Python packages - run: | - pip install -r scripts/requirements-actions.txt --require-hashes - - - name: Fetch MAINTAINERS.yml from pull request - run: | - git fetch origin pull/${{ github.event.pull_request.number }}/merge - git show FETCH_HEAD:MAINTAINERS.yml > pr_MAINTAINERS.yml - - - name: Check maintainer file changes - env: - GITHUB_TOKEN: ${{ secrets.ZB_PR_ASSIGNER_GITHUB_TOKEN }} - run: | - python ./scripts/ci/check_maintainer_changes.py \ - --repo zephyrproject-rtos/zephyr MAINTAINERS.yml pr_MAINTAINERS.yml From 7746d06a131ebdf525149e24186e9e30329b2b41 Mon Sep 17 00:00:00 2001 From: Anas Nashif Date: Fri, 17 Oct 2025 07:36:32 -0400 Subject: [PATCH 06/10] scripts: move set_assignee.py into scripts/ci Scripts only used by CI, so move it into that directory. Signed-off-by: Anas Nashif --- .github/workflows/assigner.yml | 2 +- .ruff-excludes.toml | 1 + MAINTAINERS.yml | 4 ++-- scripts/{ => ci}/set_assignees.py | 0 scripts/ci/twister_ignore.txt | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) rename scripts/{ => ci}/set_assignees.py (100%) diff --git a/.github/workflows/assigner.yml b/.github/workflows/assigner.yml index 47be3e2b6ae..c0963be64dc 100644 --- a/.github/workflows/assigner.yml +++ b/.github/workflows/assigner.yml @@ -72,7 +72,7 @@ jobs: echo "Unknown event: ${{ github.event_name }}" exit 1 fi - python3 scripts/set_assignees.py $FLAGS + python3 scripts/ci/set_assignees.py $FLAGS - name: Check maintainer file changes if: > diff --git a/.ruff-excludes.toml b/.ruff-excludes.toml index d162d15d140..7857825e007 100644 --- a/.ruff-excludes.toml +++ b/.ruff-excludes.toml @@ -1214,6 +1214,7 @@ exclude = [ "./scripts/ci/coverage/coverage_analysis.py", "./scripts/ci/errno.py", "./scripts/ci/guideline_check.py", + "./scripts/ci/set_assignees.py", "./scripts/ci/stats/merged_prs.py", "./scripts/ci/test_plan.py", "./scripts/ci/twister_report_analyzer.py", diff --git a/MAINTAINERS.yml b/MAINTAINERS.yml index 186f6a28f31..9b693cdb8d4 100644 --- a/MAINTAINERS.yml +++ b/MAINTAINERS.yml @@ -967,7 +967,7 @@ Continuous Integration: - scripts/make_bugs_pickle.py - .checkpatch.conf - scripts/gitlint/ - - scripts/set_assignees.py + - scripts/ci/set_assignees.py labels: - "area: Continuous Integration" @@ -3283,7 +3283,7 @@ MAINTAINERS file: files: - MAINTAINERS.yml - scripts/get_maintainer.py - - scripts/set_assignees.py + - scripts/ci/set_assignees.py - scripts/check_maintainers.py labels: - "area: MAINTAINER File" diff --git a/scripts/set_assignees.py b/scripts/ci/set_assignees.py similarity index 100% rename from scripts/set_assignees.py rename to scripts/ci/set_assignees.py diff --git a/scripts/ci/twister_ignore.txt b/scripts/ci/twister_ignore.txt index 9e5b417df53..ee086b61f78 100644 --- a/scripts/ci/twister_ignore.txt +++ b/scripts/ci/twister_ignore.txt @@ -52,7 +52,7 @@ scripts/checkpatch.pl scripts/ci/pylintrc scripts/footprint/* scripts/make_bugs_pickle.py -scripts/set_assignees.py +scripts/ci/set_assignees.py scripts/gitlint/zephyr_commit_rules.py scripts/west_commands/runners/canopen_program.py scripts/ci/check_maintainer_changes.py From cf22f6f08bb53404069da8d49b5cf6b36e2e9613 Mon Sep 17 00:00:00 2001 From: Anas Nashif Date: Fri, 10 Oct 2025 10:43:00 -0400 Subject: [PATCH 07/10] scripts: set_assignee: request review from maintainers of changed areas Also request reviewes from maintainers of changes areas in the maintainer file. Signed-off-by: Anas Nashif --- .github/workflows/assigner.yml | 2 +- scripts/ci/set_assignees.py | 130 ++++++++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 3 deletions(-) diff --git a/.github/workflows/assigner.yml b/.github/workflows/assigner.yml index c0963be64dc..1dd1d3fe670 100644 --- a/.github/workflows/assigner.yml +++ b/.github/workflows/assigner.yml @@ -63,7 +63,7 @@ jobs: 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" + FLAGS+=" -P ${{ github.event.pull_request.number }} --updated-manifest pr_west.yml --updated-mantainer-file pr_MAINTAINERS.yml" elif [ "${{ github.event_name }}" = "issues" ]; then FLAGS+=" -I ${{ github.event.issue.number }}" elif [ "${{ github.event_name }}" = "schedule" ]; then diff --git a/scripts/ci/set_assignees.py b/scripts/ci/set_assignees.py index 330b6811f7c..a0838717230 100755 --- a/scripts/ci/set_assignees.py +++ b/scripts/ci/set_assignees.py @@ -9,16 +9,24 @@ import sys import time from collections import defaultdict +from pathlib import Path +import yaml from github import Auth, Github, GithubException from github.GithubException import UnknownObjectException from west.manifest import Manifest, ManifestProject TOP_DIR = os.path.join(os.path.dirname(__file__)) -sys.path.insert(0, os.path.join(TOP_DIR, "scripts")) +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from get_maintainer import Maintainers # noqa: E402 -zephyr_base = os.getenv('ZEPHYR_BASE', os.path.join(TOP_DIR, '..')) +ZEPHYR_BASE = os.environ.get('ZEPHYR_BASE') +if ZEPHYR_BASE: + ZEPHYR_BASE = Path(ZEPHYR_BASE) +else: + ZEPHYR_BASE = Path(__file__).resolve().parents[2] + # Propagate this decision to child processes. + os.environ['ZEPHYR_BASE'] = str(ZEPHYR_BASE) def log(s): @@ -71,10 +79,22 @@ def parse_args(): help="Updated manifest file to compare against current west.yml", ) + parser.add_argument( + "--updated-maintainer-file", + default=None, + help="Updated maintainer file to compare against current MAINTAINERS.yml", + ) + parser.add_argument("-v", "--verbose", action="count", default=0, help="Verbose Output") args = parser.parse_args() +def load_areas(filename: str): + with open(filename) as f: + doc = yaml.safe_load(f) + return { + k: v for k, v in doc.items() if isinstance(v, dict) and ("files" in v or "files-regex" in v) + } def process_manifest(old_manifest_file): log("Processing manifest changes") @@ -104,6 +124,93 @@ def process_manifest(old_manifest_file): log(f'manifest areas: {areas}') return areas +def set_or_empty(d, key): + return set(d.get(key, []) or []) + +def compare_areas(old, new, repo_fullname=None, token=None): + old_areas = set(old.keys()) + new_areas = set(new.keys()) + + changed_areas = set() + added_areas = new_areas - old_areas + removed_areas = old_areas - new_areas + common_areas = old_areas & new_areas + + print("=== Areas Added ===") + for area in sorted(added_areas): + print(f"+ {area}") + + print("\n=== Areas Removed ===") + for area in sorted(removed_areas): + print(f"- {area}") + + print("\n=== Area Changes ===") + for area in sorted(common_areas): + changes = [] + old_entry = old[area] + new_entry = new[area] + + # Compare maintainers + old_maint = set_or_empty(old_entry, "maintainers") + new_maint = set_or_empty(new_entry, "maintainers") + added_maint = new_maint - old_maint + removed_maint = old_maint - new_maint + if added_maint: + changes.append(f" Maintainers added: {', '.join(sorted(added_maint))}") + if removed_maint: + changes.append(f" Maintainers removed: {', '.join(sorted(removed_maint))}") + + # Compare collaborators + old_collab = set_or_empty(old_entry, "collaborators") + new_collab = set_or_empty(new_entry, "collaborators") + added_collab = new_collab - old_collab + removed_collab = old_collab - new_collab + if added_collab: + changes.append(f" Collaborators added: {', '.join(sorted(added_collab))}") + if removed_collab: + changes.append(f" Collaborators removed: {', '.join(sorted(removed_collab))}") + + # Compare status + old_status = old_entry.get("status") + new_status = new_entry.get("status") + if old_status != new_status: + changes.append(f" Status changed: {old_status} -> {new_status}") + + # Compare labels + old_labels = set_or_empty(old_entry, "labels") + new_labels = set_or_empty(new_entry, "labels") + added_labels = new_labels - old_labels + removed_labels = old_labels - new_labels + if added_labels: + changes.append(f" Labels added: {', '.join(sorted(added_labels))}") + if removed_labels: + changes.append(f" Labels removed: {', '.join(sorted(removed_labels))}") + + # Compare files + old_files = set_or_empty(old_entry, "files") + new_files = set_or_empty(new_entry, "files") + added_files = new_files - old_files + removed_files = old_files - new_files + if added_files: + changes.append(f" Files added: {', '.join(sorted(added_files))}") + if removed_files: + changes.append(f" Files removed: {', '.join(sorted(removed_files))}") + + # Compare files-regex + old_regex = set_or_empty(old_entry, "files-regex") + new_regex = set_or_empty(new_entry, "files-regex") + added_regex = new_regex - old_regex + removed_regex = old_regex - new_regex + if added_regex: + changes.append(f" files-regex added: {', '.join(sorted(added_regex))}") + if removed_regex: + changes.append(f" files-regex removed: {', '.join(sorted(removed_regex))}") + + if changes: + changed_areas.add(area) + print(f"area changed: {area}") + + return added_areas | removed_areas | changed_areas def process_pr(gh, maintainer_file, number): gh_repo = gh.get_repo(f"{args.org}/{args.repo}") @@ -129,6 +236,7 @@ def process_pr(gh, maintainer_file, number): meta_areas = ['Release Notes', 'Documentation', 'Samples', 'Tests'] collab_per_path = [] + additional_reviews = set() for changed_file in fn: num_files += 1 log(f"file: {changed_file.filename}") @@ -144,6 +252,22 @@ def process_pr(gh, maintainer_file, number): area_match = maintainer_file.name2areas(_area) if area_match: areas.extend(area_match) + elif changed_file.filename in ['MAINTAINERS.yml']: + areas = maintainer_file.path2areas(changed_file.filename) + if args.updated_maintainer_file: + log( + "No updated maintainer file, cannot process MAINTAINERS.yml changes, skipping..." + ) + + old_areas = load_areas(args.updated_maintainer_file) + new_areas = load_areas('MAINTAINERS.yml') + changed_areas = compare_areas(old_areas, new_areas) + for _area in changed_areas: + area_match = maintainer_file.name2areas(_area) + if area_match: + # get list of maintainers for changed area + additional_reviews.update(maintainer_file.areas[_area].maintainers) + log(f"MAINTAINERS.yml changed, adding reviewrs: {additional_reviews}") else: areas = maintainer_file.path2areas(changed_file.filename) for _area in areas: @@ -192,6 +316,8 @@ def process_pr(gh, maintainer_file, number): collab += collab_per_path collab = list(dict.fromkeys(collab)) + # add more reviewers based on maintainer file changes. + collab += list(additional_reviews) log(f"collab: {collab}") _all_maintainers = dict( From 2dd643510dcb4678c2d87e6897cc3b9cd069b23a Mon Sep 17 00:00:00 2001 From: Anas Nashif Date: Tue, 4 Nov 2025 20:23:37 -0500 Subject: [PATCH 08/10] fix typo Signed-off-by: Anas Nashif --- .github/workflows/assigner.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/assigner.yml b/.github/workflows/assigner.yml index 1dd1d3fe670..1cadb71c04e 100644 --- a/.github/workflows/assigner.yml +++ b/.github/workflows/assigner.yml @@ -63,7 +63,7 @@ jobs: 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 --updated-mantainer-file pr_MAINTAINERS.yml" + FLAGS+=" -P ${{ github.event.pull_request.number }} --updated-manifest pr_west.yml --updated-maintainer-file pr_MAINTAINERS.yml" elif [ "${{ github.event_name }}" = "issues" ]; then FLAGS+=" -I ${{ github.event.issue.number }}" elif [ "${{ github.event_name }}" = "schedule" ]; then From 9148494cf0c04ad5b578b44c736d575cf709760e Mon Sep 17 00:00:00 2001 From: Anas Nashif Date: Tue, 4 Nov 2025 20:31:36 -0500 Subject: [PATCH 09/10] test file groups Signed-off-by: Anas Nashif --- MAINTAINERS.yml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/MAINTAINERS.yml b/MAINTAINERS.yml index 9b693cdb8d4..a1cfc0cbc1e 100644 --- a/MAINTAINERS.yml +++ b/MAINTAINERS.yml @@ -957,9 +957,22 @@ Continuous Integration: maintainers: - stephanosio - nashif - collaborators: - - fabiobaltieri - - kartben + file-groups: + - name: CI scripts + collaborators: + - kartben + files: + - scripts/ci/ + - name: Gitlint + collaborators: + - fabiobaltieri + files: + - scripts/gitlint/ + - name: Assignee setter + collaborators: + - kartben + files: + - scripts/ci/set_assignees.py files: - .github/ - scripts/requirements-actions.* From 4e1358aeac595fae13d40b492bfb26a8ad609415 Mon Sep 17 00:00:00 2001 From: Anas Nashif Date: Tue, 4 Nov 2025 20:33:40 -0500 Subject: [PATCH 10/10] dummy change Signed-off-by: Anas Nashif --- scripts/ci/set_assignees.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/ci/set_assignees.py b/scripts/ci/set_assignees.py index a0838717230..9d8b8e81a5a 100755 --- a/scripts/ci/set_assignees.py +++ b/scripts/ci/set_assignees.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 + + # Copyright (c) 2022 Intel Corp. # SPDX-License-Identifier: Apache-2.0