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
16 changes: 13 additions & 3 deletions .github/workflows/assigner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: >
Expand All @@ -62,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-maintainer-file pr_MAINTAINERS.yml"
elif [ "${{ github.event_name }}" = "issues" ]; then
FLAGS+=" -I ${{ github.event.issue.number }}"
elif [ "${{ github.event_name }}" = "schedule" ]; then
Expand All @@ -71,4 +72,13 @@ 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: >
github.event_name == 'pull_request_target'
env:
GITHUB_TOKEN: ${{ secrets.ZB_PR_ASSIGNER_GITHUB_TOKEN }}
run: |
python ./scripts/ci/check_maintainer_changes.py \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
python ./scripts/ci/check_maintainer_changes.py \
python ./scripts/ci/check_maintainer_changes.py \

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bummer, how did I miss that, release blocker :-D

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have added it but you asked to only do it on issues...

--repo zephyrproject-rtos/zephyr MAINTAINERS.yml pr_MAINTAINERS.yml
43 changes: 0 additions & 43 deletions .github/workflows/maintainer_check.yml

This file was deleted.

1 change: 1 addition & 0 deletions .ruff-excludes.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
32 changes: 30 additions & 2 deletions MAINTAINERS.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,34 @@
# 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.
#
# File groups inherit file patterns from their parent area. A file will only
# match a file group if it first matches the parent area's patterns, and then
# also matches the file group's own patterns. This allows file groups to
# further filter and subdivide files that are already covered by the area.
Comment on lines +69 to +71
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation: tabs are used instead of spaces in the documentation comment. Lines 69-71 should use consistent spacing to match the rest of the file.

Suggested change
# match a file group if it first matches the parent area's patterns, and then
# also matches the file group's own patterns. This allows file groups to
# further filter and subdivide files that are already covered by the area.
# match a file group if it first matches the parent area's patterns, and then
# also matches the file group's own patterns. This allows file groups to
# further filter and subdivide files that are already covered by the area.

Copilot uses AI. Check for mistakes.
#
# Each group should have the following structure:
# - name: <group name>
# collaborators:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these collaborators are added in addition to the area collaborators, right? Could maybe be mentioned.

# - <GitHub handle>
# - <GitHub handle>
# files:
# - <file path>
# - <file path>
# files-regex:
# - <regex pattern>
# - <regex pattern>
# files-exclude:
# - <file path>
# - <file path>
# files-regex-exclude:
# - <regex pattern>
# - <regex pattern>
#
# description: >-
# Plain-English description. Describe what the system is about, from an
# outsider's perspective.
Expand Down Expand Up @@ -948,7 +976,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"

Expand Down Expand Up @@ -3264,7 +3292,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"
Expand Down
141 changes: 138 additions & 3 deletions scripts/set_assignees.py → scripts/ci/set_assignees.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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}")
Expand All @@ -128,6 +235,8 @@ 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 = set()
additional_reviews = set()
for changed_file in fn:
num_files += 1
log(f"file: {changed_file.filename}")
Expand All @@ -139,13 +248,32 @@ 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.update(_area.get_collaborators_for_path(changed_file.filename))
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(
"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:
collab_per_path.update(_area.get_collaborators_for_path(changed_file.filename))

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

if not areas:
continue
Expand Down Expand Up @@ -173,6 +301,9 @@ def process_pr(gh, maintainer_file, number):
if 'Platform' in area.name:
is_instance = True

for _area in sorted_areas:
collab_per_path.update(_area.get_collaborators_for_path(changed_file.filename))

area_counter = dict(sorted(area_counter.items(), key=lambda item: item[1], reverse=True))
log(f"Area matches: {area_counter}")
log(f"labels: {labels}")
Expand All @@ -182,7 +313,11 @@ 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))
# add more reviewers based on maintainer file changes.
collab += list(additional_reviews)
log(f"collab: {collab}")

_all_maintainers = dict(
Expand Down
2 changes: 1 addition & 1 deletion scripts/ci/twister_ignore.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Add one pattern per line.

Check warning on line 1 in scripts/ci/twister_ignore.txt

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

Copyright missing

scripts/ci/twister_ignore.txt:1 File has no SPDX-FileCopyrightText header, consider adding one.

Check warning on line 1 in scripts/ci/twister_ignore.txt

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

License missing

scripts/ci/twister_ignore.txt:1 File has no SPDX-License-Identifier header, consider adding one.
#
# The patterns listed in this file will be compared with the list of files
# changed in a patch series (Pull Request) and if all files in the pull request
Expand Down Expand Up @@ -52,7 +52,7 @@
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
Expand Down
Loading
Loading