diff --git a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php index 843ffd4702..b56c7de7f7 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php @@ -1,151 +1,155 @@ getViewer(); $id = $request->getURIData('id'); $action = $request->getURIData('action'); $via = $request->getURIData('via'); $build = id(new HarbormasterBuildQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$build) { return new Aphront404Response(); } switch ($action) { case HarbormasterBuildCommand::COMMAND_RESTART: $can_issue = $build->canRestartBuild(); break; case HarbormasterBuildCommand::COMMAND_PAUSE: $can_issue = $build->canPauseBuild(); break; case HarbormasterBuildCommand::COMMAND_RESUME: $can_issue = $build->canResumeBuild(); break; case HarbormasterBuildCommand::COMMAND_ABORT: $can_issue = $build->canAbortBuild(); break; default: return new Aphront400Response(); } $build->assertCanIssueCommand($viewer, $action); switch ($via) { case 'buildable': $return_uri = '/'.$build->getBuildable()->getMonogram(); break; default: $return_uri = $this->getApplicationURI('/build/'.$build->getID().'/'); break; } if ($request->isDialogFormPost() && $can_issue) { $build->sendMessage($viewer, $action); return id(new AphrontRedirectResponse())->setURI($return_uri); } switch ($action) { case HarbormasterBuildCommand::COMMAND_RESTART: if ($can_issue) { $title = pht('Really restart build?'); $body = pht( 'Progress on this build will be discarded and the build will '. 'restart. Side effects of the build will occur again. Really '. 'restart build?'); $submit = pht('Restart Build'); + } else if (!$build->getBuildPlan()->canRestartBuildPlan()) { + $title = pht('Not Restartable'); + $body = pht( + 'The build plan for this build is not restartable, so you '. + 'can not restart the build.'); } else { $title = pht('Unable to Restart Build'); if ($build->isRestarting()) { $body = pht( 'This build is already restarting. You can not reissue a '. 'restart command to a restarting build.'); } else { $body = pht('You can not restart this build.'); } } break; case HarbormasterBuildCommand::COMMAND_ABORT: if ($can_issue) { $title = pht('Really abort build?'); $body = pht( 'Progress on this build will be discarded. Really '. 'abort build?'); $submit = pht('Abort Build'); } else { $title = pht('Unable to Abort Build'); $body = pht('You can not abort this build.'); } break; case HarbormasterBuildCommand::COMMAND_PAUSE: if ($can_issue) { $title = pht('Really pause build?'); $body = pht( 'If you pause this build, work will halt once the current steps '. 'complete. You can resume the build later.'); $submit = pht('Pause Build'); } else { $title = pht('Unable to Pause Build'); if ($build->isComplete()) { $body = pht( 'This build is already complete. You can not pause a completed '. 'build.'); } else if ($build->isPaused()) { $body = pht( 'This build is already paused. You can not pause a build which '. 'has already been paused.'); } else if ($build->isPausing()) { $body = pht( 'This build is already pausing. You can not reissue a pause '. 'command to a pausing build.'); } else { $body = pht( 'This build can not be paused.'); } } break; case HarbormasterBuildCommand::COMMAND_RESUME: if ($can_issue) { $title = pht('Really resume build?'); $body = pht( 'Work will continue on the build. Really resume?'); $submit = pht('Resume Build'); } else { $title = pht('Unable to Resume Build'); if ($build->isResuming()) { $body = pht( 'This build is already resuming. You can not reissue a resume '. 'command to a resuming build.'); } else if (!$build->isPaused()) { $body = pht( 'This build is not paused. You can only resume a paused '. 'build.'); } } break; } - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) + $dialog = $this->newDialog() ->setTitle($title) ->appendChild($body) ->addCancelButton($return_uri); if ($can_issue) { $dialog->addSubmitButton($submit); } - return id(new AphrontDialogResponse())->setDialog($dialog); + return $dialog; } } diff --git a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php index 893d351065..66c186a0d1 100644 --- a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php +++ b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php @@ -1,364 +1,368 @@ key = $key; return $this; } public function getKey() { return $this->key; } public function setName($name) { $this->name = $name; return $this; } public function getName() { return $this->name; } public function setEditInstructions($edit_instructions) { $this->editInstructions = $edit_instructions; return $this; } public function getEditInstructions() { return $this->editInstructions; } public function getOptionMap() { return mpull($this->options, 'getName', 'getKey'); } public function setOptions(array $options) { assert_instances_of($options, 'HarbormasterBuildPlanBehaviorOption'); $key_map = array(); $default = null; foreach ($options as $option) { $key = $option->getKey(); if (isset($key_map[$key])) { throw new Exception( pht( 'Multiple behavior options (for behavior "%s") have the same '. 'key ("%s"). Each option must have a unique key.', $this->getKey(), $key)); } $key_map[$key] = true; if ($option->getIsDefault()) { if ($default === null) { $default = $key; } else { throw new Exception( pht( 'Multiple behavior options (for behavior "%s") are marked as '. 'default options ("%s" and "%s"). Exactly one option must be '. 'marked as the default option.', $this->getKey(), $default, $key)); } } } if ($default === null) { throw new Exception( pht( 'No behavior option is marked as the default option (for '. 'behavior "%s"). Exactly one option must be marked as the '. 'default option.', $this->getKey())); } $this->options = mpull($options, null, 'getKey'); $this->defaultKey = $default; return $this; } public function getOptions() { return $this->options; } public function getPlanOption(HarbormasterBuildPlan $plan) { $behavior_key = $this->getKey(); $storage_key = self::getStorageKeyForBehaviorKey($behavior_key); $plan_value = $plan->getPlanProperty($storage_key); if (isset($this->options[$plan_value])) { return $this->options[$plan_value]; } return idx($this->options, $this->defaultKey); } public static function getTransactionMetadataKey() { return 'behavior-key'; } public static function getStorageKeyForBehaviorKey($behavior_key) { return sprintf('behavior.%s', $behavior_key); } public static function getBehavior($key) { $behaviors = self::newPlanBehaviors(); if (!isset($behaviors[$key])) { throw new Exception( pht( 'No build plan behavior with key "%s" exists.', $key)); } return $behaviors[$key]; } public static function newPlanBehaviors() { $draft_options = array( id(new HarbormasterBuildPlanBehaviorOption()) ->setKey('always') ->setIcon('fa-check-circle-o green') ->setName(pht('Always')) ->setIsDefault(true) ->setDescription( pht( 'Revisions are not sent for review until the build completes, '. 'and are returned to the author for updates if the build fails.')), id(new HarbormasterBuildPlanBehaviorOption()) ->setKey('building') ->setIcon('fa-pause-circle-o yellow') ->setName(pht('If Building')) ->setDescription( pht( 'Revisions are not sent for review until the build completes, '. 'but they will be sent for review even if it fails.')), id(new HarbormasterBuildPlanBehaviorOption()) ->setKey('never') ->setIcon('fa-circle-o red') ->setName(pht('Never')) ->setDescription( pht( 'Revisions are sent for review regardless of the status of the '. 'build.')), ); $land_options = array( id(new HarbormasterBuildPlanBehaviorOption()) ->setKey('always') ->setIcon('fa-check-circle-o green') ->setName(pht('Always')) ->setIsDefault(true) ->setDescription( pht( '"arc land" warns if the build is still running or has '. 'failed.')), id(new HarbormasterBuildPlanBehaviorOption()) ->setKey('building') ->setIcon('fa-pause-circle-o yellow') ->setName(pht('If Building')) ->setDescription( pht( '"arc land" warns if the build is still running, but ignores '. 'the build if it has failed.')), id(new HarbormasterBuildPlanBehaviorOption()) ->setKey('complete') ->setIcon('fa-dot-circle-o yellow') ->setName(pht('If Complete')) ->setDescription( pht( '"arc land" warns if the build has failed, but ignores the '. 'build if it is still running.')), id(new HarbormasterBuildPlanBehaviorOption()) ->setKey('never') ->setIcon('fa-circle-o red') ->setName(pht('Never')) ->setDescription( pht( '"arc land" never warns that the build is still running or '. 'has failed.')), ); $aggregate_options = array( id(new HarbormasterBuildPlanBehaviorOption()) ->setKey('always') ->setIcon('fa-check-circle-o green') ->setName(pht('Always')) ->setIsDefault(true) ->setDescription( pht( 'The buildable waits for the build, and fails if the '. 'build fails.')), id(new HarbormasterBuildPlanBehaviorOption()) ->setKey('building') ->setIcon('fa-pause-circle-o yellow') ->setName(pht('If Building')) ->setDescription( pht( 'The buildable waits for the build, but does not fail '. 'if the build fails.')), id(new HarbormasterBuildPlanBehaviorOption()) ->setKey('never') ->setIcon('fa-circle-o red') ->setName(pht('Never')) ->setDescription( pht( 'The buildable does not wait for the build.')), ); $restart_options = array( id(new HarbormasterBuildPlanBehaviorOption()) - ->setKey('always') + ->setKey(self::RESTARTABLE_ALWAYS) ->setIcon('fa-repeat green') ->setName(pht('Always')) ->setIsDefault(true) ->setDescription( pht('The build may be restarted.')), id(new HarbormasterBuildPlanBehaviorOption()) - ->setKey('never') + ->setKey(self::RESTARTABLE_NEVER) ->setIcon('fa-times red') ->setName(pht('Never')) ->setDescription( pht('The build may not be restarted.')), ); $run_options = array( id(new HarbormasterBuildPlanBehaviorOption()) ->setKey(self::RUNNABLE_IF_EDITABLE) ->setIcon('fa-pencil green') ->setName(pht('If Editable')) ->setIsDefault(true) ->setDescription( pht('Only users who can edit the plan can run it manually.')), id(new HarbormasterBuildPlanBehaviorOption()) ->setKey(self::RUNNABLE_IF_VIEWABLE) ->setIcon('fa-exclamation-triangle yellow') ->setName(pht('If Viewable')) ->setDescription( pht( 'Any user who can view the plan can run it manually.')), ); $behaviors = array( id(new self()) ->setKey('hold-drafts') ->setName(pht('Hold Drafts')) ->setEditInstructions( pht( 'When users create revisions in Differential, the default '. 'behavior is to hold them in the "Draft" state until all builds '. 'pass. Once builds pass, the revisions promote and are sent for '. 'review, which notifies reviewers.'. "\n\n". 'The general intent of this workflow is to make sure reviewers '. 'are only spending time on review once changes survive automated '. 'tests. If a change does not pass tests, it usually is not '. 'really ready for review.'. "\n\n". 'If you want to promote revisions out of "Draft" before builds '. 'pass, or promote revisions even when builds fail, you can '. 'change the promotion behavior. This may be useful if you have '. 'very long-running builds, or some builds which are not very '. 'important.'. "\n\n". 'Users may always use "Request Review" to promote a "Draft" '. 'revision, even if builds have failed or are still in progress.')) ->setOptions($draft_options), id(new self()) ->setKey('arc-land') ->setName(pht('Warn When Landing')) ->setEditInstructions( pht( 'When a user attempts to `arc land` a revision and that revision '. 'has ongoing or failed builds, the default behavior of `arc` is '. 'to warn them about those builds and give them a chance to '. 'reconsider: they may want to wait for ongoing builds to '. 'complete, or fix failed builds before landing the change.'. "\n\n". 'If you do not want to warn users about this build, you can '. 'change the warning behavior. This may be useful if the build '. 'takes a long time to run (so you do not expect users to wait '. 'for it) or the outcome is not important.'. "\n\n". 'This warning is only advisory. Users may always elect to ignore '. 'this warning and continue, even if builds have failed.')) ->setOptions($land_options), id(new self()) ->setKey('buildable') ->setEditInstructions( pht( 'The overall state of a buildable (like a commit or revision) is '. 'normally the aggregation of the individual states of all builds '. 'that have run against it.'. "\n\n". 'Buildables are "building" until all builds pass (which changes '. 'them to "pass"), or any build fails (which changes them to '. '"fail").'. "\n\n". 'You can change this behavior if you do not want to wait for this '. 'build, or do not care if it fails.')) ->setName(pht('Affects Buildable')) ->setOptions($aggregate_options), id(new self()) - ->setKey('restartable') + ->setKey(self::BEHAVIOR_RESTARTABLE) ->setEditInstructions( pht( 'Usually, builds may be restarted. This may be useful if you '. 'suspect a build has failed for environmental or circumstantial '. 'reasons unrelated to the actual code, and want to give it '. 'another chance at glory.'. "\n\n". 'If you want to prevent a build from being restarted, you can '. 'change the behavior here. This may be useful to prevent '. 'accidents where a build with a dangerous side effect (like '. 'deployment) is restarted improperly.')) ->setName(pht('Restartable')) ->setOptions($restart_options), id(new self()) ->setKey(self::BEHAVIOR_RUNNABLE) ->setEditInstructions( pht( 'To run a build manually, you normally must have permission to '. 'edit the related build plan. If you would prefer that anyone who '. 'can see the build plan be able to run and restart the build, you '. 'can change the behavior here.'. "\n\n". 'Note that this controls access to all build management actions: '. '"Run Plan Manually", "Restart", "Abort", "Pause", and "Resume".'. "\n\n". 'WARNING: This may be unsafe, particularly if the build has '. 'side effects like deployment.'. "\n\n". 'If you weaken this policy, an attacker with control of an '. 'account that has "Can View" permission but not "Can Edit" '. 'permission can manually run this build against any old version '. 'of the code, including versions with known security issues.'. "\n\n". 'If running the build has a side effect like deploying code, '. 'they can force deployment of a vulnerable version and then '. 'escalate into an attack against the deployed service.')) ->setName(pht('Runnable')) ->setOptions($run_options), ); return mpull($behaviors, null, 'getKey'); } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index 9b7b64d06b..063f81ff1e 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -1,513 +1,518 @@ 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->getBuildStatusObject()->isBuilding(); } 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 $this->getBuildStatusObject()->isComplete(); } public function isPaused() { return $this->getBuildStatusObject()->isPaused(); } public function isPassed() { return $this->getBuildStatusObject()->isPassed(); } public function getURI() { $id = $this->getID(); return "/harbormaster/build/{$id}/"; } protected function getBuildStatusObject() { $status_key = $this->getBuildStatus(); return HarbormasterBuildStatus::newBuildStatusObject($status_key); } public function getObjectName() { return pht('Build %d', $this->getID()); } /* -( 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; } + $plan = $this->getBuildPlan(); + if (!$plan->canRestartBuildPlan()) { + 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) { $plan = $this->getBuildPlan(); $need_edit = true; switch ($command) { case HarbormasterBuildCommand::COMMAND_RESTART: case HarbormasterBuildCommand::COMMAND_PAUSE: case HarbormasterBuildCommand::COMMAND_RESUME: case HarbormasterBuildCommand::COMMAND_ABORT: if ($plan->canRunWithoutEditCapability()) { $need_edit = false; } 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, $plan, PhabricatorPolicyCapability::CAN_EDIT); } } public function sendMessage(PhabricatorUser $viewer, $command) { // TODO: This should not be an editor transaction, but there are plans to // merge BuildCommand into BuildMessage which should moot this. As this // exists today, it can race against BuildEngine. // This is a bogus content source, but this whole flow should be obsolete // soon. $content_source = PhabricatorContentSource::newForSource( PhabricatorConsoleContentSource::SOURCECONST); $editor = id(new HarbormasterBuildTransactionEditor()) ->setActor($viewer) ->setContentSource($content_source) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $viewer_phid = $viewer->getPHID(); if (!$viewer_phid) { $acting_phid = id(new PhabricatorHarbormasterApplication())->getPHID(); $editor->setActingAsPHID($acting_phid); } $xaction = id(new HarbormasterBuildTransaction()) ->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND) ->setNewValue($command); $editor->applyTransactions($this, array($xaction)); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HarbormasterBuildTransactionEditor(); } public function getApplicationTransactionTemplate() { return new HarbormasterBuildTransaction(); } /* -( 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'), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $viewer = $engine->getViewer(); $this->openTransaction(); $targets = id(new HarbormasterBuildTargetQuery()) ->setViewer($viewer) ->withBuildPHIDs(array($this->getPHID())) ->execute(); foreach ($targets as $target) { $engine->destroyObject($target); } $messages = id(new HarbormasterBuildMessageQuery()) ->setViewer($viewer) ->withReceiverPHIDs(array($this->getPHID())) ->execute(); foreach ($messages as $message) { $engine->destroyObject($message); } $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php index 798201f490..efe62a6f84 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php @@ -1,308 +1,318 @@ setViewer($actor) ->withClasses(array('PhabricatorHarbormasterApplication')) ->executeOne(); $view_policy = $app->getPolicy( HarbormasterBuildPlanDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy( HarbormasterBuildPlanDefaultEditCapability::CAPABILITY); return id(new HarbormasterBuildPlan()) ->setName('') ->setPlanStatus(self::STATUS_ACTIVE) ->attachBuildSteps(array()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort128', 'planStatus' => 'text32', 'planAutoKey' => 'text32?', ), self::CONFIG_KEY_SCHEMA => array( 'key_status' => array( 'columns' => array('planStatus'), ), 'key_name' => array( 'columns' => array('name'), ), 'key_planautokey' => array( 'columns' => array('planAutoKey'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterBuildPlanPHIDType::TYPECONST); } public function attachBuildSteps(array $steps) { assert_instances_of($steps, 'HarbormasterBuildStep'); $this->buildSteps = $steps; return $this; } public function getBuildSteps() { return $this->assertAttached($this->buildSteps); } public function isDisabled() { return ($this->getPlanStatus() == self::STATUS_DISABLED); } public function getURI() { return urisprintf( '/harbormaster/plan/%s/', $this->getID()); } public function getObjectName() { return pht('Plan %d', $this->getID()); } public function getPlanProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function setPlanProperty($key, $value) { $this->properties[$key] = $value; return $this; } /* -( Autoplans )---------------------------------------------------------- */ public function isAutoplan() { return ($this->getPlanAutoKey() !== null); } public function getAutoplan() { if (!$this->isAutoplan()) { return null; } return HarbormasterBuildAutoplan::getAutoplan($this->getPlanAutoKey()); } public function canRunManually() { if ($this->isAutoplan()) { return false; } return true; } public function getName() { $autoplan = $this->getAutoplan(); if ($autoplan) { return $autoplan->getAutoplanName(); } return parent::getName(); } public function hasRunCapability(PhabricatorUser $viewer) { try { $this->assertHasRunCapability($viewer); return true; } catch (PhabricatorPolicyException $ex) { return false; } } public function canRunWithoutEditCapability() { $runnable = HarbormasterBuildPlanBehavior::BEHAVIOR_RUNNABLE; $if_viewable = HarbormasterBuildPlanBehavior::RUNNABLE_IF_VIEWABLE; $option = HarbormasterBuildPlanBehavior::getBehavior($runnable) ->getPlanOption($this); return ($option->getKey() === $if_viewable); } public function assertHasRunCapability(PhabricatorUser $viewer) { if ($this->canRunWithoutEditCapability()) { $capability = PhabricatorPolicyCapability::CAN_VIEW; } else { $capability = PhabricatorPolicyCapability::CAN_EDIT; } PhabricatorPolicyFilter::requireCapability( $viewer, $this, $capability); } + public function canRestartBuildPlan() { + $restartable = HarbormasterBuildPlanBehavior::BEHAVIOR_RESTARTABLE; + $is_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_ALWAYS; + + $option = HarbormasterBuildPlanBehavior::getBehavior($restartable) + ->getPlanOption($this); + + return ($option->getKey() === $is_restartable); + } + /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return false; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HarbormasterBuildPlanEditor(); } public function getApplicationTransactionTemplate() { return new HarbormasterBuildPlanTransaction(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if ($this->isAutoplan()) { return PhabricatorPolicies::getMostOpenPolicy(); } return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: if ($this->isAutoplan()) { return PhabricatorPolicies::POLICY_NOONE; } return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { $messages = array(); switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: if ($this->isAutoplan()) { $messages[] = pht( 'This is an autoplan (a builtin plan provided by an application) '. 'so it can not be edited.'); } break; } return $messages; } /* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { return array( id(new HarbormasterBuildPlanNameNgrams()) ->setValue($this->getName()), ); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of this build plan.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('map') ->setDescription(pht('The current status of this build plan.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('behaviors') ->setType('map') ->setDescription(pht('Behavior configuration for the build plan.')), ); } public function getFieldValuesForConduit() { $behavior_map = array(); $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors(); foreach ($behaviors as $behavior) { $option = $behavior->getPlanOption($this); $behavior_map[$behavior->getKey()] = array( 'value' => $option->getKey(), ); } return array( 'name' => $this->getName(), 'status' => array( 'value' => $this->getPlanStatus(), ), 'behaviors' => $behavior_map, ); } public function getConduitSearchAttachments() { return array(); } /* -( PhabricatorPolicyCodexInterface )------------------------------------ */ public function newPolicyCodex() { return new HarbormasterBuildPlanPolicyCodex(); } }