diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php index c92de97c04..1ce17bf4f4 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php @@ -1,478 +1,479 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $this->id; $plan = id(new HarbormasterBuildPlanQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$plan) { return new Aphront404Response(); } $xactions = id(new HarbormasterBuildPlanTransactionQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($plan->getPHID())) ->execute(); $xaction_view = id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($plan->getPHID()) ->setTransactions($xactions) ->setShouldTerminate(true); $title = pht('Plan %d', $id); $header = id(new PHUIHeaderView()) ->setHeader($title) ->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, $xaction_view, ), array( 'title' => $title, )); } private function buildStepList(HarbormasterBuildPlan $plan) { $request = $this->getRequest(); $viewer = $request->getUser(); $run_order = HarbormasterBuildGraph::determineDependencyExecution($plan); $steps = id(new HarbormasterBuildStepQuery()) ->setViewer($viewer) ->withBuildPlanPHIDs(array($plan->getPHID())) ->execute(); $steps = mpull($steps, null, 'getPHID'); $can_edit = $this->hasApplicationCapability( HarbormasterManagePlansCapability::CAPABILITY); $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')) ->setBarColor('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->setBarColor('red'); } if ($run_ref['cycle']) { $is_deadlocking = true; } if ($is_deadlocking) { $item->setBarColor('red'); } $step_list->addItem($item); } return array($step_list, $has_any_conflicts, $is_deadlocking); } private function buildActionList(HarbormasterBuildPlan $plan) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $plan->getID(); $list = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($plan) ->setObjectURI($this->getApplicationURI("plan/{$id}/")); $can_edit = $this->hasApplicationCapability( HarbormasterManagePlansCapability::CAPABILITY); $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')); } $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Add Build Step')) ->setHref($this->getApplicationURI("step/add/{$id}/")) ->setWorkflow(true) ->setDisabled(!$can_edit) ->setIcon('fa-plus')); $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Run Plan Manually')) ->setHref($this->getApplicationURI("plan/run/{$id}/")) ->setWorkflow(true) ->setDisabled(!$can_edit) ->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/engine/HarbormasterBuildGraph.php b/src/applications/harbormaster/engine/HarbormasterBuildGraph.php index 3691f4629b..a696a2b3d6 100644 --- a/src/applications/harbormaster/engine/HarbormasterBuildGraph.php +++ b/src/applications/harbormaster/engine/HarbormasterBuildGraph.php @@ -1,60 +1,61 @@ setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuildPlanPHIDs(array($plan->getPHID())) ->execute(); $steps_by_phid = mpull($steps, null, 'getPHID'); $step_phids = mpull($steps, 'getPHID'); if (count($steps) === 0) { return array(); } $graph = id(new HarbormasterBuildGraph($steps_by_phid)) ->addNodes($step_phids); $raw_results = $graph->getBestEffortTopographicallySortedNodes(); $results = array(); foreach ($raw_results as $node) { $results[] = array( 'node' => $steps_by_phid[$node['node']], 'depth' => $node['depth'], 'cycle' => $node['cycle']); } return $results; } public function __construct($step_map) { $this->stepMap = $step_map; } protected function loadEdges(array $nodes) { $map = array(); foreach ($nodes as $node) { - $deps = $this->stepMap[$node]->getDetail('dependsOn', array()); + $step = $this->stepMap[$node]; + $deps = $step->getStepImplementation()->getDependencies($step); $map[$node] = array(); foreach ($deps as $dep) { $map[$node][] = $dep; } } return $map; } } diff --git a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php index a4e09f6123..433b11165c 100644 --- a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php @@ -1,205 +1,225 @@ setAncestorClass('HarbormasterBuildStepImplementation') ->loadObjects(); } public static function getImplementation($class) { $base = idx(self::getImplementations(), $class); if ($base) { return (clone $base); } return null; } public static function requireImplementation($class) { if (!$class) { throw new Exception(pht('No implementation is specified!')); } $implementation = self::getImplementation($class); if (!$implementation) { throw new Exception(pht('No such implementation "%s" exists!', $class)); } return $implementation; } /** * The name of the implementation. */ abstract public function getName(); /** * The generic description of the implementation. */ public function getGenericDescription() { return ''; } /** * The description of the implementation, based on the current settings. */ public function getDescription() { return $this->getGenericDescription(); } /** * Run the build target against the specified build. */ abstract public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target); /** * Gets the settings for this build step. */ public function getSettings() { return $this->settings; } public function getSetting($key, $default = null) { return idx($this->settings, $key, $default); } /** * Loads the settings for this build step implementation from a build * step or target. */ public final function loadSettings($build_object) { $this->settings = $build_object->getDetails(); return $this; } /** * Return the name of artifacts produced by this command. * * Something like: * * return array( * 'some_name_input_by_user' => 'host'); * * Future steps will calculate all available artifact mappings * before them and filter on the type. * * @return array The mappings of artifact names to their types. */ public function getArtifactInputs() { return array(); } public function getArtifactOutputs() { return array(); } public function getDependencies(HarbormasterBuildStep $build_step) { - return $build_step->getDetail('dependsOn', array()); + $dependencies = $build_step->getDetail('dependsOn', array()); + + $inputs = $build_step->getStepImplementation()->getArtifactInputs(); + $inputs = ipull($inputs, null, 'key'); + + $artifacts = $this->getAvailableArtifacts( + $build_step->getBuildPlan(), + $build_step, + null); + + foreach ($artifacts as $key => $type) { + if (!array_key_exists($key, $inputs)) { + unset($artifacts[$key]); + } + } + + $artifact_steps = ipull($artifacts, 'step'); + $artifact_steps = mpull($artifact_steps, 'getPHID'); + + $dependencies = array_merge($dependencies, $artifact_steps); + + return $dependencies; } /** * Returns a list of all artifacts made available in the build plan. */ public static function getAvailableArtifacts( HarbormasterBuildPlan $build_plan, $current_build_step, $artifact_type) { $steps = id(new HarbormasterBuildStepQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuildPlanPHIDs(array($build_plan->getPHID())) ->execute(); + $artifacts = array(); + $artifact_arrays = array(); foreach ($steps as $step) { if ($current_build_step !== null && $step->getPHID() === $current_build_step->getPHID()) { continue; } $implementation = $step->getStepImplementation(); - $artifact_arrays[] = $implementation->getArtifactOutputs(); - } - - $artifacts = array(); - foreach ($artifact_arrays as $array) { + $array = $implementation->getArtifactOutputs(); $array = ipull($array, 'type', 'key'); foreach ($array as $name => $type) { if ($type !== $artifact_type && $artifact_type !== null) { continue; } - $artifacts[$name] = $type; + $artifacts[$name] = array('type' => $type, 'step' => $step); } } + return $artifacts; } /** * Convert a user-provided string with variables in it, like: * * ls ${dirname} * * ...into a string with variables merged into it safely: * * ls 'dir with spaces' * * @param string Name of a `vxsprintf` function, like @{function:vcsprintf}. * @param string User-provided pattern string containing `${variables}`. * @param dict List of available replacement variables. * @return string String with variables replaced safely into it. */ protected function mergeVariables($function, $pattern, array $variables) { $regexp = '/\\$\\{(?P[a-z\\.]+)\\}/'; $matches = null; preg_match_all($regexp, $pattern, $matches); $argv = array(); foreach ($matches['name'] as $name) { if (!array_key_exists($name, $variables)) { throw new Exception(pht("No such variable '%s'!", $name)); } $argv[] = $variables[$name]; } $pattern = str_replace('%', '%%', $pattern); $pattern = preg_replace($regexp, '%s', $pattern); return call_user_func($function, $pattern, $argv); } public function getFieldSpecifications() { return array(); } protected function formatSettingForDescription($key, $default = null) { return $this->formatValueForDescription($this->getSetting($key, $default)); } protected function formatValueForDescription($value) { if (strlen($value)) { return phutil_tag('strong', array(), $value); } else { return phutil_tag('em', array(), pht('(null)')); } } public function supportsWaitForMessage() { return false; } public function shouldWaitForMessage(HarbormasterBuildTarget $target) { if (!$this->supportsWaitForMessage()) { return false; } return (bool)$target->getDetail('builtin.wait-for-message'); } }