@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 HarbormasterBuild extends HarbormasterDAO
4 implements
5 PhabricatorApplicationTransactionInterface,
6 PhabricatorPolicyInterface,
7 PhabricatorConduitResultInterface,
8 PhabricatorDestructibleInterface {
9
10 protected $buildablePHID;
11 protected $buildPlanPHID;
12 protected $buildStatus;
13 protected $buildGeneration;
14 protected $buildParameters = array();
15 protected $initiatorPHID;
16 protected $planAutoKey;
17
18 private $buildable = self::ATTACHABLE;
19 private $buildPlan = self::ATTACHABLE;
20 private $buildTargets = self::ATTACHABLE;
21 private $unprocessedMessages = self::ATTACHABLE;
22
23 public static function initializeNewBuild(PhabricatorUser $actor) {
24 return id(new HarbormasterBuild())
25 ->setBuildStatus(HarbormasterBuildStatus::STATUS_INACTIVE)
26 ->setBuildGeneration(0);
27 }
28
29 public function delete() {
30 $this->openTransaction();
31 $this->deleteUnprocessedMessages();
32 $result = parent::delete();
33 $this->saveTransaction();
34
35 return $result;
36 }
37
38 protected function getConfiguration() {
39 return array(
40 self::CONFIG_AUX_PHID => true,
41 self::CONFIG_SERIALIZATION => array(
42 'buildParameters' => self::SERIALIZATION_JSON,
43 ),
44 self::CONFIG_COLUMN_SCHEMA => array(
45 'buildStatus' => 'text32',
46 'buildGeneration' => 'uint32',
47 'planAutoKey' => 'text32?',
48 'initiatorPHID' => 'phid?',
49 ),
50 self::CONFIG_KEY_SCHEMA => array(
51 'key_buildable' => array(
52 'columns' => array('buildablePHID'),
53 ),
54 'key_plan' => array(
55 'columns' => array('buildPlanPHID'),
56 ),
57 'key_status' => array(
58 'columns' => array('buildStatus'),
59 ),
60 'key_planautokey' => array(
61 'columns' => array('buildablePHID', 'planAutoKey'),
62 'unique' => true,
63 ),
64 'key_initiator' => array(
65 'columns' => array('initiatorPHID'),
66 ),
67 ),
68 ) + parent::getConfiguration();
69 }
70
71 public function generatePHID() {
72 return PhabricatorPHID::generateNewPHID(
73 HarbormasterBuildPHIDType::TYPECONST);
74 }
75
76 public function attachBuildable(HarbormasterBuildable $buildable) {
77 $this->buildable = $buildable;
78 return $this;
79 }
80
81 public function getBuildable() {
82 return $this->assertAttached($this->buildable);
83 }
84
85 public function getName() {
86 if ($this->getBuildPlan()) {
87 return $this->getBuildPlan()->getName();
88 }
89 return pht('Build');
90 }
91
92 public function attachBuildPlan(
93 ?HarbormasterBuildPlan $build_plan = null) {
94 $this->buildPlan = $build_plan;
95 return $this;
96 }
97
98 public function getBuildPlan() {
99 return $this->assertAttached($this->buildPlan);
100 }
101
102 public function getBuildTargets() {
103 return $this->assertAttached($this->buildTargets);
104 }
105
106 public function attachBuildTargets(array $targets) {
107 $this->buildTargets = $targets;
108 return $this;
109 }
110
111 public function isBuilding() {
112 return $this->getBuildStatusObject()->isBuilding();
113 }
114
115 public function isAutobuild() {
116 return ($this->getPlanAutoKey() !== null);
117 }
118
119 public function retrieveVariablesFromBuild() {
120 $results = array(
121 'buildable.diff' => null,
122 'buildable.revision' => null,
123 'buildable.commit' => null,
124 'repository.callsign' => null,
125 'repository.phid' => null,
126 'repository.vcs' => null,
127 'repository.uri' => null,
128 'step.timestamp' => null,
129 'build.id' => null,
130 'initiator.phid' => null,
131
132 'buildable.phid' => null,
133 'buildable.object.phid' => null,
134 'buildable.container.phid' => null,
135 'build.phid' => null,
136 );
137
138 foreach ($this->getBuildParameters() as $key => $value) {
139 $results['build/'.$key] = $value;
140 }
141
142 $buildable = $this->getBuildable();
143 $object = $buildable->getBuildableObject();
144
145 $object_variables = $object->getBuildVariables();
146
147 $results = $object_variables + $results;
148
149 $results['step.timestamp'] = time();
150 $results['build.id'] = $this->getID();
151 $results['initiator.phid'] = $this->getInitiatorPHID();
152
153 $results['buildable.phid'] = $buildable->getPHID();
154 $results['buildable.object.phid'] = $object->getPHID();
155 $results['buildable.container.phid'] = $buildable->getContainerPHID();
156 $results['build.phid'] = $this->getPHID();
157
158 return $results;
159 }
160
161 public static function getAvailableBuildVariables() {
162 $objects = id(new PhutilClassMapQuery())
163 ->setAncestorClass(HarbormasterBuildableInterface::class)
164 ->execute();
165
166 $variables = array();
167 $variables[] = array(
168 'step.timestamp' => pht('The current UNIX timestamp.'),
169 'build.id' => pht('The ID of the current build.'),
170 'target.phid' => pht('The PHID of the current build target.'),
171 'initiator.phid' => pht(
172 'The PHID of the user or Object that initiated the build, '.
173 'if applicable.'),
174 'buildable.phid' => pht(
175 'The object PHID of the Harbormaster Buildable being built.'),
176 'buildable.object.phid' => pht(
177 'The object PHID of the object (usually a diff or commit) '.
178 'being built.'),
179 'buildable.container.phid' => pht(
180 'The object PHID of the container (usually a revision or repository) '.
181 'for the object being built.'),
182 'build.phid' => pht(
183 'The object PHID of the Harbormaster Build being built.'),
184 );
185
186 foreach ($objects as $object) {
187 $variables[] = $object->getAvailableBuildVariables();
188 }
189
190 $variables = array_mergev($variables);
191 return $variables;
192 }
193
194 public function isComplete() {
195 return $this->getBuildStatusObject()->isComplete();
196 }
197
198 public function isPaused() {
199 return $this->getBuildStatusObject()->isPaused();
200 }
201
202 public function isPassed() {
203 return $this->getBuildStatusObject()->isPassed();
204 }
205
206 public function isFailed() {
207 return $this->getBuildStatusObject()->isFailed();
208 }
209
210 public function isPending() {
211 return $this->getBuildstatusObject()->isPending();
212 }
213
214 public function getURI() {
215 $id = $this->getID();
216 return "/harbormaster/build/{$id}/";
217 }
218
219 public function getBuildPendingStatusObject() {
220 list($pending_status) = $this->getUnprocessedMessageState();
221
222 if ($pending_status !== null) {
223 return HarbormasterBuildStatus::newBuildStatusObject($pending_status);
224 }
225
226 return $this->getBuildStatusObject();
227 }
228
229 protected function getBuildStatusObject() {
230 $status_key = $this->getBuildStatus();
231 return HarbormasterBuildStatus::newBuildStatusObject($status_key);
232 }
233
234 public function getObjectName() {
235 return pht('Build %d', $this->getID());
236 }
237
238
239/* -( Build Messages )----------------------------------------------------- */
240
241
242 private function getUnprocessedMessages() {
243 return $this->assertAttached($this->unprocessedMessages);
244 }
245
246 public function getUnprocessedMessagesForApply() {
247 $unprocessed_state = $this->getUnprocessedMessageState();
248 list($pending_status, $apply_messages) = $unprocessed_state;
249
250 return $apply_messages;
251 }
252
253 private function getUnprocessedMessageState() {
254 // NOTE: If a build has multiple unprocessed messages, we'll ignore
255 // messages that are obsoleted by a later or stronger message.
256 //
257 // For example, if a build has both "pause" and "abort" messages in queue,
258 // we just ignore the "pause" message and perform an "abort", since pausing
259 // first wouldn't affect the final state, so we can just skip it.
260 //
261 // Likewise, if a build has both "restart" and "abort" messages, the most
262 // recent message is controlling: we'll take whichever action a command
263 // was most recently issued for.
264
265 $is_restarting = false;
266 $is_aborting = false;
267 $is_pausing = false;
268 $is_resuming = false;
269
270 $apply_messages = array();
271
272 foreach ($this->getUnprocessedMessages() as $message_object) {
273 $message_type = $message_object->getType();
274 switch ($message_type) {
275 case HarbormasterBuildMessageRestartTransaction::MESSAGETYPE:
276 $is_restarting = true;
277 $is_aborting = false;
278 $apply_messages = array($message_object);
279 break;
280 case HarbormasterBuildMessageAbortTransaction::MESSAGETYPE:
281 $is_aborting = true;
282 $is_restarting = false;
283 $apply_messages = array($message_object);
284 break;
285 case HarbormasterBuildMessagePauseTransaction::MESSAGETYPE:
286 $is_pausing = true;
287 $is_resuming = false;
288 $apply_messages = array($message_object);
289 break;
290 case HarbormasterBuildMessageResumeTransaction::MESSAGETYPE:
291 $is_resuming = true;
292 $is_pausing = false;
293 $apply_messages = array($message_object);
294 break;
295 }
296 }
297
298 $pending_status = null;
299 if ($is_restarting) {
300 $pending_status = HarbormasterBuildStatus::PENDING_RESTARTING;
301 } else if ($is_aborting) {
302 $pending_status = HarbormasterBuildStatus::PENDING_ABORTING;
303 } else if ($is_pausing) {
304 $pending_status = HarbormasterBuildStatus::PENDING_PAUSING;
305 } else if ($is_resuming) {
306 $pending_status = HarbormasterBuildStatus::PENDING_RESUMING;
307 }
308
309 return array($pending_status, $apply_messages);
310 }
311
312 /**
313 * @param array<HarbormasterBuildMessage> $messages
314 */
315 public function attachUnprocessedMessages(array $messages) {
316 assert_instances_of($messages, HarbormasterBuildMessage::class);
317 $this->unprocessedMessages = $messages;
318 return $this;
319 }
320
321 public function isPausing() {
322 return $this->getBuildPendingStatusObject()->isPausing();
323 }
324
325 public function isResuming() {
326 return $this->getBuildPendingStatusObject()->isResuming();
327 }
328
329 public function isRestarting() {
330 return $this->getBuildPendingStatusObject()->isRestarting();
331 }
332
333 public function isAborting() {
334 return $this->getBuildPendingStatusObject()->isAborting();
335 }
336
337 public function markUnprocessedMessagesAsProcessed() {
338 foreach ($this->getUnprocessedMessages() as $key => $message_object) {
339 $message_object
340 ->setIsConsumed(1)
341 ->save();
342 }
343
344 return $this;
345 }
346
347 public function deleteUnprocessedMessages() {
348 foreach ($this->getUnprocessedMessages() as $key => $message_object) {
349 $message_object->delete();
350 unset($this->unprocessedMessages[$key]);
351 }
352
353 return $this;
354 }
355
356 public function sendMessage(PhabricatorUser $viewer, $message_type) {
357 HarbormasterBuildMessage::initializeNewMessage($viewer)
358 ->setReceiverPHID($this->getPHID())
359 ->setType($message_type)
360 ->save();
361
362 PhabricatorWorker::scheduleTask(
363 'HarbormasterBuildWorker',
364 array(
365 'buildID' => $this->getID(),
366 ),
367 array(
368 'objectPHID' => $this->getPHID(),
369 'containerPHID' => $this->getBuildablePHID(),
370 ));
371 }
372
373 public function releaseAllArtifacts(PhabricatorUser $viewer) {
374 $targets = id(new HarbormasterBuildTargetQuery())
375 ->setViewer($viewer)
376 ->withBuildPHIDs(array($this->getPHID()))
377 ->withBuildGenerations(array($this->getBuildGeneration()))
378 ->execute();
379
380 if (!$targets) {
381 return;
382 }
383
384 $target_phids = mpull($targets, 'getPHID');
385
386 $artifacts = id(new HarbormasterBuildArtifactQuery())
387 ->setViewer($viewer)
388 ->withBuildTargetPHIDs($target_phids)
389 ->withIsReleased(false)
390 ->execute();
391 foreach ($artifacts as $artifact) {
392 $artifact->releaseArtifact();
393 }
394 }
395
396 public function restartBuild(PhabricatorUser $viewer) {
397 // TODO: This should become transactional.
398
399 // We're restarting the build, so release all previous artifacts.
400 $this->releaseAllArtifacts($viewer);
401
402 // Increment the build generation counter on the build.
403 $this->setBuildGeneration($this->getBuildGeneration() + 1);
404
405 // Currently running targets should periodically check their build
406 // generation (which won't have changed) against the build's generation.
407 // If it is different, they will automatically stop what they're doing
408 // and abort.
409
410 // Previously we used to delete targets, logs and artifacts here. Instead,
411 // leave them around so users can view previous generations of this build.
412 }
413
414
415/* -( PhabricatorApplicationTransactionInterface )------------------------- */
416
417
418 public function getApplicationTransactionEditor() {
419 return new HarbormasterBuildTransactionEditor();
420 }
421
422 public function getApplicationTransactionTemplate() {
423 return new HarbormasterBuildTransaction();
424 }
425
426
427/* -( PhabricatorPolicyInterface )----------------------------------------- */
428
429
430 public function getCapabilities() {
431 return array(
432 PhabricatorPolicyCapability::CAN_VIEW,
433 PhabricatorPolicyCapability::CAN_EDIT,
434 );
435 }
436
437 public function getPolicy($capability) {
438 return $this->getBuildable()->getPolicy($capability);
439 }
440
441 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
442 return $this->getBuildable()->hasAutomaticCapability(
443 $capability,
444 $viewer);
445 }
446
447 public function describeAutomaticCapability($capability) {
448 return pht('A build inherits policies from its buildable.');
449 }
450
451
452/* -( PhabricatorConduitResultInterface )---------------------------------- */
453
454
455 public function getFieldSpecificationsForConduit() {
456 return array(
457 id(new PhabricatorConduitSearchFieldSpecification())
458 ->setKey('buildablePHID')
459 ->setType('phid')
460 ->setDescription(pht('PHID of the object this build is building.')),
461 id(new PhabricatorConduitSearchFieldSpecification())
462 ->setKey('buildPlanPHID')
463 ->setType('phid')
464 ->setDescription(pht('PHID of the build plan being run.')),
465 id(new PhabricatorConduitSearchFieldSpecification())
466 ->setKey('buildStatus')
467 ->setType('map<string, wild>')
468 ->setDescription(pht('The current status of this build.')),
469 id(new PhabricatorConduitSearchFieldSpecification())
470 ->setKey('initiatorPHID')
471 ->setType('phid')
472 ->setDescription(pht('The person (or thing) that started this build.')),
473 id(new PhabricatorConduitSearchFieldSpecification())
474 ->setKey('name')
475 ->setType('string')
476 ->setDescription(pht('The name of this build.')),
477 );
478 }
479
480 public function getFieldValuesForConduit() {
481 $status = $this->getBuildStatus();
482 return array(
483 'buildablePHID' => $this->getBuildablePHID(),
484 'buildPlanPHID' => $this->getBuildPlanPHID(),
485 'buildStatus' => array(
486 'value' => $status,
487 'name' => HarbormasterBuildStatus::getBuildStatusName($status),
488 'color.ansi' =>
489 HarbormasterBuildStatus::getBuildStatusANSIColor($status),
490 ),
491 'initiatorPHID' => nonempty($this->getInitiatorPHID(), null),
492 'name' => $this->getName(),
493 );
494 }
495
496 public function getConduitSearchAttachments() {
497 return array(
498 id(new HarbormasterQueryBuildsSearchEngineAttachment())
499 ->setAttachmentKey('querybuilds'),
500 );
501 }
502
503
504/* -( PhabricatorDestructibleInterface )----------------------------------- */
505
506 public function destroyObjectPermanently(
507 PhabricatorDestructionEngine $engine) {
508 $viewer = $engine->getViewer();
509
510 $this->openTransaction();
511 $targets = id(new HarbormasterBuildTargetQuery())
512 ->setViewer($viewer)
513 ->withBuildPHIDs(array($this->getPHID()))
514 ->execute();
515 foreach ($targets as $target) {
516 $engine->destroyObject($target);
517 }
518
519 $messages = id(new HarbormasterBuildMessageQuery())
520 ->setViewer($viewer)
521 ->withReceiverPHIDs(array($this->getPHID()))
522 ->execute();
523 foreach ($messages as $message) {
524 $engine->destroyObject($message);
525 }
526
527 $this->delete();
528 $this->saveTransaction();
529 }
530
531
532}