@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.

Add a "Build with CircleCI" build step

Summary: Ref T9456. Some rough edges and we can't complete the build yet since I haven't written a webhook, but this mostly seems to be working.

Test Plan:
- Ran this build on some stuff.
- Ran a normal HTTP step build to make sure I didn't break that.

{F880301}

{F880302}

{F880303}

Reviewers: chad

Reviewed By: chad

Subscribers: JustinTulloss, joshma

Maniphest Tasks: T9456

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

+427 -29
+5
src/__phutil_library_map__.php
··· 1106 1106 'HarbormasterBuildableTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildableTransactionQuery.php', 1107 1107 'HarbormasterBuildableViewController' => 'applications/harbormaster/controller/HarbormasterBuildableViewController.php', 1108 1108 'HarbormasterBuiltinBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterBuiltinBuildStepGroup.php', 1109 + 'HarbormasterCircleCIBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php', 1110 + 'HarbormasterCircleCIBuildableInterface' => 'applications/harbormaster/interface/HarbormasterCircleCIBuildableInterface.php', 1109 1111 'HarbormasterConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php', 1110 1112 'HarbormasterController' => 'applications/harbormaster/controller/HarbormasterController.php', 1111 1113 'HarbormasterCreateArtifactConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php', ··· 4517 4519 'DifferentialDAO', 4518 4520 'PhabricatorPolicyInterface', 4519 4521 'HarbormasterBuildableInterface', 4522 + 'HarbormasterCircleCIBuildableInterface', 4520 4523 'PhabricatorApplicationTransactionInterface', 4521 4524 'PhabricatorDestructibleInterface', 4522 4525 ), ··· 5334 5337 'HarbormasterBuildableTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 5335 5338 'HarbormasterBuildableViewController' => 'HarbormasterController', 5336 5339 'HarbormasterBuiltinBuildStepGroup' => 'HarbormasterBuildStepGroup', 5340 + 'HarbormasterCircleCIBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 5337 5341 'HarbormasterConduitAPIMethod' => 'ConduitAPIMethod', 5338 5342 'HarbormasterController' => 'PhabricatorController', 5339 5343 'HarbormasterCreateArtifactConduitAPIMethod' => 'HarbormasterConduitAPIMethod', ··· 7652 7656 'PhabricatorSubscribableInterface', 7653 7657 'PhabricatorMentionableInterface', 7654 7658 'HarbormasterBuildableInterface', 7659 + 'HarbormasterCircleCIBuildableInterface', 7655 7660 'PhabricatorCustomFieldInterface', 7656 7661 'PhabricatorApplicationTransactionInterface', 7657 7662 'PhabricatorFulltextInterface',
+67
src/applications/differential/storage/DifferentialDiff.php
··· 5 5 implements 6 6 PhabricatorPolicyInterface, 7 7 HarbormasterBuildableInterface, 8 + HarbormasterCircleCIBuildableInterface, 8 9 PhabricatorApplicationTransactionInterface, 9 10 PhabricatorDestructibleInterface { 10 11 ··· 522 523 'repository.staging.ref' => 523 524 pht('The ref name for this change in the staging repository.'), 524 525 ); 526 + } 527 + 528 + 529 + /* -( HarbormasterCircleCIBuildableInterface )----------------------------- */ 530 + 531 + 532 + public function getCircleCIGitHubRepositoryURI() { 533 + $diff_phid = $this->getPHID(); 534 + $repository_phid = $this->getRepositoryPHID(); 535 + if (!$repository_phid) { 536 + throw new Exception( 537 + pht( 538 + 'This diff ("%s") is not associated with a repository. A diff '. 539 + 'must belong to a tracked repository to be built by CircleCI.', 540 + $diff_phid)); 541 + } 542 + 543 + $repository = id(new PhabricatorRepositoryQuery()) 544 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 545 + ->withPHIDs(array($repository_phid)) 546 + ->executeOne(); 547 + if (!$repository) { 548 + throw new Exception( 549 + pht( 550 + 'This diff ("%s") is associated with a repository ("%s") which '. 551 + 'could not be loaded.', 552 + $diff_phid, 553 + $repository_phid)); 554 + } 555 + 556 + $staging_uri = $repository->getStagingURI(); 557 + if (!$staging_uri) { 558 + throw new Exception( 559 + pht( 560 + 'This diff ("%s") is associated with a repository ("%s") that '. 561 + 'does not have a Staging Area configured. You must configure a '. 562 + 'Staging Area to use CircleCI integration.', 563 + $diff_phid, 564 + $repository_phid)); 565 + } 566 + 567 + $path = HarbormasterCircleCIBuildStepImplementation::getGitHubPath( 568 + $staging_uri); 569 + if (!$path) { 570 + throw new Exception( 571 + pht( 572 + 'This diff ("%s") is associated with a repository ("%s") that '. 573 + 'does not have a Staging Area ("%s") that is hosted on GitHub. '. 574 + 'CircleCI can only build from GitHub, so the Staging Area for '. 575 + 'the repository must be hosted there.', 576 + $diff_phid, 577 + $repository_phid, 578 + $staging_uri)); 579 + } 580 + 581 + return $staging_uri; 582 + } 583 + 584 + public function getCircleCIBuildIdentifierType() { 585 + return 'tag'; 586 + } 587 + 588 + public function getCircleCIBuildIdentifier() { 589 + $ref = $this->getStagingRef(); 590 + $ref = preg_replace('(^refs/tags/)', '', $ref); 591 + return $ref; 525 592 } 526 593 527 594 public function getStagingRef() {
+13 -7
src/applications/harbormaster/controller/HarbormasterStepEditController.php
··· 136 136 } 137 137 138 138 $form = id(new AphrontFormView()) 139 - ->setUser($viewer) 140 - ->appendChild( 141 - id(new AphrontFormTextControl()) 142 - ->setName('name') 143 - ->setLabel(pht('Name')) 144 - ->setError($e_name) 145 - ->setValue($v_name)); 139 + ->setUser($viewer); 140 + 141 + $instructions = $implementation->getEditInstructions(); 142 + if (strlen($instructions)) { 143 + $form->appendRemarkupInstructions($instructions); 144 + } 145 + 146 + $form->appendChild( 147 + id(new AphrontFormTextControl()) 148 + ->setName('name') 149 + ->setLabel(pht('Name')) 150 + ->setError($e_name) 151 + ->setValue($v_name)); 146 152 147 153 $form->appendChild(id(new AphrontFormDividerControl())); 148 154
+12
src/applications/harbormaster/interface/HarbormasterCircleCIBuildableInterface.php
··· 1 + <?php 2 + 3 + /** 4 + * Support for CircleCI. 5 + */ 6 + interface HarbormasterCircleCIBuildableInterface { 7 + 8 + public function getCircleCIGitHubRepositoryURI(); 9 + public function getCircleCIBuildIdentifierType(); 10 + public function getCircleCIBuildIdentifier(); 11 + 12 + }
+35
src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php
··· 69 69 return $this->getGenericDescription(); 70 70 } 71 71 72 + public function getEditInstructions() { 73 + return null; 74 + } 75 + 72 76 /** 73 77 * Run the build target against the specified build. 74 78 */ ··· 264 268 } 265 269 266 270 } 271 + 272 + protected function logHTTPResponse( 273 + HarbormasterBuild $build, 274 + HarbormasterBuildTarget $build_target, 275 + BaseHTTPFuture $future, 276 + $label) { 277 + 278 + list($status, $body, $headers) = $future->resolve(); 279 + 280 + $header_lines = array(); 281 + 282 + // TODO: We don't currently preserve the entire "HTTP" response header, but 283 + // should. Once we do, reproduce it here faithfully. 284 + $status_code = $status->getStatusCode(); 285 + $header_lines[] = "HTTP {$status_code}"; 286 + 287 + foreach ($headers as $header) { 288 + list($head, $tail) = $header; 289 + $header_lines[] = "{$head}: {$tail}"; 290 + } 291 + $header_lines = implode("\n", $header_lines); 292 + 293 + $build_target 294 + ->newLog($label, 'http.head') 295 + ->append($header_lines); 296 + 297 + $build_target 298 + ->newLog($label, 'http.body') 299 + ->append($body); 300 + } 301 + 267 302 268 303 269 304 /* -( Automatic Targets )-------------------------------------------------- */
+246
src/applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php
··· 1 + <?php 2 + 3 + final class HarbormasterCircleCIBuildStepImplementation 4 + extends HarbormasterBuildStepImplementation { 5 + 6 + public function getName() { 7 + return pht('Build with CircleCI'); 8 + } 9 + 10 + public function getGenericDescription() { 11 + return pht('Trigger a build in CircleCI.'); 12 + } 13 + 14 + public function getBuildStepGroupKey() { 15 + return HarbormasterExternalBuildStepGroup::GROUPKEY; 16 + } 17 + 18 + public function getDescription() { 19 + return pht('Run a build in CircleCI.'); 20 + } 21 + 22 + public function getEditInstructions() { 23 + return pht(<<<EOTEXT 24 + WARNING: This build step is new and experimental! 25 + 26 + To build **revisions** with CircleCI, they must: 27 + 28 + - belong to a tracked repository; 29 + - the repository must have a Staging Area configured; 30 + - the Staging Area must be hosted on GitHub; and 31 + - you must configure the webhook described below. 32 + 33 + To build **commits** with CircleCI, they must: 34 + 35 + - belong to a repository that is being imported from GitHub; and 36 + - you must configure the webhook described below. 37 + 38 + Webhook Configuration 39 + ===================== 40 + 41 + IMPORTANT: This has not been implemented yet. 42 + 43 + Environment 44 + =========== 45 + 46 + These variables will be available in the build environment: 47 + 48 + | Variable | Description | 49 + |----------|-------------| 50 + | `HARBORMASTER_BUILD_TARGET_PHID` | PHID of the Build Target. 51 + 52 + EOTEXT 53 + ); 54 + } 55 + 56 + public static function getGitHubPath($uri) { 57 + $uri_object = new PhutilURI($uri); 58 + $domain = $uri_object->getDomain(); 59 + 60 + if (!strlen($domain)) { 61 + $uri_object = new PhutilGitURI($uri); 62 + $domain = $uri_object->getDomain(); 63 + } 64 + 65 + $domain = phutil_utf8_strtolower($domain); 66 + switch ($domain) { 67 + case 'github.com': 68 + case 'www.github.com': 69 + return $uri_object->getPath(); 70 + default: 71 + return null; 72 + } 73 + } 74 + 75 + public function execute( 76 + HarbormasterBuild $build, 77 + HarbormasterBuildTarget $build_target) { 78 + $viewer = PhabricatorUser::getOmnipotentUser(); 79 + 80 + $buildable = $build->getBuildable(); 81 + 82 + $object = $buildable->getBuildableObject(); 83 + $object_phid = $object->getPHID(); 84 + if (!($object instanceof HarbormasterCircleCIBuildableInterface)) { 85 + throw new Exception( 86 + pht( 87 + 'Object ("%s") does not implement interface "%s". Only objects '. 88 + 'which implement this interface can be built with CircleCI.', 89 + $object_phid, 90 + 'HarbormasterCircleCIBuildableInterface')); 91 + } 92 + 93 + $github_uri = $object->getCircleCIGitHubRepositoryURI(); 94 + $build_type = $object->getCircleCIBuildIdentifierType(); 95 + $build_identifier = $object->getCircleCIBuildIdentifier(); 96 + 97 + $path = self::getGitHubPath($github_uri); 98 + if ($path === null) { 99 + throw new Exception( 100 + pht( 101 + 'Object ("%s") claims "%s" is a GitHub repository URI, but the '. 102 + 'domain does not appear to be GitHub.', 103 + $object_phid, 104 + $github_uri)); 105 + } 106 + 107 + $path_parts = trim($path, '/'); 108 + $path_parts = explode('/', $path_parts); 109 + if (count($path_parts) < 2) { 110 + throw new Exception( 111 + pht( 112 + 'Object ("%s") claims "%s" is a GitHub repository URI, but the '. 113 + 'path ("%s") does not have enough components (expected at least '. 114 + 'two).', 115 + $object_phid, 116 + $github_uri, 117 + $path)); 118 + } 119 + 120 + list($github_namespace, $github_name) = $path_parts; 121 + $github_name = preg_replace('(\\.git$)', '', $github_name); 122 + 123 + $credential_phid = $this->getSetting('token'); 124 + $api_token = id(new PassphraseCredentialQuery()) 125 + ->setViewer($viewer) 126 + ->withPHIDs(array($credential_phid)) 127 + ->needSecrets(true) 128 + ->executeOne(); 129 + if (!$api_token) { 130 + throw new Exception( 131 + pht( 132 + 'Unable to load API token ("%s")!', 133 + $credential_phid)); 134 + } 135 + 136 + // When we pass "revision", the branch is ignored (and does not even need 137 + // to exist), and only shows up in the UI. Use a cute string which will 138 + // certainly never break anything or cause any kind of problem. 139 + $ship = "\xF0\x9F\x9A\xA2"; 140 + $branch = "{$ship}Harbormaster"; 141 + 142 + $token = $api_token->getSecret()->openEnvelope(); 143 + $parts = array( 144 + 'https://circleci.com/api/v1/project', 145 + phutil_escape_uri($github_namespace), 146 + phutil_escape_uri($github_name)."?circle-token={$token}", 147 + ); 148 + 149 + $uri = implode('/', $parts); 150 + 151 + $data_structure = array(); 152 + switch ($build_type) { 153 + case 'tag': 154 + $data_structure['tag'] = $build_identifier; 155 + break; 156 + case 'revision': 157 + $data_structure['revision'] = $build_identifier; 158 + break; 159 + default: 160 + throw new Exception( 161 + pht( 162 + 'Unknown CircleCI build type "%s". Expected "%s" or "%s".', 163 + $build_type, 164 + 'tag', 165 + 'revision')); 166 + } 167 + 168 + $data_structure['build_parameters'] = array( 169 + 'HARBORMASTER_BUILD_TARGET_PHID' => $build_target->getPHID(), 170 + ); 171 + 172 + $json_data = phutil_json_encode($data_structure); 173 + 174 + $future = id(new HTTPSFuture($uri, $json_data)) 175 + ->setMethod('POST') 176 + ->addHeader('Content-Type', 'application/json') 177 + ->addHeader('Accept', 'application/json') 178 + ->setTimeout(60); 179 + 180 + $this->resolveFutures( 181 + $build, 182 + $build_target, 183 + array($future)); 184 + 185 + $this->logHTTPResponse($build, $build_target, $future, pht('CircleCI')); 186 + 187 + list($status, $body) = $future->resolve(); 188 + if ($status->isError()) { 189 + throw new HarbormasterBuildFailureException(); 190 + } 191 + 192 + $response = phutil_json_decode($body); 193 + $build_uri = idx($response, 'build_url'); 194 + if (!$build_uri) { 195 + throw new Exception( 196 + pht( 197 + 'CircleCI did not return a "%s"!', 198 + 'build_url')); 199 + } 200 + 201 + $target_phid = $build_target->getPHID(); 202 + 203 + // Write an artifact to create a link to the external build in CircleCI. 204 + 205 + $api_method = 'harbormaster.createartifact'; 206 + $api_params = array( 207 + 'buildTargetPHID' => $target_phid, 208 + 'artifactType' => HarbormasterURIArtifact::ARTIFACTCONST, 209 + 'artifactKey' => 'circleci.uri', 210 + 'artifactData' => array( 211 + 'uri' => $build_uri, 212 + 'name' => pht('View in CircleCI'), 213 + 'ui.external' => true, 214 + ), 215 + ); 216 + 217 + id(new ConduitCall($api_method, $api_params)) 218 + ->setUser($viewer) 219 + ->execute(); 220 + } 221 + 222 + public function getFieldSpecifications() { 223 + return array( 224 + 'token' => array( 225 + 'name' => pht('API Token'), 226 + 'type' => 'credential', 227 + 'credential.type' 228 + => PassphraseTokenCredentialType::CREDENTIAL_TYPE, 229 + 'credential.provides' 230 + => PassphraseTokenCredentialType::PROVIDES_TYPE, 231 + 'required' => true, 232 + ), 233 + ); 234 + } 235 + 236 + public function supportsWaitForMessage() { 237 + // NOTE: We always wait for a message, but don't need to show the UI 238 + // control since "Wait" is the only valid choice. 239 + return false; 240 + } 241 + 242 + public function shouldWaitForMessage(HarbormasterBuildTarget $target) { 243 + return true; 244 + } 245 + 246 + }
+2 -22
src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php
··· 72 72 $build_target, 73 73 array($future)); 74 74 75 - list($status, $body, $headers) = $future->resolve(); 76 - 77 - $header_lines = array(); 78 - 79 - // TODO: We don't currently preserve the entire "HTTP" response header, but 80 - // should. Once we do, reproduce it here faithfully. 81 - $status_code = $status->getStatusCode(); 82 - $header_lines[] = "HTTP {$status_code}"; 83 - 84 - foreach ($headers as $header) { 85 - list($head, $tail) = $header; 86 - $header_lines[] = "{$head}: {$tail}"; 87 - } 88 - $header_lines = implode("\n", $header_lines); 89 - 90 - $build_target 91 - ->newLog($uri, 'http.head') 92 - ->append($header_lines); 93 - 94 - $build_target 95 - ->newLog($uri, 'http.body') 96 - ->append($body); 75 + $this->logHTTPResponse($build, $build_target, $future, $uri); 97 76 77 + list($status) = $future->resolve(); 98 78 if ($status->isError()) { 99 79 throw new HarbormasterBuildFailureException(); 100 80 }
+47
src/applications/repository/storage/PhabricatorRepositoryCommit.php
··· 10 10 PhabricatorSubscribableInterface, 11 11 PhabricatorMentionableInterface, 12 12 HarbormasterBuildableInterface, 13 + HarbormasterCircleCIBuildableInterface, 13 14 PhabricatorCustomFieldInterface, 14 15 PhabricatorApplicationTransactionInterface, 15 16 PhabricatorFulltextInterface { ··· 408 409 'repository.uri' => 409 410 pht('The URI to clone or checkout the repository from.'), 410 411 ); 412 + } 413 + 414 + 415 + /* -( HarbormasterCircleCIBuildableInterface )----------------------------- */ 416 + 417 + 418 + public function getCircleCIGitHubRepositoryURI() { 419 + $repository = $this->getRepository(); 420 + 421 + $commit_phid = $this->getPHID(); 422 + $repository_phid = $repository->getPHID(); 423 + 424 + if ($repository->isHosted()) { 425 + throw new Exception( 426 + pht( 427 + 'This commit ("%s") is associated with a hosted repository '. 428 + '("%s"). Repositories must be imported from GitHub to be built '. 429 + 'with CircleCI.', 430 + $commit_phid, 431 + $repository_phid)); 432 + } 433 + 434 + $remote_uri = $repository->getRemoteURI(); 435 + $path = HarbormasterCircleCIBuildStepImplementation::getGitHubPath( 436 + $remote_uri); 437 + if (!$path) { 438 + throw new Exception( 439 + pht( 440 + 'This commit ("%s") is associated with a repository ("%s") that '. 441 + 'with a remote URI ("%s") that does not appear to be hosted on '. 442 + 'GitHub. Repositories must be hosted on GitHub to be built with '. 443 + 'CircleCI.', 444 + $commit_phid, 445 + $repository_phid, 446 + $remote_uri)); 447 + } 448 + 449 + return $remote_uri; 450 + } 451 + 452 + public function getCircleCIBuildIdentifierType() { 453 + return 'revision'; 454 + } 455 + 456 + public function getCircleCIBuildIdentifier() { 457 + return $this->getCommitIdentifier(); 411 458 } 412 459 413 460