ci: add member check
Verify that collaborators/maintainers added to the MAINTAINERS.yml file actually have access to the project and are members. Only those who already gained access following the process shall be added to the file. Signed-off-by: Anas Nashif <anas.nashif@intel.com>
This commit is contained in:
parent
5707ad142d
commit
43971ea0d5
3 changed files with 265 additions and 0 deletions
43
.github/workflows/maintainer_check.yml
vendored
Normal file
43
.github/workflows/maintainer_check.yml
vendored
Normal file
|
@ -0,0 +1,43 @@
|
|||
name: Maintainer file check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
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@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 mainline_MAINTAINERS.yml MAINTAINERS.yml
|
220
scripts/ci/check_maintainer_changes.py
Normal file
220
scripts/ci/check_maintainer_changes.py
Normal file
|
@ -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 access level 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 level 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()
|
|
@ -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…
Add table
Add a link
Reference in a new issue