diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2315,6 +2315,7 @@ 'PhabricatorTypeaheadMonogramDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadMonogramDatasource.php', 'PhabricatorTypeaheadResult' => 'applications/typeahead/storage/PhabricatorTypeaheadResult.php', 'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadRuntimeCompositeDatasource.php', + 'PhabricatorTypeaheadStepDependenciesDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadStepDependenciesDatasourceController.php', 'PhabricatorUIConfigOptions' => 'applications/config/option/PhabricatorUIConfigOptions.php', 'PhabricatorUIExample' => 'applications/uiexample/examples/PhabricatorUIExample.php', 'PhabricatorUIExampleRenderController' => 'applications/uiexample/controller/PhabricatorUIExampleRenderController.php', @@ -5211,6 +5212,7 @@ 'PhabricatorTypeaheadModularDatasourceController' => 'PhabricatorTypeaheadDatasourceController', 'PhabricatorTypeaheadMonogramDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'PhabricatorTypeaheadCompositeDatasource', + 'PhabricatorTypeaheadStepDependenciesDatasourceController' => 'PhabricatorTypeaheadDatasourceController', 'PhabricatorUIConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorUIExampleRenderController' => 'PhabricatorController', 'PhabricatorUIListFilterExample' => 'PhabricatorUIExample', diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php --- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php @@ -158,17 +158,23 @@ ->setHref( $this->getApplicationURI('step/delete/'.$step->getID().'/'))); + $depends = $step->getStepImplementation()->getDependencies($step); $inputs = $step->getStepImplementation()->getArtifactInputs(); $outputs = $step->getStepImplementation()->getArtifactOutputs(); $has_conflicts = false; - if ($inputs || $outputs) { + if ($depends || $inputs || $outputs) { $available_artifacts = HarbormasterBuildStepImplementation::loadAvailableArtifacts( $plan, $step, null); + list($depends_ui, $has_conflicts) = $this->buildDependsOnList( + $depends, + pht('Depends On'), + $steps); + list($inputs_ui, $has_conflicts) = $this->buildArtifactList( $inputs, 'in', @@ -188,6 +194,7 @@ 'class' => 'harbormaster-artifact-io', ), array( + $depends_ui, $inputs_ui, $outputs_ui, ))); @@ -292,7 +299,6 @@ return array(null, $has_conflicts); } - $this->requireResource('harbormaster-css'); $header = phutil_tag( @@ -385,4 +391,71 @@ return array($ui, $has_conflicts); } + private function buildDependsOnList( + array $artifacts, + $name, + array $steps) { + $has_conflicts = false; + + $this->requireResource('harbormaster-css'); + + $steps = mpull($steps, null, 'getPHID'); + + $header = phutil_tag( + 'div', + array( + 'class' => 'harbormaster-artifact-summary-header', + ), + $name); + + $list = new PHUIStatusListView(); + foreach ($artifacts as $artifact) { + $error = null; + + $type = idx($artifact, 'type'); + $key = idx($artifact, 'key'); + if ($type !== HarbormasterBuildArtifact::TYPE_BUILD_STATE) { + $icon = PHUIStatusItemView::ICON_WARNING; + $color = 'red'; + $icon_label = pht('Invalid Dependency'); + $has_conflicts = true; + $error = pht( + 'This dependency is specified, but is not another build step.'); + } else if (idx($steps, $key) === 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, $key)->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/controller/HarbormasterStepEditController.php b/src/applications/harbormaster/controller/HarbormasterStepEditController.php --- a/src/applications/harbormaster/controller/HarbormasterStepEditController.php +++ b/src/applications/harbormaster/controller/HarbormasterStepEditController.php @@ -66,12 +66,34 @@ $e_name = true; $v_name = $step->getName(); + $e_depends_on = true; + $raw_depends_on = $step->getDetail('depends_on', array()); + + $steps = id(new HarbormasterBuildStepQuery()) + ->setViewer($viewer) + ->withBuildPlanPHIDs(array($plan->getPHID())) + ->execute(); + $steps = mpull($steps, null, 'getPHID'); + + // TODO: This is pretty horrible! + $v_depends_on = array(); + foreach ($raw_depends_on as $key) { + $ref_step = idx($steps, $key); + if ($ref_step !== null) { + $v_depends_on[] = id(new PhabricatorObjectHandle()) + ->setName($ref_step->getName()) + ->setURI('/') + ->setPHID($key); + } + } $errors = array(); $validation_exception = null; if ($request->isFormPost()) { $e_name = null; $v_name = $request->getStr('name'); + $e_depends_on = null; + $v_depends_on = $request->getArr('dependsOn'); $xactions = $field_list->buildFieldTransactionsFromRequest( new HarbormasterBuildStepTransaction(), @@ -87,6 +109,12 @@ ->setNewValue($v_name); array_unshift($xactions, $name_xaction); + $depends_on_xaction = id(new HarbormasterBuildStepTransaction()) + ->setTransactionType( + HarbormasterBuildStepTransaction::TYPE_DEPENDS_ON) + ->setNewValue($v_depends_on); + array_unshift($xactions, $depends_on_xaction); + if ($is_new) { // This is okay, but a little iffy. We should move it inside the editor // if we create plans elsewhere. @@ -109,6 +137,12 @@ } } + $datasource_uri = new PhutilURI('/typeahead/stepdependencies/'); + $datasource_uri->setQueryParam('planPHID', $plan->getPHID()); + if (!$is_new) { + $datasource_uri->setQueryParam('stepPHID', $step->getPHID()); + } + $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( @@ -118,6 +152,28 @@ ->setError($e_name) ->setValue($v_name)); + // Check if there are any build steps that we can use. + $step_ref = null; + if (!$is_new) { + $step_ref = $step; + } + $previous_artifacts = + HarbormasterBuildStepImplementation::loadAvailableArtifacts( + $plan, + $step_ref, + HarbormasterBuildArtifact::TYPE_BUILD_STATE); + + if (count($previous_artifacts) > 0) { + $form + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setDatasource((string)$datasource_uri) + ->setName('dependsOn') + ->setLabel(pht('Depends On')) + ->setError($e_depends_on) + ->setValue($v_depends_on)); + } + $field_list->appendFieldsToForm($form); if ($is_new) { diff --git a/src/applications/harbormaster/editor/HarbormasterBuildStepEditor.php b/src/applications/harbormaster/editor/HarbormasterBuildStepEditor.php --- a/src/applications/harbormaster/editor/HarbormasterBuildStepEditor.php +++ b/src/applications/harbormaster/editor/HarbormasterBuildStepEditor.php @@ -8,6 +8,7 @@ $types[] = HarbormasterBuildStepTransaction::TYPE_CREATE; $types[] = HarbormasterBuildStepTransaction::TYPE_NAME; + $types[] = HarbormasterBuildStepTransaction::TYPE_DEPENDS_ON; return $types; } @@ -24,6 +25,11 @@ return null; } return $object->getName(); + case HarbormasterBuildStepTransaction::TYPE_DEPENDS_ON: + if ($this->getIsNewObject()) { + return null; + } + return $object->getDetail('depends_on', array()); } return parent::getCustomTransactionOldValue($object, $xaction); @@ -37,6 +43,7 @@ case HarbormasterBuildStepTransaction::TYPE_CREATE: return true; case HarbormasterBuildStepTransaction::TYPE_NAME: + case HarbormasterBuildStepTransaction::TYPE_DEPENDS_ON: return $xaction->getNewValue(); } @@ -52,6 +59,8 @@ return; case HarbormasterBuildStepTransaction::TYPE_NAME: return $object->setName($xaction->getNewValue()); + case HarbormasterBuildStepTransaction::TYPE_DEPENDS_ON: + return $object->setDetail('depends_on', $xaction->getNewValue()); } return parent::applyCustomInternalTransaction($object, $xaction); @@ -64,6 +73,7 @@ switch ($xaction->getTransactionType()) { case HarbormasterBuildStepTransaction::TYPE_CREATE: case HarbormasterBuildStepTransaction::TYPE_NAME: + case HarbormasterBuildStepTransaction::TYPE_DEPENDS_ON: return; } diff --git a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php --- a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php +++ b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php @@ -246,22 +246,14 @@ // Identify all the steps which are ready to run (because all their // depdendencies are complete). - $previous_step = null; $runnable = array(); foreach ($steps as $step) { - // TODO: For now, we're hard coding sequential dependencies into build - // steps. In the future, we can be smart about this instead. - - if ($previous_step) { - $dependencies = array($previous_step); - } else { - $dependencies = array(); - } + $dependencies = $step->getDetail('depends_on'); if (isset($queued[$step->getPHID()])) { $can_run = true; foreach ($dependencies as $dependency) { - if (empty($complete[$dependency->getPHID()])) { + if (empty($complete[$dependency])) { $can_run = false; break; } @@ -271,13 +263,12 @@ $runnable[] = $step; } } - - $previous_step = $step; } if (!$runnable && !$waiting && !$underway) { - // TODO: This means the build is deadlocked, probably? It should not - // normally be possible yet, but we should communicate it more clearly. + // This means the build is deadlocked, and the user has configured + // circular dependencies. It should not normally be possible yet, + // but we should communicate it more clearly. $build->setBuildStatus(HarbormasterBuild::STATUS_FAILED); $build->save(); return; diff --git a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php --- a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php @@ -98,12 +98,42 @@ return array(); } + public function getDependencies(HarbormasterBuildStep $build_step) { + $depends_on = $build_step->getDetail('depends_on', array()); + $artifacts = array(); + foreach ($depends_on as $dependency) { + $artifacts[] = array( + 'name' => pht('Build State'), + 'key' => $dependency, + 'type' => HarbormasterBuildArtifact::TYPE_BUILD_STATE, + ); + } + return $artifacts; + } + + public function getFullArtifactInputs(HarbormasterBuildStep $build_step) { + $artifacts = $this->getArtifactInputs(); + $artifacts += $this->getDependencies($build_step); + return $artifacts; + } + + public function getFullArtifactOutputs(HarbormasterBuildStep $build_step) { + $artifacts = $this->getArtifactOutputs(); + $artifacts[] = array( + 'name' => pht('Build State'), + 'key' => $build_step->getPHID(), + 'type' => HarbormasterBuildArtifact::TYPE_BUILD_STATE, + ); + + return $artifacts; + } + /** * Returns a list of all artifacts made available by previous build steps. */ public static function loadAvailableArtifacts( HarbormasterBuildPlan $build_plan, - HarbormasterBuildStep $current_build_step, + $current_build_step, $artifact_type) { $build_steps = $build_plan->loadOrderedBuildSteps(); @@ -121,18 +151,20 @@ public static function getAvailableArtifacts( HarbormasterBuildPlan $build_plan, array $build_steps, - HarbormasterBuildStep $current_build_step, + $current_build_step, $artifact_type) { - $previous_implementations = array(); + $artifact_arrays = array(); foreach ($build_steps as $build_step) { - if ($build_step->getPHID() === $current_build_step->getPHID()) { + if ($current_build_step !== null && + $build_step->getPHID() === $current_build_step->getPHID()) { break; } - $previous_implementations[] = $build_step->getStepImplementation(); + + $implementation = $build_step->getStepImplementation(); + $artifact_arrays[] = $implementation->getFullArtifactOutputs($build_step); } - $artifact_arrays = mpull($previous_implementations, 'getArtifactOutputs'); $artifacts = array(); foreach ($artifact_arrays as $array) { $array = ipull($array, 'type', 'key'); diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php --- a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php @@ -13,6 +13,7 @@ const TYPE_FILE = 'file'; const TYPE_HOST = 'host'; + const TYPE_BUILD_STATE = 'buildstate'; public static function initializeNewBuildArtifact( HarbormasterBuildTarget $build_target) { diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStepTransaction.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStepTransaction.php --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStepTransaction.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStepTransaction.php @@ -5,6 +5,7 @@ const TYPE_CREATE = 'harbormaster:step:create'; const TYPE_NAME = 'harbormaster:step:name'; + const TYPE_DEPENDS_ON = 'harbormaster:step:depends'; public function getApplicationName() { return 'harbormaster'; diff --git a/src/applications/typeahead/application/PhabricatorApplicationTypeahead.php b/src/applications/typeahead/application/PhabricatorApplicationTypeahead.php --- a/src/applications/typeahead/application/PhabricatorApplicationTypeahead.php +++ b/src/applications/typeahead/application/PhabricatorApplicationTypeahead.php @@ -7,6 +7,8 @@ '/typeahead/' => array( 'common/(?P\w+)/' => 'PhabricatorTypeaheadCommonDatasourceController', + 'stepdependencies/' + => 'PhabricatorTypeaheadStepDependenciesDatasourceController', 'class/(?:(?P\w+)/)?' => 'PhabricatorTypeaheadModularDatasourceController', ), diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadStepDependenciesDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadStepDependenciesDatasourceController.php new file mode 100644 --- /dev/null +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadStepDependenciesDatasourceController.php @@ -0,0 +1,87 @@ +getRequest(); + $viewer = $request->getUser(); + + $plan_phid = $request->getStr('planPHID'); + $step_phid = $request->getStr('stepPHID'); + + $plan = id(new HarbormasterBuildPlanQuery()) + ->setViewer($viewer) + ->withPHIDs(array($plan_phid)) + ->executeOne(); + + $steps = id(new HarbormasterBuildStepQuery()) + ->setViewer($viewer) + ->withBuildPlanPHIDs(array($plan_phid)) + ->execute(); + $steps = mpull($steps, null, 'getPHID'); + + $current_step = idx($steps, $step_phid); + + $artifacts = HarbormasterBuildStepImplementation::loadAvailableArtifacts( + $plan, + $current_step, + HarbormasterBuildArtifact::TYPE_BUILD_STATE); + + $results = array(); + foreach ($artifacts as $key => $name) { + $ref_step = idx($steps, $key); + if ($ref_step !== null) { + $results[] = id(new PhabricatorTypeaheadResult()) + ->setName($ref_step->getName()) + ->setURI('/') + ->setPHID($key); + } + } + + $content = mpull($results, 'getWireFormat'); + + if ($request->isAjax()) { + return id(new AphrontAjaxResponse())->setContent($content); + } + + // If there's a non-Ajax request to this endpoint, show results in a tabular + // format to make it easier to debug typeahead output. + + $rows = array(); + foreach ($results as $result) { + $wire = $result->getWireFormat(); + $rows[] = $wire; + } + + $table = new AphrontTableView($rows); + $table->setHeaders( + array( + 'Name', + 'URI', + 'PHID', + 'Priority', + 'Display Name', + 'Display Type', + 'Image URI', + 'Priority Type', + 'Sprite Class', + )); + + $panel = new AphrontPanelView(); + $panel->setHeader('Typeahead Results'); + $panel->appendChild($table); + + return $this->buildStandardPageResponse( + $panel, + array( + 'title' => pht('Typeahead Results'), + 'device' => true + )); + } + +}