diff --git a/src/applications/harbormaster/conduit/HarbormasterQueryBuildsConduitAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterQueryBuildsConduitAPIMethod.php index a76f53a362..c4622a902c 100644 --- a/src/applications/harbormaster/conduit/HarbormasterQueryBuildsConduitAPIMethod.php +++ b/src/applications/harbormaster/conduit/HarbormasterQueryBuildsConduitAPIMethod.php @@ -1,76 +1,86 @@ 'optional list', 'phids' => 'optional list', 'buildStatuses' => 'optional list', 'buildablePHIDs' => 'optional list', 'buildPlanPHIDs' => 'optional list', ) + self::getPagerParamTypes(); } protected function defineReturnType() { return 'wild'; } protected function execute(ConduitAPIRequest $request) { $viewer = $request->getUser(); $call = new ConduitCall( 'harbormaster.build.search', array_filter(array( 'constraints' => array_filter(array( 'ids' => $request->getValue('ids'), 'phids' => $request->getValue('phids'), 'statuses' => $request->getValue('buildStatuses'), 'buildables' => $request->getValue('buildablePHIDs'), 'plans' => $request->getValue('buildPlanPHIDs'), )), 'attachments' => array( 'querybuilds' => true, ), 'limit' => $request->getValue('limit'), 'before' => $request->getValue('before'), 'after' => $request->getValue('after'), ))); $subsumption = $call->setUser($viewer) ->execute(); $data = array(); foreach ($subsumption['data'] as $build_data) { $querybuilds = idxv( $build_data, array('attachments', 'querybuilds'), array()); $fields = idx($build_data, 'fields', array()); unset($build_data['fields']); unset($build_data['attachments']); + + // To retain backward compatibility, remove newer keys from the + // result array. + $fields['buildStatus'] = array_select_keys( + $fields['buildStatus'], + array( + 'value', + 'name', + )); + $data[] = array_mergev(array($build_data, $querybuilds, $fields)); } $subsumption['data'] = $data; return $subsumption; } } diff --git a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php index a2cdba5b63..b0e3105d78 100644 --- a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php +++ b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php @@ -1,145 +1,169 @@ pht('Inactive'), - self::STATUS_PENDING => pht('Pending'), - self::STATUS_BUILDING => pht('Building'), - self::STATUS_PASSED => pht('Passed'), - self::STATUS_FAILED => pht('Failed'), - self::STATUS_ABORTED => pht('Aborted'), - self::STATUS_ERROR => pht('Unexpected Error'), - self::STATUS_PAUSED => pht('Paused'), - self::STATUS_DEADLOCKED => pht('Deadlocked'), - ); + $specs = self::getBuildStatusSpecMap(); + return ipull($specs, 'name'); } public static function getBuildStatusIcon($status) { - switch ($status) { - case self::STATUS_INACTIVE: - case self::STATUS_PENDING: - return PHUIStatusItemView::ICON_OPEN; - case self::STATUS_BUILDING: - return PHUIStatusItemView::ICON_RIGHT; - case self::STATUS_PASSED: - return PHUIStatusItemView::ICON_ACCEPT; - case self::STATUS_FAILED: - return PHUIStatusItemView::ICON_REJECT; - case self::STATUS_ABORTED: - return PHUIStatusItemView::ICON_MINUS; - case self::STATUS_ERROR: - return PHUIStatusItemView::ICON_MINUS; - case self::STATUS_PAUSED: - return PHUIStatusItemView::ICON_MINUS; - case self::STATUS_DEADLOCKED: - return PHUIStatusItemView::ICON_WARNING; - default: - return PHUIStatusItemView::ICON_QUESTION; - } + $spec = self::getBuildStatusSpec($status); + return idx($spec, 'icon', 'fa-question-circle'); } public static function getBuildStatusColor($status) { - switch ($status) { - case self::STATUS_INACTIVE: - return 'dark'; - case self::STATUS_PENDING: - case self::STATUS_BUILDING: - return 'blue'; - case self::STATUS_PASSED: - return 'green'; - case self::STATUS_FAILED: - case self::STATUS_ABORTED: - case self::STATUS_ERROR: - case self::STATUS_DEADLOCKED: - return 'red'; - case self::STATUS_PAUSED: - return 'dark'; - default: - return 'bluegrey'; - } + $spec = self::getBuildStatusSpec($status); + return idx($spec, 'color', 'bluegrey'); + } + + public static function getBuildStatusANSIColor($status) { + $spec = self::getBuildStatusSpec($status); + return idx($spec, 'color.ansi', 'magenta'); } public static function getWaitingStatusConstants() { return array( self::STATUS_INACTIVE, self::STATUS_PENDING, ); } public static function getActiveStatusConstants() { return array( self::STATUS_BUILDING, self::STATUS_PAUSED, ); } public static function getCompletedStatusConstants() { return array( self::STATUS_PASSED, self::STATUS_FAILED, self::STATUS_ABORTED, self::STATUS_ERROR, self::STATUS_DEADLOCKED, ); } + private static function getBuildStatusSpecMap() { + return array( + self::STATUS_INACTIVE => array( + 'name' => pht('Inactive'), + 'icon' => 'fa-circle-o', + 'color' => 'dark', + 'color.ansi' => 'yellow', + ), + self::STATUS_PENDING => array( + 'name' => pht('Pending'), + 'icon' => 'fa-circle-o', + 'color' => 'blue', + 'color.ansi' => 'yellow', + ), + self::STATUS_BUILDING => array( + 'name' => pht('Building'), + 'icon' => 'fa-chevron-circle-right', + 'color' => 'blue', + 'color.ansi' => 'yellow', + ), + self::STATUS_PASSED => array( + 'name' => pht('Passed'), + 'icon' => 'fa-check-circle', + 'color' => 'green', + 'color.ansi' => 'green', + ), + self::STATUS_FAILED => array( + 'name' => pht('Failed'), + 'icon' => 'fa-times-circle', + 'color' => 'red', + 'color.ansi' => 'red', + ), + self::STATUS_ABORTED => array( + 'name' => pht('Aborted'), + 'icon' => 'fa-minus-circle', + 'color' => 'red', + 'color.ansi' => 'red', + ), + self::STATUS_ERROR => array( + 'name' => pht('Unexpected Error'), + 'icon' => 'fa-minus-circle', + 'color' => 'red', + 'color.ansi' => 'red', + ), + self::STATUS_PAUSED => array( + 'name' => pht('Paused'), + 'icon' => 'fa-minus-circle', + 'color' => 'dark', + 'color.ansi' => 'yellow', + ), + self::STATUS_DEADLOCKED => array( + 'name' => pht('Deadlocked'), + 'icon' => 'fa-exclamation-circle', + 'color' => 'red', + 'color.ansi' => 'red', + ), + ); + } + + private static function getBuildStatusSpec($status) { + return idx(self::getBuildStatusSpecMap(), $status, array()); + } + } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index 958eaa1f2b..92d4293913 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -1,451 +1,453 @@ setBuildStatus(HarbormasterBuildStatus::STATUS_INACTIVE) ->setBuildGeneration(0); } public function delete() { $this->openTransaction(); $this->deleteUnprocessedCommands(); $result = parent::delete(); $this->saveTransaction(); return $result; } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'buildParameters' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'buildStatus' => 'text32', 'buildGeneration' => 'uint32', 'planAutoKey' => 'text32?', 'initiatorPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_buildable' => array( 'columns' => array('buildablePHID'), ), 'key_plan' => array( 'columns' => array('buildPlanPHID'), ), 'key_status' => array( 'columns' => array('buildStatus'), ), 'key_planautokey' => array( 'columns' => array('buildablePHID', 'planAutoKey'), 'unique' => true, ), 'key_initiator' => array( 'columns' => array('initiatorPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterBuildPHIDType::TYPECONST); } public function attachBuildable(HarbormasterBuildable $buildable) { $this->buildable = $buildable; return $this; } public function getBuildable() { return $this->assertAttached($this->buildable); } public function getName() { if ($this->getBuildPlan()) { return $this->getBuildPlan()->getName(); } return pht('Build'); } public function attachBuildPlan( HarbormasterBuildPlan $build_plan = null) { $this->buildPlan = $build_plan; return $this; } public function getBuildPlan() { return $this->assertAttached($this->buildPlan); } public function getBuildTargets() { return $this->assertAttached($this->buildTargets); } public function attachBuildTargets(array $targets) { $this->buildTargets = $targets; return $this; } public function isBuilding() { return $this->getBuildStatus() === HarbormasterBuildStatus::STATUS_PENDING || $this->getBuildStatus() === HarbormasterBuildStatus::STATUS_BUILDING; } public function isAutobuild() { return ($this->getPlanAutoKey() !== null); } public function retrieveVariablesFromBuild() { $results = array( 'buildable.diff' => null, 'buildable.revision' => null, 'buildable.commit' => null, 'repository.callsign' => null, 'repository.phid' => null, 'repository.vcs' => null, 'repository.uri' => null, 'step.timestamp' => null, 'build.id' => null, 'initiator.phid' => null, ); foreach ($this->getBuildParameters() as $key => $value) { $results['build/'.$key] = $value; } $buildable = $this->getBuildable(); $object = $buildable->getBuildableObject(); $object_variables = $object->getBuildVariables(); $results = $object_variables + $results; $results['step.timestamp'] = time(); $results['build.id'] = $this->getID(); $results['initiator.phid'] = $this->getInitiatorPHID(); return $results; } public static function getAvailableBuildVariables() { $objects = id(new PhutilClassMapQuery()) ->setAncestorClass('HarbormasterBuildableInterface') ->execute(); $variables = array(); $variables[] = array( 'step.timestamp' => pht('The current UNIX timestamp.'), 'build.id' => pht('The ID of the current build.'), 'target.phid' => pht('The PHID of the current build target.'), 'initiator.phid' => pht( 'The PHID of the user or Object that initiated the build, '. 'if applicable.'), ); foreach ($objects as $object) { $variables[] = $object->getAvailableBuildVariables(); } $variables = array_mergev($variables); return $variables; } public function isComplete() { return in_array( $this->getBuildStatus(), HarbormasterBuildStatus::getCompletedStatusConstants()); } public function isPaused() { return ($this->getBuildStatus() == HarbormasterBuildStatus::STATUS_PAUSED); } public function getURI() { $id = $this->getID(); return "/harbormaster/build/{$id}/"; } /* -( Build Commands )----------------------------------------------------- */ private function getUnprocessedCommands() { return $this->assertAttached($this->unprocessedCommands); } public function attachUnprocessedCommands(array $commands) { $this->unprocessedCommands = $commands; return $this; } public function canRestartBuild() { if ($this->isAutobuild()) { return false; } return !$this->isRestarting(); } public function canPauseBuild() { if ($this->isAutobuild()) { return false; } return !$this->isComplete() && !$this->isPaused() && !$this->isPausing(); } public function canAbortBuild() { if ($this->isAutobuild()) { return false; } return !$this->isComplete(); } public function canResumeBuild() { if ($this->isAutobuild()) { return false; } return $this->isPaused() && !$this->isResuming(); } public function isPausing() { $is_pausing = false; foreach ($this->getUnprocessedCommands() as $command_object) { $command = $command_object->getCommand(); switch ($command) { case HarbormasterBuildCommand::COMMAND_PAUSE: $is_pausing = true; break; case HarbormasterBuildCommand::COMMAND_RESUME: case HarbormasterBuildCommand::COMMAND_RESTART: $is_pausing = false; break; case HarbormasterBuildCommand::COMMAND_ABORT: $is_pausing = true; break; } } return $is_pausing; } public function isResuming() { $is_resuming = false; foreach ($this->getUnprocessedCommands() as $command_object) { $command = $command_object->getCommand(); switch ($command) { case HarbormasterBuildCommand::COMMAND_RESTART: case HarbormasterBuildCommand::COMMAND_RESUME: $is_resuming = true; break; case HarbormasterBuildCommand::COMMAND_PAUSE: $is_resuming = false; break; case HarbormasterBuildCommand::COMMAND_ABORT: $is_resuming = false; break; } } return $is_resuming; } public function isRestarting() { $is_restarting = false; foreach ($this->getUnprocessedCommands() as $command_object) { $command = $command_object->getCommand(); switch ($command) { case HarbormasterBuildCommand::COMMAND_RESTART: $is_restarting = true; break; } } return $is_restarting; } public function isAborting() { $is_aborting = false; foreach ($this->getUnprocessedCommands() as $command_object) { $command = $command_object->getCommand(); switch ($command) { case HarbormasterBuildCommand::COMMAND_ABORT: $is_aborting = true; break; } } return $is_aborting; } public function deleteUnprocessedCommands() { foreach ($this->getUnprocessedCommands() as $key => $command_object) { $command_object->delete(); unset($this->unprocessedCommands[$key]); } return $this; } public function canIssueCommand(PhabricatorUser $viewer, $command) { try { $this->assertCanIssueCommand($viewer, $command); return true; } catch (Exception $ex) { return false; } } public function assertCanIssueCommand(PhabricatorUser $viewer, $command) { $need_edit = false; switch ($command) { case HarbormasterBuildCommand::COMMAND_RESTART: break; case HarbormasterBuildCommand::COMMAND_PAUSE: case HarbormasterBuildCommand::COMMAND_RESUME: case HarbormasterBuildCommand::COMMAND_ABORT: $need_edit = true; break; default: throw new Exception( pht( 'Invalid Harbormaster build command "%s".', $command)); } // Issuing these commands requires that you be able to edit the build, to // prevent enemy engineers from sabotaging your builds. See T9614. if ($need_edit) { PhabricatorPolicyFilter::requireCapability( $viewer, $this->getBuildPlan(), PhabricatorPolicyCapability::CAN_EDIT); } } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HarbormasterBuildTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new HarbormasterBuildTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getBuildable()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBuildable()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('A build inherits policies from its buildable.'); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('buildablePHID') ->setType('phid') ->setDescription(pht('PHID of the object this build is building.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('buildPlanPHID') ->setType('phid') ->setDescription(pht('PHID of the build plan being run.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('buildStatus') ->setType('map') ->setDescription(pht('The current status of this build.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('initiatorPHID') ->setType('phid') ->setDescription(pht('The person (or thing) that started this build.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of this build.')), ); } public function getFieldValuesForConduit() { $status = $this->getBuildStatus(); return array( 'buildablePHID' => $this->getBuildablePHID(), 'buildPlanPHID' => $this->getBuildPlanPHID(), 'buildStatus' => array( 'value' => $status, 'name' => HarbormasterBuildStatus::getBuildStatusName($status), + 'color.ansi' => + HarbormasterBuildStatus::getBuildStatusANSIColor($status), ), 'initiatorPHID' => nonempty($this->getInitiatorPHID(), null), 'name' => $this->getName(), ); } public function getConduitSearchAttachments() { return array( id(new HarbormasterQueryBuildsSearchEngineAttachment()) ->setAttachmentKey('querybuilds'), ); } }