1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-20 04:20:55 +01:00

Implement build simulation; convert Harbormaster to be purely dependency based

Summary:
Depends on D9806.  This implements the build simulator, which is used to calculate the order of build steps in the plan editor.  This includes a migration script to convert existing plans from sequential based to dependency based, and then drops the sequence column.

Because build plans are now dependency based, the grippable and re-order behaviour has been removed.

Test Plan: Tested the migration, saw the dependencies appear correctly.

Reviewers: epriestley, #blessed_reviewers

Reviewed By: epriestley, #blessed_reviewers

Subscribers: epriestley, Korvin

Differential Revision: https://secure.phabricator.com/D9847
This commit is contained in:
James Rhodes 2014-07-31 11:39:49 +10:00
parent 31343e61ce
commit cad41ea294
21 changed files with 378 additions and 213 deletions

View file

@ -0,0 +1,54 @@
<?php
$plan_table = new HarbormasterBuildPlan();
$step_table = new HarbormasterBuildStep();
$conn_w = $plan_table->establishConnection('w');
foreach (new LiskMigrationIterator($plan_table) as $plan) {
echo pht(
"Migrating build plan %d: %s...\n",
$plan->getID(),
$plan->getName());
// Load all build steps in order using the step sequence.
$steps = queryfx_all(
$conn_w,
'SELECT id FROM %T WHERE buildPlanPHID = %s ORDER BY sequence ASC;',
$step_table->getTableName(),
$plan->getPHID());
$previous_step = null;
foreach ($steps as $step) {
$id = $step['id'];
$loaded_step = id(new HarbormasterBuildStep())->load($id);
$depends_on = $loaded_step->getDetail('dependsOn');
if ($depends_on !== null) {
// This plan already contains steps with depends_on set, so
// we skip since there's nothing to migrate.
break;
}
if ($previous_step === null) {
$depends_on = array();
} else {
$depends_on = array($previous_step->getPHID());
}
$loaded_step->setDetail('dependsOn', $depends_on);
queryfx(
$conn_w,
'UPDATE %T SET details = %s WHERE id = %d',
$step_table->getTableName(),
json_encode($loaded_step->getDetails()),
$loaded_step->getID());
$previous_step = $loaded_step;
echo pht(
" Migrated build step %d.\n",
$loaded_step->getID());
}
}

View file

@ -652,7 +652,9 @@ phutil_register_library_map(array(
'HarbormasterBuildArtifact' => 'applications/harbormaster/storage/build/HarbormasterBuildArtifact.php', 'HarbormasterBuildArtifact' => 'applications/harbormaster/storage/build/HarbormasterBuildArtifact.php',
'HarbormasterBuildArtifactQuery' => 'applications/harbormaster/query/HarbormasterBuildArtifactQuery.php', 'HarbormasterBuildArtifactQuery' => 'applications/harbormaster/query/HarbormasterBuildArtifactQuery.php',
'HarbormasterBuildCommand' => 'applications/harbormaster/storage/HarbormasterBuildCommand.php', 'HarbormasterBuildCommand' => 'applications/harbormaster/storage/HarbormasterBuildCommand.php',
'HarbormasterBuildDependencyDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildDependencyDatasource.php',
'HarbormasterBuildEngine' => 'applications/harbormaster/engine/HarbormasterBuildEngine.php', 'HarbormasterBuildEngine' => 'applications/harbormaster/engine/HarbormasterBuildEngine.php',
'HarbormasterBuildGraph' => 'applications/harbormaster/engine/HarbormasterBuildGraph.php',
'HarbormasterBuildItem' => 'applications/harbormaster/storage/build/HarbormasterBuildItem.php', 'HarbormasterBuildItem' => 'applications/harbormaster/storage/build/HarbormasterBuildItem.php',
'HarbormasterBuildItemPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildItemPHIDType.php', 'HarbormasterBuildItemPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildItemPHIDType.php',
'HarbormasterBuildItemQuery' => 'applications/harbormaster/query/HarbormasterBuildItemQuery.php', 'HarbormasterBuildItemQuery' => 'applications/harbormaster/query/HarbormasterBuildItemQuery.php',
@ -715,7 +717,6 @@ phutil_register_library_map(array(
'HarbormasterPlanDisableController' => 'applications/harbormaster/controller/HarbormasterPlanDisableController.php', 'HarbormasterPlanDisableController' => 'applications/harbormaster/controller/HarbormasterPlanDisableController.php',
'HarbormasterPlanEditController' => 'applications/harbormaster/controller/HarbormasterPlanEditController.php', 'HarbormasterPlanEditController' => 'applications/harbormaster/controller/HarbormasterPlanEditController.php',
'HarbormasterPlanListController' => 'applications/harbormaster/controller/HarbormasterPlanListController.php', 'HarbormasterPlanListController' => 'applications/harbormaster/controller/HarbormasterPlanListController.php',
'HarbormasterPlanOrderController' => 'applications/harbormaster/controller/HarbormasterPlanOrderController.php',
'HarbormasterPlanRunController' => 'applications/harbormaster/controller/HarbormasterPlanRunController.php', 'HarbormasterPlanRunController' => 'applications/harbormaster/controller/HarbormasterPlanRunController.php',
'HarbormasterPlanViewController' => 'applications/harbormaster/controller/HarbormasterPlanViewController.php', 'HarbormasterPlanViewController' => 'applications/harbormaster/controller/HarbormasterPlanViewController.php',
'HarbormasterPublishFragmentBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php', 'HarbormasterPublishFragmentBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php',
@ -3391,7 +3392,9 @@ phutil_register_library_map(array(
), ),
'HarbormasterBuildArtifactQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildArtifactQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildCommand' => 'HarbormasterDAO', 'HarbormasterBuildCommand' => 'HarbormasterDAO',
'HarbormasterBuildDependencyDatasource' => 'PhabricatorTypeaheadDatasource',
'HarbormasterBuildEngine' => 'Phobject', 'HarbormasterBuildEngine' => 'Phobject',
'HarbormasterBuildGraph' => 'AbstractDirectedGraph',
'HarbormasterBuildItem' => 'HarbormasterDAO', 'HarbormasterBuildItem' => 'HarbormasterDAO',
'HarbormasterBuildItemPHIDType' => 'PhabricatorPHIDType', 'HarbormasterBuildItemPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildItemQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildItemQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
@ -3476,7 +3479,6 @@ phutil_register_library_map(array(
'HarbormasterPlanDisableController' => 'HarbormasterPlanController', 'HarbormasterPlanDisableController' => 'HarbormasterPlanController',
'HarbormasterPlanEditController' => 'HarbormasterPlanController', 'HarbormasterPlanEditController' => 'HarbormasterPlanController',
'HarbormasterPlanListController' => 'HarbormasterPlanController', 'HarbormasterPlanListController' => 'HarbormasterPlanController',
'HarbormasterPlanOrderController' => 'HarbormasterController',
'HarbormasterPlanRunController' => 'HarbormasterController', 'HarbormasterPlanRunController' => 'HarbormasterController',
'HarbormasterPlanViewController' => 'HarbormasterPlanController', 'HarbormasterPlanViewController' => 'HarbormasterPlanController',
'HarbormasterPublishFragmentBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterPublishFragmentBuildStepImplementation' => 'HarbormasterBuildStepImplementation',

View file

@ -179,29 +179,7 @@ final class HarbormasterBuildableViewController
->setHref($view_uri); ->setHref($view_uri);
$status = $build->getBuildStatus(); $status = $build->getBuildStatus();
switch ($status) { $item->setBarColor(HarbormasterBuild::getBuildStatusColor($status));
case HarbormasterBuild::STATUS_INACTIVE:
$item->setBarColor('grey');
break;
case HarbormasterBuild::STATUS_PENDING:
$item->setBarColor('blue');
break;
case HarbormasterBuild::STATUS_BUILDING:
$item->setBarColor('yellow');
break;
case HarbormasterBuild::STATUS_PASSED:
$item->setBarColor('green');
break;
case HarbormasterBuild::STATUS_FAILED:
$item->setBarColor('red');
break;
case HarbormasterBuild::STATUS_ERROR:
$item->setBarColor('red');
break;
case HarbormasterBuild::STATUS_STOPPED:
$item->setBarColor('black');
break;
}
$item->addAttribute(HarbormasterBuild::getBuildStatusName($status)); $item->addAttribute(HarbormasterBuild::getBuildStatusName($status));

View file

@ -1,53 +0,0 @@
<?php
final class HarbormasterPlanOrderController extends HarbormasterController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$request->validateCSRF();
$this->requireApplicationCapability(
HarbormasterManagePlansCapability::CAPABILITY);
$plan = id(new HarbormasterBuildPlanQuery())
->setViewer($user)
->withIDs(array($this->id))
->executeOne();
if (!$plan) {
return new Aphront404Response();
}
// Load all steps.
$order = $request->getStrList('order');
$steps = id(new HarbormasterBuildStepQuery())
->setViewer($user)
->withIDs($order)
->execute();
$steps = array_select_keys($steps, $order);
$reordered_steps = array();
// Apply sequences.
$sequence = 1;
foreach ($steps as $step) {
$step->setSequence($sequence++);
$step->save();
$reordered_steps[] = $step;
}
// NOTE: Reordering steps may invalidate artifacts. This is fine; the UI
// will show that there are ordering issues.
// Force the page to re-render.
return id(new AphrontRedirectResponse());
}
}

View file

@ -49,9 +49,21 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
$crumbs = $this->buildApplicationCrumbs(); $crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Plan %d', $id)); $crumbs->addTextCrumb(pht('Plan %d', $id));
list($step_list, $has_any_conflicts) = $this->buildStepList($plan); list($step_list, $has_any_conflicts, $would_deadlock) =
$this->buildStepList($plan);
if ($has_any_conflicts) { 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( $box->setFormErrors(
array( array(
pht( pht(
@ -76,31 +88,37 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
$request = $this->getRequest(); $request = $this->getRequest();
$viewer = $request->getUser(); $viewer = $request->getUser();
$list_id = celerity_generate_unique_node_id(); $run_order =
HarbormasterBuildGraph::determineDependencyExecution($plan);
$steps = id(new HarbormasterBuildStepQuery()) $steps = id(new HarbormasterBuildStepQuery())
->setViewer($viewer) ->setViewer($viewer)
->withBuildPlanPHIDs(array($plan->getPHID())) ->withBuildPlanPHIDs(array($plan->getPHID()))
->execute(); ->execute();
$steps = mpull($steps, null, 'getPHID');
$can_edit = $this->hasApplicationCapability( $can_edit = $this->hasApplicationCapability(
HarbormasterManagePlansCapability::CAPABILITY); HarbormasterManagePlansCapability::CAPABILITY);
$i = 1;
$step_list = id(new PHUIObjectItemListView()) $step_list = id(new PHUIObjectItemListView())
->setUser($viewer) ->setUser($viewer)
->setNoDataString( ->setNoDataString(
pht('This build plan does not have any build steps yet.')) pht('This build plan does not have any build steps yet.'));
->setID($list_id);
Javelin::initBehavior(
'harbormaster-reorder-steps',
array(
'listID' => $list_id,
'orderURI' => '/harbormaster/plan/order/'.$plan->getID().'/',
));
$i = 1;
$last_depth = 0;
$has_any_conflicts = false; $has_any_conflicts = false;
foreach ($steps as $step) { $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; $implementation = null;
try { try {
$implementation = $step->getStepImplementation(); $implementation = $step->getStepImplementation();
@ -108,7 +126,7 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
// We can't initialize the implementation. This might be because // We can't initialize the implementation. This might be because
// it's been renamed or no longer exists. // it's been renamed or no longer exists.
$item = id(new PHUIObjectItemView()) $item = id(new PHUIObjectItemView())
->setObjectName(pht('Step %d', $i++)) ->setObjectName(pht('Step %d.%d', $depth, $i))
->setHeader(pht('Unknown Implementation')) ->setHeader(pht('Unknown Implementation'))
->setBarColor('red') ->setBarColor('red')
->addAttribute(pht( ->addAttribute(pht(
@ -127,7 +145,7 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
continue; continue;
} }
$item = id(new PHUIObjectItemView()) $item = id(new PHUIObjectItemView())
->setObjectName('Step '.$i++) ->setObjectName(pht('Step %d.%d', $depth, $i))
->setHeader($step->getName()); ->setHeader($step->getName());
$item->addAttribute($implementation->getDescription()); $item->addAttribute($implementation->getDescription());
@ -138,12 +156,6 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
if ($can_edit) { if ($can_edit) {
$item->setHref($edit_uri); $item->setHref($edit_uri);
$item->setGrippable(true);
$item->addSigil('build-step');
$item->setMetadata(
array(
'stepID' => $step->getID(),
));
} }
$item $item
@ -157,17 +169,23 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
->setHref( ->setHref(
$this->getApplicationURI('step/delete/'.$step->getID().'/'))); $this->getApplicationURI('step/delete/'.$step->getID().'/')));
$depends = $step->getStepImplementation()->getDependencies($step);
$inputs = $step->getStepImplementation()->getArtifactInputs(); $inputs = $step->getStepImplementation()->getArtifactInputs();
$outputs = $step->getStepImplementation()->getArtifactOutputs(); $outputs = $step->getStepImplementation()->getArtifactOutputs();
$has_conflicts = false; $has_conflicts = false;
if ($inputs || $outputs) { if ($depends || $inputs || $outputs) {
$available_artifacts = $available_artifacts =
HarbormasterBuildStepImplementation::loadAvailableArtifacts( HarbormasterBuildStepImplementation::getAvailableArtifacts(
$plan, $plan,
$step, $step,
null); null);
list($depends_ui, $has_conflicts) = $this->buildDependsOnList(
$depends,
pht('Depends On'),
$steps);
list($inputs_ui, $has_conflicts) = $this->buildArtifactList( list($inputs_ui, $has_conflicts) = $this->buildArtifactList(
$inputs, $inputs,
'in', 'in',
@ -187,6 +205,7 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
'class' => 'harbormaster-artifact-io', 'class' => 'harbormaster-artifact-io',
), ),
array( array(
$depends_ui,
$inputs_ui, $inputs_ui,
$outputs_ui, $outputs_ui,
))); )));
@ -197,10 +216,18 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
$item->setBarColor('red'); $item->setBarColor('red');
} }
if ($run_ref['cycle']) {
$is_deadlocking = true;
}
if ($is_deadlocking) {
$item->setBarColor('red');
}
$step_list->addItem($item); $step_list->addItem($item);
} }
return array($step_list, $has_any_conflicts); return array($step_list, $has_any_conflicts, $is_deadlocking);
} }
private function buildActionList(HarbormasterBuildPlan $plan) { private function buildActionList(HarbormasterBuildPlan $plan) {
@ -291,7 +318,6 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
return array(null, $has_conflicts); return array(null, $has_conflicts);
} }
$this->requireResource('harbormaster-css'); $this->requireResource('harbormaster-css');
$header = phutil_tag( $header = phutil_tag(
@ -384,4 +410,69 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
return array($ui, $has_conflicts); 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);
}
} }

View file

@ -65,12 +65,21 @@ final class HarbormasterStepEditController extends HarbormasterController {
$e_name = true; $e_name = true;
$v_name = $step->getName(); $v_name = $step->getName();
$e_depends_on = true;
$raw_depends_on = $step->getDetail('dependsOn', array());
$v_depends_on = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs($raw_depends_on)
->execute();
$errors = array(); $errors = array();
$validation_exception = null; $validation_exception = null;
if ($request->isFormPost()) { if ($request->isFormPost()) {
$e_name = null; $e_name = null;
$v_name = $request->getStr('name'); $v_name = $request->getStr('name');
$e_depends_on = null;
$v_depends_on = $request->getArr('dependsOn');
$xactions = $field_list->buildFieldTransactionsFromRequest( $xactions = $field_list->buildFieldTransactionsFromRequest(
new HarbormasterBuildStepTransaction(), new HarbormasterBuildStepTransaction(),
@ -86,12 +95,13 @@ final class HarbormasterStepEditController extends HarbormasterController {
->setNewValue($v_name); ->setNewValue($v_name);
array_unshift($xactions, $name_xaction); array_unshift($xactions, $name_xaction);
if ($is_new) { $depends_on_xaction = id(new HarbormasterBuildStepTransaction())
// This is okay, but a little iffy. We should move it inside the editor ->setTransactionType(
// if we create plans elsewhere. HarbormasterBuildStepTransaction::TYPE_DEPENDS_ON)
$steps = $plan->loadOrderedBuildSteps(); ->setNewValue($v_depends_on);
$step->setSequence(count($steps) + 1); array_unshift($xactions, $depends_on_xaction);
if ($is_new) {
// When creating a new step, make sure we have a create transaction // When creating a new step, make sure we have a create transaction
// so we'll apply the transactions even if the step has no // so we'll apply the transactions even if the step has no
// configurable options. // configurable options.
@ -117,6 +127,19 @@ final class HarbormasterStepEditController extends HarbormasterController {
->setError($e_name) ->setError($e_name)
->setValue($v_name)); ->setValue($v_name));
$form
->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource(id(new HarbormasterBuildDependencyDatasource())
->setParameters(array(
'planPHID' => $plan->getPHID(),
'stepPHID' => $is_new ? null : $step->getPHID(),
)))
->setName('dependsOn')
->setLabel(pht('Depends On'))
->setError($e_depends_on)
->setValue($v_depends_on));
$field_list->appendFieldsToForm($form); $field_list->appendFieldsToForm($form);
if ($is_new) { if ($is_new) {

View file

@ -8,6 +8,7 @@ final class HarbormasterBuildStepEditor
$types[] = HarbormasterBuildStepTransaction::TYPE_CREATE; $types[] = HarbormasterBuildStepTransaction::TYPE_CREATE;
$types[] = HarbormasterBuildStepTransaction::TYPE_NAME; $types[] = HarbormasterBuildStepTransaction::TYPE_NAME;
$types[] = HarbormasterBuildStepTransaction::TYPE_DEPENDS_ON;
return $types; return $types;
} }
@ -24,6 +25,11 @@ final class HarbormasterBuildStepEditor
return null; return null;
} }
return $object->getName(); return $object->getName();
case HarbormasterBuildStepTransaction::TYPE_DEPENDS_ON:
if ($this->getIsNewObject()) {
return null;
}
return $object->getDetail('dependsOn', array());
} }
return parent::getCustomTransactionOldValue($object, $xaction); return parent::getCustomTransactionOldValue($object, $xaction);
@ -37,6 +43,7 @@ final class HarbormasterBuildStepEditor
case HarbormasterBuildStepTransaction::TYPE_CREATE: case HarbormasterBuildStepTransaction::TYPE_CREATE:
return true; return true;
case HarbormasterBuildStepTransaction::TYPE_NAME: case HarbormasterBuildStepTransaction::TYPE_NAME:
case HarbormasterBuildStepTransaction::TYPE_DEPENDS_ON:
return $xaction->getNewValue(); return $xaction->getNewValue();
} }
@ -52,6 +59,8 @@ final class HarbormasterBuildStepEditor
return; return;
case HarbormasterBuildStepTransaction::TYPE_NAME: case HarbormasterBuildStepTransaction::TYPE_NAME:
return $object->setName($xaction->getNewValue()); return $object->setName($xaction->getNewValue());
case HarbormasterBuildStepTransaction::TYPE_DEPENDS_ON:
return $object->setDetail('dependsOn', $xaction->getNewValue());
} }
return parent::applyCustomInternalTransaction($object, $xaction); return parent::applyCustomInternalTransaction($object, $xaction);
@ -64,6 +73,7 @@ final class HarbormasterBuildStepEditor
switch ($xaction->getTransactionType()) { switch ($xaction->getTransactionType()) {
case HarbormasterBuildStepTransaction::TYPE_CREATE: case HarbormasterBuildStepTransaction::TYPE_CREATE:
case HarbormasterBuildStepTransaction::TYPE_NAME: case HarbormasterBuildStepTransaction::TYPE_NAME:
case HarbormasterBuildStepTransaction::TYPE_DEPENDS_ON:
return; return;
} }

View file

@ -246,22 +246,14 @@ final class HarbormasterBuildEngine extends Phobject {
// Identify all the steps which are ready to run (because all their // Identify all the steps which are ready to run (because all their
// dependencies are complete). // dependencies are complete).
$previous_step = null;
$runnable = array(); $runnable = array();
foreach ($steps as $step) { foreach ($steps as $step) {
// TODO: For now, we're hard coding sequential dependencies into build $dependencies = $step->getStepImplementation()->getDependencies($step);
// steps. In the future, we can be smart about this instead.
if ($previous_step) {
$dependencies = array($previous_step);
} else {
$dependencies = array();
}
if (isset($queued[$step->getPHID()])) { if (isset($queued[$step->getPHID()])) {
$can_run = true; $can_run = true;
foreach ($dependencies as $dependency) { foreach ($dependencies as $dependency) {
if (empty($complete[$dependency->getPHID()])) { if (empty($complete[$dependency])) {
$can_run = false; $can_run = false;
break; break;
} }
@ -271,14 +263,12 @@ final class HarbormasterBuildEngine extends Phobject {
$runnable[] = $step; $runnable[] = $step;
} }
} }
$previous_step = $step;
} }
if (!$runnable && !$waiting && !$underway) { if (!$runnable && !$waiting && !$underway) {
// TODO: This means the build is deadlocked, probably? It should not // This means the build is deadlocked, and the user has configured
// normally be possible yet, but we should communicate it more clearly. // circular dependencies.
$build->setBuildStatus(HarbormasterBuild::STATUS_FAILED); $build->setBuildStatus(HarbormasterBuild::STATUS_DEADLOCKED);
$build->save(); $build->save();
return; return;
} }
@ -292,7 +282,6 @@ final class HarbormasterBuildEngine extends Phobject {
$this->queueNewBuildTarget($target); $this->queueNewBuildTarget($target);
} }
} }
@ -378,7 +367,8 @@ final class HarbormasterBuildEngine extends Phobject {
$all_pass = false; $all_pass = false;
} }
if ($build->getBuildStatus() == HarbormasterBuild::STATUS_FAILED || if ($build->getBuildStatus() == HarbormasterBuild::STATUS_FAILED ||
$build->getBuildStatus() == HarbormasterBuild::STATUS_ERROR) { $build->getBuildStatus() == HarbormasterBuild::STATUS_ERROR ||
$build->getBuildStatus() == HarbormasterBuild::STATUS_DEADLOCKED) {
$any_fail = true; $any_fail = true;
} }
} }

View file

@ -0,0 +1,60 @@
<?php
/**
* Directed graph representing a build plan
*/
final class HarbormasterBuildGraph extends AbstractDirectedGraph {
private $stepMap;
public static function determineDependencyExecution(
HarbormasterBuildPlan $plan) {
$steps = id(new HarbormasterBuildStepQuery())
->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());
$map[$node] = array();
foreach ($deps as $dep) {
$map[$node][] = $dep;
}
}
return $map;
}
}

View file

@ -27,6 +27,10 @@ final class HarbormasterBuildStepPHIDType extends PhabricatorPHIDType {
foreach ($handles as $phid => $handle) { foreach ($handles as $phid => $handle) {
$build_step = $objects[$phid]; $build_step = $objects[$phid];
$name = $build_step->getName();
$handle->setName($name);
} }
} }

View file

@ -22,14 +22,6 @@ final class HarbormasterBuildStepQuery
return $this; return $this;
} }
public function getPagingColumn() {
return 'sequence';
}
public function getReversePaging() {
return true;
}
protected function loadPage() { protected function loadPage() {
$table = new HarbormasterBuildStep(); $table = new HarbormasterBuildStep();
$conn_r = $table->establishConnection('r'); $conn_r = $table->establishConnection('r');

View file

@ -98,41 +98,35 @@ abstract class HarbormasterBuildStepImplementation {
return array(); return array();
} }
/** public function getDependencies(HarbormasterBuildStep $build_step) {
* Returns a list of all artifacts made available by previous build steps. return $build_step->getDetail('dependsOn', array());
*/
public static function loadAvailableArtifacts(
HarbormasterBuildPlan $build_plan,
HarbormasterBuildStep $current_build_step,
$artifact_type) {
$build_steps = $build_plan->loadOrderedBuildSteps();
return self::getAvailableArtifacts(
$build_plan,
$build_steps,
$current_build_step,
$artifact_type);
} }
/** /**
* Returns a list of all artifacts made available by previous build steps. * Returns a list of all artifacts made available in the build plan.
*/ */
public static function getAvailableArtifacts( public static function getAvailableArtifacts(
HarbormasterBuildPlan $build_plan, HarbormasterBuildPlan $build_plan,
array $build_steps, $current_build_step,
HarbormasterBuildStep $current_build_step,
$artifact_type) { $artifact_type) {
$previous_implementations = array(); $steps = id(new HarbormasterBuildStepQuery())
foreach ($build_steps as $build_step) { ->setViewer(PhabricatorUser::getOmnipotentUser())
if ($build_step->getPHID() === $current_build_step->getPHID()) { ->withBuildPlanPHIDs(array($build_plan->getPHID()))
break; ->execute();
$artifact_arrays = array();
foreach ($steps as $step) {
if ($current_build_step !== null &&
$step->getPHID() === $current_build_step->getPHID()) {
continue;
} }
$previous_implementations[] = $build_step->getStepImplementation();
$implementation = $step->getStepImplementation();
$artifact_arrays[] = $implementation->getArtifactOutputs();
} }
$artifact_arrays = mpull($previous_implementations, 'getArtifactOutputs');
$artifacts = array(); $artifacts = array();
foreach ($artifact_arrays as $array) { foreach ($artifact_arrays as $array) {
$array = ipull($array, 'type', 'key'); $array = ipull($array, 'type', 'key');

View file

@ -47,6 +47,11 @@ final class HarbormasterBuild extends HarbormasterDAO
*/ */
const STATUS_STOPPED = 'stopped'; const STATUS_STOPPED = 'stopped';
/**
* The build has been deadlocked.
*/
const STATUS_DEADLOCKED = 'deadlocked';
/** /**
* Get a human readable name for a build status constant. * Get a human readable name for a build status constant.
@ -70,6 +75,8 @@ final class HarbormasterBuild extends HarbormasterDAO
return pht('Unexpected Error'); return pht('Unexpected Error');
case self::STATUS_STOPPED: case self::STATUS_STOPPED:
return pht('Stopped'); return pht('Stopped');
case self::STATUS_DEADLOCKED:
return pht('Deadlocked');
default: default:
return pht('Unknown'); return pht('Unknown');
} }
@ -90,6 +97,8 @@ final class HarbormasterBuild extends HarbormasterDAO
return PHUIStatusItemView::ICON_MINUS; return PHUIStatusItemView::ICON_MINUS;
case self::STATUS_STOPPED: case self::STATUS_STOPPED:
return PHUIStatusItemView::ICON_MINUS; return PHUIStatusItemView::ICON_MINUS;
case self::STATUS_DEADLOCKED:
return PHUIStatusItemView::ICON_WARNING;
default: default:
return PHUIStatusItemView::ICON_QUESTION; return PHUIStatusItemView::ICON_QUESTION;
} }
@ -106,6 +115,7 @@ final class HarbormasterBuild extends HarbormasterDAO
return 'green'; return 'green';
case self::STATUS_FAILED: case self::STATUS_FAILED:
case self::STATUS_ERROR: case self::STATUS_ERROR:
case self::STATUS_DEADLOCKED:
return 'red'; return 'red';
case self::STATUS_STOPPED: case self::STATUS_STOPPED:
return 'dark'; return 'dark';

View file

@ -13,6 +13,7 @@ final class HarbormasterBuildArtifact extends HarbormasterDAO
const TYPE_FILE = 'file'; const TYPE_FILE = 'file';
const TYPE_HOST = 'host'; const TYPE_HOST = 'host';
const TYPE_BUILD_STATE = 'buildstate';
public static function initializeNewBuildArtifact( public static function initializeNewBuildArtifact(
HarbormasterBuildTarget $build_target) { HarbormasterBuildTarget $build_target) {

View file

@ -39,19 +39,6 @@ final class HarbormasterBuildPlan extends HarbormasterDAO
return $this->assertAttached($this->buildSteps); return $this->assertAttached($this->buildSteps);
} }
/**
* Returns a standard, ordered list of build steps for this build plan.
*
* This method should be used to load build steps for a given build plan
* so that the ordering is consistent.
*/
public function loadOrderedBuildSteps() {
return id(new HarbormasterBuildStepQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withBuildPlanPHIDs(array($this->getPHID()))
->execute();
}
public function isDisabled() { public function isDisabled() {
return ($this->getPlanStatus() == self::STATUS_DISABLED); return ($this->getPlanStatus() == self::STATUS_DISABLED);
} }

View file

@ -9,7 +9,7 @@ final class HarbormasterBuildStep extends HarbormasterDAO
protected $buildPlanPHID; protected $buildPlanPHID;
protected $className; protected $className;
protected $details = array(); protected $details = array();
protected $sequence; protected $sequence = 0;
private $buildPlan = self::ATTACHABLE; private $buildPlan = self::ATTACHABLE;
private $customFields = self::ATTACHABLE; private $customFields = self::ATTACHABLE;

View file

@ -5,6 +5,7 @@ final class HarbormasterBuildStepTransaction
const TYPE_CREATE = 'harbormaster:step:create'; const TYPE_CREATE = 'harbormaster:step:create';
const TYPE_NAME = 'harbormaster:step:name'; const TYPE_NAME = 'harbormaster:step:name';
const TYPE_DEPENDS_ON = 'harbormaster:step:depends';
public function getApplicationName() { public function getApplicationName() {
return 'harbormaster'; return 'harbormaster';

View file

@ -0,0 +1,45 @@
<?php
final class HarbormasterBuildDependencyDatasource
extends PhabricatorTypeaheadDatasource {
public function getPlaceholderText() {
return pht('Type another build step name...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorHarbormasterApplication';
}
public function loadResults() {
$viewer = $this->getViewer();
$plan_phid = $this->getParameter('planPHID');
$step_phid = $this->getParameter('stepPHID');
$steps = id(new HarbormasterBuildStepQuery())
->setViewer($viewer)
->withBuildPlanPHIDs(array($plan_phid))
->execute();
$steps = mpull($steps, null, 'getPHID');
if (count($steps) === 0) {
return array();
}
$results = array();
foreach ($steps as $phid => $step) {
if ($step->getPHID() === $step_phid) {
continue;
}
$results[] = id(new PhabricatorTypeaheadResult())
->setName($step->getName())
->setURI('/')
->setPHID($phid);
}
return $results;
}
}

View file

@ -30,6 +30,7 @@ final class PhabricatorTypeaheadModularDatasourceController
if (isset($sources[$this->class])) { if (isset($sources[$this->class])) {
$source = $sources[$this->class]; $source = $sources[$this->class];
$source->setParameters($request->getRequestData());
$composite = new PhabricatorTypeaheadRuntimeCompositeDatasource(); $composite = new PhabricatorTypeaheadRuntimeCompositeDatasource();
$composite->addDatasource($source); $composite->addDatasource($source);

View file

@ -6,6 +6,7 @@ abstract class PhabricatorTypeaheadDatasource extends Phobject {
private $query; private $query;
private $rawQuery; private $rawQuery;
private $limit; private $limit;
private $parameters = array();
public function setLimit($limit) { public function setLimit($limit) {
$this->limit = $limit; $this->limit = $limit;
@ -43,8 +44,23 @@ abstract class PhabricatorTypeaheadDatasource extends Phobject {
return $this->query; return $this->query;
} }
public function setParameters(array $params) {
$this->parameters = $params;
return $this;
}
public function getParameters() {
return $this->parameters;
}
public function getParameter($name, $default = null) {
return idx($this->parameters, $name, $default);
}
public function getDatasourceURI() { public function getDatasourceURI() {
return '/typeahead/class/'.get_class($this).'/'; $uri = new PhutilURI('/typeahead/class/'.get_class($this).'/');
$uri->setQueryParams($this->parameters);
return (string)$uri;
} }
abstract public function getPlaceholderText(); abstract public function getPlaceholderText();

View file

@ -1,41 +0,0 @@
/**
* @provides javelin-behavior-harbormaster-reorder-steps
* @requires javelin-behavior
* javelin-stratcom
* javelin-workflow
* javelin-dom
* phabricator-draggable-list
*/
JX.behavior('harbormaster-reorder-steps', function(config) {
var root = JX.$(config.listID);
var list = new JX.DraggableList('build-step', root)
.setFindItemsHandler(function() {
return JX.DOM.scry(root, 'li', 'build-step');
});
list.listen('didDrop', function(node) {
var nodes = list.findItems();
var order = [];
var key;
for (var ii = 0; ii < nodes.length; ii++) {
key = JX.Stratcom.getData(nodes[ii]).stepID;
if (key) {
order.push(key);
}
}
list.lock();
JX.DOM.alterClass(node, 'drag-sending', true);
new JX.Workflow(config.orderURI, {order: order.join()})
.setHandler(function() {
JX.DOM.alterClass(node, 'drag-sending', false);
list.unlock();
})
.start();
});
});