diff --git a/src/applications/harbormaster/controller/HarbormasterPlanRunController.php b/src/applications/harbormaster/controller/HarbormasterPlanRunController.php index 0f5673087d..980c7fad4b 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanRunController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanRunController.php @@ -1,96 +1,104 @@ getViewer(); $this->requireApplicationCapability( HarbormasterManagePlansCapability::CAPABILITY); $plan_id = $request->getURIData('id'); // NOTE: At least for now, this only requires the "Can Manage Plans" // capability, not the "Can Edit" capability. Possibly it should have // a more stringent requirement, though. $plan = id(new HarbormasterBuildPlanQuery()) ->setViewer($viewer) ->withIDs(array($plan_id)) ->executeOne(); if (!$plan) { return new Aphront404Response(); } + $cancel_uri = $this->getApplicationURI("plan/{$plan_id}/"); + + if (!$plan->canRunManually()) { + return $this->newDialog() + ->setTitle(pht('Can Not Run Plan')) + ->appendParagraph(pht('This plan can not be run manually.')) + ->addCancelButton($cancel_uri); + } + $e_name = true; $v_name = null; $errors = array(); if ($request->isFormPost()) { $buildable = HarbormasterBuildable::initializeNewBuildable($viewer) ->setIsManualBuildable(true); $v_name = $request->getStr('buildablePHID'); if ($v_name) { $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withNames(array($v_name)) ->executeOne(); if ($object instanceof HarbormasterBuildableInterface) { $buildable ->setBuildablePHID($object->getHarbormasterBuildablePHID()) ->setContainerPHID($object->getHarbormasterContainerPHID()); } else { $e_name = pht('Invalid'); $errors[] = pht('Enter the name of a revision or commit.'); } } else { $e_name = pht('Required'); $errors[] = pht('You must choose a revision or commit to build.'); } if (!$errors) { $buildable->save(); $buildable->applyPlan($plan); $buildable_uri = '/B'.$buildable->getID(); return id(new AphrontRedirectResponse())->setURI($buildable_uri); } } if ($errors) { $errors = id(new PHUIInfoView())->setErrors($errors); } $title = pht('Run Build Plan Manually'); - $cancel_uri = $this->getApplicationURI("plan/{$plan_id}/"); $save_button = pht('Run Plan Manually'); $form = id(new PHUIFormLayoutView()) ->setUser($viewer) ->appendRemarkupInstructions( pht( "Enter the name of a commit or revision to run this plan on (for ". "example, `rX123456` or `D123`).\n\n". "For more detailed output, you can also run manual builds from ". "the command line:\n\n". " phabricator/ $ ./bin/harbormaster build --plan %s", $plan->getID())) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Buildable Name')) ->setName('buildablePHID') ->setError($e_name) ->setValue($v_name)); return $this->newDialog() ->setWidth(AphrontDialogView::WIDTH_FULL) ->setTitle($title) ->appendChild($form) ->addCancelButton($cancel_uri) ->addSubmitButton($save_button); } } diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php index da35e389a0..95db4c52c0 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php @@ -1,492 +1,494 @@ getviewer(); $id = $request->getURIData('id'); $plan = id(new HarbormasterBuildPlanQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$plan) { return new Aphront404Response(); } $timeline = $this->buildTransactionTimeline( $plan, new HarbormasterBuildPlanTransactionQuery()); $timeline->setShouldTerminate(true); $title = $plan->getName(); $header = id(new PHUIHeaderView()) ->setHeader($plan->getName()) ->setUser($viewer) ->setPolicyObject($plan); $box = id(new PHUIObjectBoxView()) ->setHeader($header); $actions = $this->buildActionList($plan); $this->buildPropertyLists($box, $plan, $actions); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Plan %d', $id)); list($step_list, $has_any_conflicts, $would_deadlock) = $this->buildStepList($plan); if ($would_deadlock) { $box->setFormErrors( array( pht( 'This build plan will deadlock when executed, due to '. 'circular dependencies present in the build plan. '. 'Examine the step list and resolve the deadlock.'), )); } else if ($has_any_conflicts) { // A deadlocking build will also cause all the artifacts to be // invalid, so we just skip showing this message if that's the // case. $box->setFormErrors( array( pht( 'This build plan has conflicts in one or more build steps. '. 'Examine the step list and resolve the listed errors.'), )); } return $this->buildApplicationPage( array( $crumbs, $box, $step_list, $timeline, ), array( 'title' => $title, )); } private function buildStepList(HarbormasterBuildPlan $plan) { $viewer = $this->getViewer(); $run_order = HarbormasterBuildGraph::determineDependencyExecution($plan); $steps = id(new HarbormasterBuildStepQuery()) ->setViewer($viewer) ->withBuildPlanPHIDs(array($plan->getPHID())) ->execute(); $steps = mpull($steps, null, 'getPHID'); $has_manage = $this->hasApplicationCapability( HarbormasterManagePlansCapability::CAPABILITY); $has_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $plan, PhabricatorPolicyCapability::CAN_EDIT); $can_edit = ($has_manage && $has_edit); $step_list = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setNoDataString( pht('This build plan does not have any build steps yet.')); $i = 1; $last_depth = 0; $has_any_conflicts = false; $is_deadlocking = false; foreach ($run_order as $run_ref) { $step = $steps[$run_ref['node']->getPHID()]; $depth = $run_ref['depth'] + 1; if ($last_depth !== $depth) { $last_depth = $depth; $i = 1; } else { $i++; } $implementation = null; try { $implementation = $step->getStepImplementation(); } catch (Exception $ex) { // We can't initialize the implementation. This might be because // it's been renamed or no longer exists. $item = id(new PHUIObjectItemView()) ->setObjectName(pht('Step %d.%d', $depth, $i)) ->setHeader(pht('Unknown Implementation')) ->setStatusIcon('fa-warning red') ->addAttribute(pht( 'This step has an invalid implementation (%s).', $step->getClassName())) ->addAction( id(new PHUIListItemView()) ->setIcon('fa-times') ->addSigil('harbormaster-build-step-delete') ->setWorkflow(true) ->setRenderNameAsTooltip(true) ->setName(pht('Delete')) ->setHref( $this->getApplicationURI('step/delete/'.$step->getID().'/'))); $step_list->addItem($item); continue; } $item = id(new PHUIObjectItemView()) ->setObjectName(pht('Step %d.%d', $depth, $i)) ->setHeader($step->getName()); $item->addAttribute($implementation->getDescription()); $step_id = $step->getID(); $edit_uri = $this->getApplicationURI("step/edit/{$step_id}/"); $delete_uri = $this->getApplicationURI("step/delete/{$step_id}/"); if ($can_edit) { $item->setHref($edit_uri); } $item ->setHref($edit_uri) ->addAction( id(new PHUIListItemView()) ->setIcon('fa-times') ->addSigil('harbormaster-build-step-delete') ->setWorkflow(true) ->setDisabled(!$can_edit) ->setHref( $this->getApplicationURI('step/delete/'.$step->getID().'/'))); $depends = $step->getStepImplementation()->getDependencies($step); $inputs = $step->getStepImplementation()->getArtifactInputs(); $outputs = $step->getStepImplementation()->getArtifactOutputs(); $has_conflicts = false; if ($depends || $inputs || $outputs) { $available_artifacts = HarbormasterBuildStepImplementation::getAvailableArtifacts( $plan, $step, null); $available_artifacts = ipull($available_artifacts, 'type'); list($depends_ui, $has_conflicts) = $this->buildDependsOnList( $depends, pht('Depends On'), $steps); list($inputs_ui, $has_conflicts) = $this->buildArtifactList( $inputs, 'in', pht('Input Artifacts'), $available_artifacts); list($outputs_ui) = $this->buildArtifactList( $outputs, 'out', pht('Output Artifacts'), array()); $item->appendChild( phutil_tag( 'div', array( 'class' => 'harbormaster-artifact-io', ), array( $depends_ui, $inputs_ui, $outputs_ui, ))); } if ($has_conflicts) { $has_any_conflicts = true; $item->setStatusIcon('fa-warning red'); } if ($run_ref['cycle']) { $is_deadlocking = true; } if ($is_deadlocking) { $item->setStatusIcon('fa-warning red'); } $step_list->addItem($item); } $step_list->setFlush(true); $plan_id = $plan->getID(); $header = id(new PHUIHeaderView()) ->setHeader(pht('Build Steps')) ->addActionLink( id(new PHUIButtonView()) ->setText(pht('Add Build Step')) ->setHref($this->getApplicationURI("step/add/{$plan_id}/")) ->setTag('a') ->setIcon( id(new PHUIIconView()) ->setIconFont('fa-plus')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $step_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($step_list); return array($step_box, $has_any_conflicts, $is_deadlocking); } private function buildActionList(HarbormasterBuildPlan $plan) { $viewer = $this->getViewer(); $id = $plan->getID(); $list = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($plan) ->setObjectURI($this->getApplicationURI("plan/{$id}/")); $has_manage = $this->hasApplicationCapability( HarbormasterManagePlansCapability::CAPABILITY); $has_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $plan, PhabricatorPolicyCapability::CAN_EDIT); $can_edit = ($has_manage && $has_edit); $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Plan')) ->setHref($this->getApplicationURI("plan/edit/{$id}/")) ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit) ->setIcon('fa-pencil')); if ($plan->isDisabled()) { $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Enable Plan')) ->setHref($this->getApplicationURI("plan/disable/{$id}/")) ->setWorkflow(true) ->setDisabled(!$can_edit) ->setIcon('fa-check')); } else { $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Disable Plan')) ->setHref($this->getApplicationURI("plan/disable/{$id}/")) ->setWorkflow(true) ->setDisabled(!$can_edit) ->setIcon('fa-ban')); } + $can_run = ($has_manage && $plan->canRunManually()); + $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Run Plan Manually')) ->setHref($this->getApplicationURI("plan/run/{$id}/")) ->setWorkflow(true) - ->setDisabled(!$has_manage) + ->setDisabled(!$can_run) ->setIcon('fa-play-circle')); return $list; } private function buildPropertyLists( PHUIObjectBoxView $box, HarbormasterBuildPlan $plan, PhabricatorActionListView $actions) { $request = $this->getRequest(); $viewer = $request->getUser(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($plan) ->setActionList($actions); $box->addPropertyList($properties); $properties->addProperty( pht('Created'), phabricator_datetime($plan->getDateCreated(), $viewer)); } private function buildArtifactList( array $artifacts, $kind, $name, array $available_artifacts) { $has_conflicts = false; if (!$artifacts) { return array(null, $has_conflicts); } $this->requireResource('harbormaster-css'); $header = phutil_tag( 'div', array( 'class' => 'harbormaster-artifact-summary-header', ), $name); $is_input = ($kind == 'in'); $list = new PHUIStatusListView(); foreach ($artifacts as $artifact) { $error = null; $key = idx($artifact, 'key'); if (!strlen($key)) { $bound = phutil_tag('em', array(), pht('(null)')); if ($is_input) { // This is an unbound input. For now, all inputs are always required. $icon = PHUIStatusItemView::ICON_WARNING; $color = 'red'; $icon_label = pht('Required Input'); $has_conflicts = true; $error = pht('This input is required, but not configured.'); } else { // This is an unnamed output. Outputs do not necessarily need to be // named. $icon = PHUIStatusItemView::ICON_OPEN; $color = 'bluegrey'; $icon_label = pht('Unused Output'); } } else { $bound = phutil_tag('strong', array(), $key); if ($is_input) { if (isset($available_artifacts[$key])) { if ($available_artifacts[$key] == idx($artifact, 'type')) { $icon = PHUIStatusItemView::ICON_ACCEPT; $color = 'green'; $icon_label = pht('Valid Input'); } else { $icon = PHUIStatusItemView::ICON_WARNING; $color = 'red'; $icon_label = pht('Bad Input Type'); $has_conflicts = true; $error = pht( 'This input is bound to the wrong artifact type. It is bound '. 'to a "%s" artifact, but should be bound to a "%s" artifact.', $available_artifacts[$key], idx($artifact, 'type')); } } else { $icon = PHUIStatusItemView::ICON_QUESTION; $color = 'red'; $icon_label = pht('Unknown Input'); $has_conflicts = true; $error = pht( 'This input is bound to an artifact ("%s") which does not exist '. 'at this stage in the build process.', $key); } } else { $icon = PHUIStatusItemView::ICON_DOWN; $color = 'green'; $icon_label = pht('Valid Output'); } } if ($error) { $note = array( phutil_tag('strong', array(), pht('ERROR:')), ' ', $error, ); } else { $note = $bound; } $list->addItem( id(new PHUIStatusItemView()) ->setIcon($icon, $color, $icon_label) ->setTarget($artifact['name']) ->setNote($note)); } $ui = array( $header, $list, ); return array($ui, $has_conflicts); } private function buildDependsOnList( array $step_phids, $name, array $steps) { $has_conflicts = false; if (count($step_phids) === 0) { return null; } $this->requireResource('harbormaster-css'); $steps = mpull($steps, null, 'getPHID'); $header = phutil_tag( 'div', array( 'class' => 'harbormaster-artifact-summary-header', ), $name); $list = new PHUIStatusListView(); foreach ($step_phids as $step_phid) { $error = null; if (idx($steps, $step_phid) === null) { $icon = PHUIStatusItemView::ICON_WARNING; $color = 'red'; $icon_label = pht('Missing Dependency'); $has_conflicts = true; $error = pht( "This dependency specifies a build step which doesn't exist."); } else { $bound = phutil_tag( 'strong', array(), idx($steps, $step_phid)->getName()); $icon = PHUIStatusItemView::ICON_ACCEPT; $color = 'green'; $icon_label = pht('Valid Input'); } if ($error) { $note = array( phutil_tag('strong', array(), pht('ERROR:')), ' ', $error, ); } else { $note = $bound; } $list->addItem( id(new PHUIStatusItemView()) ->setIcon($icon, $color, $icon_label) ->setTarget(pht('Build Step')) ->setNote($note)); } $ui = array( $header, $list, ); return array($ui, $has_conflicts); } } diff --git a/src/applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php b/src/applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php index 86b8596fe3..fc0f670633 100644 --- a/src/applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php +++ b/src/applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php @@ -1,94 +1,99 @@ setName('build') ->setExamples('**build** [__options__] __buildable__ --plan __id__') ->setSynopsis(pht('Run plan __id__ on __buildable__.')) ->setArguments( array( array( 'name' => 'plan', 'param' => 'id', 'help' => pht('ID of build plan to run.'), ), array( 'name' => 'buildable', 'wildcard' => true, ), )); } public function execute(PhutilArgumentParser $args) { $viewer = $this->getViewer(); $names = $args->getArg('buildable'); if (count($names) != 1) { throw new PhutilArgumentUsageException( pht('Specify exactly one buildable object, by object name.')); } $name = head($names); $buildable = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withNames($names) ->executeOne(); if (!$buildable) { throw new PhutilArgumentUsageException( pht('No such buildable "%s"!', $name)); } if (!($buildable instanceof HarbormasterBuildableInterface)) { throw new PhutilArgumentUsageException( pht('Object "%s" is not a buildable!', $name)); } $plan_id = $args->getArg('plan'); if (!$plan_id) { throw new PhutilArgumentUsageException( pht( 'Use %s to specify a build plan to run.', '--plan')); } $plan = id(new HarbormasterBuildPlanQuery()) ->setViewer($viewer) ->withIDs(array($plan_id)) ->executeOne(); if (!$plan) { throw new PhutilArgumentUsageException( pht('Build plan "%s" does not exist.', $plan_id)); } + if (!$plan->canRunManually()) { + throw new PhutilArgumentUsageException( + pht('This build plan can not be run manually.')); + } + $console = PhutilConsole::getConsole(); $buildable = HarbormasterBuildable::initializeNewBuildable($viewer) ->setIsManualBuildable(true) ->setBuildablePHID($buildable->getHarbormasterBuildablePHID()) ->setContainerPHID($buildable->getHarbormasterContainerPHID()) ->save(); $console->writeOut( "%s\n", pht( 'Applying plan %s to new buildable %s...', $plan->getID(), 'B'.$buildable->getID())); $console->writeOut( "\n %s\n\n", PhabricatorEnv::getProductionURI('/B'.$buildable->getID())); PhabricatorWorker::setRunAllTasksInProcess(true); $buildable->applyPlan($plan); $console->writeOut("%s\n", pht('Done.')); return 0; } } diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php index 7b15b0034c..cf3d9ee7d7 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php @@ -1,183 +1,192 @@ setName('') ->setPlanStatus(self::STATUS_ACTIVE) ->attachBuildSteps(array()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, 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); } /* -( 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(); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return false; } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HarbormasterBuildPlanEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new HarbormasterBuildPlanTransaction(); } 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) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::getMostOpenPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: // NOTE: In practice, this policy is always limited by the "Mangage // Build Plans" policy. if ($this->isAutoplan()) { return PhabricatorPolicies::POLICY_NOONE; } return PhabricatorPolicies::getMostOpenPolicy(); } } 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; } }