diff --git a/src/applications/config/option/PhabricatorCoreConfigOptions.php b/src/applications/config/option/PhabricatorCoreConfigOptions.php index 6efd27d9a3..0c9e69ebf2 100644 --- a/src/applications/config/option/PhabricatorCoreConfigOptions.php +++ b/src/applications/config/option/PhabricatorCoreConfigOptions.php @@ -1,320 +1,324 @@ deformat(pht(<<newOption('phabricator.base-uri', 'string', null) ->setLocked(true) ->setSummary(pht('URI where Phabricator is installed.')) ->setDescription( pht( 'Set the URI where Phabricator is installed. Setting this '. 'improves security by preventing cookies from being set on other '. 'domains, and allows daemons to send emails with links that have '. 'the correct domain.')) ->addExample('http://phabricator.example.com/', pht('Valid Setting')), $this->newOption('phabricator.production-uri', 'string', null) ->setSummary( pht('Primary install URI, for multi-environment installs.')) ->setDescription( pht( 'If you have multiple Phabricator environments (like a '. 'development/staging environment for working on testing '. 'Phabricator, and a production environment for deploying it), '. 'set the production environment URI here so that emails and other '. 'durable URIs will always generate with links pointing at the '. 'production environment. If unset, defaults to `%s`. Most '. 'installs do not need to set this option.', 'phabricator.base-uri')) ->addExample('http://phabricator.example.com/', pht('Valid Setting')), $this->newOption('phabricator.allowed-uris', 'list', array()) ->setLocked(true) ->setSummary(pht('Alternative URIs that can access Phabricator.')) ->setDescription( pht( "These alternative URIs will be able to access 'normal' pages ". "on your Phabricator install. Other features such as OAuth ". "won't work. The major use case for this is moving installs ". "across domains.")) ->addExample( "http://phabricator2.example.com/\n". "http://phabricator3.example.com/", pht('Valid Setting')), $this->newOption('phabricator.timezone', 'string', null) ->setSummary( pht('The timezone Phabricator should use.')) ->setDescription( pht( "PHP requires that you set a timezone in your php.ini before ". "using date functions, or it will emit a warning. If this isn't ". "possible (for instance, because you are using HPHP) you can set ". "some valid constant for %s here and Phabricator will set it on ". "your behalf, silencing the warning.", 'date_default_timezone_set()')) ->addExample('America/New_York', pht('US East (EDT)')) ->addExample('America/Chicago', pht('US Central (CDT)')) ->addExample('America/Boise', pht('US Mountain (MDT)')) ->addExample('America/Los_Angeles', pht('US West (PDT)')), $this->newOption('phabricator.cookie-prefix', 'string', null) ->setLocked(true) ->setSummary( pht( 'Set a string Phabricator should use to prefix cookie names.')) ->setDescription( pht( 'Cookies set for x.com are also sent for y.x.com. Assuming '. 'Phabricator instances are running on both domains, this will '. 'create a collision preventing you from logging in.')) ->addExample('dev', pht('Prefix cookie with "%s"', 'dev')), $this->newOption('phabricator.show-prototypes', 'bool', false) ->setLocked(true) ->setBoolOptions( array( pht('Enable Prototypes'), pht('Disable Prototypes'), )) ->setSummary( pht( 'Install applications which are still under development.')) ->setDescription( pht( "IMPORTANT: The upstream does not provide support for prototype ". "applications.". "\n\n". "Phabricator includes prototype applications which are in an ". "**early stage of development**. By default, prototype ". "applications are not installed, because they are often not yet ". "developed enough to be generally usable. You can enable ". "this option to install them if you're developing Phabricator ". "or are interested in previewing upcoming features.". "\n\n". "To learn more about prototypes, see [[ %s | %s ]].". "\n\n". "After enabling prototypes, you can selectively uninstall them ". "(like normal applications).", $proto_doc_href, $proto_doc_name)), $this->newOption('phabricator.serious-business', 'bool', false) ->setBoolOptions( array( pht('Serious business'), pht('Shenanigans'), // That should be interesting to translate. :P )) ->setSummary( pht('Allows you to remove levity and jokes from the UI.')) ->setDescription( pht( 'By default, Phabricator includes some flavor text in the UI, '. 'like a prompt to "Weigh In" rather than "Add Comment" in '. 'Maniphest. If you\'d prefer more traditional UI strings like '. '"Add Comment", you can set this flag to disable most of the '. 'extra flavor.')), $this->newOption('remarkup.ignored-object-names', 'string', '/^(Q|V)\d$/') ->setSummary( pht('Text values that match this regex and are also object names '. 'will not be linked.')) ->setDescription( pht( 'By default, Phabricator links object names in Remarkup fields '. 'to the corresponding object. This regex can be used to modify '. 'this behavior; object names that match this regex will not be '. 'linked.')), $this->newOption('environment.append-paths', 'list', $paths) ->setSummary( pht( 'These paths get appended to your %s environment variable.', '$PATH')) ->setDescription( pht( "Phabricator occasionally shells out to other binaries on the ". "server. An example of this is the `%s` command, used to ". "syntax-highlight code written in languages other than PHP. By ". "default, it is assumed that these binaries are in the %s of the ". "user running Phabricator (normally 'apache', 'httpd', or ". "'nobody'). Here you can add extra directories to the %s ". "environment variable, for when these binaries are in ". "non-standard locations.\n\n". "Note that you can also put binaries in `%s` (for example, by ". "symlinking them).\n\n". "The current value of PATH after configuration is applied is:\n\n". " lang=text\n". " %s", 'pygmentize', '$PATH', '$PATH', 'phabricator/support/bin/', $path)) ->setLocked(true) ->addExample('/usr/local/bin', pht('Add One Path')) ->addExample("/usr/bin\n/usr/local/bin", pht('Add Multiple Paths')), $this->newOption('config.lock', 'set', array()) ->setLocked(true) ->setDescription(pht('Additional configuration options to lock.')), $this->newOption('config.hide', 'set', array()) ->setLocked(true) ->setDescription(pht('Additional configuration options to hide.')), $this->newOption('config.ignore-issues', 'set', array()) ->setLocked(true) ->setDescription(pht('Setup issues to ignore.')), $this->newOption('phabricator.env', 'string', null) ->setLocked(true) ->setDescription(pht('Internal.')), $this->newOption('test.value', 'wild', null) ->setLocked(true) ->setDescription(pht('Unit test value.')), $this->newOption('phabricator.uninstalled-applications', 'set', array()) ->setLocked(true) ->setLockedMessage(pht( 'Use the %s to manage installed applications.', phutil_tag( 'a', array( 'href' => $applications_app_href, ), pht('Applications application')))) ->setDescription( pht('Array containing list of uninstalled applications.')), $this->newOption('phabricator.application-settings', 'wild', array()) ->setLocked(true) ->setDescription( pht('Customized settings for Phabricator applications.')), $this->newOption('phabricator.cache-namespace', 'string', 'phabricator') ->setLocked(true) ->setDescription(pht('Cache namespace.')), $this->newOption('phabricator.allow-email-users', 'bool', false) ->setBoolOptions( array( pht('Allow'), pht('Disallow'), )) ->setDescription( pht('Allow non-members to interact with tasks over email.')), $this->newOption('phabricator.silent', 'bool', false) ->setLocked(true) ->setBoolOptions( array( pht('Run Silently'), pht('Run Normally'), )) ->setSummary(pht('Stop Phabricator from sending any email, etc.')) - ->setDescription( - pht( - 'This option allows you to stop Phabricator from sending '. - 'any data to external services. Among other things, it will '. - 'disable email, SMS, repository mirroring, and HTTP hooks.'. - "\n\n". - 'This option is intended to allow a Phabricator instance to '. - 'be exported, copied, imported, and run in a test environment '. - 'without impacting users. For example, if you are migrating '. - 'to new hardware, you could perform a test migration first, '. - 'make sure things work, and then do a production cutover '. - 'later with higher confidence and less disruption. Without '. - 'this flag, users would receive duplicate email during the '. - 'time the test instance and old production instance were '. - 'both in operation.')), + ->setDescription($silent_description), ); } protected function didValidateOption( PhabricatorConfigOption $option, $value) { $key = $option->getKey(); if ($key == 'phabricator.base-uri' || $key == 'phabricator.production-uri') { $uri = new PhutilURI($value); $protocol = $uri->getProtocol(); if ($protocol !== 'http' && $protocol !== 'https') { throw new PhabricatorConfigValidationException( pht( "Config option '%s' is invalid. The URI must start with ". "%s' or '%s'.", 'http://', 'https://', $key)); } $domain = $uri->getDomain(); if (strpos($domain, '.') === false) { throw new PhabricatorConfigValidationException( pht( "Config option '%s' is invalid. The URI must contain a dot ". "('%s'), like '%s', not just a bare name like '%s'. Some web ". "browsers will not set cookies on domains with no TLD.", '.', 'http://example.com/', 'http://example/', $key)); } $path = $uri->getPath(); if ($path !== '' && $path !== '/') { throw new PhabricatorConfigValidationException( pht( "Config option '%s' is invalid. The URI must NOT have a path, ". "e.g. '%s' is OK, but '%s' is not. Phabricator must be installed ". "on an entire domain; it can not be installed on a path.", $key, 'http://phabricator.example.com/', 'http://example.com/phabricator/')); } } if ($key === 'phabricator.timezone') { $old = date_default_timezone_get(); $ok = @date_default_timezone_set($value); @date_default_timezone_set($old); if (!$ok) { throw new PhabricatorConfigValidationException( pht( "Config option '%s' is invalid. The timezone identifier must ". "be a valid timezone identifier recognized by PHP, like '%s'. "." You can find a list of valid identifiers here: %s", $key, 'America/Los_Angeles', 'http://php.net/manual/timezones.php')); } } } } diff --git a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php index ca6f30bd36..47af992630 100644 --- a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php @@ -1,315 +1,327 @@ currentWorkerTaskID = $id; return $this; } public function getCurrentWorkerTaskID() { return $this->currentWorkerTaskID; } public static function getImplementations() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->execute(); } public static function getImplementation($class) { $base = idx(self::getImplementations(), $class); if ($base) { return (clone $base); } return null; } public static function requireImplementation($class) { if (!$class) { throw new Exception(pht('No implementation is specified!')); } $implementation = self::getImplementation($class); if (!$implementation) { throw new Exception(pht('No such implementation "%s" exists!', $class)); } return $implementation; } /** * The name of the implementation. */ abstract public function getName(); public function getBuildStepGroupKey() { return HarbormasterOtherBuildStepGroup::GROUPKEY; } /** * The generic description of the implementation. */ public function getGenericDescription() { return ''; } /** * The description of the implementation, based on the current settings. */ public function getDescription() { return $this->getGenericDescription(); } public function getEditInstructions() { return null; } /** * Run the build target against the specified build. */ abstract public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target); /** * Gets the settings for this build step. */ public function getSettings() { return $this->settings; } public function getSetting($key, $default = null) { return idx($this->settings, $key, $default); } /** * Loads the settings for this build step implementation from a build * step or target. */ final public function loadSettings($build_object) { $this->settings = $build_object->getDetails(); return $this; } /** * Return the name of artifacts produced by this command. * * Future steps will calculate all available artifact mappings * before them and filter on the type. * * @return array The mappings of artifact names to their types. */ public function getArtifactInputs() { return array(); } public function getArtifactOutputs() { return array(); } public function getDependencies(HarbormasterBuildStep $build_step) { $dependencies = $build_step->getDetail('dependsOn', array()); $inputs = $build_step->getStepImplementation()->getArtifactInputs(); $inputs = ipull($inputs, null, 'key'); $artifacts = $this->getAvailableArtifacts( $build_step->getBuildPlan(), $build_step, null); foreach ($artifacts as $key => $type) { if (!array_key_exists($key, $inputs)) { unset($artifacts[$key]); } } $artifact_steps = ipull($artifacts, 'step'); $artifact_steps = mpull($artifact_steps, 'getPHID'); $dependencies = array_merge($dependencies, $artifact_steps); return $dependencies; } /** * Returns a list of all artifacts made available in the build plan. */ public static function getAvailableArtifacts( HarbormasterBuildPlan $build_plan, $current_build_step, $artifact_type) { $steps = id(new HarbormasterBuildStepQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuildPlanPHIDs(array($build_plan->getPHID())) ->execute(); $artifacts = array(); $artifact_arrays = array(); foreach ($steps as $step) { if ($current_build_step !== null && $step->getPHID() === $current_build_step->getPHID()) { continue; } $implementation = $step->getStepImplementation(); $array = $implementation->getArtifactOutputs(); $array = ipull($array, 'type', 'key'); foreach ($array as $name => $type) { if ($type !== $artifact_type && $artifact_type !== null) { continue; } $artifacts[$name] = array('type' => $type, 'step' => $step); } } return $artifacts; } /** * Convert a user-provided string with variables in it, like: * * ls ${dirname} * * ...into a string with variables merged into it safely: * * ls 'dir with spaces' * * @param string Name of a `vxsprintf` function, like @{function:vcsprintf}. * @param string User-provided pattern string containing `${variables}`. * @param dict List of available replacement variables. * @return string String with variables replaced safely into it. */ protected function mergeVariables($function, $pattern, array $variables) { $regexp = '@\\$\\{(?P[a-z\\./-]+)\\}@'; $matches = null; preg_match_all($regexp, $pattern, $matches); $argv = array(); foreach ($matches['name'] as $name) { if (!array_key_exists($name, $variables)) { throw new Exception(pht("No such variable '%s'!", $name)); } $argv[] = $variables[$name]; } $pattern = str_replace('%', '%%', $pattern); $pattern = preg_replace($regexp, '%s', $pattern); return call_user_func($function, $pattern, $argv); } public function getFieldSpecifications() { return array(); } protected function formatSettingForDescription($key, $default = null) { return $this->formatValueForDescription($this->getSetting($key, $default)); } protected function formatValueForDescription($value) { if (strlen($value)) { return phutil_tag('strong', array(), $value); } else { return phutil_tag('em', array(), pht('(null)')); } } public function supportsWaitForMessage() { return false; } public function shouldWaitForMessage(HarbormasterBuildTarget $target) { if (!$this->supportsWaitForMessage()) { return false; } $wait = $target->getDetail('builtin.wait-for-message'); return ($wait == 'wait'); } protected function shouldAbort( HarbormasterBuild $build, HarbormasterBuildTarget $target) { return $build->getBuildGeneration() !== $target->getBuildGeneration(); } protected function resolveFutures( HarbormasterBuild $build, HarbormasterBuildTarget $target, array $futures) { $futures = new FutureIterator($futures); foreach ($futures->setUpdateInterval(5) as $key => $future) { if ($future === null) { $build->reload(); if ($this->shouldAbort($build, $target)) { throw new HarbormasterBuildAbortedException(); } } } } protected function logHTTPResponse( HarbormasterBuild $build, HarbormasterBuildTarget $build_target, BaseHTTPFuture $future, $label) { list($status, $body, $headers) = $future->resolve(); $header_lines = array(); // TODO: We don't currently preserve the entire "HTTP" response header, but // should. Once we do, reproduce it here faithfully. $status_code = $status->getStatusCode(); $header_lines[] = "HTTP {$status_code}"; foreach ($headers as $header) { list($head, $tail) = $header; $header_lines[] = "{$head}: {$tail}"; } $header_lines = implode("\n", $header_lines); $build_target ->newLog($label, 'http.head') ->append($header_lines); $build_target ->newLog($label, 'http.body') ->append($body); } + protected function logSilencedCall( + HarbormasterBuild $build, + HarbormasterBuildTarget $build_target, + $label) { + + $build_target + ->newLog($label, 'silenced') + ->append( + pht( + 'Declining to make service call because `phabricator.silent` is '. + 'enabled in configuration.')); + } /* -( Automatic Targets )-------------------------------------------------- */ public function getBuildStepAutotargetStepKey() { return null; } public function getBuildStepAutotargetPlanKey() { throw new PhutilMethodNotImplementedException(); } public function shouldRequireAutotargeting() { return false; } } diff --git a/src/applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php index 3ecd6553da..89d4002eef 100644 --- a/src/applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php @@ -1,210 +1,215 @@ logSilencedCall($build, $build_target, pht('Buildkite')); + throw new HarbormasterBuildFailureException(); + } + $buildable = $build->getBuildable(); $object = $buildable->getBuildableObject(); if (!($object instanceof HarbormasterBuildkiteBuildableInterface)) { throw new Exception( pht('This object does not support builds with Buildkite.')); } $organization = $this->getSetting('organization'); $pipeline = $this->getSetting('pipeline'); $uri = urisprintf( 'https://api.buildkite.com/v2/organizations/%s/pipelines/%s/builds', $organization, $pipeline); $data_structure = array( 'commit' => $object->getBuildkiteCommit(), 'branch' => $object->getBuildkiteBranch(), 'message' => pht( 'Harbormaster Build %s ("%s") for %s', $build->getID(), $build->getName(), $buildable->getMonogram()), 'env' => array( 'HARBORMASTER_BUILD_TARGET_PHID' => $build_target->getPHID(), ), 'meta_data' => array( 'buildTargetPHID' => $build_target->getPHID(), ), ); $json_data = phutil_json_encode($data_structure); $credential_phid = $this->getSetting('token'); $api_token = id(new PassphraseCredentialQuery()) ->setViewer($viewer) ->withPHIDs(array($credential_phid)) ->needSecrets(true) ->executeOne(); if (!$api_token) { throw new Exception( pht( 'Unable to load API token ("%s")!', $credential_phid)); } $token = $api_token->getSecret()->openEnvelope(); $future = id(new HTTPSFuture($uri, $json_data)) ->setMethod('POST') ->addHeader('Content-Type', 'application/json') ->addHeader('Accept', 'application/json') ->addHeader('Authorization', "Bearer {$token}") ->setTimeout(60); $this->resolveFutures( $build, $build_target, array($future)); $this->logHTTPResponse($build, $build_target, $future, pht('Buildkite')); list($status, $body) = $future->resolve(); if ($status->isError()) { throw new HarbormasterBuildFailureException(); } $response = phutil_json_decode($body); $uri_key = 'web_url'; $build_uri = idx($response, $uri_key); if (!$build_uri) { throw new Exception( pht( 'Buildkite did not return a "%s"!', $uri_key)); } $target_phid = $build_target->getPHID(); $api_method = 'harbormaster.createartifact'; $api_params = array( 'buildTargetPHID' => $target_phid, 'artifactType' => HarbormasterURIArtifact::ARTIFACTCONST, 'artifactKey' => 'buildkite.uri', 'artifactData' => array( 'uri' => $build_uri, 'name' => pht('View in Buildkite'), 'ui.external' => true, ), ); id(new ConduitCall($api_method, $api_params)) ->setUser($viewer) ->execute(); } public function getFieldSpecifications() { return array( 'token' => array( 'name' => pht('API Token'), 'type' => 'credential', 'credential.type' => PassphraseTokenCredentialType::CREDENTIAL_TYPE, 'credential.provides' => PassphraseTokenCredentialType::PROVIDES_TYPE, 'required' => true, ), 'organization' => array( 'name' => pht('Organization Name'), 'type' => 'text', 'required' => true, ), 'pipeline' => array( 'name' => pht('Pipeline Name'), 'type' => 'text', 'required' => true, ), 'webhook.token' => array( 'name' => pht('Webhook Token'), 'type' => 'text', 'required' => true, ), ); } public function supportsWaitForMessage() { return false; } public function shouldWaitForMessage(HarbormasterBuildTarget $target) { return true; } } diff --git a/src/applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php index e74e25a395..f0104779ff 100644 --- a/src/applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php @@ -1,253 +1,258 @@ getDomain(); $domain = phutil_utf8_strtolower($domain); switch ($domain) { case 'github.com': case 'www.github.com': return $uri_object->getPath(); default: return null; } } public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { $viewer = PhabricatorUser::getOmnipotentUser(); + if (PhabricatorEnv::getEnvConfig('phabricator.silent')) { + $this->logSilencedCall($build, $build_target, pht('CircleCI')); + throw new HarbormasterBuildFailureException(); + } + $buildable = $build->getBuildable(); $object = $buildable->getBuildableObject(); $object_phid = $object->getPHID(); if (!($object instanceof HarbormasterCircleCIBuildableInterface)) { throw new Exception( pht( 'Object ("%s") does not implement interface "%s". Only objects '. 'which implement this interface can be built with CircleCI.', $object_phid, 'HarbormasterCircleCIBuildableInterface')); } $github_uri = $object->getCircleCIGitHubRepositoryURI(); $build_type = $object->getCircleCIBuildIdentifierType(); $build_identifier = $object->getCircleCIBuildIdentifier(); $path = self::getGitHubPath($github_uri); if ($path === null) { throw new Exception( pht( 'Object ("%s") claims "%s" is a GitHub repository URI, but the '. 'domain does not appear to be GitHub.', $object_phid, $github_uri)); } $path_parts = trim($path, '/'); $path_parts = explode('/', $path_parts); if (count($path_parts) < 2) { throw new Exception( pht( 'Object ("%s") claims "%s" is a GitHub repository URI, but the '. 'path ("%s") does not have enough components (expected at least '. 'two).', $object_phid, $github_uri, $path)); } list($github_namespace, $github_name) = $path_parts; $github_name = preg_replace('(\\.git$)', '', $github_name); $credential_phid = $this->getSetting('token'); $api_token = id(new PassphraseCredentialQuery()) ->setViewer($viewer) ->withPHIDs(array($credential_phid)) ->needSecrets(true) ->executeOne(); if (!$api_token) { throw new Exception( pht( 'Unable to load API token ("%s")!', $credential_phid)); } // When we pass "revision", the branch is ignored (and does not even need // to exist), and only shows up in the UI. Use a cute string which will // certainly never break anything or cause any kind of problem. $ship = "\xF0\x9F\x9A\xA2"; $branch = "{$ship}Harbormaster"; $token = $api_token->getSecret()->openEnvelope(); $parts = array( 'https://circleci.com/api/v1/project', phutil_escape_uri($github_namespace), phutil_escape_uri($github_name)."?circle-token={$token}", ); $uri = implode('/', $parts); $data_structure = array(); switch ($build_type) { case 'tag': $data_structure['tag'] = $build_identifier; break; case 'revision': $data_structure['revision'] = $build_identifier; break; default: throw new Exception( pht( 'Unknown CircleCI build type "%s". Expected "%s" or "%s".', $build_type, 'tag', 'revision')); } $data_structure['build_parameters'] = array( 'HARBORMASTER_BUILD_TARGET_PHID' => $build_target->getPHID(), ); $json_data = phutil_json_encode($data_structure); $future = id(new HTTPSFuture($uri, $json_data)) ->setMethod('POST') ->addHeader('Content-Type', 'application/json') ->addHeader('Accept', 'application/json') ->setTimeout(60); $this->resolveFutures( $build, $build_target, array($future)); $this->logHTTPResponse($build, $build_target, $future, pht('CircleCI')); list($status, $body) = $future->resolve(); if ($status->isError()) { throw new HarbormasterBuildFailureException(); } $response = phutil_json_decode($body); $build_uri = idx($response, 'build_url'); if (!$build_uri) { throw new Exception( pht( 'CircleCI did not return a "%s"!', 'build_url')); } $target_phid = $build_target->getPHID(); // Write an artifact to create a link to the external build in CircleCI. $api_method = 'harbormaster.createartifact'; $api_params = array( 'buildTargetPHID' => $target_phid, 'artifactType' => HarbormasterURIArtifact::ARTIFACTCONST, 'artifactKey' => 'circleci.uri', 'artifactData' => array( 'uri' => $build_uri, 'name' => pht('View in CircleCI'), 'ui.external' => true, ), ); id(new ConduitCall($api_method, $api_params)) ->setUser($viewer) ->execute(); } public function getFieldSpecifications() { return array( 'token' => array( 'name' => pht('API Token'), 'type' => 'credential', 'credential.type' => PassphraseTokenCredentialType::CREDENTIAL_TYPE, 'credential.provides' => PassphraseTokenCredentialType::PROVIDES_TYPE, 'required' => true, ), ); } public function supportsWaitForMessage() { // NOTE: We always wait for a message, but don't need to show the UI // control since "Wait" is the only valid choice. return false; } public function shouldWaitForMessage(HarbormasterBuildTarget $target) { return true; } } diff --git a/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php index 016f29642e..59866d3b73 100644 --- a/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php @@ -1,110 +1,116 @@ getSetting('uri'); if ($uri) { $domain = id(new PhutilURI($uri))->getDomain(); } $method = $this->formatSettingForDescription('method', 'POST'); $domain = $this->formatValueForDescription($domain); if ($this->getSetting('credential')) { return pht( 'Make an authenticated HTTP %s request to %s.', $method, $domain); } else { return pht( 'Make an HTTP %s request to %s.', $method, $domain); } } public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { $viewer = PhabricatorUser::getOmnipotentUser(); + + if (PhabricatorEnv::getEnvConfig('phabricator.silent')) { + $this->logSilencedCall($build, $build_target, pht('HTTP Request')); + throw new HarbormasterBuildFailureException(); + } + $settings = $this->getSettings(); $variables = $build_target->getVariables(); $uri = $this->mergeVariables( 'vurisprintf', $settings['uri'], $variables); $method = nonempty(idx($settings, 'method'), 'POST'); $future = id(new HTTPSFuture($uri)) ->setMethod($method) ->setTimeout(60); $credential_phid = $this->getSetting('credential'); if ($credential_phid) { $key = PassphrasePasswordKey::loadFromPHID( $credential_phid, $viewer); $future->setHTTPBasicAuthCredentials( $key->getUsernameEnvelope()->openEnvelope(), $key->getPasswordEnvelope()); } $this->resolveFutures( $build, $build_target, array($future)); $this->logHTTPResponse($build, $build_target, $future, $uri); list($status) = $future->resolve(); if ($status->isError()) { throw new HarbormasterBuildFailureException(); } } public function getFieldSpecifications() { return array( 'uri' => array( 'name' => pht('URI'), 'type' => 'text', 'required' => true, ), 'method' => array( 'name' => pht('HTTP Method'), 'type' => 'select', 'options' => array_fuse(array('POST', 'GET', 'PUT', 'DELETE')), ), 'credential' => array( 'name' => pht('Credentials'), 'type' => 'credential', 'credential.type' => PassphrasePasswordCredentialType::CREDENTIAL_TYPE, 'credential.provides' => PassphrasePasswordCredentialType::PROVIDES_TYPE, ), ); } public function supportsWaitForMessage() { return true; } }