diff --git a/.github/workflows/maintainers.yaml b/.github/workflows/maintainers.yaml new file mode 100644 index 0000000000..857be0b416 --- /dev/null +++ b/.github/workflows/maintainers.yaml @@ -0,0 +1,28 @@ +name: Ping maintainers + +on: + pull_request: + +jobs: + maintainer_ping: + runs-on: ubuntu-latest + + container: qmkfm/qmk_cli + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - uses: trilom/file-changes-action@v1.2.4 + id: file_changes + with: + output: " " + fileOutput: " " + + - name: Install dependencies + run: pip3 install -r requirements-dev.txt + + - name: Ping maintainers and request reviews + shell: "bash {0}" + run: qmk ping-maintainers --pr ${{ github.event.number }} ${{ steps.file_changes.outputs.files }} diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000..2518b6475a --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +* @qmk/collaborators +/lib/python/* @qmk/python diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index f45e33240c..49f32b2965 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py @@ -60,10 +60,12 @@ subcommands = [ 'qmk.cli.lint', 'qmk.cli.list.keyboards', 'qmk.cli.list.keymaps', + 'qmk.cli.list.maintainers', 'qmk.cli.kle2json', 'qmk.cli.multibuild', 'qmk.cli.new.keyboard', 'qmk.cli.new.keymap', + 'qmk.cli.ping.maintainers', 'qmk.cli.pyformat', 'qmk.cli.pytest', ] diff --git a/lib/python/qmk/cli/list/maintainers.py b/lib/python/qmk/cli/list/maintainers.py new file mode 100644 index 0000000000..a9186451b3 --- /dev/null +++ b/lib/python/qmk/cli/list/maintainers.py @@ -0,0 +1,16 @@ +"""List the keymaps for a specific keyboard +""" +from pathlib import Path + +from milc import cli + +from qmk.maintainers import maintainers + + +@cli.argument("files", type=Path, arg_only=True, nargs='*', help="File to check") +@cli.subcommand("List the maintainers for a file.") +def list_maintainers(cli): + """List the maintainers for a file. + """ + for file in cli.args.files: + cli.echo('%s: %s', file, ', '.join(maintainers(file))) diff --git a/lib/python/qmk/cli/ping/maintainers.py b/lib/python/qmk/cli/ping/maintainers.py new file mode 100644 index 0000000000..c8ce636c1a --- /dev/null +++ b/lib/python/qmk/cli/ping/maintainers.py @@ -0,0 +1,51 @@ +"""Generate a message to ping people responsible for one or more files. +""" +from pathlib import Path + +from milc import cli + +from qmk.maintainers import maintainers + + +@cli.argument('--pr', type=int, arg_only=True, help="PR to send ping to (optional)") +@cli.argument('--owner', default='qmk', arg_only=True, help="Owner for the repo (Default: qmk)") +@cli.argument('--repo', default='qmk_firmware', arg_only=True, help="Repo to send pings to (Default: qmk_firmware)") +@cli.argument("files", type=Path, arg_only=True, nargs='*', help="File to ping maintainers for.") +@cli.subcommand("Ping the maintainers and request reviews for one or more files.") +def ping_maintainers(cli): + """Ping the maintainers for one or more files. + """ + github_maintainers = set() + github_teams = set() + + for file in cli.args.files: + for maintainer in maintainers(file): + if '/' in maintainer: + github_teams.add(maintainer) + else: + github_maintainers.add(maintainer) + + if cli.args.pr: + from ghapi.all import GhApi + + ghapi = GhApi(owner=cli.args.owner, repo=cli.args.repo) + pr = ghapi.pulls.get(cli.args.pr) + + if not pr.draft: + for team in pr.requested_teams: + team_name = f'@{cli.args.owner}/{team.slug}' + + if team_name in github_teams: + cli.log.info('Found %s in reviews already, skipping', team_name) + github_teams.remove(team_name) + + for team in github_teams: + cli.log.info('Requesting review from team %s', team.split('/', 1)[1]) + ghapi.pulls.request_reviewers(pull_number=cli.args.pr, team_reviewers=team.split('/', 1)[1]) + + if github_maintainers: + ghapi.issues.create_comment(cli.args.pr, f'If you were pinged by this comment you have one or more files being changed by this PR: {" ".join(sorted(github_maintainers))}') + + else: + print(f'Team Reviews: {" ".join(sorted(github_teams))}') + print(f'Individual Reviews: {" ".join(sorted(github_maintainers))}') diff --git a/lib/python/qmk/maintainers.py b/lib/python/qmk/maintainers.py new file mode 100644 index 0000000000..c851c34fb9 --- /dev/null +++ b/lib/python/qmk/maintainers.py @@ -0,0 +1,27 @@ +from pathlib import Path + +from qmk.json_schema import json_load + + +def maintainers(file): + """Yields maintainers for a file. + """ + from codeowners import CodeOwners + + codeowners_file = Path('CODEOWNERS') + codeowners = CodeOwners(codeowners_file.read_text()) + maintainers = [owner[1] for owner in codeowners.of(str(file))] + file_dir = file if file.is_dir() else file.parent + + cur_path = Path('.') + + for path_part in file_dir.parts: + cur_path = cur_path / path_part + info_file = cur_path / 'info.json' + if info_file.exists(): + new_info_data = json_load(info_file) + maintainers = new_info_data.get('maintainer').replace(',', ' ').split() + maintainers = ['@' + maintainer for maintainer in maintainers] + + for maintainer in maintainers: + yield '@qmk/collaborators' if maintainer == 'qmk' else maintainer diff --git a/requirements-dev.txt b/requirements-dev.txt index 1db3b6d733..a5d59a3d54 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,7 +2,9 @@ -r requirements.txt # Python development requirements -nose2 +codeowners flake8 +ghapi +nose2 pep8-naming yapf