diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 00000000..39bc025e --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,26 @@ +name: Backport + +on: + pull_request: + types: [opened, reopened, edited] + +env: + GH_AUTH: ${{ secrets.GH_AUTH }} + +jobs: + backport: + name: Check and update backport label of PR + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.x" + cache: pip + cache-dependency-path: "*requirements.txt" + - name: Install Dependencies + run: python3 -m pip install coverage -U pip -r dev-requirements.txt + - name: Run code + run: python3 -m bedevere.backport diff --git a/.github/workflows/close_pr.yml b/.github/workflows/close_pr.yml new file mode 100644 index 00000000..c6f76e7d --- /dev/null +++ b/.github/workflows/close_pr.yml @@ -0,0 +1,26 @@ +name: Close PR + +on: + pull_request: + types: [opened, synchronize] + +env: + GH_AUTH: ${{ secrets.GH_AUTH }} + +jobs: + close_pr: + name: close invalid PR + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.x" + cache: pip + cache-dependency-path: "*requirements.txt" + - name: Install Dependencies + run: python3 -m pip install coverage -U pip -r dev-requirements.txt + - name: Run close_pr.py file + run: python3 -m bedevere.close_pr diff --git a/.github/workflows/filepaths.yml b/.github/workflows/filepaths.yml new file mode 100644 index 00000000..497c3a72 --- /dev/null +++ b/.github/workflows/filepaths.yml @@ -0,0 +1,26 @@ +name: Filepaths + +on: + pull_request: + types: [opened, reopened, edited] + +env: + GH_AUTH: ${{ secrets.GH_AUTH }} + +jobs: + filepaths: + name: Checks filepaths on a PR + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.x" + cache: pip + cache-dependency-path: "*requirements.txt" + - name: Install Dependencies + run: python3 -m pip install coverage -U pip -r dev-requirements.txt + - name: Run filepaths.py file + run: python3 -m bedevere.filepaths diff --git a/.github/workflows/review_pr.yml b/.github/workflows/review_pr.yml new file mode 100644 index 00000000..276723ef --- /dev/null +++ b/.github/workflows/review_pr.yml @@ -0,0 +1,26 @@ +name: Review PR + +on: + pull_request: + types: [review_requested] + +env: + GH_AUTH: ${{ secrets.GH_AUTH }} + +jobs: + review_invalid_pr: + name: Dismiss review request from the invalid PR + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.x" + cache: pip + cache-dependency-path: "*requirements.txt" + - name: Install Dependencies + run: python3 -m pip install coverage -U pip -r dev-requirements.txt + - name: Run review_pr.py file + run: python3 -m bedevere.review_pr diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 191a5e1c..84aa8989 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,83 +1,17 @@ Contributing and Maintenance Guide ================================== -Bedevere web service is deployed to Heroku, which is managed by The PSF. +Bedevere web service is launched with GitHub Actions, which is managed by The PSF. -Deployment ----------- +All actions are listed in the `Actions`_ tab of this repository. -There are two ways to have bedevere deployed: automatic deployment, and -manual deployment. -Automatic Deployment (currently broken) -''''''''''''''''''''''''''''''''''''''' - -When the automatic deployment is enabled (on Heroku side), any merged PR -will automatically be deployed to Heroku. This process takes less than 5 minutes. - -If after 10 minutes you did not see the changes reflected, please ping one -of the collaborators listed below. - -To enable Automatic deployment: - -- On the Heroku dashboard for bedevere, choose the "Deploy" tab. -- Scroll down to the "Automatic deploys" section -- Enter the name of the branch to be deployed (in this case: ``main``) -- Check the "Wait for CI to pass before deploy" button -- Press the "Enable automatic deploys" button. - -Once done, merging a PR against the ``main`` branch will trigger a new -deployment using a webhook that is already set up in the repo settings. - - -.. note:: - - Due to recent `security incident`_, the Heroku GitHub integration is broken. - Automatic deployment does not currently work. Until this gets resolved, - maintainers have to deploy bedevere to Heroku manually. - - -Manual Deployment -''''''''''''''''' - -The app can be deployed manually to Heroku by collaborators and members of the ``bedevere`` app on Heroku. -Heroku admins can do it too. - -#. Install Heroku CLI - - Details at: https://devcenter.heroku.com/articles/heroku-cli - -#. Login to Heroku CLI on the command line and follow instructions:: - - heroku login - - -#. If you haven't already, get a clone of the bedevere repo:: - - git clone git@github.com:python/bedevere.git - - Or, using `GitHub CLI`_:: - - gh repo clone python/bedevere - -#. From the ``bedevere`` directory, add the ``bedevere`` Heroku app as remote branch:: - - heroku git:remote -a bedevere - - -#. From the ``bedevere`` directory, push to Heroku:: - - git push heroku main - - -After a successful push, the deployment will begin. - -Heroku app collaborators and members -'''''''''''''''''''''''''''''''''''' +App collaborators and members +''''''''''''''''''''''''''''' - @Mariatta - @ambv - @brettcannon +- @sabderemane -.. _security incident: https://status.heroku.com/incidents/2413 -.. _GitHub CLI: https://cli.github.com/ +.. _Actions: https://github.com/python/bedevere/actions diff --git a/README.md b/README.md index 8e4b24ed..2aea3778 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ This bot is meant to help identify issues with a CPython pull request. - ### Identifies missing GitHub issue numbers in the title If no GitHub issue number is found the status fails and the "Details" link points to the relevant -[section of the devguide](https://devguide.python.org/pullrequest/#submitting). -- ### Links to bugs.python.org +[section of the devguide](https://devguide.python.org/getting-started/pull-request-lifecycle/#submitting). +- ### Links to github.com/python/cpython/issues If an issue number is found then the "Details" link points to the relevant issue itself, making it easier to navigate from PR to issue. - ### Identifies missing news entry diff --git a/bedevere/backport.py b/bedevere/backport.py index ed248839..5482be38 100644 --- a/bedevere/backport.py +++ b/bedevere/backport.py @@ -1,16 +1,18 @@ -"""Automatically remove a backport label, and check backport PR validity.""" +import asyncio import functools +import json +import os import re +import traceback -import gidgethub.routing +import aiohttp +from gidgethub.aiohttp import GitHubAPI from . import util create_status = functools.partial(util.create_status, 'bedevere/maintenance-branch-pr') -router = gidgethub.routing.Router() - TITLE_RE = re.compile(r'\s*\[(?P\d+\.\d+)\].+\((?:GH-|#)(?P\d+)\)', re.IGNORECASE) MAINTENANCE_BRANCH_TITLE_RE = re.compile(r'\s*\[(?P\d+\.\d+)\].+') MAINTENANCE_BRANCH_RE = re.compile(r'\s*(?P\d+\.\d+)') @@ -18,9 +20,13 @@ MESSAGE_TEMPLATE = ('[GH-{pr}](https://github.com/python/cpython/pull/{pr}) is ' 'a backport of this pull request to the ' '[{branch} branch](https://github.com/python/cpython/tree/{branch}).') +BACKPORT_TITLE_DEVGUIDE_URL = "https://devguide.python.org/core-developers/committing/#backport-pr-title" + +async def issue_for_PR(gh, pull_request): + """Get the issue data for a pull request.""" + return await gh.getitem(pull_request["issue_url"]) -BACKPORT_TITLE_DEVGUIDE_URL = "https://devguide.python.org/committing/#backport-pr-title" async def _copy_over_labels(gh, original_issue, backport_issue): """Copy over relevant labels from the original PR to the backport PR.""" @@ -44,12 +50,12 @@ async def _remove_backport_label(gh, original_issue, branch, backport_pr_number) await gh.post(original_issue['comments_url'], data={'body': message}) -@router.register("pull_request", action="opened") -@router.register("pull_request", action="edited") -async def manage_labels(event, gh, *args, **kwargs): - if event.data["action"] == "edited" and "title" not in event.data["changes"]: +async def manage_labels(gh, *args, **kwargs): + with open(os.environ["GITHUB_EVENT_PATH"]) as f: + event = json.load(f) + if event.get("action") == "edited" and "title" not in event.get("changes"): return - pull_request = event.data["pull_request"] + pull_request = event["pull_request"] title = util.normalize_title(pull_request['title'], pull_request['body']) title_match = TITLE_RE.match(title) @@ -58,12 +64,12 @@ async def manage_labels(event, gh, *args, **kwargs): branch = title_match.group('branch') original_pr_number = title_match.group('pr') - original_issue = await gh.getitem(event.data['repository']['issues_url'], + original_issue = await gh.getitem(event['repository']['issues_url'], {'number': original_pr_number}) await _remove_backport_label(gh, original_issue, branch, - event.data["number"]) + event["number"]) - backport_issue = await util.issue_for_PR(gh, pull_request) + backport_issue = await issue_for_PR(gh, pull_request) await _copy_over_labels(gh, original_issue, backport_issue) @@ -82,11 +88,7 @@ def is_maintenance_branch(ref): return bool(re.fullmatch(maintenance_branch_pattern, ref)) -@router.register("pull_request", action="opened") -@router.register("pull_request", action="reopened") -@router.register("pull_request", action="edited") -@router.register("pull_request", action="synchronize") -async def validate_maintenance_branch_pr(event, gh, *args, **kwargs): +async def validate_maintenance_branch_pr(gh, *args, **kwargs): """Check the PR title for maintenance branch pull requests. If the PR was made against maintenance branch, and the title does not @@ -94,9 +96,11 @@ async def validate_maintenance_branch_pr(event, gh, *args, **kwargs): The maintenance branch PR has to start with `[X.Y]` """ - if event.data["action"] == "edited" and "title" not in event.data["changes"]: + with open(os.environ["GITHUB_EVENT_PATH"]) as f: + event = json.load(f) + if event.get("action") == "edited" and "title" not in event.get("changes"): return - pull_request = event.data["pull_request"] + pull_request = event["pull_request"] base_branch = pull_request["base"]["ref"] if not is_maintenance_branch(base_branch): @@ -116,8 +120,7 @@ async def validate_maintenance_branch_pr(event, gh, *args, **kwargs): await util.post_status(gh, event, status) -@router.register("create", ref_type="branch") -async def maintenance_branch_created(event, gh, *args, **kwargs): +async def maintenance_branch_created(gh, *args, **kwargs): """Create the `needs backport label` when the maintenance branch is created. Also post a reminder to add the maintenance branch to the list of @@ -128,7 +131,9 @@ async def maintenance_branch_created(event, gh, *args, **kwargs): The maintenance branch PR has to start with `[X.Y]` """ - branch_name = event.data["ref"] + with open(os.environ["GITHUB_EVENT_PATH"]) as f: + event = json.load(f) + branch_name = event["pull_request"]["head"]["ref"] if MAINTENANCE_BRANCH_RE.match(branch_name): await gh.post( @@ -147,3 +152,18 @@ async def maintenance_branch_created(event, gh, *args, **kwargs): ), }, ) + + +async def main(): + try: + async with aiohttp.ClientSession() as session: + gh = GitHubAPI(session, "sabderemane", oauth_token=os.getenv("GH_AUTH")) + await maintenance_branch_created(gh) + await validate_maintenance_branch_pr(gh) + await manage_labels(gh) + except Exception: + traceback.print_exc() + + +loop = asyncio.get_event_loop() +loop.run_until_complete(main()) diff --git a/bedevere/close_pr.py b/bedevere/close_pr.py index b09adc58..5c178e6b 100644 --- a/bedevere/close_pr.py +++ b/bedevere/close_pr.py @@ -1,7 +1,12 @@ """Automatically close PR that tries to merge maintenance branch into main.""" +import asyncio +import json +import os import re +import traceback -import gidgethub.routing +import aiohttp +from gidgethub.aiohttp import GitHubAPI PYTHON_MAINT_BRANCH_RE = re.compile(r'^\w+:\d+\.\d+$') @@ -9,53 +14,43 @@ INVALID_PR_COMMENT = """\ PRs attempting to merge a maintenance branch into the \ main branch are deemed to be spam and automatically closed. \ -If you were attempting to report a bug, please go to bugs.python.org; \ +If you were attempting to report a bug, please go to \ +https://github.com/python/cpython/issues; \ see devguide.python.org for further instruction as needed.""" - -router = gidgethub.routing.Router() - -@router.register("pull_request", action="opened") -@router.register("pull_request", action="synchronize") -async def close_invalid_pr(event, gh, *args, **kwargs): +async def close_invalid_pr(gh, *args, **kwargs): """Close the invalid PR, add 'invalid' label, and post a message. PR is considered invalid if: * base_label is 'python:main' * head_label is ':' """ - head_label = event.data["pull_request"]["head"]["label"] - base_label = event.data["pull_request"]["base"]["label"] + with open(os.environ["GITHUB_EVENT_PATH"]) as f: + event = json.load(f) + head_label = event["pull_request"]["head"]["label"] + base_label = event["pull_request"]["base"]["label"] if PYTHON_MAINT_BRANCH_RE.match(head_label) and \ base_label == "python:main": data = {'state': 'closed'} - await gh.patch(event.data["pull_request"]["url"], data=data) + await gh.patch(event["pull_request"]["url"], data=data) await gh.post( - f'{event.data["pull_request"]["issue_url"]}/labels', + f'{event["pull_request"]["issue_url"]}/labels', data=["invalid"] ) await gh.post( - f'{event.data["pull_request"]["issue_url"]}/comments', + f'{event["pull_request"]["issue_url"]}/comments', data={'body': INVALID_PR_COMMENT} ) +async def main(): + try: + async with aiohttp.ClientSession() as session: + gh = GitHubAPI(session, "sabderemane", oauth_token=os.getenv("GH_AUTH")) + await close_invalid_pr(gh) + except Exception: + traceback.print_exc() -@router.register("pull_request", action="review_requested") -async def dismiss_invalid_pr_review_request(event, gh, *args, **kwargs): - """Dismiss review request from the invalid PR. - - PR is considered invalid if: - * base_label is 'python:main' - * head_label is ':' - """ - head_label = event.data["pull_request"]["head"]["label"] - base_label = event.data["pull_request"]["base"]["label"] - if PYTHON_MAINT_BRANCH_RE.match(head_label) and \ - base_label == "python:main": - data = {"reviewers": [reviewer["login"] for reviewer in event.data["pull_request"]["requested_reviewers"]], - "team_reviewers": [team["name"] for team in event.data["pull_request"]["requested_teams"]] - } - await gh.delete(f'{event.data["pull_request"]["url"]}/requested_reviewers', - data=data) +loop = asyncio.get_event_loop() +loop.run_until_complete(main()) diff --git a/bedevere/filepaths.py b/bedevere/filepaths.py index 37936fdf..a0f02e1c 100644 --- a/bedevere/filepaths.py +++ b/bedevere/filepaths.py @@ -1,24 +1,39 @@ """Checks related to filepaths on a pull request.""" -import gidgethub.routing +import asyncio +import json +import os +import traceback + +import aiohttp +from gidgethub.aiohttp import GitHubAPI from . import news from . import prtype from . import util -router = gidgethub.routing.Router() - - -@router.register('pull_request', action='opened') -@router.register('pull_request', action='synchronize') -@router.register('pull_request', action='reopened') -async def check_file_paths(event, gh, *args, **kwargs): - pull_request = event.data['pull_request'] +async def check_file_paths(gh, *args, **kwargs): + with open(os.environ["GITHUB_EVENT_PATH"]) as f: + event = json.load(f) + pull_request = event['pull_request'] files = await util.files_for_PR(gh, pull_request) filenames = [file['file_name'] for file in files] - if event.data['action'] == 'opened': + if event['action'] == 'opened': labels = await prtype.classify_by_filepaths(gh, pull_request, filenames) if prtype.Labels.skip_news not in labels: await news.check_news(gh, pull_request, files) else: await news.check_news(gh, pull_request, files) + await news.check_news(gh, pull_request, files) + +async def main(): + try: + async with aiohttp.ClientSession() as session: + gh = GitHubAPI(session, "sabderemane", oauth_token=os.getenv("GH_AUTH")) + await check_file_paths(gh) + except Exception: + traceback.print_exc() + + +loop = asyncio.get_event_loop() +loop.run_until_complete(main()) diff --git a/bedevere/old/backport.py b/bedevere/old/backport.py new file mode 100644 index 00000000..ddb743bd --- /dev/null +++ b/bedevere/old/backport.py @@ -0,0 +1,149 @@ +"""Automatically remove a backport label, and check backport PR validity.""" +import functools +import re + +import gidgethub.routing + +from . import util + +create_status = functools.partial(util.create_status, 'bedevere/maintenance-branch-pr') + + +router = gidgethub.routing.Router() + +TITLE_RE = re.compile(r'\s*\[(?P\d+\.\d+)\].+\((?:GH-|#)(?P\d+)\)', re.IGNORECASE) +MAINTENANCE_BRANCH_TITLE_RE = re.compile(r'\s*\[(?P\d+\.\d+)\].+') +MAINTENANCE_BRANCH_RE = re.compile(r'\s*(?P\d+\.\d+)') +BACKPORT_LABEL = 'needs backport to {branch}' +MESSAGE_TEMPLATE = ('[GH-{pr}](https://github.com/python/cpython/pull/{pr}) is ' + 'a backport of this pull request to the ' + '[{branch} branch](https://github.com/python/cpython/tree/{branch}).') + + +BACKPORT_TITLE_DEVGUIDE_URL = "https://devguide.python.org/committing/#backport-pr-title" + +async def _copy_over_labels(gh, original_issue, backport_issue): + """Copy over relevant labels from the original PR to the backport PR.""" + label_prefixes = "skip", "type", "sprint" + labels = list(filter(lambda x: x.startswith(label_prefixes), + util.labels(original_issue))) + if labels: + await gh.post(backport_issue["labels_url"], data=labels) + + +async def _remove_backport_label(gh, original_issue, branch, backport_pr_number): + """Remove the appropriate "backport to" label on the original PR. + + Also leave a comment on the original PR referencing the backport PR. + """ + backport_label = BACKPORT_LABEL.format(branch=branch) + if backport_label not in util.labels(original_issue): + return + await gh.delete(original_issue['labels_url'], {'name': backport_label}) + message = MESSAGE_TEMPLATE.format(branch=branch, pr=backport_pr_number) + await gh.post(original_issue['comments_url'], data={'body': message}) + + +@router.register("pull_request", action="opened") +@router.register("pull_request", action="edited") +async def manage_labels(event, gh, *args, **kwargs): + if event.data["action"] == "edited" and "title" not in event.data["changes"]: + return + pull_request = event.data["pull_request"] + title = util.normalize_title(pull_request['title'], + pull_request['body']) + title_match = TITLE_RE.match(title) + if title_match is None: + return + branch = title_match.group('branch') + original_pr_number = title_match.group('pr') + + original_issue = await gh.getitem(event.data['repository']['issues_url'], + {'number': original_pr_number}) + await _remove_backport_label(gh, original_issue, branch, + event.data["number"]) + + backport_issue = await util.issue_for_PR(gh, pull_request) + await _copy_over_labels(gh, original_issue, backport_issue) + + +def is_maintenance_branch(ref): + """ + Return True if the ref refers to a maintenance branch. + + >>> is_maintenance_branch("3.11") + True + >>> is_maintenance_branch("main") + False + >>> is_maintenance_branch("gh-1234/something-completely-different") + False + """ + maintenance_branch_pattern = r'\d+\.\d+' + return bool(re.fullmatch(maintenance_branch_pattern, ref)) + + +@router.register("pull_request", action="opened") +@router.register("pull_request", action="reopened") +@router.register("pull_request", action="edited") +@router.register("pull_request", action="synchronize") +async def validate_maintenance_branch_pr(event, gh, *args, **kwargs): + """Check the PR title for maintenance branch pull requests. + + If the PR was made against maintenance branch, and the title does not + match the maintenance branch PR pattern, then post a failure status. + + The maintenance branch PR has to start with `[X.Y]` + """ + if event.data["action"] == "edited" and "title" not in event.data["changes"]: + return + pull_request = event.data["pull_request"] + base_branch = pull_request["base"]["ref"] + + if not is_maintenance_branch(base_branch): + return + + title = util.normalize_title(pull_request["title"], + pull_request["body"]) + title_match = MAINTENANCE_BRANCH_TITLE_RE.match(title) + + if title_match is None: + status = create_status(util.StatusState.FAILURE, + description="Not a valid maintenance branch PR title.", + target_url=BACKPORT_TITLE_DEVGUIDE_URL) + else: + status = create_status(util.StatusState.SUCCESS, + description="Valid maintenance branch PR title.") + await util.post_status(gh, event, status) + + +@router.register("create", ref_type="branch") +async def maintenance_branch_created(event, gh, *args, **kwargs): + """Create the `needs backport label` when the maintenance branch is created. + + Also post a reminder to add the maintenance branch to the list of + `ALLOWED_BRANCHES` in CPython-emailer-webhook. + + If a maintenance branch was created (e.g.: 3.9, or 4.0), + automatically create the `needs backport to ` label. + + The maintenance branch PR has to start with `[X.Y]` + """ + branch_name = event.data["ref"] + + if MAINTENANCE_BRANCH_RE.match(branch_name): + await gh.post( + "/repos/python/cpython/labels", + data={"name": f"needs backport to {branch_name}", "color": "#c2e0c6"}, + ) + + await gh.post( + "/repos/berkerpeksag/cpython-emailer-webhook/issues", + data={ + "title": f"Please add {branch_name} to ALLOWED_BRANCHES", + "body": ( + f"A new CPython maintenance branch `{branch_name}` has just been created.", + "\nThis is a reminder to add `{branch_name}` to the list of `ALLOWED_BRANCHES`", + "\nhttps://github.com/berkerpeksag/cpython-emailer-webhook/blob/e164cb9a6735d56012a4e557fd67dd7715c16d7b/mailer.py#L15", + ), + }, + ) \ No newline at end of file diff --git a/bedevere/old/close_pr.py b/bedevere/old/close_pr.py new file mode 100644 index 00000000..b09adc58 --- /dev/null +++ b/bedevere/old/close_pr.py @@ -0,0 +1,61 @@ +"""Automatically close PR that tries to merge maintenance branch into main.""" +import re + +import gidgethub.routing + + +PYTHON_MAINT_BRANCH_RE = re.compile(r'^\w+:\d+\.\d+$') + +INVALID_PR_COMMENT = """\ +PRs attempting to merge a maintenance branch into the \ +main branch are deemed to be spam and automatically closed. \ +If you were attempting to report a bug, please go to bugs.python.org; \ +see devguide.python.org for further instruction as needed.""" + + +router = gidgethub.routing.Router() + +@router.register("pull_request", action="opened") +@router.register("pull_request", action="synchronize") +async def close_invalid_pr(event, gh, *args, **kwargs): + """Close the invalid PR, add 'invalid' label, and post a message. + + PR is considered invalid if: + * base_label is 'python:main' + * head_label is ':' + """ + head_label = event.data["pull_request"]["head"]["label"] + base_label = event.data["pull_request"]["base"]["label"] + + if PYTHON_MAINT_BRANCH_RE.match(head_label) and \ + base_label == "python:main": + data = {'state': 'closed'} + await gh.patch(event.data["pull_request"]["url"], data=data) + await gh.post( + f'{event.data["pull_request"]["issue_url"]}/labels', + data=["invalid"] + ) + await gh.post( + f'{event.data["pull_request"]["issue_url"]}/comments', + data={'body': INVALID_PR_COMMENT} + ) + + +@router.register("pull_request", action="review_requested") +async def dismiss_invalid_pr_review_request(event, gh, *args, **kwargs): + """Dismiss review request from the invalid PR. + + PR is considered invalid if: + * base_label is 'python:main' + * head_label is ':' + """ + head_label = event.data["pull_request"]["head"]["label"] + base_label = event.data["pull_request"]["base"]["label"] + + if PYTHON_MAINT_BRANCH_RE.match(head_label) and \ + base_label == "python:main": + data = {"reviewers": [reviewer["login"] for reviewer in event.data["pull_request"]["requested_reviewers"]], + "team_reviewers": [team["name"] for team in event.data["pull_request"]["requested_teams"]] + } + await gh.delete(f'{event.data["pull_request"]["url"]}/requested_reviewers', + data=data) diff --git a/bedevere/old/filepaths.py b/bedevere/old/filepaths.py new file mode 100644 index 00000000..515ba8b3 --- /dev/null +++ b/bedevere/old/filepaths.py @@ -0,0 +1,22 @@ +"""Checks related to filepaths on a pull request.""" +import gidgethub.routing + +from . import news +from . import prtype +from . import util + + +router = gidgethub.routing.Router() + + +@router.register('pull_request', action='opened') +@router.register('pull_request', action='synchronize') +@router.register('pull_request', action='reopened') +async def check_file_paths(event, gh, *args, **kwargs): + pull_request = event.data['pull_request'] + files = await util.files_for_PR(gh, pull_request) + filenames = [file['file_name'] for file in files] + await news.check_news(gh, pull_request, files) + if event.data['action'] == 'opened': + await prtype.classify_by_filepaths(gh, pull_request, filenames) + diff --git a/bedevere/review_pr.py b/bedevere/review_pr.py new file mode 100644 index 00000000..1275ed12 --- /dev/null +++ b/bedevere/review_pr.py @@ -0,0 +1,54 @@ +"""Automatically close PR that tries to merge maintenance branch into main.""" +import asyncio +import json +import os +import re +import traceback + +import aiohttp +from gidgethub.aiohttp import GitHubAPI +# import gidgethub.routing + + +PYTHON_MAINT_BRANCH_RE = re.compile(r'^\w+:\d+\.\d+$') + +INVALID_PR_COMMENT = """\ +PRs attempting to merge a maintenance branch into the \ +main branch are deemed to be spam and automatically closed. \ +If you were attempting to report a bug, please go to \ +https://github.com/python/cpython/issues; \ +see devguide.python.org for further instruction as needed.""" + + +# @router.register("pull_request", action="review_requested") +async def dismiss_invalid_pr_review_request(gh, *args, **kwargs): + """Dismiss review request from the invalid PR. + + PR is considered invalid if: + * base_label is 'python:main' + * head_label is ':' + """ + with open(os.environ["GITHUB_EVENT_PATH"]) as f: + event = json.load(f) + head_label = event["pull_request"]["head"]["label"] + base_label = event["pull_request"]["base"]["label"] + + if (PYTHON_MAINT_BRANCH_RE.match(head_label) and + base_label == "python:main"): + pr = event["pull_request"] + reviewers = [reviewer["login"] for reviewer in pr["requested_reviewers"] + team_reviewers = [team["name"] for team in pr["requested_teams"] + await gh.delete(f'{event["pull_request"]["url"]}/requested_reviewers', + data=dict(reviewers=reviewers, team_reviewers=team_reviewers)) + +async def main(): + try: + async with aiohttp.ClientSession() as session: + gh = GitHubAPI(session, "sabderemane", oauth_token=os.getenv("GH_AUTH")) + await dismiss_invalid_pr_review_request(gh) + except Exception: + traceback.print_exc() + + +loop = asyncio.get_event_loop() +loop.run_until_complete(main()) diff --git a/bedevere/util.py b/bedevere/util.py index 88b6f567..db3fee61 100644 --- a/bedevere/util.py +++ b/bedevere/util.py @@ -45,7 +45,8 @@ def create_status(context, state, *, description=None, target_url=None): async def post_status(gh, event, status): """Post a status in reaction to an event.""" - await gh.post(event.data["pull_request"]["statuses_url"], data=status) + response = await gh.post(event.data["pull_request"]["statuses_url"], data=status) + return response def skip_label(what):