diff --git a/.github/workflows/assigner.yml b/.github/workflows/assigner.yml index f5bb122ceb4b2..1dd1d3fe670f8 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: > @@ -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-mantainer-file pr_MAINTAINERS.yml" elif [ "${{ github.event_name }}" = "issues" ]; then FLAGS+=" -I ${{ github.event.issue.number }}" elif [ "${{ github.event_name }}" = "schedule" ]; then @@ -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 \ + --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 a44efeb1272ad..0000000000000 --- 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 diff --git a/.ruff-excludes.toml b/.ruff-excludes.toml index 384e588276bfe..28d8a07be3255 100644 --- a/.ruff-excludes.toml +++ b/.ruff-excludes.toml @@ -1397,7 +1397,7 @@ exclude = [ "./scripts/release/bug_bash.py", "./scripts/release/list_backports.py", "./scripts/release/list_devicetree_bindings_changes.py", - "./scripts/set_assignees.py", + "./scripts/ci/set_assignees.py", "./scripts/snippets.py", "./scripts/tests/twister/conftest.py", "./scripts/tests/twister/pytest_integration/test_harness_pytest.py", diff --git a/MAINTAINERS.yml b/MAINTAINERS.yml index a3272c770cc83..86ac8fb517155 100644 --- a/MAINTAINERS.yml +++ b/MAINTAINERS.yml @@ -947,7 +947,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" @@ -3259,7 +3259,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 75% rename from scripts/set_assignees.py rename to scripts/ci/set_assignees.py index 1cf2a29867ac0..350722d01a001 100755 --- a/scripts/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}") @@ -128,6 +235,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'] + additional_reviews = set() for changed_file in fn: num_files += 1 log(f"file: {changed_file.filename}") @@ -142,6 +250,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) @@ -183,6 +307,8 @@ def process_pr(gh, maintainer_file, number): collab += maintainer_file.areas[area.name].maintainers collab += maintainer_file.areas[area.name].collaborators collab = list(dict.fromkeys(collab)) + # add more reviewers based on maintainer file changes. + collab += list(additional_reviews) log(f"collab: {collab}") _all_maintainers = dict( diff --git a/scripts/ci/twister_ignore.txt b/scripts/ci/twister_ignore.txt index 9e5b417df538e..ee086b61f7825 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