diff --git a/endpoints/trigger.py b/endpoints/trigger.py index 38587bc00..4e8efbeb2 100644 --- a/endpoints/trigger.py +++ b/endpoints/trigger.py @@ -318,7 +318,7 @@ class BitbucketBuildTrigger(BuildTriggerHandler): return True def is_active(self): - return 'hook_id' in self.config + return 'webhook_id' in self.config def activate(self, standard_webhook_url): config = self.config @@ -333,51 +333,58 @@ class BitbucketBuildTrigger(BuildTriggerHandler): ] repository = self._get_repository_client() - (result, data, err_msg) = repository.deploykeys().create( + (result, created_deploykey, err_msg) = repository.deploykeys().create( app.config['REGISTRY_TITLE'] + ' webhook key', public_key) if not result: msg = 'Unable to add deploy key to repository: %s' % err_msg raise TriggerActivationException(msg) - config['deploy_key_id'] = data['pk'] + config['deploy_key_id'] = created_deploykey['pk'] # Add a webhook callback. - (result, data, err_msg) = repository.services().create('POST', URL=standard_webhook_url) + description = 'Webhook for invoking builds on %s' % app.config['REGISTRY_TITLE_SHORT'] + webhook_events = ['repo:push'] + (result, created_webhook, err_msg) = repository.webhooks().create( + description, standard_webhook_url, webhook_events) + if not result: msg = 'Unable to add webhook to repository: %s' % err_msg raise TriggerActivationException(msg) - config['hook_id'] = data['id'] + config['webhook_id'] = created_webhook['uuid'] self.config = config return config, {'private_key': private_key} def deactivate(self): config = self.config - hook_id = config.pop('hook_id', None) + # TODO(jschorr): Remove the old hook removal code once everyone is migrated. + old_hook_id = config.pop('hook_id', None) + webhook_id = config.pop('webhook_id', None) deploy_key_id = config.pop('deploy_key_id', None) + repository = self._get_repository_client() - try: - repository = self._get_repository_client() + # Remove the old service hook. + if old_hook_id is not None: + (result, _, err_msg) = repository.services().delete(old_hook_id) + if not result: + msg = 'Unable to remove service hook from repository: %s' % err_msg + raise TriggerDeactivationException(msg) - # Remove the webhook link. - if hook_id is not None: - (result, _, err_msg) = repository.services().delete(hook_id) - if not result: - msg = 'Unable to remove webhook from repository: %s' % err_msg - raise TriggerDeactivationException(msg) + # Remove the webhook. + if webhook_id is not None: + (result, _, err_msg) = repository.webhooks().delete(webhook_id) + if not result: + msg = 'Unable to remove webhook from repository: %s' % err_msg + raise TriggerDeactivationException(msg) - # Remove the public key. - if deploy_key_id is not None: - (result, _, err_msg) = repository.deploykeys().delete(deploy_key_id) - if not result: - msg = 'Unable to remove deploy key from repository: %s' % err_msg - raise TriggerDeactivationException(msg) - except GitHubBadCredentialsException: - msg = ('Unable to remove trigger as credentials are invalid. ' + - 'Please manually remove the webhook and deploy key.') - raise TriggerDeactivationException(msg) + # Remove the public key. + if deploy_key_id is not None: + (result, _, err_msg) = repository.deploykeys().delete(deploy_key_id) + if not result: + msg = 'Unable to remove deploy key from repository: %s' % err_msg + raise TriggerDeactivationException(msg) return config @@ -470,7 +477,17 @@ class BitbucketBuildTrigger(BuildTriggerHandler): return None - def _prepare_build(self, commit_sha, ref, is_manual): + _BITBUCKET_COMMIT_URL = 'https://bitbucket.org/%s/%s/commits/%s' + + def _prepare_build(self, commit_sha, ref, is_manual, target=None, actor=None): + def _build_user_block(info): + return { + 'username': info['username'], + 'url': info['links']['html']['href'], + 'avatar_url': info['links']['avatar']['href'], + 'display_name': info['display_name'] + } + config = self.config repository = self._get_repository_client() @@ -478,23 +495,33 @@ class BitbucketBuildTrigger(BuildTriggerHandler): # the tags. default_branch = self._get_default_branch(repository) - # Lookup the commit sha. - (result, data, _) = repository.changesets().get(commit_sha) - if not result: - raise TriggerStartException('Could not lookup commit SHA') + # Lookup the commit sha (if necessary) + data = {} + if target is None: + (result, data, _) = repository.changesets().get(commit_sha) + if not result: + raise TriggerStartException('Could not lookup commit SHA') namespace = repository.namespace name = repository.repository_name + # Build the commit information. + commit_url = self._BITBUCKET_COMMIT_URL % (namespace, name, commit_sha) + if target is not None and 'links' in target: + commit_url = target['links']['html']['href'] + commit_info = { - 'url': 'https://bitbucket.org/%s/%s/commits/%s' % (namespace, name, commit_sha), - 'message': data['message'], - 'date': data['timestamp'] + 'url': commit_url, + 'message': target['message'] if target else data['message'], + 'date': target['date'] if target else data['timestamp'] } - # Try to lookup the author by email address. The raw_author field (if it exists) is returned - # in the form: "Joseph Schorr " - if data.get('raw_author'): + # Add the commit's author. + if target.get('author') and 'user' in target['author']: + commit_info['author'] = _build_user_block(target['author']['user']) + elif data.get('raw_author'): + # Try to lookup the author by email address. The raw_author field (if it exists) is returned + # in the form: "Joseph Schorr " match = re.compile(r'.*<(.+)>').match(data['raw_author']) if match: email_address = match.group(1) @@ -507,6 +534,10 @@ class BitbucketBuildTrigger(BuildTriggerHandler): 'avatar_url': data['user']['avatar'] } + # Add the commit's actor (committer). + if actor is not None: + commit_info['committer'] = _build_user_block(actor) + metadata = { 'commit': commit_sha, 'ref': ref, @@ -524,8 +555,61 @@ class BitbucketBuildTrigger(BuildTriggerHandler): return prepared - def handle_trigger_request(self, request): + # Check for a payload field. If found, this is a V1 service request. + payload_json = request.form.get('payload') + if payload_json: + return self.handle_V1_trigger_request(request) + + # Otherwise, this is a V2 webhook request. + return self.handle_V2_trigger_request(request) + + def handle_V2_trigger_request(self, request): + payload = request.get_json() + if not 'push' in payload: + logger.debug('Skipping BitBucket request due to missing push data in payload') + raise SkipRequestException() + + push_payload = payload['push'] + if not 'changes' in push_payload or not push_payload['changes']: + logger.debug('Skipping BitBucket request due to empty changes list') + raise SkipRequestException() + + # Make sure we have a new change. + changes = push_payload['changes'] + last_change = changes[-1] + if not last_change.get('new'): + logger.debug('Skipping BitBucket request due to change being a deletion') + raise SkipRequestException() + + change_info = last_change['new'] + change_target = change_info.get('target') + if not change_target: + logger.debug('Skipping BitBucket request due to missing change target') + raise SkipRequestException() + + # Check if this build should be skipped by commit message. + commit_message = change_target.get('message', '') + if should_skip_commit(commit_message): + logger.debug('Skipping BitBucket request due to commit message request') + raise SkipRequestException() + + # Check to see if this build should be skipped by ref. + ref = ('refs/heads/' + change_info['name'] if change_info['type'] == 'branch' + else 'refs/tags/' + change_info['name']) + + logger.debug('Checking BitBucket request: %s', ref) + raise_if_skipped(self.config, ref) + + # Prepare the build. + commit_sha = change_target['hash'] + return self._prepare_build(commit_sha, ref, False, target=change_target, + actor=payload.get('actor')) + + + def handle_V1_trigger_request(self, request): + # TODO(jschorr): Delete this code after the migration has successfully completed. + # Parse the JSON payload. payload_json = request.form.get('payload') if not payload_json: