def _get_pr_files(self, repo, number): """ Get Files that belong to the Pull Request :param repo: the repo full name, ``{owner}/{project}``. e.g. ``buildbot/buildbot`` :param number: the pull request number. """ headers = {"User-Agent": "Buildbot"} if self._token: p = Properties() p.master = self.master token = yield p.render(self._token) headers["Authorization"] = "token " + token url = f"/repos/{repo}/pulls/{number}/files" http = yield httpclientservice.HTTPClientService.getService( self.master, self.github_api_endpoint, headers=headers, debug=self.debug, verify=self.verify, ) res = yield http.get(url) if 200 <= res.code < 300: data = yield res.json() return [f["filename"] for f in data] log.msg(f'Failed fetching PR files: response code {res.code}') return []
def _downloadSshPrivateKeyIfNeeded(self): if self.sshPrivateKey is None: defer.returnValue(RC_SUCCESS) p = Properties() p.master = self.master private_key = yield p.render(self.sshPrivateKey) host_key = yield p.render(self.sshHostKey) # not using self.workdir because it may be changed depending on step # options workdir = self._getSshDataWorkDir() yield self.runMkdir(self._getSshDataPath()) if not self.supportsSshPrivateKeyAsEnvOption: yield self.downloadFileContentToWorker(self._getSshWrapperScriptPath(), self._getSshWrapperScript(), workdir=workdir, mode=0o700) yield self.downloadFileContentToWorker(self._getSshPrivateKeyPath(), private_key, workdir=workdir, mode=0o400) if self.sshHostKey is not None: known_hosts_contents = getSshKnownHostsContents(host_key) yield self.downloadFileContentToWorker(self._getSshHostKeyPath(), known_hosts_contents, workdir=workdir, mode=0o400) self.didDownloadSshPrivateKey = True defer.returnValue(RC_SUCCESS)
def verifyCode(self, code): # everything in deferToThread is not counted with trial --coverage :-( def thd(client_id, client_secret): url = self.tokenUri data = {'redirect_uri': self.loginUri, 'code': code, 'grant_type': self.grantType} auth = None if self.getTokenUseAuthHeaders: auth = (client_id, client_secret) else: data.update( {'client_id': client_id, 'client_secret': client_secret}) data.update(self.tokenUriAdditionalParams) response = requests.post( url, data=data, auth=auth, verify=self.sslVerify) response.raise_for_status() responseContent = bytes2unicode(response.content) try: content = json.loads(responseContent) except ValueError: content = parse_qs(responseContent) for k, v in content.items(): content[k] = v[0] except TypeError: content = responseContent session = self.createSessionFromToken(content) return self.getUserInfoFromOAuthClient(session) p = Properties() p.master = self.master client_id = yield p.render(self.clientId) client_secret = yield p.render(self.clientSecret) result = yield threads.deferToThread(thd, client_id, client_secret) return result
def _downloadSshPrivateKeyIfNeeded(self): if self.sshPrivateKey is None: defer.returnValue(RC_SUCCESS) p = Properties() p.master = self.master private_key = yield p.render(self.sshPrivateKey) # not using self.workdir because it may be changed depending on step # options workdir = self._getSshDataWorkDir() rel_key_path = self.build.path_module.relpath( self._getSshPrivateKeyPath(), workdir) rel_wrapper_script_path = self.build.path_module.relpath( self._getSshWrapperScriptPath(), workdir) yield self.runMkdir(self._getSshDataPath()) if not self.supportsSshPrivateKeyAsEnvOption: yield self.downloadFileContentToWorker(rel_wrapper_script_path, self._getSshWrapperScript(), workdir=workdir, mode=0o700) yield self.downloadFileContentToWorker(rel_key_path, private_key, workdir=workdir, mode=0o400) self.didDownloadSshPrivateKey = True defer.returnValue(RC_SUCCESS)
def reconfigServiceWithSibling(self, sibling): # only reconfigure if sibling is configured differently. # sibling == self is using ComparableMixin's implementation # only compare compare_attrs if self.configured and sibling == self: defer.returnValue(None) self.configured = True # render renderables in parallel # Properties import to resolve cyclic import issue from buildbot.process.properties import Properties p = Properties() p.master = self.master # render renderables in parallel secrets = [] kwargs = {} accumulateClassList(self.__class__, 'secrets', secrets) for k, v in sibling._config_kwargs.items(): if k in secrets: value = yield p.render(v) setattr(self, k, value) kwargs.update({k: value}) else: kwargs.update({k: v}) d = yield self.reconfigService(*sibling._config_args, **sibling._config_kwargs) defer.returnValue(d)
def reconfigServiceWithSibling(self, sibling): # only reconfigure if sibling is configured differently. # sibling == self is using ComparableMixin's implementation # only compare compare_attrs if self.configured and sibling == self: return None self.configured = True # render renderables in parallel # Properties import to resolve cyclic import issue from buildbot.process.properties import Properties p = Properties() p.master = self.master # render renderables in parallel secrets = [] kwargs = {} accumulateClassList(self.__class__, 'secrets', secrets) for k, v in sibling._config_kwargs.items(): if k in secrets: # for non reconfigurable services, we force the attribute v = yield p.render(v) setattr(sibling, k, v) setattr(self, k, v) kwargs[k] = v d = yield self.reconfigService(*sibling._config_args, **kwargs) return d
def reconfigServiceWithSibling(self, sibling): # only reconfigure if sibling is configured differently. # sibling == self is using ComparableMixin's implementation # only compare compare_attrs if self.configured and sibling == self: defer.returnValue(None) self.configured = True # render renderables in parallel # Properties import to resolve cyclic import issue from buildbot.process.properties import Properties p = Properties() p.master = self.master # render renderables in parallel secrets = [] kwargs = {} accumulateClassList(self.__class__, 'secrets', secrets) for k, v in sibling._config_kwargs.items(): if k in secrets: # for non reconfigurable services, we force the attribute v = yield p.render(v) setattr(sibling, k, v) setattr(self, k, v) kwargs[k] = v d = yield self.reconfigService(*sibling._config_args, **kwargs) defer.returnValue(d)
def _get_commit_msg(self, repo, sha): ''' :param repo: the repo full name, ``{owner}/{project}``. e.g. ``buildbot/buildbot`` ''' headers = { 'User-Agent': 'Buildbot', } if self._token: p = Properties() p.master = self.master token = yield p.render(self._token) headers['Authorization'] = 'token ' + token url = f'/repos/{repo}/commits/{sha}' http = yield httpclientservice.HTTPClientService.getService( self.master, self.github_api_endpoint, headers=headers, debug=self.debug, verify=self.verify) res = yield http.get(url) if 200 <= res.code < 300: data = yield res.json() return data['commit']['message'] log.msg(f'Failed fetching PR commit message: response code {res.code}') return 'No message field'
def renderSecrets(self, *args): # Properties import to resolve cyclic import issue from buildbot.process.properties import Properties p = Properties() p.master = self.master if len(args) == 1: return p.render(args[0]) return defer.gatherResults([p.render(s) for s in args], consumeErrors=True)
def getChanges(self, request): """ Reponds only to POST events and starts the build process :arguments: request the http request object """ expected_secret = isinstance(self.options, dict) and self.options.get('secret') if expected_secret: received_secret = request.getHeader(_HEADER_GITLAB_TOKEN) received_secret = bytes2unicode(received_secret) p = Properties() p.master = self.master expected_secret_value = yield p.render(expected_secret) if received_secret != expected_secret_value: raise ValueError("Invalid secret") try: content = request.content.read() payload = json.loads(bytes2unicode(content)) except Exception as e: raise ValueError("Error loading JSON: " + str(e)) from e event_type = request.getHeader(_HEADER_EVENT) event_type = bytes2unicode(event_type) # newer version of gitlab have a object_kind parameter, # which allows not to use the http header event_type = payload.get('object_kind', event_type) codebase = request.args.get(b'codebase', [None])[0] codebase = bytes2unicode(codebase) if event_type in ("push", "tag_push", "Push Hook"): user = payload['user_name'] repo = payload['repository']['name'] repo_url = payload['repository']['url'] changes = self._process_change(payload, user, repo, repo_url, event_type, codebase=codebase) elif event_type == 'merge_request': changes = self._process_merge_request_change(payload, event_type, codebase=codebase) else: changes = [] if changes: log.msg( f"Received {len(changes)} changes from {event_type} gitlab event" ) return (changes, 'git')
def _get_payload(self, request): content = request.content.read() content = bytes2unicode(content) signature = request.getHeader(_HEADER_SIGNATURE) signature = bytes2unicode(signature) if not signature and self._strict: raise ValueError('Request has no required signature') if self._secret and signature: try: hash_type, hexdigest = signature.split('=') except ValueError: raise ValueError( 'Wrong signature format: {}'.format(signature)) if hash_type != 'sha1': raise ValueError('Unknown hash type: {}'.format(hash_type)) p = Properties() p.master = self.master rendered_secret = yield p.render(self._secret) mac = hmac.new(unicode2bytes(rendered_secret), msg=unicode2bytes(content), digestmod=sha1) def _cmp(a, b): try: # try the more secure compare_digest() first from hmac import compare_digest return compare_digest(a, b) except ImportError: # pragma: no cover # and fallback to the insecure simple comparison otherwise return a == b if not _cmp(bytes2unicode(mac.hexdigest()), hexdigest): raise ValueError('Hash mismatch') content_type = request.getHeader(b'Content-Type') if content_type == b'application/json': payload = json.loads(content) elif content_type == b'application/x-www-form-urlencoded': payload = json.loads(bytes2unicode(request.args[b'payload'][0])) else: raise ValueError('Unknown content type: {}'.format(content_type)) log.msg("Payload: {}".format(payload), logLevel=logging.DEBUG) return payload
def getLoginURL(self, redirect_url): """ Returns the url to redirect the user to for user consent """ p = Properties() p.master = self.master clientId = yield p.render(self.clientId) oauth_params = {'redirect_uri': self.loginUri, 'client_id': clientId, 'response_type': 'code'} if redirect_url is not None: oauth_params['state'] = urlencode(dict(redirect=redirect_url)) oauth_params.update(self.authUriAdditionalParams) sorted_oauth_params = sorted(oauth_params.items(), key=lambda val: val[0]) return "%s?%s" % (self.authUri, urlencode(sorted_oauth_params))
def newChangeSource(self, owner, repo, endpoint='https://api.github.com', **kwargs): http_headers = {'User-Agent': 'Buildbot'} token = kwargs.get('token', None) if token: p = Properties() p.master = self.master token = yield p.render(token) http_headers.update({'Authorization': 'token ' + token}) self._http = yield fakehttpclientservice.HTTPClientService.getService( self.master, self, endpoint, headers=http_headers) self.changesource = GitHubPullrequestPoller(owner, repo, **kwargs)
def _downloadSshPrivateKeyIfNeeded(self): if self.sshPrivateKey is None: return RC_SUCCESS p = Properties() p.master = self.master private_key = yield p.render(self.sshPrivateKey) host_key = yield p.render(self.sshHostKey) known_hosts_contents = yield p.render(self.sshKnownHosts) # not using self.workdir because it may be changed depending on step # options workdir = self._getSshDataWorkDir() ssh_data_path = self._getSshDataPath() yield self.runMkdir(ssh_data_path) private_key_path = self._getSshPrivateKeyPath(ssh_data_path) yield self.downloadFileContentToWorker(private_key_path, private_key, workdir=workdir, mode=0o400) known_hosts_path = None if self.sshHostKey is not None or self.sshKnownHosts is not None: known_hosts_path = self._getSshHostKeyPath(ssh_data_path) if self.sshHostKey is not None: known_hosts_contents = getSshKnownHostsContents(host_key) yield self.downloadFileContentToWorker(known_hosts_path, known_hosts_contents, workdir=workdir, mode=0o400) if not self.supportsSshPrivateKeyAsEnvOption: script_path = self._getSshWrapperScriptPath(ssh_data_path) script_contents = getSshWrapperScriptContents( private_key_path, known_hosts_path) yield self.downloadFileContentToWorker(script_path, script_contents, workdir=workdir, mode=0o700) self.didDownloadSshPrivateKey = True return RC_SUCCESS
def _get_payload(self, request): content = request.content.read() content = bytes2unicode(content) signature = request.getHeader(_HEADER_SIGNATURE) signature = bytes2unicode(signature) if not signature and self._strict: raise ValueError('Request has no required signature') if self._secret and signature: try: hash_type, hexdigest = signature.split('=') except ValueError as e: raise ValueError(f'Wrong signature format: {signature}') from e if hash_type != 'sha1': raise ValueError(f'Unknown hash type: {hash_type}') p = Properties() p.master = self.master rendered_secret = yield p.render(self._secret) mac = hmac.new(unicode2bytes(rendered_secret), msg=unicode2bytes(content), digestmod=sha1) def _cmp(a, b): return hmac.compare_digest(a, b) if not _cmp(bytes2unicode(mac.hexdigest()), hexdigest): raise ValueError('Hash mismatch') content_type = request.getHeader(b'Content-Type') if content_type == b'application/json': payload = json.loads(content) elif content_type == b'application/x-www-form-urlencoded': payload = json.loads(bytes2unicode(request.args[b'payload'][0])) else: raise ValueError(f'Unknown content type: {content_type}') log.msg(f"Payload: {payload}", logLevel=logging.DEBUG) return payload
async def _client(self): # return if the service has been already initialized if self._http: return self._http # render the secrets passed to tokens props = Properties() props.master = self.master tokens = [await props.render(token) for token in self._tokens] self._http = await GithubClientService.getService( self.master, self.github_api_endpoint, tokens=tokens, headers=self.headers, debug=self.debug, verify=self.verify) return self._http
def requestAvatarId(self, creds): p = Properties() p.master = self.master username = bytes2unicode(creds.username) try: yield self.master.initLock.acquire() if username in self.users: password, _ = self.users[username] password = yield p.render(password) matched = yield defer.maybeDeferred(creds.checkPassword, unicode2bytes(password)) if not matched: log.msg("invalid login from user '{}'".format(username)) raise error.UnauthorizedLogin() defer.returnValue(creds.username) log.msg("invalid login from unknown user '{}'".format(username)) raise error.UnauthorizedLogin() finally: eventually(self.master.initLock.release)
def requestAvatarId(self, creds): p = Properties() p.master = self.master username = bytes2unicode(creds.username) try: yield self.master.initLock.acquire() if username in self.users: password, _ = self.users[username] password = yield p.render(password) matched = yield defer.maybeDeferred( creds.checkPassword, unicode2bytes(password)) if not matched: log.msg("invalid login from user '%s'" % creds.username) raise error.UnauthorizedLogin() defer.returnValue(creds.username) log.msg("invalid login from unknown user '%s'" % creds.username) raise error.UnauthorizedLogin() finally: yield self.master.initLock.release()
def requestAvatarId(self, creds): p = Properties() p.master = self.master username = bytes2unicode(creds.username) try: yield self.master.initLock.acquire() if username in self.users: password, _ = self.users[username] password = yield p.render(password) matched = creds.checkPassword(unicode2bytes(password)) if not matched: log.msg("invalid login from user '{}'".format(username)) raise error.UnauthorizedLogin() return creds.username log.msg("invalid login from unknown user '{}'".format(username)) raise error.UnauthorizedLogin() finally: # brake the callback stack by returning to the reactor # before waking up other waiters eventually(self.master.initLock.release)
def requestAvatarId(self, creds): p = Properties() p.master = self.master username = bytes2unicode(creds.username) try: yield self.master.initLock.acquire() if username in self.users: password, _ = self.users[username] password = yield p.render(password) matched = yield defer.maybeDeferred( creds.checkPassword, unicode2bytes(password)) if not matched: log.msg("invalid login from user '{}'".format(username)) raise error.UnauthorizedLogin() return creds.username log.msg("invalid login from unknown user '{}'".format(username)) raise error.UnauthorizedLogin() finally: # brake the callback stack by returning to the reactor # before waking up other waiters eventually(self.master.initLock.release)
def _downloadSshPrivateKeyIfNeeded(self): if self.sshPrivateKey is None: return RC_SUCCESS p = Properties() p.master = self.master private_key = yield p.render(self.sshPrivateKey) host_key = yield p.render(self.sshHostKey) # not using self.workdir because it may be changed depending on step # options workdir = self._getSshDataWorkDir() ssh_data_path = self._getSshDataPath() yield self.runMkdir(ssh_data_path) if not self.supportsSshPrivateKeyAsEnvOption: script_path = self._getSshWrapperScriptPath(ssh_data_path) script_contents = getSshWrapperScriptContents( self._getSshPrivateKeyPath(ssh_data_path)) yield self.downloadFileContentToWorker(script_path, script_contents, workdir=workdir, mode=0o700) private_key_path = self._getSshPrivateKeyPath(ssh_data_path) yield self.downloadFileContentToWorker(private_key_path, private_key, workdir=workdir, mode=0o400) if self.sshHostKey is not None: known_hosts_path = self._getSshHostKeyPath(ssh_data_path) known_hosts_contents = getSshKnownHostsContents(host_key) yield self.downloadFileContentToWorker(known_hosts_path, known_hosts_contents, workdir=workdir, mode=0o400) self.didDownloadSshPrivateKey = True return RC_SUCCESS
def getChanges(self, request): secret = None if isinstance(self.options, dict): secret = self.options.get('secret') try: content = request.content.read() content_text = bytes2unicode(content) payload = json.loads(content_text) except Exception as exception: raise ValueError('Error loading JSON: ' + str(exception)) if secret is not None: p = Properties() p.master = self.master rendered_secret = yield p.render(secret) signature = hmac.new(unicode2bytes(rendered_secret), unicode2bytes(content_text.strip()), digestmod=hashlib.sha256) header_signature = bytes2unicode( request.getHeader(_HEADER_SIGNATURE)) if signature.hexdigest() != header_signature: raise ValueError('Invalid secret') event_type = bytes2unicode(request.getHeader(_HEADER_EVENT_TYPE)) log.msg("Received event '{}' from gitea".format(event_type)) codebases = request.args.get('codebase', [None]) codebase = bytes2unicode(codebases[0]) changes = [] handler_function = getattr(self, 'process_{}'.format(event_type), None) if not handler_function: log.msg("Ignoring gitea event '{}'".format(event_type)) else: changes = handler_function(payload, event_type, codebase) return (changes, 'git')
def command_FORCE(self, args): # FIXME: NEED TO THINK ABOUT! errReply = "try '%s'" % (self.command_FORCE.usage) args = self.splitArgs(args) if not args: raise UsageError(errReply) what = args.pop(0) if what != "build": raise UsageError(errReply) opts = ForceOptions() opts.parseOptions(args) builderName = opts['builder'] builder = yield self.getBuilder(buildername=builderName) branch = opts['branch'] revision = opts['revision'] codebase = opts['codebase'] project = opts['project'] reason = opts['reason'] props = opts['props'] if builderName is None: raise UsageError("you must provide a Builder, " + errReply) # keep weird stuff out of the branch, revision, and properties args. branch_validate = self.master.config.validation['branch'] revision_validate = self.master.config.validation['revision'] pname_validate = self.master.config.validation['property_name'] pval_validate = self.master.config.validation['property_value'] if branch and not branch_validate.match(branch): log.msg("bad branch '%s'" % branch) self.send("sorry, bad branch '%s'" % branch) return if revision and not revision_validate.match(revision): log.msg("bad revision '%s'" % revision) self.send("sorry, bad revision '%s'" % revision) return properties = Properties() properties.master = self.master if props: # split props into name:value dict pdict = {} propertylist = props.split(",") for prop in propertylist: splitproperty = prop.split("=", 1) pdict[splitproperty[0]] = splitproperty[1] # set properties for prop in pdict: pname = prop pvalue = pdict[prop] if not pname_validate.match(pname) \ or not pval_validate.match(pvalue): log.msg("bad property name='%s', value='%s'" % (pname, pvalue)) self.send("sorry, bad property name='%s', value='%s'" % (pname, pvalue)) return properties.setProperty(pname, pvalue, "Force Build chat") reason = "forced: by %s: %s" % (self.describeUser(), reason) try: yield self.master.data.updates.addBuildset( builderids=[builder['builderid']], # For now, we just use # this as the id. scheduler="status.words", sourcestamps=[{ 'codebase': codebase, 'branch': branch, 'revision': revision, 'project': project, 'repository': "null" }], reason=reason, properties=properties.asDict(), waited_for=False) except AssertionError as e: self.send("I can't: " + str(e))
def command_FORCE(self, args): # FIXME: NEED TO THINK ABOUT! errReply = "try '%s'" % (self.command_FORCE.usage) args = self.splitArgs(args) if not args: raise UsageError(errReply) what = args.pop(0) if what != "build": raise UsageError(errReply) opts = ForceOptions() opts.parseOptions(args) builderName = opts['builder'] builder = yield self.getBuilder(buildername=builderName) branch = opts['branch'] revision = opts['revision'] codebase = opts['codebase'] project = opts['project'] reason = opts['reason'] props = opts['props'] if builderName is None: raise UsageError("you must provide a Builder, " + errReply) # keep weird stuff out of the branch, revision, and properties args. branch_validate = self.master.config.validation['branch'] revision_validate = self.master.config.validation['revision'] pname_validate = self.master.config.validation['property_name'] pval_validate = self.master.config.validation['property_value'] if branch and not branch_validate.match(branch): log.msg("bad branch '%s'" % branch) self.send("sorry, bad branch '%s'" % branch) return if revision and not revision_validate.match(revision): log.msg("bad revision '%s'" % revision) self.send("sorry, bad revision '%s'" % revision) return properties = Properties() properties.master = self.master if props: # split props into name:value dict pdict = {} propertylist = props.split(",") for prop in propertylist: splitproperty = prop.split("=", 1) pdict[splitproperty[0]] = splitproperty[1] # set properties for prop in pdict: pname = prop pvalue = pdict[prop] if not pname_validate.match(pname) \ or not pval_validate.match(pvalue): log.msg("bad property name='%s', value='%s'" % (pname, pvalue)) self.send("sorry, bad property name='%s', value='%s'" % (pname, pvalue)) return properties.setProperty(pname, pvalue, "Force Build chat") reason = "forced: by %s: %s" % (self.describeUser(), reason) try: yield self.master.data.updates.addBuildset(builderids=[builder['builderid']], # For now, we just use # this as the id. scheduler="status.words", sourcestamps=[{ 'codebase': codebase, 'branch': branch, 'revision': revision, 'project': project, 'repository': "null"}], reason=reason, properties=properties.asDict(), waited_for=False) except AssertionError as e: self.send("I can't: " + str(e))
def command_FORCE(self, args, **kwargs): """force a build""" # FIXME: NEED TO THINK ABOUT! errReply = f"Try '{self.bot.commandPrefix}{self.command_FORCE.usage}'" args = self.splitArgs(args) if not args: raise UsageError(errReply) what = args.pop(0) if what != "build": raise UsageError(errReply) opts = ForceOptions() opts.parseOptions(args) builderName = opts['builder'] builder = yield self.bot.getBuilder(buildername=builderName) branch = opts['branch'] revision = opts['revision'] codebase = opts['codebase'] project = opts['project'] reason = opts['reason'] props = opts['props'] if builderName is None: raise UsageError("you must provide a Builder, " + errReply) # keep weird stuff out of the branch, revision, and properties args. branch_validate = self.master.config.validation['branch'] revision_validate = self.master.config.validation['revision'] pname_validate = self.master.config.validation['property_name'] pval_validate = self.master.config.validation['property_value'] if branch and not branch_validate.match(branch): self.bot.log(f"Force: bad branch '{branch}'") self.send(f"Sorry, bad branch '{branch}'") return if revision and not revision_validate.match(revision): self.bot.log(f"Force: bad revision '{revision}'") self.send(f"Sorry, bad revision '{revision}'") return properties = Properties() properties.master = self.master if props: # split props into name:value dict pdict = {} propertylist = props.split(",") for prop in propertylist: splitproperty = prop.split("=", 1) pdict[splitproperty[0]] = splitproperty[1] # set properties for pname, pvalue in pdict.items(): if not pname_validate.match(pname) \ or not pval_validate.match(pvalue): self.bot.log( f"Force: bad property name='{pname}', value='{pvalue}'" ) self.send( f"Sorry, bad property name='{pname}', value='{pvalue}'" ) return properties.setProperty(pname, pvalue, "Force Build Chat") properties.setProperty("reason", reason, "Force Build Chat") properties.setProperty("owner", self.describeUser(), "Force Build Chat") reason = f"forced: by {self.describeUser()}: {reason}" try: yield self.master.data.updates.addBuildset( builderids=[builder['builderid']], # For now, we just use # this as the id. scheduler="status.words", sourcestamps=[{ 'codebase': codebase, 'branch': branch, 'revision': revision, 'project': project, 'repository': "" }], reason=reason, properties=properties.asDict(), waited_for=False) except AssertionError as e: self.send("I can't: " + str(e)) else: self.send("Force build successfully requested.")