@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class HarbormasterBuildkiteBuildStepImplementation
4 extends HarbormasterBuildStepImplementation {
5
6 public function getName() {
7 return pht('Build with Buildkite');
8 }
9
10 public function getGenericDescription() {
11 return pht('Trigger a build in Buildkite.');
12 }
13
14 public function getBuildStepGroupKey() {
15 return HarbormasterExternalBuildStepGroup::GROUPKEY;
16 }
17
18 public function getDescription() {
19 return pht('Run a build in Buildkite.');
20 }
21
22 public function getEditInstructions() {
23 $hook_uri = '/harbormaster/hook/buildkite/';
24 $hook_uri = PhabricatorEnv::getProductionURI($hook_uri);
25
26 return pht(<<<EOTEXT
27WARNING: This build step is new and experimental!
28
29To build **revisions** with Buildkite, they must:
30
31 - belong to a tracked repository;
32 - the repository must have a Staging Area configured;
33 - you must configure a Buildkite pipeline for that Staging Area; and
34 - you must configure the webhook described below.
35
36To build **commits** with Buildkite, they must:
37
38 - belong to a tracked repository;
39 - you must configure a Buildkite pipeline for that repository; and
40 - you must configure the webhook described below.
41
42Webhook Configuration
43=====================
44
45In {nav Settings} for your Organization in Buildkite, under
46{nav Notification Services}, add a new **Webhook Notification**.
47
48Use these settings:
49
50 - **Webhook URL**: %s
51 - **Token**: The "Webhook Token" field below and the "Token" field in
52 Buildkite should both be set to the same nonempty value (any random
53 secret). You can use copy/paste the value Buildkite generates into
54 this form.
55 - **Events**: Only **build.finish** needs to be active.
56
57Environment
58===========
59
60These variables will be available in the build environment:
61
62| Variable | Description |
63|----------|-------------|
64| `HARBORMASTER_BUILD_TARGET_PHID` | PHID of the Build Target. |
65EOTEXT
66 ,
67 $hook_uri);
68 }
69
70 public function execute(
71 HarbormasterBuild $build,
72 HarbormasterBuildTarget $build_target) {
73 $viewer = PhabricatorUser::getOmnipotentUser();
74
75 if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
76 $this->logSilencedCall($build, $build_target, pht('Buildkite'));
77 throw new HarbormasterBuildFailureException();
78 }
79
80 $buildable = $build->getBuildable();
81
82 $object = $buildable->getBuildableObject();
83 if (!($object instanceof HarbormasterBuildkiteBuildableInterface)) {
84 throw new Exception(
85 pht('This object does not support builds with Buildkite.'));
86 }
87
88 $organization = $this->getSetting('organization');
89 $pipeline = $this->getSetting('pipeline');
90
91 $uri = urisprintf(
92 'https://api.buildkite.com/v2/organizations/%s/pipelines/%s/builds',
93 $organization,
94 $pipeline);
95
96 $data_structure = array(
97 'commit' => $object->getBuildkiteCommit(),
98 'branch' => $object->getBuildkiteBranch(),
99 'message' => pht(
100 'Harbormaster Build %s ("%s") for %s',
101 $build->getID(),
102 $build->getName(),
103 $buildable->getMonogram()),
104 'env' => array(
105 'HARBORMASTER_BUILD_TARGET_PHID' => $build_target->getPHID(),
106 ),
107 'meta_data' => array(
108 'buildTargetPHID' => $build_target->getPHID(),
109
110 // See PHI611. These are undocumented secret magic.
111 'phabricator:build:id' => (int)$build->getID(),
112 'phabricator:build:url' =>
113 PhabricatorEnv::getProductionURI($build->getURI()),
114 'phabricator:buildable:id' => (int)$buildable->getID(),
115 'phabricator:buildable:url' =>
116 PhabricatorEnv::getProductionURI($buildable->getURI()),
117 ),
118 );
119
120 $engine = HarbormasterBuildableEngine::newForObject(
121 $object,
122 $viewer);
123
124 $author_identity = $engine->getAuthorIdentity();
125 if ($author_identity) {
126 $data_structure += array(
127 'author' => array(
128 'name' => $author_identity->getIdentityDisplayName(),
129 'email' => $author_identity->getIdentityEmailAddress(),
130 ),
131 );
132 }
133
134 $json_data = phutil_json_encode($data_structure);
135
136 $credential_phid = $this->getSetting('token');
137 $api_token = id(new PassphraseCredentialQuery())
138 ->setViewer($viewer)
139 ->withPHIDs(array($credential_phid))
140 ->needSecrets(true)
141 ->executeOne();
142 if (!$api_token) {
143 throw new Exception(
144 pht(
145 'Unable to load API token ("%s")!',
146 $credential_phid));
147 }
148
149 $token = $api_token->getSecret()->openEnvelope();
150
151 $future = id(new HTTPSFuture($uri, $json_data))
152 ->setMethod('POST')
153 ->addHeader('Content-Type', 'application/json')
154 ->addHeader('Accept', 'application/json')
155 ->addHeader('Authorization', "Bearer {$token}")
156 ->setTimeout(60);
157
158 $this->resolveFutures(
159 $build,
160 $build_target,
161 array($future));
162
163 $this->logHTTPResponse($build, $build_target, $future, pht('Buildkite'));
164
165 list($status, $body) = $future->resolve();
166 if ($status->isError()) {
167 throw new HarbormasterBuildFailureException();
168 }
169
170 $response = phutil_json_decode($body);
171
172 $uri_key = 'web_url';
173 $build_uri = idx($response, $uri_key);
174 if (!$build_uri) {
175 throw new Exception(
176 pht(
177 'Buildkite did not return a "%s"!',
178 $uri_key));
179 }
180
181 $target_phid = $build_target->getPHID();
182
183 $api_method = 'harbormaster.createartifact';
184 $api_params = array(
185 'buildTargetPHID' => $target_phid,
186 'artifactType' => HarbormasterURIArtifact::ARTIFACTCONST,
187 'artifactKey' => 'buildkite.uri',
188 'artifactData' => array(
189 'uri' => $build_uri,
190 'name' => pht('View in Buildkite'),
191 'ui.external' => true,
192 ),
193 );
194
195 id(new ConduitCall($api_method, $api_params))
196 ->setUser($viewer)
197 ->execute();
198 }
199
200 public function getFieldSpecifications() {
201 return array(
202 'token' => array(
203 'name' => pht('API Token'),
204 'type' => 'credential',
205 'credential.type'
206 => PassphraseTokenCredentialType::CREDENTIAL_TYPE,
207 'credential.provides'
208 => PassphraseTokenCredentialType::PROVIDES_TYPE,
209 'required' => true,
210 ),
211 'organization' => array(
212 'name' => pht('Organization Name'),
213 'type' => 'text',
214 'required' => true,
215 ),
216 'pipeline' => array(
217 'name' => pht('Pipeline Name'),
218 'type' => 'text',
219 'required' => true,
220 ),
221 'webhook.token' => array(
222 'name' => pht('Webhook Token'),
223 'type' => 'text',
224 'required' => true,
225 ),
226 );
227 }
228
229 public function supportsWaitForMessage() {
230 return false;
231 }
232
233 public function shouldWaitForMessage(HarbormasterBuildTarget $target) {
234 return true;
235 }
236
237}