mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-23 14:00:56 +01:00
Integrate Harbormaster with Buildkite
Summary: Ref T12173. This might need some additional work but the basics seem like they're in good shape. Test Plan: - Buildkite is "bring your own hardware", so you need to launch a host to test anything. - Launched a host in AWS. - Configured Buildkite to use that host to run builds. - Added a Buildkite build step to a new Harbormaster build plan. - Used `bin/harbormaster build ...` to run the plan. - Saw buildkite execute builds and report status back to Harbormaster {F2553076} {F2553077} Reviewers: chad Reviewed By: chad Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T12173 Differential Revision: https://secure.phabricator.com/D17270
This commit is contained in:
parent
aca0f642a3
commit
bd99a2b81e
4 changed files with 326 additions and 0 deletions
|
@ -1210,6 +1210,8 @@ phutil_register_library_map(array(
|
|||
'HarbormasterBuildableTransactionEditor' => 'applications/harbormaster/editor/HarbormasterBuildableTransactionEditor.php',
|
||||
'HarbormasterBuildableTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildableTransactionQuery.php',
|
||||
'HarbormasterBuildableViewController' => 'applications/harbormaster/controller/HarbormasterBuildableViewController.php',
|
||||
'HarbormasterBuildkiteBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php',
|
||||
'HarbormasterBuildkiteHookController' => 'applications/harbormaster/controller/HarbormasterBuildkiteHookController.php',
|
||||
'HarbormasterBuiltinBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterBuiltinBuildStepGroup.php',
|
||||
'HarbormasterCircleCIBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php',
|
||||
'HarbormasterCircleCIBuildableInterface' => 'applications/harbormaster/interface/HarbormasterCircleCIBuildableInterface.php',
|
||||
|
@ -6017,6 +6019,8 @@ phutil_register_library_map(array(
|
|||
'HarbormasterBuildableTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
|
||||
'HarbormasterBuildableTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
|
||||
'HarbormasterBuildableViewController' => 'HarbormasterController',
|
||||
'HarbormasterBuildkiteBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
|
||||
'HarbormasterBuildkiteHookController' => 'HarbormasterController',
|
||||
'HarbormasterBuiltinBuildStepGroup' => 'HarbormasterBuildStepGroup',
|
||||
'HarbormasterCircleCIBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
|
||||
'HarbormasterCircleCIHookController' => 'HarbormasterController',
|
||||
|
|
|
@ -94,6 +94,7 @@ final class PhabricatorHarbormasterApplication extends PhabricatorApplication {
|
|||
),
|
||||
'hook/' => array(
|
||||
'circleci/' => 'HarbormasterCircleCIHookController',
|
||||
'buildkite/' => 'HarbormasterBuildkiteHookController',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
final class HarbormasterBuildkiteHookController
|
||||
extends HarbormasterController {
|
||||
|
||||
public function shouldRequireLogin() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phutil-external-symbol class PhabricatorStartup
|
||||
*/
|
||||
public function handleRequest(AphrontRequest $request) {
|
||||
$raw_body = PhabricatorStartup::getRawInput();
|
||||
$body = phutil_json_decode($raw_body);
|
||||
|
||||
$event = idx($body, 'event');
|
||||
if ($event != 'build.finished') {
|
||||
return $this->newHookResponse(pht('OK: Ignored event.'));
|
||||
}
|
||||
|
||||
$build = idx($body, 'build');
|
||||
if (!is_array($build)) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Expected "%s" property to contain a dictionary.',
|
||||
'build'));
|
||||
}
|
||||
|
||||
$meta_data = idx($build, 'meta_data');
|
||||
if (!is_array($meta_data)) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Expected "%s" property to contain a dictionary.',
|
||||
'build.meta_data'));
|
||||
}
|
||||
|
||||
$target_phid = idx($meta_data, 'buildTargetPHID');
|
||||
if (!$target_phid) {
|
||||
return $this->newHookResponse(pht('OK: No Harbormaster target PHID.'));
|
||||
}
|
||||
|
||||
$viewer = PhabricatorUser::getOmnipotentUser();
|
||||
$target = id(new HarbormasterBuildTargetQuery())
|
||||
->setViewer($viewer)
|
||||
->withPHIDs(array($target_phid))
|
||||
->needBuildSteps(true)
|
||||
->executeOne();
|
||||
if (!$target) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Harbormaster build target "%s" does not exist.',
|
||||
$target_phid));
|
||||
}
|
||||
|
||||
$step = $target->getBuildStep();
|
||||
$impl = $step->getStepImplementation();
|
||||
if (!($impl instanceof HarbormasterBuildkiteBuildStepImplementation)) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Harbormaster build target "%s" is not a Buildkite build step. '.
|
||||
'Only Buildkite steps may be updated via the Buildkite hook.',
|
||||
$target_phid));
|
||||
}
|
||||
|
||||
$webhook_token = $impl->getSetting('webhook.token');
|
||||
$request_token = $request->getHTTPHeader('X-Buildkite-Token');
|
||||
|
||||
if (!phutil_hashes_are_identical($webhook_token, $request_token)) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Buildkite request to target "%s" had the wrong authentication '.
|
||||
'token. The Buildkite pipeline and Harbormaster build step must '.
|
||||
'be configured with the same token.',
|
||||
$target_phid));
|
||||
}
|
||||
|
||||
$state = idx($build, 'state');
|
||||
switch ($state) {
|
||||
case 'passed':
|
||||
$message_type = HarbormasterMessageType::MESSAGE_PASS;
|
||||
break;
|
||||
default:
|
||||
$message_type = HarbormasterMessageType::MESSAGE_FAIL;
|
||||
break;
|
||||
}
|
||||
|
||||
$api_method = 'harbormaster.sendmessage';
|
||||
$api_params = array(
|
||||
'buildTargetPHID' => $target_phid,
|
||||
'type' => $message_type,
|
||||
);
|
||||
|
||||
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
||||
|
||||
id(new ConduitCall($api_method, $api_params))
|
||||
->setUser($viewer)
|
||||
->execute();
|
||||
|
||||
unset($unguarded);
|
||||
|
||||
return $this->newHookResponse(pht('OK: Processed event.'));
|
||||
}
|
||||
|
||||
private function newHookResponse($message) {
|
||||
$response = new AphrontWebpageResponse();
|
||||
$response->setContent($message);
|
||||
return $response;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
<?php
|
||||
|
||||
final class HarbormasterBuildkiteBuildStepImplementation
|
||||
extends HarbormasterBuildStepImplementation {
|
||||
|
||||
public function getName() {
|
||||
return pht('Build with Buildkite');
|
||||
}
|
||||
|
||||
public function getGenericDescription() {
|
||||
return pht('Trigger a build in Buildkite.');
|
||||
}
|
||||
|
||||
public function getBuildStepGroupKey() {
|
||||
return HarbormasterExternalBuildStepGroup::GROUPKEY;
|
||||
}
|
||||
|
||||
public function getDescription() {
|
||||
return pht('Run a build in Buildkite.');
|
||||
}
|
||||
|
||||
public function getEditInstructions() {
|
||||
$hook_uri = '/harbormaster/hook/buildkite/';
|
||||
$hook_uri = PhabricatorEnv::getProductionURI($hook_uri);
|
||||
|
||||
return pht(<<<EOTEXT
|
||||
WARNING: This build step is new and experimental!
|
||||
|
||||
To build **revisions** with Buildkite, they must:
|
||||
|
||||
- belong to a tracked repository;
|
||||
- the repository must have a Staging Area configured;
|
||||
- you must configure a Buildkite pipeline for that Staging Area; and
|
||||
- you must configure the webhook described below.
|
||||
|
||||
To build **commits** with Buildkite, they must:
|
||||
|
||||
- belong to a tracked repository;
|
||||
- you must configure a Buildkite pipeline for that repository; and
|
||||
- you must configure the webhook described below.
|
||||
|
||||
Webhook Configuration
|
||||
=====================
|
||||
|
||||
In {nav Settings} for your Organization in Buildkite, under
|
||||
{nav Notification Services}, add a new **Webook Notification**.
|
||||
|
||||
Use these settings:
|
||||
|
||||
- **Webhook URL**: %s
|
||||
- **Token**: The "Webhook Token" field below and the "Token" field in
|
||||
Buildkite should both be set to the same nonempty value (any random
|
||||
secret). You can use copy/paste the value Buildkite generates into
|
||||
this form.
|
||||
- **Events**: Only **build.finish** needs to be active.
|
||||
|
||||
Environment
|
||||
===========
|
||||
|
||||
These variables will be available in the build environment:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `HARBORMASTER_BUILD_TARGET_PHID` | PHID of the Build Target.
|
||||
EOTEXT
|
||||
,
|
||||
$hook_uri);
|
||||
}
|
||||
|
||||
public function execute(
|
||||
HarbormasterBuild $build,
|
||||
HarbormasterBuildTarget $build_target) {
|
||||
$viewer = PhabricatorUser::getOmnipotentUser();
|
||||
|
||||
$buildable = $build->getBuildable();
|
||||
|
||||
$object = $buildable->getBuildableObject();
|
||||
if (!($object instanceof HarbormasterCircleCIBuildableInterface)) {
|
||||
throw new Exception(
|
||||
pht('This object does not support builds with Buildkite.'));
|
||||
}
|
||||
|
||||
$organization = $this->getSetting('organization');
|
||||
$pipeline = $this->getSetting('pipeline');
|
||||
|
||||
$uri = urisprintf(
|
||||
'https://api.buildkite.com/v2/organizations/%s/pipelines/%s/builds',
|
||||
$organization,
|
||||
$pipeline);
|
||||
|
||||
$data_structure = array(
|
||||
'commit' => $object->getCircleCIBuildIdentifier(),
|
||||
'branch' => 'master',
|
||||
'message' => pht(
|
||||
'Harbormaster Build %s ("%s") for %s',
|
||||
$build->getID(),
|
||||
$build->getName(),
|
||||
$buildable->getMonogram()),
|
||||
'env' => array(
|
||||
'HARBORMASTER_BUILD_TARGET_PHID' => $build_target->getPHID(),
|
||||
),
|
||||
'meta_data' => array(
|
||||
'buildTargetPHID' => $build_target->getPHID(),
|
||||
),
|
||||
);
|
||||
|
||||
$json_data = phutil_json_encode($data_structure);
|
||||
|
||||
$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));
|
||||
}
|
||||
|
||||
$token = $api_token->getSecret()->openEnvelope();
|
||||
|
||||
$future = id(new HTTPSFuture($uri, $json_data))
|
||||
->setMethod('POST')
|
||||
->addHeader('Content-Type', 'application/json')
|
||||
->addHeader('Accept', 'application/json')
|
||||
->addHeader('Authorization', "Bearer {$token}")
|
||||
->setTimeout(60);
|
||||
|
||||
$this->resolveFutures(
|
||||
$build,
|
||||
$build_target,
|
||||
array($future));
|
||||
|
||||
$this->logHTTPResponse($build, $build_target, $future, pht('Buildkite'));
|
||||
|
||||
list($status, $body) = $future->resolve();
|
||||
if ($status->isError()) {
|
||||
throw new HarbormasterBuildFailureException();
|
||||
}
|
||||
|
||||
$response = phutil_json_decode($body);
|
||||
|
||||
$uri_key = 'web_url';
|
||||
$build_uri = idx($response, $uri_key);
|
||||
if (!$build_uri) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Buildkite did not return a "%s"!',
|
||||
$uri_key));
|
||||
}
|
||||
|
||||
$target_phid = $build_target->getPHID();
|
||||
|
||||
$api_method = 'harbormaster.createartifact';
|
||||
$api_params = array(
|
||||
'buildTargetPHID' => $target_phid,
|
||||
'artifactType' => HarbormasterURIArtifact::ARTIFACTCONST,
|
||||
'artifactKey' => 'buildkite.uri',
|
||||
'artifactData' => array(
|
||||
'uri' => $build_uri,
|
||||
'name' => pht('View in Buildkite'),
|
||||
'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,
|
||||
),
|
||||
'organization' => array(
|
||||
'name' => pht('Organization Name'),
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
),
|
||||
'pipeline' => array(
|
||||
'name' => pht('Pipeline Name'),
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
),
|
||||
'webhook.token' => array(
|
||||
'name' => pht('Webhook Token'),
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function supportsWaitForMessage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function shouldWaitForMessage(HarbormasterBuildTarget $target) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue