blob: 34081a8335b020281ef9901e31ba5d39ce0ea5a1 [file] [log] [blame]
# Copyright 2019 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from urlparse import urlparse
from recipe_engine import recipe_api
from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb2
_DEFAULT_REF = 'refs/heads/master'
class BuildInputResolverApi(recipe_api.RecipeApi):
"""API for resolving the Gitiles commit for the current task.
# Overview
The build input is provided by LUCI's BuildBucketApi. Recipes use it to
understand which Gerrit changes or Gitiles commits to test. If it points to a
repository's HEAD, this can lead to subtle errors where separate pieces of
code resolve HEAD to different revisions. If it is empty, such as when testing
with led or running a job triggered on cron schedule, builds fail. This API
ensures a Gitiles commit is always present and resolved to a particular
revision.
# Resolution
If the original build input is a Gerrit change, the Gitiles commit is the
resolved HEAD that the Gerrit change should be tested against and submitted on
top of. If there are multiple Gerrit changes (rare), only the first change
is considered.
If the original build input is a Gitiles commit, and its revision is HEAD, its
revision is resolved to the HEAD commit. Otherwise it is unchanged.
If the original build input contained no changes, the Gitiles commit is
resolved to the HEAD commit in `default_project_url` refs/heads/master.
# Usage
Call `resolve` to modify the input build in-place. Calling `resolve` multiple
times will always result in the same result. This should be called at the
start of recipe execution. Code that executes after resolution can read the
resolved input directly from api.buildbucket.
"""
def __init__(self, *args, **kwargs):
super(BuildInputResolverApi, self).__init__(*args, **kwargs)
self._resolved_commit = None
def resolve(self, default_project_url=None):
"""Resolves the current Buildbucket input commit by modifying it in-place.
Args:
default_project_url (string): URL to the project to set in the
output BuildInput when the original build input is empty. Must
but a valid URL matching: $scheme://$netloc/$path
"""
with self.m.context(infra_steps=True):
if not self._resolved_commit:
self._resolved_commit = self._resolve_once(default_project_url)
self._bb_input.gitiles_commit.CopyFrom(self._resolved_commit)
@property
def _bb_input(self):
"""Convenience alias for accessing the buildbucket input."""
return self.m.buildbucket.build.input
def _resolve_once(self, default_project_url):
assert not self._resolved_commit, 'input has already been resolved!'
if self._bb_input.gerrit_changes:
return self._resolve_gerrit_change()
# Empty proto message is still trueish.
elif str(self._bb_input.gitiles_commit).strip():
return self._resolve_gitiles_commit()
assert default_project_url, 'need default_project_url. build input is empty'
return self._resolve_default(default_project_url)
def _resolve_gerrit_change(self):
"""Resolves a commit from a Gerrit change."""
change = self._bb_input.gerrit_changes[0]
details = self.m.gerrit.change_details(
'get_gerrit_details',
change_id=str(change.change),
gerrit_host='https://{}'.format(change.host))
host = change.host.replace('-review.googlesource', '.googlesource')
url = 'https://{}/{}'.format(host, change.project)
# This doesn't work for branches like 'refs/meta/config', but that
# shouldn't be an issue.
ref = 'refs/heads/%s' % details['branch']
return common_pb2.GitilesCommit(
project=change.project,
host=host,
ref=ref,
id=self._resolve_ref(url, ref),
)
def _resolve_gitiles_commit(self):
"""Resolves a commit revision from the input commit.
The input commit may hold a ref that could point to different commits at
different points in time, or it may not point to a specific ref at all.
In either case, we want to resolve a fixed commit hash.
"""
commit = self._bb_input.gitiles_commit
if commit.id and commit.id != 'HEAD':
return commit
ref = commit.ref or self.m.properties.get('branch', _DEFAULT_REF)
url = 'https://{}/{}'.format(commit.host, commit.project)
revision = self._resolve_ref(url, ref)
return common_pb2.GitilesCommit(
host=commit.host,
project=commit.project,
ref=ref,
id=revision,
)
def _resolve_default(self, project_url):
"""Resolves a BuildInput from an empty Buildbucket build input."""
ref = _DEFAULT_REF
revision = self._resolve_ref(project_url, ref)
url = urlparse(project_url)
return common_pb2.GitilesCommit(
host=url.netloc,
project=url.path.lstrip('/'), # /foo -> foo
ref=ref,
id=revision,
)
def _resolve_ref(self, url, ref):
"""Resolves the given url and ref into a commit hash.
Raises:
AssertionError if the ref is not found.
"""
# TODO(crbug.com/961538): When refspath is e.g. "refs/heads/master" the
# output JSON has a malformed key "refs/heads/master/refs/heads/master". We
# Fix the tool and use the input ref directly.
revision = self.m.gitiles.refs(url, refspath='refs/heads').get(ref, None)
assert revision, 'no revision found for {} in {}'.format(url, ref)
return revision