Path: blob/master/src/applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php
12256 views
<?php12final class HarbormasterCircleCIBuildStepImplementation3extends HarbormasterBuildStepImplementation {45public function getName() {6return pht('Build with CircleCI');7}89public function getGenericDescription() {10return pht('Trigger a build in CircleCI.');11}1213public function getBuildStepGroupKey() {14return HarbormasterExternalBuildStepGroup::GROUPKEY;15}1617public function getDescription() {18return pht('Run a build in CircleCI.');19}2021public function getEditInstructions() {22$hook_uri = '/harbormaster/hook/circleci/';23$hook_uri = PhabricatorEnv::getProductionURI($hook_uri);2425return pht(<<<EOTEXT26WARNING: This build step is new and experimental!2728To build **revisions** with CircleCI, they must:2930- belong to a tracked repository;31- the repository must have a Staging Area configured;32- the Staging Area must be hosted on GitHub; and33- you must configure the webhook described below.3435To build **commits** with CircleCI, they must:3637- belong to a repository that is being imported from GitHub; and38- you must configure the webhook described below.3940Webhook Configuration41=====================4243Add this webhook to your `circle.yml` file to make CircleCI report results44to Harbormaster. Until you install this hook, builds will hang waiting for45a response from CircleCI.4647```lang=yml48notify:49webhooks:50- url: %s51```5253Environment54===========5556These variables will be available in the build environment:5758| Variable | Description |59|----------|-------------|60| `HARBORMASTER_BUILD_TARGET_PHID` | PHID of the Build Target.6162EOTEXT63,64$hook_uri);65}6667public static function getGitHubPath($uri) {68$uri_object = new PhutilURI($uri);69$domain = $uri_object->getDomain();7071$domain = phutil_utf8_strtolower($domain);72switch ($domain) {73case 'github.com':74case 'www.github.com':75return $uri_object->getPath();76default:77return null;78}79}8081public function execute(82HarbormasterBuild $build,83HarbormasterBuildTarget $build_target) {84$viewer = PhabricatorUser::getOmnipotentUser();8586if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {87$this->logSilencedCall($build, $build_target, pht('CircleCI'));88throw new HarbormasterBuildFailureException();89}9091$buildable = $build->getBuildable();9293$object = $buildable->getBuildableObject();94$object_phid = $object->getPHID();95if (!($object instanceof HarbormasterCircleCIBuildableInterface)) {96throw new Exception(97pht(98'Object ("%s") does not implement interface "%s". Only objects '.99'which implement this interface can be built with CircleCI.',100$object_phid,101'HarbormasterCircleCIBuildableInterface'));102}103104$github_uri = $object->getCircleCIGitHubRepositoryURI();105$build_type = $object->getCircleCIBuildIdentifierType();106$build_identifier = $object->getCircleCIBuildIdentifier();107108$path = self::getGitHubPath($github_uri);109if ($path === null) {110throw new Exception(111pht(112'Object ("%s") claims "%s" is a GitHub repository URI, but the '.113'domain does not appear to be GitHub.',114$object_phid,115$github_uri));116}117118$path_parts = trim($path, '/');119$path_parts = explode('/', $path_parts);120if (count($path_parts) < 2) {121throw new Exception(122pht(123'Object ("%s") claims "%s" is a GitHub repository URI, but the '.124'path ("%s") does not have enough components (expected at least '.125'two).',126$object_phid,127$github_uri,128$path));129}130131list($github_namespace, $github_name) = $path_parts;132$github_name = preg_replace('(\\.git$)', '', $github_name);133134$credential_phid = $this->getSetting('token');135$api_token = id(new PassphraseCredentialQuery())136->setViewer($viewer)137->withPHIDs(array($credential_phid))138->needSecrets(true)139->executeOne();140if (!$api_token) {141throw new Exception(142pht(143'Unable to load API token ("%s")!',144$credential_phid));145}146147// When we pass "revision", the branch is ignored (and does not even need148// to exist), and only shows up in the UI. Use a cute string which will149// certainly never break anything or cause any kind of problem.150$ship = "\xF0\x9F\x9A\xA2";151$branch = "{$ship}Harbormaster";152153$token = $api_token->getSecret()->openEnvelope();154$parts = array(155'https://circleci.com/api/v1/project',156phutil_escape_uri($github_namespace),157phutil_escape_uri($github_name)."?circle-token={$token}",158);159160$uri = implode('/', $parts);161162$data_structure = array();163switch ($build_type) {164case 'tag':165$data_structure['tag'] = $build_identifier;166break;167case 'revision':168$data_structure['revision'] = $build_identifier;169break;170default:171throw new Exception(172pht(173'Unknown CircleCI build type "%s". Expected "%s" or "%s".',174$build_type,175'tag',176'revision'));177}178179$data_structure['build_parameters'] = array(180'HARBORMASTER_BUILD_TARGET_PHID' => $build_target->getPHID(),181);182183$json_data = phutil_json_encode($data_structure);184185$future = id(new HTTPSFuture($uri, $json_data))186->setMethod('POST')187->addHeader('Content-Type', 'application/json')188->addHeader('Accept', 'application/json')189->setTimeout(60);190191$this->resolveFutures(192$build,193$build_target,194array($future));195196$this->logHTTPResponse($build, $build_target, $future, pht('CircleCI'));197198list($status, $body) = $future->resolve();199if ($status->isError()) {200throw new HarbormasterBuildFailureException();201}202203$response = phutil_json_decode($body);204$build_uri = idx($response, 'build_url');205if (!$build_uri) {206throw new Exception(207pht(208'CircleCI did not return a "%s"!',209'build_url'));210}211212$target_phid = $build_target->getPHID();213214// Write an artifact to create a link to the external build in CircleCI.215216$api_method = 'harbormaster.createartifact';217$api_params = array(218'buildTargetPHID' => $target_phid,219'artifactType' => HarbormasterURIArtifact::ARTIFACTCONST,220'artifactKey' => 'circleci.uri',221'artifactData' => array(222'uri' => $build_uri,223'name' => pht('View in CircleCI'),224'ui.external' => true,225),226);227228id(new ConduitCall($api_method, $api_params))229->setUser($viewer)230->execute();231}232233public function getFieldSpecifications() {234return array(235'token' => array(236'name' => pht('API Token'),237'type' => 'credential',238'credential.type'239=> PassphraseTokenCredentialType::CREDENTIAL_TYPE,240'credential.provides'241=> PassphraseTokenCredentialType::PROVIDES_TYPE,242'required' => true,243),244);245}246247public function supportsWaitForMessage() {248// NOTE: We always wait for a message, but don't need to show the UI249// control since "Wait" is the only valid choice.250return false;251}252253public function shouldWaitForMessage(HarbormasterBuildTarget $target) {254return true;255}256257}258259260