Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14395286
D14286.id34486.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
17 KB
Referenced Files
None
Subscribers
None
D14286.id34486.diff
View Options
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1028,6 +1028,8 @@
'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',
'HarbormasterCommandBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php',
'HarbormasterConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php',
'HarbormasterController' => 'applications/harbormaster/controller/HarbormasterController.php',
@@ -4080,6 +4082,7 @@
'DifferentialDAO',
'PhabricatorPolicyInterface',
'HarbormasterBuildableInterface',
+ 'HarbormasterCircleCIBuildableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
@@ -4862,6 +4865,7 @@
'HarbormasterBuildableTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'HarbormasterBuildableViewController' => 'HarbormasterController',
'HarbormasterBuiltinBuildStepGroup' => 'HarbormasterBuildStepGroup',
+ 'HarbormasterCircleCIBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterCommandBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterConduitAPIMethod' => 'ConduitAPIMethod',
'HarbormasterController' => 'PhabricatorController',
@@ -6900,6 +6904,7 @@
'PhabricatorSubscribableInterface',
'PhabricatorMentionableInterface',
'HarbormasterBuildableInterface',
+ 'HarbormasterCircleCIBuildableInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorApplicationTransactionInterface',
),
diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php
--- a/src/applications/differential/storage/DifferentialDiff.php
+++ b/src/applications/differential/storage/DifferentialDiff.php
@@ -5,6 +5,7 @@
implements
PhabricatorPolicyInterface,
HarbormasterBuildableInterface,
+ HarbormasterCircleCIBuildableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface {
@@ -482,6 +483,66 @@
);
}
+
+/* -( 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 getCircleCIBuildIdentifier() {
+ return $this->getStagingRef();
+ }
+
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
diff --git a/src/applications/harbormaster/controller/HarbormasterStepEditController.php b/src/applications/harbormaster/controller/HarbormasterStepEditController.php
--- a/src/applications/harbormaster/controller/HarbormasterStepEditController.php
+++ b/src/applications/harbormaster/controller/HarbormasterStepEditController.php
@@ -129,13 +129,19 @@
}
$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()));
diff --git a/src/applications/harbormaster/interface/HarbormasterCircleCIBuildableInterface.php b/src/applications/harbormaster/interface/HarbormasterCircleCIBuildableInterface.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/interface/HarbormasterCircleCIBuildableInterface.php
@@ -0,0 +1,11 @@
+<?php
+
+/**
+ * Support for CircleCI.
+ */
+interface HarbormasterCircleCIBuildableInterface {
+
+ public function getCircleCIGitHubRepositoryURI();
+ public function getCircleCIBuildIdentifier();
+
+}
diff --git a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php
--- a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php
@@ -69,6 +69,10 @@
return $this->getGenericDescription();
}
+ public function getEditInstructions() {
+ return null;
+ }
+
/**
* Run the build target against the specified build.
*/
@@ -265,6 +269,37 @@
}
+ 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 )-------------------------------------------------- */
diff --git a/src/applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php
@@ -0,0 +1,224 @@
+<?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_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),
+ 'tree',
+ "{$branch}?circle-token={$token}",
+ );
+
+ $uri = implode('/', $parts);
+
+ $data_structure = array(
+ 'revision' => $build_identifier,
+ '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();
+
+ $pair = id(new HarbormasterKeyValuePair())
+ ->setObjectPHID($target_phid)
+ ->setPairKey("circleci.build({$build_uri})", $target_phid)
+ ->setPairValue(
+ array(
+ 'buildTargetPHID' => $target_phid,
+ ))
+ ->save();
+ }
+
+ 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;
+ }
+
+}
diff --git a/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php
--- a/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php
@@ -72,29 +72,9 @@
$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();
}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php
--- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php
@@ -10,6 +10,7 @@
PhabricatorSubscribableInterface,
PhabricatorMentionableInterface,
HarbormasterBuildableInterface,
+ HarbormasterCircleCIBuildableInterface,
PhabricatorCustomFieldInterface,
PhabricatorApplicationTransactionInterface {
@@ -354,6 +355,48 @@
}
+/* -( 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 getCircleCIBuildIdentifier() {
+ return $this->getCommitIdentifier();
+ }
+
+
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Mon, Dec 23, 3:59 AM (19 h, 10 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6920530
Default Alt Text
D14286.id34486.diff (17 KB)
Attached To
Mode
D14286: Add a "Build with CircleCI" build step
Attached
Detach File
Event Timeline
Log In to Comment