diff --git a/Harbormaster-Teamcity-Plugin/HarbormasterTeamCityBuildStepImplementation.php b/Harbormaster-Teamcity-Plugin/HarbormasterTeamCityBuildStepImplementation.php new file mode 100644 index 0000000..122b63c --- /dev/null +++ b/Harbormaster-Teamcity-Plugin/HarbormasterTeamCityBuildStepImplementation.php @@ -0,0 +1,147 @@ +getSetting('uri'); + if ($uri) { + $domain = id(new PhutilURI($uri))->getDomain(); + } + + $method = $this->formatSettingForDescription('method', 'POST'); + $domain = $this->formatValueForDescription($domain); + + if ($this->getSetting('credential')) { + return pht( + 'Make an authenticated HTTP %s request to %s.', + $method, + $domain); + } else { + return pht( + 'Make an HTTP %s request to %s.', + $method, + $domain); + } + } + + public function execute( + HarbormasterBuild $build, + HarbormasterBuildTarget $build_target) { + + $viewer = PhabricatorUser::getOmnipotentUser(); + + // Settings includes the custom fields set in getFieldSpecifications() + $settings = $this->getSettings(); + $variables = $build_target->getVariables(); + + // Combined TeamCity URI + $uri = $settings['uri'] . '/app/rest/buildQueue'; + + $method = 'POST'; + $contentType = 'application/xml'; + + // Using TeamCityXmlBuildBuilder create the payload to send to + // TeamCity server + $xmlBuilder = new TeamCityXmlBuildBuilder(); + $payload = $xmlBuilder + ->addBuildId($settings['buildId']) + ->addBranchName(implode(array("D", $variables['buildable.revision'], "-", $variables['build.id']))) + ->addPhabBuildId($variables['build.id']) + ->addDiffId($variables['buildable.diff']) + ->addHarbormasterPHID($variables['target.phid']) + ->addRevisionId(implode(array("D", $variables['buildable.revision']))) + ->build(); + + $future = id(new HTTPSFuture($uri, $payload)) + ->setMethod($method) + ->addHeader('Content-Type', $contentType) + ->addHeader('Origin', $settings['uri']) + ->setTimeout(60); + + // Add credentials to HTTP request if they have been set + $credential_phid = $this->getSetting('credential'); + if ($credential_phid) { + $key = PassphraseTokenKey::loadFromPHID( + $credential_phid, + $viewer); + $future->addHeader( + 'Authorization', + implode(array("Bearer ", $key->getPasswordEnvelope()->openEnvelope())) + ); + } + + $this->resolveFutures( + $build, + $build_target, + array($future)); + + list($status, $body, $headers) = $future->resolve(); + + $header_lines = array(); + + // TODO: We don't currently preserve the entire "HTTP" response header, but + // should. Once we do, reproduce it here faithfully. + $status_code = $status->getStatusCode(); + $header_lines[] = "HTTP {$status_code}"; + + foreach ($headers as $header) { + list($head, $tail) = $header; + $header_lines[] = "{$head}: {$tail}"; + } + $header_lines = implode("\n", $header_lines); + + $build_target + ->newLog($uri, 'http.head') + ->append($header_lines); + + $build_target + ->newLog($uri, 'http.body') + ->append($body); + + if ($status->isError()) { + throw new HarbormasterBuildFailureException(); + } + } + + public function getFieldSpecifications() { + return array( + 'uri' => array( + 'name' => pht('URI'), + 'type' => 'text', + 'required' => true, + ), + 'buildId' => array( + 'name' => pht('TeamCity Build Configuration ID'), + 'type' => 'text', + 'required' => true, + ), + 'credential' => array( + 'name' => pht('TeamCity Credentials'), + 'type' => 'credential', + 'required' => true, + 'credential.type' + => PassphraseTokenCredentialType::CREDENTIAL_TYPE, + 'credential.provides' + => PassphraseTokenCredentialType::PROVIDES_TYPE, + ), + ); + } + + public function supportsWaitForMessage() { + return true; + } +} diff --git a/Harbormaster-Teamcity-Plugin/PassphraseTokenKey.php b/Harbormaster-Teamcity-Plugin/PassphraseTokenKey.php new file mode 100644 index 0000000..1c67690 --- /dev/null +++ b/Harbormaster-Teamcity-Plugin/PassphraseTokenKey.php @@ -0,0 +1,17 @@ +loadAndValidateFromPHID( + $phid, + $viewer, + PassphraseTokenCredentialType::PROVIDES_TYPE); + } + + public function getPasswordEnvelope() { + return $this->requireCredential()->getSecret(); + } + +} \ No newline at end of file diff --git a/Harbormaster-Teamcity-Plugin/TeamCityXmlBuildBuilder.php b/Harbormaster-Teamcity-Plugin/TeamCityXmlBuildBuilder.php new file mode 100644 index 0000000..3818a4d --- /dev/null +++ b/Harbormaster-Teamcity-Plugin/TeamCityXmlBuildBuilder.php @@ -0,0 +1,80 @@ +xml = new DOMDocument('1.0', 'UTF-8'); + $this->root = $this->xml->createElement('build'); + } + + function addBuildId($buildId){ + $buildIdElement = + $this-> + xml-> + createElement('buildType'); + + $buildIdElement->setAttribute('id', $buildId); + + $this->root->appendChild($buildIdElement); + + return $this; + } + + function addPhabBuildId($buildId){ + $this->addProperty("env.buildId", $buildId); + return $this; + } + + function addRevisionId($revisionId){ + $this->addProperty("env.revisionId", $revisionId); + return $this; + } + + function addBranchName($branchName){ + // $this-> + // root-> + // setAttribute('branchName', $branchName); + $this->addProperty("env.branchName", $branchName); + + return $this; + } + + function addHarbormasterPHID($phid){ + $this->addProperty('env.harbormasterTargetPHID', $phid); + return $this; + } + + function addDiffId($diffId){ + $this->addProperty('env.diffId', $diffId); + return $this; + } + + function build(){ + $this->xml->appendChild($this->root); + return $this->xml->saveXML(); + } + + private function addProperty($name, $value){ + $this->verifyPropertiesExist(); + + $property = $this->xml->createElement('property'); + $property->setAttribute('name', $name); + $property->setAttribute('value', $value); + + $this-> + root-> + getElementsByTagName('properties')-> + item(0)-> + appendChild($property); + } + + private function verifyPropertiesExist(){ + if($this->root->getElementsByTagName('properties')->length == 0){ + $propertiesElement = $this->xml->createElement('properties'); + $this->root->appendChild($propertiesElement); + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index 8a6b33b..f66b756 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,20 @@ _X-Lab's Linting Spine_ This repository holds the plugins created to link together our internal systems. The original premise to link Phabricator, TeamCity and SonarQube to enable linting on differential reviews. Below are the plugins: -* **Harbomaster-Teamcity-Plugin** -* **Teamcity-Phabricator-Plugin** -* **SonarQube-Phabricator-Plugin** +* Harbormaster-Teamcity-Plugin +* Teamcity-Phabricator-Plugin +* SonarQube-Phabricator-Plugin + +### Harbomaster-Teamcity-Plugin + +The harbormaster plugin allows us to trigger a build configuration within TeamCity as part of a harbormaster build plan. + +The plugin requires: +1. TeamCity URI +2. Build Configuration to trigger a build for +3. TeamCity access token to authenticate with the server + +To deploy simply drag the contents of the folder to `src/extensions/` on the Phabricator instance and then restart the application. ## Useful Links