webhook-app/webhooks.py (101 lines of code) (raw):

# Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """This module contains functions that are called whenever a particular GitHub webhook is received.""" import time import logging import re import github_helper import webhook_helper @webhook_helper.listen('ping') def pong(data): return {'msg': 'pong'} def check_for_auto_merge_trigger(text): """Checks the text for the phrases that should trigger an automerge.""" # The comment must address @dpebot directly, on the same line comment = re.search( r'@{}\s+\b(.+)'.format(github_helper.github_user()), text, re.I) if not comment: return False else: # Just get the meat of the command comment = comment.group(1).strip() satisfaction = r'\b(pass|passes|green|approv(e|al|es|ed)|happy|satisfied)' ci_tool = r'\b(travis|tests|statuses|kokoro|ci)\b' merge_action = r'\bmerge\b' triggers = ( r'{}.+({}.+)?{}'.format(merge_action, ci_tool, satisfaction), r'{}.+{},.+{}'.format(ci_tool, satisfaction, merge_action), 'lgtm', ) return any(re.search(trigger, comment, re.I) for trigger in triggers) @webhook_helper.listen('issue_comment') def acknowledge_merge_on_travis(data): """When a user comments on a pull request with one of the automerge triggers (e.g. merge on green), this hook will add the 'automerge' label. Issue comment data reference: https://developer.github.com/v3/activity/events/types/#issuecommentevent """ if data.get('action') != 'created': return # Make sure it's a PR. if not github_helper.is_pull_request(data): return # If comment has trigger text. comment = data['comment'] if not check_for_auto_merge_trigger(comment['body']): return # If user is a collaborator. gh = github_helper.get_client() repository = github_helper.get_repository(gh, data) if not repository.is_collaborator(data['sender']['login']): logging.info( '{} is not an owner and is trying to tell me what to do.'.format( data['sender']['login'])) # Write a comment about it. pr = github_helper.get_pull_request(gh, data) pr.create_comment( 'Okay! I\'ll merge when all statuses are green and all reviewers ' 'approve.') pr.issue().add_labels('automerge') pr.issue().assign(github_helper.github_user()) @webhook_helper.listen('status') def commit_status_complete_merge_on_travis(data): """When all statuses on a PR are green, this hook will automatically merge it if it's labeled with 'automerge'. Status data reference: https://developer.github.com/v3/activity/events/types/#statusevent """ # TODO: Idea - if automerge has been triggered and the status fails, # nag the committer to fix? # If it's not successful don't even bother. if data['state'] != 'success': logging.info('Status not successful, returning.') return # NOTE: I'm not sure if there's a better way to do this. But, it seems # the status change message doesn't tell you which PR the commit is # from. Indeed, it's possible for a commit to actually be in multiple # PRs. Anyways, the idea here is to get all open PRs with the # tag 'automerge' and this commit in the PR. commit_sha = data['commit']['sha'] gh = github_helper.get_client() repository = github_helper.get_repository(gh, data) # Sleep for about 15 seconds. Github's search index needs a few seconds # before it'll find the results. time.sleep(15) query = '{} type:pr label:automerge is:open repo:{}'.format( commit_sha, data['repository']['full_name']) logging.info('Querying with: {}'.format(query)) results = gh.search_issues(query=query) # Covert to pull requests so we can get the commits. pulls = [result.issue.pull_request() for result in results] logging.info('Found {} potential PRs: {}'.format( len(pulls), pulls)) # See if this commit is in the PR. # this check isn't actually strictly necessary as the search above will # only return PRs that are 'green' which means we can safely merge all # of them. But, whatever, I'll leave it here for now anyway. pulls = [ pull for pull in pulls if commit_sha in [commit.sha for commit in pull.commits()]] logging.info('Commit {} is present in PRs: {}'.format( commit_sha, pulls)) # Merge! for pull in pulls: merge_pull_request(repository, pull, commit_sha=commit_sha) @webhook_helper.listen('pull_request_review') def pull_request_review_merge_on_travis(data): """When all approvers approve and statuses pass, this hook will automatically merge it if it's labeled with 'automerge'. Status data reference: https://developer.github.com/v3/activity/events/types/#pullrequestreviewevent """ # If it's not successful don't even bother. if data['review']['state'] != 'approved': logging.info('Not approved, returning.') return # If the PR is closed, don't bother if data['pull_request']['state'] != 'open': logging.info('Closed, returning.') return gh = github_helper.get_client() repo = gh.repository( data['repository']['owner']['login'], data['repository']['name']) pr = repo.pull_request(data['pull_request']['number']) merge_pull_request(repo, pr, commit_sha=pr.head.sha) def merge_pull_request(repo, pull, commit_sha=None): """Merges a pull request.""" # only merge pulls that are labeled automerge labels = [label.name for label in pull.issue().labels()] if 'automerge' not in labels: logging.info('Not merging {}, not labeled automerge'.format(pull)) return # only merge if all required status are reported if not github_helper.has_required_statuses(pull): logging.info('Not merging {}, missing required status'.format(pull)) return # only merge pulls that have all green statuses if not github_helper.is_sha_green(repo, commit_sha): logging.info('Not merging {}, not green.'.format(pull)) return # Only merge pulls that have been approved! if not github_helper.is_pr_approved(pull): logging.info('Not merging {}, not approved.'.format(pull)) return # By supplying the sha here, it ensures that the PR will only be # merged if that sha is the HEAD of the branch. logging.info('Merging {}.'.format(pull)) github_helper.squash_merge_pr(pull, sha=commit_sha) # Delete the branch if it's in this repo. ALSO DON'T DELETE MASTER. if (pull.head.ref != 'master' and '/'.join(pull.head.repo) == repo.full_name): repo.ref('heads/{}'.format(pull.head.ref)).delete()