Skip to content

check maintainer file changes #359

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
45 changes: 45 additions & 0 deletions .github/workflows/maintainer_check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Maintainer file check

on:
pull_request:
branches:
- main
- collab-*
- v*-branch
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- 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: Install Python packages
run: |
pip install -r scripts/requirements-actions.txt --require-hashes

- name: Fetch MAINTAINERS.yml from mainline
run: |
git fetch origin main
git show origin/main:MAINTAINERS.yml > mainline_MAINTAINERS.yml

- name: Check maintainer file changes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
python ./scripts/ci/check_maintainer_changes.py \
--repo zephyrproject-rtos/zephyr-testing mainline_MAINTAINERS.yml MAINTAINERS.yml
1 change: 1 addition & 0 deletions MAINTAINERS.yml
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@
- con-pax
- vaishnavachath
- glneo
- foobar

Check failure on line 332 in MAINTAINERS.yml

View workflow job for this annotation

GitHub Actions / Check MAINTAINERS.yml changes

User lacks access

foobar does not have needed to zephyrproject-rtos/zephyr-testing
files:
- boards/beagle/
labels:
Expand Down
220 changes: 220 additions & 0 deletions scripts/ci/check_maintainer_changes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
#!/usr/bin/env python3

# SPDX-License-Identifier: Apache-2.0
# Copyright The Zephyr Project Contributors

import argparse
import os
import sys

import yaml
from github import Github


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 set_or_empty(d, key):
return set(d.get(key, []) or [])


def check_github_access(usernames, repo_fullname, token):
"""Check if each username has at least Triage access to the repo."""
gh = Github(token)
repo = gh.get_repo(repo_fullname)
missing_access = set()
for username in usernames:
try:
collab = repo.get_collaborator_permission(username)
# Permissions: admin, maintain, write, triage, read
if collab not in ("admin", "maintain", "write", "triage"):
missing_access.add(username)
except Exception:
missing_access.add(username)
return missing_access


def compare_areas(old, new, repo_fullname=None, token=None):
old_areas = set(old.keys())
new_areas = set(new.keys())

added_areas = new_areas - old_areas
removed_areas = old_areas - new_areas
common_areas = old_areas & new_areas

all_added_maintainers = set()
all_added_collaborators = set()

print("=== Areas Added ===")
for area in sorted(added_areas):
print(f"+ {area}")
entry = new[area]
all_added_maintainers.update(set_or_empty(entry, "maintainers"))
all_added_collaborators.update(set_or_empty(entry, "collaborators"))

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))}")
all_added_maintainers.update(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))}")
all_added_collaborators.update(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:
print(f"* {area}")
for c in changes:
print(c)

print("\n=== Summary ===")
print(f"Total areas added: {len(added_areas)}")
print(f"Total maintainers added: {len(all_added_maintainers)}")
if all_added_maintainers:
print(" Added maintainers: " + ", ".join(sorted(all_added_maintainers)))
print(f"Total collaborators added: {len(all_added_collaborators)}")
if all_added_collaborators:
print(" Added collaborators: " + ", ".join(sorted(all_added_collaborators)))

# Check GitHub access if repo and token are provided

print("\n=== GitHub Access Check ===")
missing_maint = check_github_access(all_added_maintainers, repo_fullname, token)
missing_collab = check_github_access(all_added_collaborators, repo_fullname, token)
if missing_maint:
print("Maintainers without at least triage access:")
for u in sorted(missing_maint):
print(f" - {u}")
if missing_collab:
print("Collaborators without at least triage access:")
for u in sorted(missing_collab):
print(f" - {u}")
if not missing_maint and not missing_collab:
print("All added maintainers and collaborators have required access.")
else:
print("Some added maintainers or collaborators do not have sufficient access.")

# --- GitHub Actions inline annotation ---
# Try to find the line number in the new file for each missing user
def find_line_for_user(yaml_file, user_set):
"""Return a dict of user -> line number in yaml_file for missing users."""
user_lines = {}
try:
with open(yaml_file) as f:
lines = f.readlines()
for idx, line in enumerate(lines, 1):
for user in user_set:
if user in line:
user_lines[user] = idx
return user_lines
except Exception:
return {}

all_missing_users = missing_maint | missing_collab
user_lines = find_line_for_user(args.new, all_missing_users)

for user, line in user_lines.items():
print(
f"::error file={args.new},line={line},title=User lacks access::"
f"{user} does not have needed to {repo_fullname}"
)

# For any missing users not found in the file, print a general error
for user in sorted(all_missing_users - set(user_lines)):
print(
f"::error title=User lacks access::{user} does not have needed "
f"access to {repo_fullname}"
)

sys.exit(1)


def main():
parser = argparse.ArgumentParser(
description="Compare two MAINTAINERS.yml files and show changes in areas, "
"maintainers, collaborators, etc.",
allow_abbrev=False,
)
parser.add_argument("old", help="Old MAINTAINERS.yml file")
parser.add_argument("new", help="New MAINTAINERS.yml file")
parser.add_argument("--repo", help="GitHub repository in org/repo format for access check")
parser.add_argument("--token", help="GitHub token for API access (required for access check)")
global args
args = parser.parse_args()

old_areas = load_areas(args.old)
new_areas = load_areas(args.new)
token = os.environ.get("GITHUB_TOKEN") or args.token

if not token or not args.repo:
print("GitHub token and repository are required for access check.")
sys.exit(1)

compare_areas(old_areas, new_areas, repo_fullname=args.repo, token=token)


if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions scripts/ci/twister_ignore.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,5 @@ scripts/make_bugs_pickle.py
scripts/set_assignees.py
scripts/gitlint/zephyr_commit_rules.py
scripts/west_commands/runners/canopen_program.py
scripts/ci/check_maintainer_changes.py
.github/workflows/maintainer_check.yml
Loading