@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Integrate Harbormaster with Buildkite

Summary: Ref T12173. This might need some additional work but the basics seem like they're in good shape.

Test Plan:
- Buildkite is "bring your own hardware", so you need to launch a host to test anything.
- Launched a host in AWS.
- Configured Buildkite to use that host to run builds.
- Added a Buildkite build step to a new Harbormaster build plan.
- Used `bin/harbormaster build ...` to run the plan.
- Saw buildkite execute builds and report status back to Harbormaster

{F2553076}

{F2553077}

Reviewers: chad

Reviewed By: chad

Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam

Maniphest Tasks: T12173

Differential Revision: https://secure.phabricator.com/D17270

+326
+4
src/__phutil_library_map__.php
··· 1210 1210 'HarbormasterBuildableTransactionEditor' => 'applications/harbormaster/editor/HarbormasterBuildableTransactionEditor.php', 1211 1211 'HarbormasterBuildableTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildableTransactionQuery.php', 1212 1212 'HarbormasterBuildableViewController' => 'applications/harbormaster/controller/HarbormasterBuildableViewController.php', 1213 + 'HarbormasterBuildkiteBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php', 1214 + 'HarbormasterBuildkiteHookController' => 'applications/harbormaster/controller/HarbormasterBuildkiteHookController.php', 1213 1215 'HarbormasterBuiltinBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterBuiltinBuildStepGroup.php', 1214 1216 'HarbormasterCircleCIBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php', 1215 1217 'HarbormasterCircleCIBuildableInterface' => 'applications/harbormaster/interface/HarbormasterCircleCIBuildableInterface.php', ··· 6017 6019 'HarbormasterBuildableTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 6018 6020 'HarbormasterBuildableTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 6019 6021 'HarbormasterBuildableViewController' => 'HarbormasterController', 6022 + 'HarbormasterBuildkiteBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 6023 + 'HarbormasterBuildkiteHookController' => 'HarbormasterController', 6020 6024 'HarbormasterBuiltinBuildStepGroup' => 'HarbormasterBuildStepGroup', 6021 6025 'HarbormasterCircleCIBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 6022 6026 'HarbormasterCircleCIHookController' => 'HarbormasterController',
+1
src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php
··· 94 94 ), 95 95 'hook/' => array( 96 96 'circleci/' => 'HarbormasterCircleCIHookController', 97 + 'buildkite/' => 'HarbormasterBuildkiteHookController', 97 98 ), 98 99 ), 99 100 );
+111
src/applications/harbormaster/controller/HarbormasterBuildkiteHookController.php
··· 1 + <?php 2 + 3 + final class HarbormasterBuildkiteHookController 4 + extends HarbormasterController { 5 + 6 + public function shouldRequireLogin() { 7 + return false; 8 + } 9 + 10 + /** 11 + * @phutil-external-symbol class PhabricatorStartup 12 + */ 13 + public function handleRequest(AphrontRequest $request) { 14 + $raw_body = PhabricatorStartup::getRawInput(); 15 + $body = phutil_json_decode($raw_body); 16 + 17 + $event = idx($body, 'event'); 18 + if ($event != 'build.finished') { 19 + return $this->newHookResponse(pht('OK: Ignored event.')); 20 + } 21 + 22 + $build = idx($body, 'build'); 23 + if (!is_array($build)) { 24 + throw new Exception( 25 + pht( 26 + 'Expected "%s" property to contain a dictionary.', 27 + 'build')); 28 + } 29 + 30 + $meta_data = idx($build, 'meta_data'); 31 + if (!is_array($meta_data)) { 32 + throw new Exception( 33 + pht( 34 + 'Expected "%s" property to contain a dictionary.', 35 + 'build.meta_data')); 36 + } 37 + 38 + $target_phid = idx($meta_data, 'buildTargetPHID'); 39 + if (!$target_phid) { 40 + return $this->newHookResponse(pht('OK: No Harbormaster target PHID.')); 41 + } 42 + 43 + $viewer = PhabricatorUser::getOmnipotentUser(); 44 + $target = id(new HarbormasterBuildTargetQuery()) 45 + ->setViewer($viewer) 46 + ->withPHIDs(array($target_phid)) 47 + ->needBuildSteps(true) 48 + ->executeOne(); 49 + if (!$target) { 50 + throw new Exception( 51 + pht( 52 + 'Harbormaster build target "%s" does not exist.', 53 + $target_phid)); 54 + } 55 + 56 + $step = $target->getBuildStep(); 57 + $impl = $step->getStepImplementation(); 58 + if (!($impl instanceof HarbormasterBuildkiteBuildStepImplementation)) { 59 + throw new Exception( 60 + pht( 61 + 'Harbormaster build target "%s" is not a Buildkite build step. '. 62 + 'Only Buildkite steps may be updated via the Buildkite hook.', 63 + $target_phid)); 64 + } 65 + 66 + $webhook_token = $impl->getSetting('webhook.token'); 67 + $request_token = $request->getHTTPHeader('X-Buildkite-Token'); 68 + 69 + if (!phutil_hashes_are_identical($webhook_token, $request_token)) { 70 + throw new Exception( 71 + pht( 72 + 'Buildkite request to target "%s" had the wrong authentication '. 73 + 'token. The Buildkite pipeline and Harbormaster build step must '. 74 + 'be configured with the same token.', 75 + $target_phid)); 76 + } 77 + 78 + $state = idx($build, 'state'); 79 + switch ($state) { 80 + case 'passed': 81 + $message_type = HarbormasterMessageType::MESSAGE_PASS; 82 + break; 83 + default: 84 + $message_type = HarbormasterMessageType::MESSAGE_FAIL; 85 + break; 86 + } 87 + 88 + $api_method = 'harbormaster.sendmessage'; 89 + $api_params = array( 90 + 'buildTargetPHID' => $target_phid, 91 + 'type' => $message_type, 92 + ); 93 + 94 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 95 + 96 + id(new ConduitCall($api_method, $api_params)) 97 + ->setUser($viewer) 98 + ->execute(); 99 + 100 + unset($unguarded); 101 + 102 + return $this->newHookResponse(pht('OK: Processed event.')); 103 + } 104 + 105 + private function newHookResponse($message) { 106 + $response = new AphrontWebpageResponse(); 107 + $response->setContent($message); 108 + return $response; 109 + } 110 + 111 + }
+210
src/applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php
··· 1 + <?php 2 + 3 + final class HarbormasterBuildkiteBuildStepImplementation 4 + extends HarbormasterBuildStepImplementation { 5 + 6 + public function getName() { 7 + return pht('Build with Buildkite'); 8 + } 9 + 10 + public function getGenericDescription() { 11 + return pht('Trigger a build in Buildkite.'); 12 + } 13 + 14 + public function getBuildStepGroupKey() { 15 + return HarbormasterExternalBuildStepGroup::GROUPKEY; 16 + } 17 + 18 + public function getDescription() { 19 + return pht('Run a build in Buildkite.'); 20 + } 21 + 22 + public function getEditInstructions() { 23 + $hook_uri = '/harbormaster/hook/buildkite/'; 24 + $hook_uri = PhabricatorEnv::getProductionURI($hook_uri); 25 + 26 + return pht(<<<EOTEXT 27 + WARNING: This build step is new and experimental! 28 + 29 + To build **revisions** with Buildkite, they must: 30 + 31 + - belong to a tracked repository; 32 + - the repository must have a Staging Area configured; 33 + - you must configure a Buildkite pipeline for that Staging Area; and 34 + - you must configure the webhook described below. 35 + 36 + To build **commits** with Buildkite, they must: 37 + 38 + - belong to a tracked repository; 39 + - you must configure a Buildkite pipeline for that repository; and 40 + - you must configure the webhook described below. 41 + 42 + Webhook Configuration 43 + ===================== 44 + 45 + In {nav Settings} for your Organization in Buildkite, under 46 + {nav Notification Services}, add a new **Webook Notification**. 47 + 48 + Use these settings: 49 + 50 + - **Webhook URL**: %s 51 + - **Token**: The "Webhook Token" field below and the "Token" field in 52 + Buildkite should both be set to the same nonempty value (any random 53 + secret). You can use copy/paste the value Buildkite generates into 54 + this form. 55 + - **Events**: Only **build.finish** needs to be active. 56 + 57 + Environment 58 + =========== 59 + 60 + These variables will be available in the build environment: 61 + 62 + | Variable | Description | 63 + |----------|-------------| 64 + | `HARBORMASTER_BUILD_TARGET_PHID` | PHID of the Build Target. 65 + EOTEXT 66 + , 67 + $hook_uri); 68 + } 69 + 70 + public function execute( 71 + HarbormasterBuild $build, 72 + HarbormasterBuildTarget $build_target) { 73 + $viewer = PhabricatorUser::getOmnipotentUser(); 74 + 75 + $buildable = $build->getBuildable(); 76 + 77 + $object = $buildable->getBuildableObject(); 78 + if (!($object instanceof HarbormasterCircleCIBuildableInterface)) { 79 + throw new Exception( 80 + pht('This object does not support builds with Buildkite.')); 81 + } 82 + 83 + $organization = $this->getSetting('organization'); 84 + $pipeline = $this->getSetting('pipeline'); 85 + 86 + $uri = urisprintf( 87 + 'https://api.buildkite.com/v2/organizations/%s/pipelines/%s/builds', 88 + $organization, 89 + $pipeline); 90 + 91 + $data_structure = array( 92 + 'commit' => $object->getCircleCIBuildIdentifier(), 93 + 'branch' => 'master', 94 + 'message' => pht( 95 + 'Harbormaster Build %s ("%s") for %s', 96 + $build->getID(), 97 + $build->getName(), 98 + $buildable->getMonogram()), 99 + 'env' => array( 100 + 'HARBORMASTER_BUILD_TARGET_PHID' => $build_target->getPHID(), 101 + ), 102 + 'meta_data' => array( 103 + 'buildTargetPHID' => $build_target->getPHID(), 104 + ), 105 + ); 106 + 107 + $json_data = phutil_json_encode($data_structure); 108 + 109 + $credential_phid = $this->getSetting('token'); 110 + $api_token = id(new PassphraseCredentialQuery()) 111 + ->setViewer($viewer) 112 + ->withPHIDs(array($credential_phid)) 113 + ->needSecrets(true) 114 + ->executeOne(); 115 + if (!$api_token) { 116 + throw new Exception( 117 + pht( 118 + 'Unable to load API token ("%s")!', 119 + $credential_phid)); 120 + } 121 + 122 + $token = $api_token->getSecret()->openEnvelope(); 123 + 124 + $future = id(new HTTPSFuture($uri, $json_data)) 125 + ->setMethod('POST') 126 + ->addHeader('Content-Type', 'application/json') 127 + ->addHeader('Accept', 'application/json') 128 + ->addHeader('Authorization', "Bearer {$token}") 129 + ->setTimeout(60); 130 + 131 + $this->resolveFutures( 132 + $build, 133 + $build_target, 134 + array($future)); 135 + 136 + $this->logHTTPResponse($build, $build_target, $future, pht('Buildkite')); 137 + 138 + list($status, $body) = $future->resolve(); 139 + if ($status->isError()) { 140 + throw new HarbormasterBuildFailureException(); 141 + } 142 + 143 + $response = phutil_json_decode($body); 144 + 145 + $uri_key = 'web_url'; 146 + $build_uri = idx($response, $uri_key); 147 + if (!$build_uri) { 148 + throw new Exception( 149 + pht( 150 + 'Buildkite did not return a "%s"!', 151 + $uri_key)); 152 + } 153 + 154 + $target_phid = $build_target->getPHID(); 155 + 156 + $api_method = 'harbormaster.createartifact'; 157 + $api_params = array( 158 + 'buildTargetPHID' => $target_phid, 159 + 'artifactType' => HarbormasterURIArtifact::ARTIFACTCONST, 160 + 'artifactKey' => 'buildkite.uri', 161 + 'artifactData' => array( 162 + 'uri' => $build_uri, 163 + 'name' => pht('View in Buildkite'), 164 + 'ui.external' => true, 165 + ), 166 + ); 167 + 168 + id(new ConduitCall($api_method, $api_params)) 169 + ->setUser($viewer) 170 + ->execute(); 171 + } 172 + 173 + public function getFieldSpecifications() { 174 + return array( 175 + 'token' => array( 176 + 'name' => pht('API Token'), 177 + 'type' => 'credential', 178 + 'credential.type' 179 + => PassphraseTokenCredentialType::CREDENTIAL_TYPE, 180 + 'credential.provides' 181 + => PassphraseTokenCredentialType::PROVIDES_TYPE, 182 + 'required' => true, 183 + ), 184 + 'organization' => array( 185 + 'name' => pht('Organization Name'), 186 + 'type' => 'text', 187 + 'required' => true, 188 + ), 189 + 'pipeline' => array( 190 + 'name' => pht('Pipeline Name'), 191 + 'type' => 'text', 192 + 'required' => true, 193 + ), 194 + 'webhook.token' => array( 195 + 'name' => pht('Webhook Token'), 196 + 'type' => 'text', 197 + 'required' => true, 198 + ), 199 + ); 200 + } 201 + 202 + public function supportsWaitForMessage() { 203 + return false; 204 + } 205 + 206 + public function shouldWaitForMessage(HarbormasterBuildTarget $target) { 207 + return true; 208 + } 209 + 210 + }