diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1210,6 +1210,8 @@ 'HarbormasterBuildableTransactionEditor' => 'applications/harbormaster/editor/HarbormasterBuildableTransactionEditor.php', 'HarbormasterBuildableTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildableTransactionQuery.php', 'HarbormasterBuildableViewController' => 'applications/harbormaster/controller/HarbormasterBuildableViewController.php', + 'HarbormasterBuildkiteBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php', + 'HarbormasterBuildkiteHookController' => 'applications/harbormaster/controller/HarbormasterBuildkiteHookController.php', 'HarbormasterBuiltinBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterBuiltinBuildStepGroup.php', 'HarbormasterCircleCIBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php', 'HarbormasterCircleCIBuildableInterface' => 'applications/harbormaster/interface/HarbormasterCircleCIBuildableInterface.php', @@ -6017,6 +6019,8 @@ 'HarbormasterBuildableTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'HarbormasterBuildableTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'HarbormasterBuildableViewController' => 'HarbormasterController', + 'HarbormasterBuildkiteBuildStepImplementation' => 'HarbormasterBuildStepImplementation', + 'HarbormasterBuildkiteHookController' => 'HarbormasterController', 'HarbormasterBuiltinBuildStepGroup' => 'HarbormasterBuildStepGroup', 'HarbormasterCircleCIBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterCircleCIHookController' => 'HarbormasterController', diff --git a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php --- a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php +++ b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php @@ -94,6 +94,7 @@ ), 'hook/' => array( 'circleci/' => 'HarbormasterCircleCIHookController', + 'buildkite/' => 'HarbormasterBuildkiteHookController', ), ), ); diff --git a/src/applications/harbormaster/controller/HarbormasterBuildkiteHookController.php b/src/applications/harbormaster/controller/HarbormasterBuildkiteHookController.php new file mode 100644 --- /dev/null +++ b/src/applications/harbormaster/controller/HarbormasterBuildkiteHookController.php @@ -0,0 +1,111 @@ +newHookResponse(pht('OK: Ignored event.')); + } + + $build = idx($body, 'build'); + if (!is_array($build)) { + throw new Exception( + pht( + 'Expected "%s" property to contain a dictionary.', + 'build')); + } + + $meta_data = idx($build, 'meta_data'); + if (!is_array($meta_data)) { + throw new Exception( + pht( + 'Expected "%s" property to contain a dictionary.', + 'build.meta_data')); + } + + $target_phid = idx($meta_data, 'buildTargetPHID'); + if (!$target_phid) { + return $this->newHookResponse(pht('OK: No Harbormaster target PHID.')); + } + + $viewer = PhabricatorUser::getOmnipotentUser(); + $target = id(new HarbormasterBuildTargetQuery()) + ->setViewer($viewer) + ->withPHIDs(array($target_phid)) + ->needBuildSteps(true) + ->executeOne(); + if (!$target) { + throw new Exception( + pht( + 'Harbormaster build target "%s" does not exist.', + $target_phid)); + } + + $step = $target->getBuildStep(); + $impl = $step->getStepImplementation(); + if (!($impl instanceof HarbormasterBuildkiteBuildStepImplementation)) { + throw new Exception( + pht( + 'Harbormaster build target "%s" is not a Buildkite build step. '. + 'Only Buildkite steps may be updated via the Buildkite hook.', + $target_phid)); + } + + $webhook_token = $impl->getSetting('webhook.token'); + $request_token = $request->getHTTPHeader('X-Buildkite-Token'); + + if (!phutil_hashes_are_identical($webhook_token, $request_token)) { + throw new Exception( + pht( + 'Buildkite request to target "%s" had the wrong authentication '. + 'token. The Buildkite pipeline and Harbormaster build step must '. + 'be configured with the same token.', + $target_phid)); + } + + $state = idx($build, 'state'); + switch ($state) { + case 'passed': + $message_type = HarbormasterMessageType::MESSAGE_PASS; + break; + default: + $message_type = HarbormasterMessageType::MESSAGE_FAIL; + break; + } + + $api_method = 'harbormaster.sendmessage'; + $api_params = array( + 'buildTargetPHID' => $target_phid, + 'type' => $message_type, + ); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + + id(new ConduitCall($api_method, $api_params)) + ->setUser($viewer) + ->execute(); + + unset($unguarded); + + return $this->newHookResponse(pht('OK: Processed event.')); + } + + private function newHookResponse($message) { + $response = new AphrontWebpageResponse(); + $response->setContent($message); + return $response; + } + +} diff --git a/src/applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php new file mode 100644 --- /dev/null +++ b/src/applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php @@ -0,0 +1,210 @@ +getBuildable(); + + $object = $buildable->getBuildableObject(); + if (!($object instanceof HarbormasterCircleCIBuildableInterface)) { + 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->getCircleCIBuildIdentifier(), + 'branch' => 'master', + '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; + } + +}