diff --git a/.github/workflows/maintainer-list.yml b/.github/workflows/maintainer-list.yml new file mode 100644 index 000000000000..e26bd2925759 --- /dev/null +++ b/.github/workflows/maintainer-list.yml @@ -0,0 +1,39 @@ +name: maintainer-list + +on: + push: + branches: + - master + - staging + - trying + tags: + - '[0-9][0-9][0-9][0-9].[0-9][0-9]-RC[0-9]*' + - '[0-9][0-9][0-9][0-9].[0-9][0-9]' + - '[0-9][0-9][0-9][0-9].[0-9][0-9].*' + pull_request: + branches: + - '*' + merge_group: + +jobs: + maintainer-list: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@main + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: 3.12 + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r dist/tools/maintainer-list/requirements.txt + - name: Update doc/doxygen/src/maintainers.md + env: + GITHUB_TOKEN: ${{ secrets.RIOT_CI_ACCESS_TOKEN }} + run: | + ./dist/tools/maintainer-list/maintainer-list.py + - uses: actions/upload-artifact@v4 + with: + path: doc/doxygen/src/maintainers.md + name: maintainers diff --git a/.github/workflows/static-test.yml b/.github/workflows/static-test.yml index 65b4030df053..f8783fbdc9ca 100644 --- a/.github/workflows/static-test.yml +++ b/.github/workflows/static-test.yml @@ -46,6 +46,7 @@ jobs: docker run --rm \ -e CI_BASE_BRANCH \ -e GITHUB_RUN_ID=${GITHUB_RUN_ID} \ + -e GITHUB_TOKEN=${GITHUB_TOKEN} \ -v $(pwd):/data/riotbuild \ riot/static-test-tools:latest \ make static-test diff --git a/MAINTAINING.md b/MAINTAINING.md index 00ee95522241..8acd614de24e 100644 --- a/MAINTAINING.md +++ b/MAINTAINING.md @@ -181,7 +181,7 @@ In case of security relevant backports (both bug fixes and reverts), the announcement can be skipped and the fix merged once at least two ACKs are there. -[list of maintainers]: https://github.com/RIOT-OS/RIOT/wiki/Maintainers +[list of maintainers]: https://doc.riot-os.org/maintainer-list.html [Best Practices]: https://github.com/RIOT-OS/RIOT/wiki/Best-Practice-for-RIOT-Programming [Comparing build sizes]: https://github.com/RIOT-OS/RIOT/wiki/Comparing-build-sizes [Coding Conventions]: CODING_CONVENTIONS.md diff --git a/dist/tools/maintainer-list/maintainer-list.py b/dist/tools/maintainer-list/maintainer-list.py new file mode 100755 index 000000000000..2e8af47a4186 --- /dev/null +++ b/dist/tools/maintainer-list/maintainer-list.py @@ -0,0 +1,163 @@ +#! /usr/bin/env python3 +# +# Copyright (C) 2024 TU Dresden +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. + +__author__ = "Martine S. Lenders " + +import re +import os +import pathlib +import sys +import urllib.parse + +import requests + + +SCRIPT_PATH = pathlib.Path(__file__).resolve().absolute().parent +RIOTBASE = (SCRIPT_PATH / ".." / ".." / "..").resolve() +TOKEN = os.environ.get("GITHUB_TOKEN") + + +NO_AREA_TEXT = "Has no chosen area of expertise." +MAINTAINER_FILE_BOILERPLATE = f"""# Maintainer List {{#maintainer-list}} + +This file contains the current list of maintainers within the RIOT community. +The file is generated by combining the information from the Maintainers, Owners and +Admin teams from the RIOT-OS GitHub organization and the +[CODEOWNERS](https://github.com/RIOT-OS/RIOT/blob/master/CODEOWNERS) file. + +If a maintainer is marked as "{NO_AREA_TEXT}", they did not have added any ownership +within CODEOWNERS. This does not mean that they do not feel responsible for any part of +the code base, they just did not declare it. + +If you are a maintainer and want to declare ownership for a part of a code base (and +receive notifications on pull requests against it), please add yourself and the path to +the part of the code base you want to be responsible for to CODEOWNERS. +""" + + +def get_team_members(team): + members = requests.get( + f"https://api.github.com/orgs/RIOT-OS/teams/{team}/members", + headers={ + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {TOKEN}", + "X-GitHub-Api-Version": "2022-11-28", + }, + ) + try: + return set(m["login"] for m in members.json()) + except Exception as exc: + print(f"Error fetching team {team}: {exc}", file=sys.stderr) + raise + + +def get_github_user(username): + user = requests.get( + f"https://api.github.com/users/{username}", + headers={ + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {TOKEN}", + "X-GitHub-Api-Version": "2022-11-28", + }, + ) + return user.json() + + +def get_maintainer_codeowner_patterns(maintainers): + maintainer_patterns = {m: [] for m in maintainers} + + with open(RIOTBASE / "CODEOWNERS") as codeowners_file: + for line in codeowners_file: + if re.search(r"^\s*#", line) or re.match(r"^\s*$", line): + # skip comments and empty lines + continue + pattern, *owners = re.split(r"\s+", line.strip()) + for owner in owners: + owner = owner.lstrip("@") + if owner in maintainer_patterns: + maintainer_patterns[owner].append(pattern) + return maintainer_patterns + + +def collect_maintainer_areas(maintainer_codeownership): + maintainer_areas = {m: set() for m in maintainer_codeownership} + for m in maintainer_areas: + if not maintainer_areas[m] and maintainer_codeownership[m]: + for rule in maintainer_codeownership[m]: + maintainer_areas[m].add(rule) + return maintainer_areas + + +def create_maintainer_markdown_file(maintainer_areas, owners, admins): + with open( + RIOTBASE / "doc" / "doxygen" / "src" / "maintainers.md", "w" + ) as maintainers_file: + print(MAINTAINER_FILE_BOILERPLATE, file=maintainers_file) + for i, maintainer in enumerate( + sorted(maintainer_areas, key=lambda x: x.lower()) + ): + github_profile = get_github_user(maintainer) + + if ( + not github_profile["name"] + or github_profile["name"] == github_profile["login"] + ): + title = f"\\@{github_profile['login']}" + else: + title = f"{github_profile['name']} (\\@{github_profile['login']})" + anchor = urllib.parse.quote(github_profile["login"]) + print(f"## {title} {{#{anchor}}}", file=maintainers_file) + print( + f"[GitHub profile]({github_profile['html_url']})", + file=maintainers_file, + ) + + if maintainer in owners: + print( + "- **Is one of the GitHub owners of RIOT.**", file=maintainers_file + ) + + if maintainer in admins: + print( + "- **Is one of the GitHub admins of RIOT.**", file=maintainers_file + ) + + for area in sorted( + maintainer_areas[maintainer], + ): + print( + f"- `{area.lstrip('/')}`", + file=maintainers_file + ) + if not maintainer_areas[maintainer]: + print("", file=maintainers_file) + print(NO_AREA_TEXT, file=maintainers_file) + if (i + 1) < len(maintainer_areas): + print("", file=maintainers_file) + + +def main(): + if not TOKEN: + print( + "Please provide a sufficient GitHub token in `GITHUB_TOKEN` " + "environment variable", + file=sys.stderr, + ) + sys.exit(1) + maintainers = get_team_members("maintainers") + admins = get_team_members("admin") + owners = get_team_members("owners") + maintainers = maintainers.union(admins) + maintainers = maintainers.union(owners) + maintainer_codeownership = get_maintainer_codeowner_patterns(maintainers) + maintainer_areas = collect_maintainer_areas(maintainer_codeownership) + create_maintainer_markdown_file(maintainer_areas, owners, admins) + + +if __name__ == "__main__": + main() diff --git a/dist/tools/maintainer-list/requirements.txt b/dist/tools/maintainer-list/requirements.txt new file mode 100644 index 000000000000..d80d9fc2a3a2 --- /dev/null +++ b/dist/tools/maintainer-list/requirements.txt @@ -0,0 +1 @@ +requests==2.32.3 diff --git a/doc/doxygen/.gitignore b/doc/doxygen/.gitignore index 53e838267f92..28c45f3ae4f3 100644 --- a/doc/doxygen/.gitignore +++ b/doc/doxygen/.gitignore @@ -1,2 +1,3 @@ src/css/variables.less src/changelog.md +src/maintainers.md diff --git a/doc/doxygen/Makefile b/doc/doxygen/Makefile index d3a9e4d453cb..e5d47bffc824 100644 --- a/doc/doxygen/Makefile +++ b/doc/doxygen/Makefile @@ -16,17 +16,17 @@ doc: $(DOCUMENTATION_FORMAT) # by marking html as phony we force make to re-run Doxygen even if the directory exists. .PHONY: html -html: src/changelog.md +html: src/changelog.md src/maintainers.md ( cat riot.doxyfile ; echo "GENERATE_HTML = yes" ) | doxygen - @echo "" @echo "RIOT documentation successfully generated at file://$(RIOTBASE)/doc/doxygen/html/index.html" .PHONY: check -check: src/changelog.md +check: src/changelog.md src/maintainers.md ( cat riot.doxyfile) | doxygen - .PHONY: man -man: src/changelog.md +man: src/changelog.md src/maintainers.md ( cat riot.doxyfile ; echo "GENERATE_MAN = yes" ) | doxygen - src/css/riot.css: src/css/riot.less src/css/variables.less @@ -39,8 +39,12 @@ src/css/variables.less: src/config.json src/changelog.md: src/changelog.md.tmp ../../release-notes.txt @./generate-changelog.py $+ $@ +src/maintainers.md: ../../dist/tools/maintainer-list/maintainer-list.py ../../dist/tools/maintainer-list/requirements.txt ../../CODEOWNERS + -@pip install --upgrade -r ../../dist/tools/maintainer-list/requirements.txt + -@../../dist/tools/maintainer-list/maintainer-list.py + .PHONY: -latex: src/changelog.md +latex: src/changelog.md src/maintainers.md ( cat riot.doxyfile ; echo "GENERATE_LATEX= yes" ) | doxygen - clean: diff --git a/doc/doxygen/riot.doxyfile b/doc/doxygen/riot.doxyfile index 652318ef6fed..b85aeb2cd535 100644 --- a/doc/doxygen/riot.doxyfile +++ b/doc/doxygen/riot.doxyfile @@ -888,6 +888,7 @@ INPUT = ../../doc.txt \ src/release-cycle.md \ src/io-mapping-and-shields.md \ src/changelog.md \ + src/maintainers.md \ ../../LOSTANDFOUND.md \ ../../makefiles/pseudomodules.inc.mk \ ../../makefiles/blob.inc.mk \