Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15335506
D17270.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
12 KB
Referenced Files
None
Subscribers
None
D17270.diff
View Options
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 @@
+<?php
+
+final class HarbormasterBuildkiteHookController
+ extends HarbormasterController {
+
+ public function shouldRequireLogin() {
+ return false;
+ }
+
+ /**
+ * @phutil-external-symbol class PhabricatorStartup
+ */
+ public function handleRequest(AphrontRequest $request) {
+ $raw_body = PhabricatorStartup::getRawInput();
+ $body = phutil_json_decode($raw_body);
+
+ $event = idx($body, 'event');
+ if ($event != 'build.finished') {
+ return $this->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 @@
+<?php
+
+final class HarbormasterBuildkiteBuildStepImplementation
+ extends HarbormasterBuildStepImplementation {
+
+ public function getName() {
+ return pht('Build with Buildkite');
+ }
+
+ public function getGenericDescription() {
+ return pht('Trigger a build in Buildkite.');
+ }
+
+ public function getBuildStepGroupKey() {
+ return HarbormasterExternalBuildStepGroup::GROUPKEY;
+ }
+
+ public function getDescription() {
+ return pht('Run a build in Buildkite.');
+ }
+
+ public function getEditInstructions() {
+ $hook_uri = '/harbormaster/hook/buildkite/';
+ $hook_uri = PhabricatorEnv::getProductionURI($hook_uri);
+
+ return pht(<<<EOTEXT
+WARNING: This build step is new and experimental!
+
+To build **revisions** with Buildkite, they must:
+
+ - belong to a tracked repository;
+ - the repository must have a Staging Area configured;
+ - you must configure a Buildkite pipeline for that Staging Area; and
+ - you must configure the webhook described below.
+
+To build **commits** with Buildkite, they must:
+
+ - belong to a tracked repository;
+ - you must configure a Buildkite pipeline for that repository; and
+ - you must configure the webhook described below.
+
+Webhook Configuration
+=====================
+
+In {nav Settings} for your Organization in Buildkite, under
+{nav Notification Services}, add a new **Webook Notification**.
+
+Use these settings:
+
+ - **Webhook URL**: %s
+ - **Token**: The "Webhook Token" field below and the "Token" field in
+ Buildkite should both be set to the same nonempty value (any random
+ secret). You can use copy/paste the value Buildkite generates into
+ this form.
+ - **Events**: Only **build.finish** needs to be active.
+
+Environment
+===========
+
+These variables will be available in the build environment:
+
+| Variable | Description |
+|----------|-------------|
+| `HARBORMASTER_BUILD_TARGET_PHID` | PHID of the Build Target.
+EOTEXT
+ ,
+ $hook_uri);
+ }
+
+ public function execute(
+ HarbormasterBuild $build,
+ HarbormasterBuildTarget $build_target) {
+ $viewer = PhabricatorUser::getOmnipotentUser();
+
+ $buildable = $build->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;
+ }
+
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sun, Mar 9, 4:06 PM (7 h, 39 m ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7389642
Default Alt Text
D17270.diff (12 KB)
Attached To
Mode
D17270: Integrate Harbormaster with Buildkite
Attached
Detach File
Event Timeline
Log In to Comment