1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-18 21:02:41 +01:00

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
This commit is contained in:
epriestley 2015-10-14 20:01:22 -07:00
parent 63d755723b
commit f82db7524b
8 changed files with 427 additions and 29 deletions

View file

@ -1106,6 +1106,8 @@ phutil_register_library_map(array(
'HarbormasterBuildableTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildableTransactionQuery.php',
'HarbormasterBuildableViewController' => 'applications/harbormaster/controller/HarbormasterBuildableViewController.php',
'HarbormasterBuiltinBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterBuiltinBuildStepGroup.php',
'HarbormasterCircleCIBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php',
'HarbormasterCircleCIBuildableInterface' => 'applications/harbormaster/interface/HarbormasterCircleCIBuildableInterface.php',
'HarbormasterConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php',
'HarbormasterController' => 'applications/harbormaster/controller/HarbormasterController.php',
'HarbormasterCreateArtifactConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php',
@ -4517,6 +4519,7 @@ phutil_register_library_map(array(
'DifferentialDAO',
'PhabricatorPolicyInterface',
'HarbormasterBuildableInterface',
'HarbormasterCircleCIBuildableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
@ -5334,6 +5337,7 @@ phutil_register_library_map(array(
'HarbormasterBuildableTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'HarbormasterBuildableViewController' => 'HarbormasterController',
'HarbormasterBuiltinBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterCircleCIBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterConduitAPIMethod' => 'ConduitAPIMethod',
'HarbormasterController' => 'PhabricatorController',
'HarbormasterCreateArtifactConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
@ -7652,6 +7656,7 @@ phutil_register_library_map(array(
'PhabricatorSubscribableInterface',
'PhabricatorMentionableInterface',
'HarbormasterBuildableInterface',
'HarbormasterCircleCIBuildableInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorFulltextInterface',

View file

@ -5,6 +5,7 @@ final class DifferentialDiff
implements
PhabricatorPolicyInterface,
HarbormasterBuildableInterface,
HarbormasterCircleCIBuildableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface {
@ -524,6 +525,72 @@ final class DifferentialDiff
);
}
/* -( HarbormasterCircleCIBuildableInterface )----------------------------- */
public function getCircleCIGitHubRepositoryURI() {
$diff_phid = $this->getPHID();
$repository_phid = $this->getRepositoryPHID();
if (!$repository_phid) {
throw new Exception(
pht(
'This diff ("%s") is not associated with a repository. A diff '.
'must belong to a tracked repository to be built by CircleCI.',
$diff_phid));
}
$repository = id(new PhabricatorRepositoryQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($repository_phid))
->executeOne();
if (!$repository) {
throw new Exception(
pht(
'This diff ("%s") is associated with a repository ("%s") which '.
'could not be loaded.',
$diff_phid,
$repository_phid));
}
$staging_uri = $repository->getStagingURI();
if (!$staging_uri) {
throw new Exception(
pht(
'This diff ("%s") is associated with a repository ("%s") that '.
'does not have a Staging Area configured. You must configure a '.
'Staging Area to use CircleCI integration.',
$diff_phid,
$repository_phid));
}
$path = HarbormasterCircleCIBuildStepImplementation::getGitHubPath(
$staging_uri);
if (!$path) {
throw new Exception(
pht(
'This diff ("%s") is associated with a repository ("%s") that '.
'does not have a Staging Area ("%s") that is hosted on GitHub. '.
'CircleCI can only build from GitHub, so the Staging Area for '.
'the repository must be hosted there.',
$diff_phid,
$repository_phid,
$staging_uri));
}
return $staging_uri;
}
public function getCircleCIBuildIdentifierType() {
return 'tag';
}
public function getCircleCIBuildIdentifier() {
$ref = $this->getStagingRef();
$ref = preg_replace('(^refs/tags/)', '', $ref);
return $ref;
}
public function getStagingRef() {
// TODO: We're just hoping to get lucky. Instead, `arc` should store
// where it sent changes and we should only provide staging details

View file

@ -136,13 +136,19 @@ final class HarbormasterStepEditController
}
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTextControl())
->setName('name')
->setLabel(pht('Name'))
->setError($e_name)
->setValue($v_name));
->setUser($viewer);
$instructions = $implementation->getEditInstructions();
if (strlen($instructions)) {
$form->appendRemarkupInstructions($instructions);
}
$form->appendChild(
id(new AphrontFormTextControl())
->setName('name')
->setLabel(pht('Name'))
->setError($e_name)
->setValue($v_name));
$form->appendChild(id(new AphrontFormDividerControl()));

View file

@ -0,0 +1,12 @@
<?php
/**
* Support for CircleCI.
*/
interface HarbormasterCircleCIBuildableInterface {
public function getCircleCIGitHubRepositoryURI();
public function getCircleCIBuildIdentifierType();
public function getCircleCIBuildIdentifier();
}

View file

@ -69,6 +69,10 @@ abstract class HarbormasterBuildStepImplementation extends Phobject {
return $this->getGenericDescription();
}
public function getEditInstructions() {
return null;
}
/**
* Run the build target against the specified build.
*/
@ -265,6 +269,37 @@ abstract class HarbormasterBuildStepImplementation extends Phobject {
}
protected function logHTTPResponse(
HarbormasterBuild $build,
HarbormasterBuildTarget $build_target,
BaseHTTPFuture $future,
$label) {
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($label, 'http.head')
->append($header_lines);
$build_target
->newLog($label, 'http.body')
->append($body);
}
/* -( Automatic Targets )-------------------------------------------------- */

View file

@ -0,0 +1,246 @@
<?php
final class HarbormasterCircleCIBuildStepImplementation
extends HarbormasterBuildStepImplementation {
public function getName() {
return pht('Build with CircleCI');
}
public function getGenericDescription() {
return pht('Trigger a build in CircleCI.');
}
public function getBuildStepGroupKey() {
return HarbormasterExternalBuildStepGroup::GROUPKEY;
}
public function getDescription() {
return pht('Run a build in CircleCI.');
}
public function getEditInstructions() {
return pht(<<<EOTEXT
WARNING: This build step is new and experimental!
To build **revisions** with CircleCI, they must:
- belong to a tracked repository;
- the repository must have a Staging Area configured;
- the Staging Area must be hosted on GitHub; and
- you must configure the webhook described below.
To build **commits** with CircleCI, they must:
- belong to a repository that is being imported from GitHub; and
- you must configure the webhook described below.
Webhook Configuration
=====================
IMPORTANT: This has not been implemented yet.
Environment
===========
These variables will be available in the build environment:
| Variable | Description |
|----------|-------------|
| `HARBORMASTER_BUILD_TARGET_PHID` | PHID of the Build Target.
EOTEXT
);
}
public static function getGitHubPath($uri) {
$uri_object = new PhutilURI($uri);
$domain = $uri_object->getDomain();
if (!strlen($domain)) {
$uri_object = new PhutilGitURI($uri);
$domain = $uri_object->getDomain();
}
$domain = phutil_utf8_strtolower($domain);
switch ($domain) {
case 'github.com':
case 'www.github.com':
return $uri_object->getPath();
default:
return null;
}
}
public function execute(
HarbormasterBuild $build,
HarbormasterBuildTarget $build_target) {
$viewer = PhabricatorUser::getOmnipotentUser();
$buildable = $build->getBuildable();
$object = $buildable->getBuildableObject();
$object_phid = $object->getPHID();
if (!($object instanceof HarbormasterCircleCIBuildableInterface)) {
throw new Exception(
pht(
'Object ("%s") does not implement interface "%s". Only objects '.
'which implement this interface can be built with CircleCI.',
$object_phid,
'HarbormasterCircleCIBuildableInterface'));
}
$github_uri = $object->getCircleCIGitHubRepositoryURI();
$build_type = $object->getCircleCIBuildIdentifierType();
$build_identifier = $object->getCircleCIBuildIdentifier();
$path = self::getGitHubPath($github_uri);
if ($path === null) {
throw new Exception(
pht(
'Object ("%s") claims "%s" is a GitHub repository URI, but the '.
'domain does not appear to be GitHub.',
$object_phid,
$github_uri));
}
$path_parts = trim($path, '/');
$path_parts = explode('/', $path_parts);
if (count($path_parts) < 2) {
throw new Exception(
pht(
'Object ("%s") claims "%s" is a GitHub repository URI, but the '.
'path ("%s") does not have enough components (expected at least '.
'two).',
$object_phid,
$github_uri,
$path));
}
list($github_namespace, $github_name) = $path_parts;
$github_name = preg_replace('(\\.git$)', '', $github_name);
$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));
}
// When we pass "revision", the branch is ignored (and does not even need
// to exist), and only shows up in the UI. Use a cute string which will
// certainly never break anything or cause any kind of problem.
$ship = "\xF0\x9F\x9A\xA2";
$branch = "{$ship}Harbormaster";
$token = $api_token->getSecret()->openEnvelope();
$parts = array(
'https://circleci.com/api/v1/project',
phutil_escape_uri($github_namespace),
phutil_escape_uri($github_name)."?circle-token={$token}",
);
$uri = implode('/', $parts);
$data_structure = array();
switch ($build_type) {
case 'tag':
$data_structure['tag'] = $build_identifier;
break;
case 'revision':
$data_structure['revision'] = $build_identifier;
break;
default:
throw new Exception(
pht(
'Unknown CircleCI build type "%s". Expected "%s" or "%s".',
$build_type,
'tag',
'revision'));
}
$data_structure['build_parameters'] = array(
'HARBORMASTER_BUILD_TARGET_PHID' => $build_target->getPHID(),
);
$json_data = phutil_json_encode($data_structure);
$future = id(new HTTPSFuture($uri, $json_data))
->setMethod('POST')
->addHeader('Content-Type', 'application/json')
->addHeader('Accept', 'application/json')
->setTimeout(60);
$this->resolveFutures(
$build,
$build_target,
array($future));
$this->logHTTPResponse($build, $build_target, $future, pht('CircleCI'));
list($status, $body) = $future->resolve();
if ($status->isError()) {
throw new HarbormasterBuildFailureException();
}
$response = phutil_json_decode($body);
$build_uri = idx($response, 'build_url');
if (!$build_uri) {
throw new Exception(
pht(
'CircleCI did not return a "%s"!',
'build_url'));
}
$target_phid = $build_target->getPHID();
// Write an artifact to create a link to the external build in CircleCI.
$api_method = 'harbormaster.createartifact';
$api_params = array(
'buildTargetPHID' => $target_phid,
'artifactType' => HarbormasterURIArtifact::ARTIFACTCONST,
'artifactKey' => 'circleci.uri',
'artifactData' => array(
'uri' => $build_uri,
'name' => pht('View in CircleCI'),
'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,
),
);
}
public function supportsWaitForMessage() {
// NOTE: We always wait for a message, but don't need to show the UI
// control since "Wait" is the only valid choice.
return false;
}
public function shouldWaitForMessage(HarbormasterBuildTarget $target) {
return true;
}
}

View file

@ -72,29 +72,9 @@ final class HarbormasterHTTPRequestBuildStepImplementation
$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);
$this->logHTTPResponse($build, $build_target, $future, $uri);
list($status) = $future->resolve();
if ($status->isError()) {
throw new HarbormasterBuildFailureException();
}

View file

@ -10,6 +10,7 @@ final class PhabricatorRepositoryCommit
PhabricatorSubscribableInterface,
PhabricatorMentionableInterface,
HarbormasterBuildableInterface,
HarbormasterCircleCIBuildableInterface,
PhabricatorCustomFieldInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorFulltextInterface {
@ -411,6 +412,52 @@ final class PhabricatorRepositoryCommit
}
/* -( HarbormasterCircleCIBuildableInterface )----------------------------- */
public function getCircleCIGitHubRepositoryURI() {
$repository = $this->getRepository();
$commit_phid = $this->getPHID();
$repository_phid = $repository->getPHID();
if ($repository->isHosted()) {
throw new Exception(
pht(
'This commit ("%s") is associated with a hosted repository '.
'("%s"). Repositories must be imported from GitHub to be built '.
'with CircleCI.',
$commit_phid,
$repository_phid));
}
$remote_uri = $repository->getRemoteURI();
$path = HarbormasterCircleCIBuildStepImplementation::getGitHubPath(
$remote_uri);
if (!$path) {
throw new Exception(
pht(
'This commit ("%s") is associated with a repository ("%s") that '.
'with a remote URI ("%s") that does not appear to be hosted on '.
'GitHub. Repositories must be hosted on GitHub to be built with '.
'CircleCI.',
$commit_phid,
$repository_phid,
$remote_uri));
}
return $remote_uri;
}
public function getCircleCIBuildIdentifierType() {
return 'revision';
}
public function getCircleCIBuildIdentifier() {
return $this->getCommitIdentifier();
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */