@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 HarbormasterCircleCIBuildStepImplementation
4 extends HarbormasterBuildStepImplementation {
5
6 public function getName() {
7 return pht('Build with CircleCI');
8 }
9
10 public function getGenericDescription() {
11 return pht('Trigger a build in CircleCI.');
12 }
13
14 public function getBuildStepGroupKey() {
15 return HarbormasterExternalBuildStepGroup::GROUPKEY;
16 }
17
18 public function getDescription() {
19 return pht('Run a build in CircleCI.');
20 }
21
22 public function getEditInstructions() {
23 $hook_uri = '/harbormaster/hook/circleci/';
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 CircleCI, they must:
30
31 - belong to a tracked repository;
32 - the repository must have a Staging Area configured;
33 - the Staging Area must be hosted on GitHub; and
34 - you must configure the webhook described below.
35
36To build **commits** with CircleCI, they must:
37
38 - belong to a repository that is being imported from GitHub; and
39 - you must configure the webhook described below.
40
41Webhook Configuration
42=====================
43
44Add this webhook to your `circle.yml` file to make CircleCI report results
45to Harbormaster. Until you install this hook, builds will hang waiting for
46a response from CircleCI.
47
48```lang=yml
49notify:
50 webhooks:
51 - url: %s
52```
53
54Environment
55===========
56
57These variables will be available in the build environment:
58
59| Variable | Description |
60|----------|-------------|
61| `HARBORMASTER_BUILD_TARGET_PHID` | PHID of the Build Target.
62
63EOTEXT
64 ,
65 $hook_uri);
66 }
67
68 public static function getGitHubPath($uri) {
69 $uri_object = new PhutilURI($uri);
70 $domain = $uri_object->getDomain();
71
72 $domain = phutil_utf8_strtolower($domain);
73 switch ($domain) {
74 case 'github.com':
75 case 'www.github.com':
76 return $uri_object->getPath();
77 default:
78 return null;
79 }
80 }
81
82 public function execute(
83 HarbormasterBuild $build,
84 HarbormasterBuildTarget $build_target) {
85 $viewer = PhabricatorUser::getOmnipotentUser();
86
87 if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
88 $this->logSilencedCall($build, $build_target, pht('CircleCI'));
89 throw new HarbormasterBuildFailureException();
90 }
91
92 $buildable = $build->getBuildable();
93
94 $object = $buildable->getBuildableObject();
95 $object_phid = $object->getPHID();
96 if (!($object instanceof HarbormasterCircleCIBuildableInterface)) {
97 throw new Exception(
98 pht(
99 'Object ("%s") does not implement interface "%s". Only objects '.
100 'which implement this interface can be built with CircleCI.',
101 $object_phid,
102 'HarbormasterCircleCIBuildableInterface'));
103 }
104
105 $github_uri = $object->getCircleCIGitHubRepositoryURI();
106 $build_type = $object->getCircleCIBuildIdentifierType();
107 $build_identifier = $object->getCircleCIBuildIdentifier();
108
109 $path = self::getGitHubPath($github_uri);
110 if ($path === null) {
111 throw new Exception(
112 pht(
113 'Object ("%s") claims "%s" is a GitHub repository URI, but the '.
114 'domain does not appear to be GitHub.',
115 $object_phid,
116 $github_uri));
117 }
118
119 $path_parts = trim($path, '/');
120 $path_parts = explode('/', $path_parts);
121 if (count($path_parts) < 2) {
122 throw new Exception(
123 pht(
124 'Object ("%s") claims "%s" is a GitHub repository URI, but the '.
125 'path ("%s") does not have enough components (expected at least '.
126 'two).',
127 $object_phid,
128 $github_uri,
129 $path));
130 }
131
132 list($github_namespace, $github_name) = $path_parts;
133 $github_name = preg_replace('(\\.git$)', '', $github_name);
134
135 $credential_phid = $this->getSetting('token');
136 $api_token = id(new PassphraseCredentialQuery())
137 ->setViewer($viewer)
138 ->withPHIDs(array($credential_phid))
139 ->needSecrets(true)
140 ->executeOne();
141 if (!$api_token) {
142 throw new Exception(
143 pht(
144 'Unable to load API token ("%s")!',
145 $credential_phid));
146 }
147
148 // When we pass "revision", the branch is ignored (and does not even need
149 // to exist), and only shows up in the UI. Use a cute string which will
150 // certainly never break anything or cause any kind of problem.
151 $ship = "\xF0\x9F\x9A\xA2";
152 $branch = "{$ship}Harbormaster";
153
154 $token = $api_token->getSecret()->openEnvelope();
155 $parts = array(
156 'https://circleci.com/api/v1/project',
157 phutil_escape_uri($github_namespace),
158 phutil_escape_uri($github_name)."?circle-token={$token}",
159 );
160
161 $uri = implode('/', $parts);
162
163 $data_structure = array();
164 switch ($build_type) {
165 case 'tag':
166 $data_structure['tag'] = $build_identifier;
167 break;
168 case 'revision':
169 $data_structure['revision'] = $build_identifier;
170 break;
171 default:
172 throw new Exception(
173 pht(
174 'Unknown CircleCI build type "%s". Expected "%s" or "%s".',
175 $build_type,
176 'tag',
177 'revision'));
178 }
179
180 $data_structure['build_parameters'] = array(
181 'HARBORMASTER_BUILD_TARGET_PHID' => $build_target->getPHID(),
182 );
183
184 $json_data = phutil_json_encode($data_structure);
185
186 $future = id(new HTTPSFuture($uri, $json_data))
187 ->setMethod('POST')
188 ->addHeader('Content-Type', 'application/json')
189 ->addHeader('Accept', 'application/json')
190 ->setTimeout(60);
191
192 $this->resolveFutures(
193 $build,
194 $build_target,
195 array($future));
196
197 $this->logHTTPResponse($build, $build_target, $future, pht('CircleCI'));
198
199 list($status, $body) = $future->resolve();
200 if ($status->isError()) {
201 throw new HarbormasterBuildFailureException();
202 }
203
204 $response = phutil_json_decode($body);
205 $build_uri = idx($response, 'build_url');
206 if (!$build_uri) {
207 throw new Exception(
208 pht(
209 'CircleCI did not return a "%s"!',
210 'build_url'));
211 }
212
213 $target_phid = $build_target->getPHID();
214
215 // Write an artifact to create a link to the external build in CircleCI.
216
217 $api_method = 'harbormaster.createartifact';
218 $api_params = array(
219 'buildTargetPHID' => $target_phid,
220 'artifactType' => HarbormasterURIArtifact::ARTIFACTCONST,
221 'artifactKey' => 'circleci.uri',
222 'artifactData' => array(
223 'uri' => $build_uri,
224 'name' => pht('View in CircleCI'),
225 'ui.external' => true,
226 ),
227 );
228
229 id(new ConduitCall($api_method, $api_params))
230 ->setUser($viewer)
231 ->execute();
232 }
233
234 public function getFieldSpecifications() {
235 return array(
236 'token' => array(
237 'name' => pht('API Token'),
238 'type' => 'credential',
239 'credential.type'
240 => PassphraseTokenCredentialType::CREDENTIAL_TYPE,
241 'credential.provides'
242 => PassphraseTokenCredentialType::PROVIDES_TYPE,
243 'required' => true,
244 ),
245 );
246 }
247
248 public function supportsWaitForMessage() {
249 // NOTE: We always wait for a message, but don't need to show the UI
250 // control since "Wait" is the only valid choice.
251 return false;
252 }
253
254 public function shouldWaitForMessage(HarbormasterBuildTarget $target) {
255 return true;
256 }
257
258}