mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-10 00:42:41 +01:00
Make Harbormaster input and output artifacts more explicit
Summary: Ref T1049. In Harbormaster, build steps may have various inputs (like a host they should run on) and outputs (like a reference to an uploaded file). - Currently, inputs aren't defined anywhere (except implicitly at runtime). - Instead, define inputs explicitly. - Currently, outputs are defined in a way that loses information when misconfigured (the keys will collide). - Instead, define inputs and outputs so they work whether a step is configured correctly or not. - Currently, there's no simple way to see a step's inputs and outputs. - Add some UI for this. - Currently, reordering steps has some surprising side effects. - Instead of invalidating steps after reordering them, validate them at display time and warn the user. Test Plan: {F133679} {F133680} Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley, chad Maniphest Tasks: T1049 Differential Revision: https://secure.phabricator.com/D8599
This commit is contained in:
parent
5b74fa0a75
commit
72337dedaf
9 changed files with 251 additions and 70 deletions
|
@ -70,6 +70,7 @@ return array(
|
|||
'rsrc/css/application/feed/feed.css' => '0d17c209',
|
||||
'rsrc/css/application/files/global-drag-and-drop.css' => '697324ad',
|
||||
'rsrc/css/application/flag/flag.css' => '5337623f',
|
||||
'rsrc/css/application/harbormaster/harbormaster.css' => 'cec833b7',
|
||||
'rsrc/css/application/herald/herald-test.css' => '2b7d0f54',
|
||||
'rsrc/css/application/herald/herald.css' => '59d48f01',
|
||||
'rsrc/css/application/maniphest/batch-editor.css' => '8f380ebc',
|
||||
|
@ -521,6 +522,7 @@ return array(
|
|||
'diviner-shared-css' => '38813222',
|
||||
'font-source-sans-pro' => '225851dd',
|
||||
'global-drag-and-drop-css' => '697324ad',
|
||||
'harbormaster-css' => 'cec833b7',
|
||||
'herald-css' => '59d48f01',
|
||||
'herald-rule-editor' => '4173dbd8',
|
||||
'herald-test-css' => '2b7d0f54',
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @group search
|
||||
*/
|
||||
final class HarbormasterPlanOrderController extends HarbormasterController {
|
||||
|
||||
private $id;
|
||||
|
@ -46,36 +43,8 @@ final class HarbormasterPlanOrderController extends HarbormasterController {
|
|||
$reordered_steps[] = $step;
|
||||
}
|
||||
|
||||
// We must ensure that steps with artifacts become invalid if they are
|
||||
// placed before the steps that produce them.
|
||||
foreach ($reordered_steps as $step) {
|
||||
$implementation = $step->getStepImplementation();
|
||||
$settings = $implementation->getSettings();
|
||||
foreach ($implementation->getSettingDefinitions() as $name => $opt) {
|
||||
switch ($opt['type']) {
|
||||
case BuildStepImplementation::SETTING_TYPE_ARTIFACT:
|
||||
$value = $settings[$name];
|
||||
$filter = $opt['artifact_type'];
|
||||
$available_artifacts =
|
||||
BuildStepImplementation::getAvailableArtifacts(
|
||||
$plan,
|
||||
$reordered_steps,
|
||||
$step,
|
||||
$filter);
|
||||
$artifact_found = false;
|
||||
foreach ($available_artifacts as $key => $type) {
|
||||
if ($key === $value) {
|
||||
$artifact_found = true;
|
||||
}
|
||||
}
|
||||
if (!$artifact_found) {
|
||||
$step->setDetail($name, null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
$step->save();
|
||||
}
|
||||
}
|
||||
// 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());
|
||||
|
|
|
@ -50,7 +50,16 @@ final class HarbormasterPlanViewController
|
|||
$crumbs = $this->buildApplicationCrumbs();
|
||||
$crumbs->addTextCrumb(pht("Plan %d", $id));
|
||||
|
||||
$step_list = $this->buildStepList($plan);
|
||||
list($step_list, $has_any_conflicts) = $this->buildStepList($plan);
|
||||
|
||||
if ($has_any_conflicts) {
|
||||
$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(
|
||||
|
@ -91,6 +100,8 @@ final class HarbormasterPlanViewController
|
|||
'listID' => $list_id,
|
||||
'orderURI' => '/harbormaster/plan/order/'.$plan->getID().'/',
|
||||
));
|
||||
|
||||
$has_any_conflicts = false;
|
||||
foreach ($steps as $step) {
|
||||
$implementation = null;
|
||||
try {
|
||||
|
@ -121,27 +132,14 @@ final class HarbormasterPlanViewController
|
|||
->setObjectName("Step ".$i++)
|
||||
->setHeader($implementation->getName());
|
||||
|
||||
if (!$implementation->validateSettings()) {
|
||||
$item
|
||||
->setBarColor('red')
|
||||
->addAttribute(pht('This step is not configured correctly.'));
|
||||
} else {
|
||||
$item->addAttribute($implementation->getDescription());
|
||||
}
|
||||
$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) {
|
||||
$edit_uri = $this->getApplicationURI("step/edit/".$step->getID()."/");
|
||||
$item
|
||||
->setHref($edit_uri)
|
||||
->addAction(
|
||||
id(new PHUIListItemView())
|
||||
->setIcon('delete')
|
||||
->addSigil('harbormaster-build-step-delete')
|
||||
->setWorkflow(true)
|
||||
->setRenderNameAsTooltip(true)
|
||||
->setName(pht("Delete"))
|
||||
->setHref(
|
||||
$this->getApplicationURI("step/delete/".$step->getID()."/")));
|
||||
$item->setHref($edit_uri);
|
||||
$item->setGrippable(true);
|
||||
$item->addSigil('build-step');
|
||||
$item->setMetadata(
|
||||
|
@ -150,10 +148,60 @@ final class HarbormasterPlanViewController
|
|||
));
|
||||
}
|
||||
|
||||
$item
|
||||
->setHref($edit_uri)
|
||||
->addAction(
|
||||
id(new PHUIListItemView())
|
||||
->setIcon('delete')
|
||||
->addSigil('harbormaster-build-step-delete')
|
||||
->setWorkflow(true)
|
||||
->setDisabled(!$can_edit)
|
||||
->setHref(
|
||||
$this->getApplicationURI("step/delete/".$step->getID()."/")));
|
||||
|
||||
$inputs = $step->getStepImplementation()->getArtifactInputs();
|
||||
$outputs = $step->getStepImplementation()->getArtifactOutputs();
|
||||
|
||||
$has_conflicts = false;
|
||||
if ($inputs || $outputs) {
|
||||
$available_artifacts = BuildStepImplementation::loadAvailableArtifacts(
|
||||
$plan,
|
||||
$step,
|
||||
null);
|
||||
|
||||
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(
|
||||
$inputs_ui,
|
||||
$outputs_ui,
|
||||
)));
|
||||
}
|
||||
|
||||
if ($has_conflicts) {
|
||||
$has_any_conflicts = true;
|
||||
$item->setBarColor('red');
|
||||
}
|
||||
|
||||
$step_list->addItem($item);
|
||||
}
|
||||
|
||||
return $step_list;
|
||||
return array($step_list, $has_any_conflicts);
|
||||
}
|
||||
|
||||
private function buildActionList(HarbormasterBuildPlan $plan) {
|
||||
|
@ -233,4 +281,102 @@ final class HarbormasterPlanViewController
|
|||
|
||||
}
|
||||
|
||||
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 = 'warning-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 = 'open';
|
||||
$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 = 'accept-green';
|
||||
$icon_label = pht('Valid Input');
|
||||
} else {
|
||||
$icon = 'warning-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 = 'question-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 = 'down-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, $icon_label)
|
||||
->setTarget($artifact['name'])
|
||||
->setNote($note));
|
||||
}
|
||||
|
||||
$ui = array(
|
||||
$header,
|
||||
$list,
|
||||
);
|
||||
|
||||
return array($ui, $has_conflicts);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -50,6 +50,10 @@ abstract class BuildStepImplementation {
|
|||
return $this->settings;
|
||||
}
|
||||
|
||||
public function getSetting($key, $default = null) {
|
||||
return idx($this->settings, $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the current settings of this build step.
|
||||
*/
|
||||
|
@ -103,7 +107,11 @@ abstract class BuildStepImplementation {
|
|||
*
|
||||
* @return array The mappings of artifact names to their types.
|
||||
*/
|
||||
public function getArtifactMappings() {
|
||||
public function getArtifactInputs() {
|
||||
return array();
|
||||
}
|
||||
|
||||
public function getArtifactOutputs() {
|
||||
return array();
|
||||
}
|
||||
|
||||
|
@ -141,9 +149,10 @@ abstract class BuildStepImplementation {
|
|||
$previous_implementations[] = $build_step->getStepImplementation();
|
||||
}
|
||||
|
||||
$artifact_arrays = mpull($previous_implementations, 'getArtifactMappings');
|
||||
$artifact_arrays = mpull($previous_implementations, 'getArtifactOutputs');
|
||||
$artifacts = array();
|
||||
foreach ($artifact_arrays as $array) {
|
||||
$array = ipull($array, 'type', 'key');
|
||||
foreach ($array as $name => $type) {
|
||||
if ($type !== $artifact_type && $artifact_type !== null) {
|
||||
continue;
|
||||
|
|
|
@ -90,6 +90,16 @@ final class CommandBuildStepImplementation
|
|||
return true;
|
||||
}
|
||||
|
||||
public function getArtifactInputs() {
|
||||
return array(
|
||||
array(
|
||||
'name' => pht('Run on Host'),
|
||||
'key' => $this->getSetting('hostartifact'),
|
||||
'type' => HarbormasterBuildArtifact::TYPE_HOST,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function getSettingDefinitions() {
|
||||
return array(
|
||||
'command' => array(
|
||||
|
|
|
@ -51,13 +51,6 @@ final class LeaseHostBuildStepImplementation
|
|||
$artifact->save();
|
||||
}
|
||||
|
||||
public function getArtifactMappings() {
|
||||
$settings = $this->getSettings();
|
||||
|
||||
return array(
|
||||
$settings['name'] => HarbormasterBuildArtifact::TYPE_HOST);
|
||||
}
|
||||
|
||||
public function validateSettings() {
|
||||
$settings = $this->getSettings();
|
||||
|
||||
|
@ -71,6 +64,16 @@ final class LeaseHostBuildStepImplementation
|
|||
return true;
|
||||
}
|
||||
|
||||
public function getArtifactOutputs() {
|
||||
return array(
|
||||
array(
|
||||
'name' => pht('Leased Host'),
|
||||
'key' => $this->getSetting('name'),
|
||||
'type' => HarbormasterBuildArtifact::TYPE_HOST,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function getSettingDefinitions() {
|
||||
return array(
|
||||
'name' => array(
|
||||
|
|
|
@ -73,6 +73,16 @@ final class PublishFragmentBuildStepImplementation
|
|||
return true;
|
||||
}
|
||||
|
||||
public function getArtifactInputs() {
|
||||
return array(
|
||||
array(
|
||||
'name' => pht('Publishes File'),
|
||||
'key' => $this->getSetting('artifact'),
|
||||
'type' => HarbormasterBuildArtifact::TYPE_FILE,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function getSettingDefinitions() {
|
||||
return array(
|
||||
'path' => array(
|
||||
|
|
|
@ -51,13 +51,6 @@ final class UploadArtifactBuildStepImplementation
|
|||
$artifact->save();
|
||||
}
|
||||
|
||||
public function getArtifactMappings() {
|
||||
$settings = $this->getSettings();
|
||||
|
||||
return array(
|
||||
$settings['name'] => HarbormasterBuildArtifact::TYPE_FILE);
|
||||
}
|
||||
|
||||
public function validateSettings() {
|
||||
$settings = $this->getSettings();
|
||||
|
||||
|
@ -77,6 +70,26 @@ final class UploadArtifactBuildStepImplementation
|
|||
return true;
|
||||
}
|
||||
|
||||
public function getArtifactInputs() {
|
||||
return array(
|
||||
array(
|
||||
'name' => pht('Upload From Host'),
|
||||
'key' => $this->getSetting('hostartifact'),
|
||||
'type' => HarbormasterBuildArtifact::TYPE_HOST,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function getArtifactOutputs() {
|
||||
return array(
|
||||
array(
|
||||
'name' => pht('Uploaded File'),
|
||||
'key' => $this->getSetting('name'),
|
||||
'type' => HarbormasterBuildArtifact::TYPE_FILE,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function getSettingDefinitions() {
|
||||
return array(
|
||||
'path' => array(
|
||||
|
|
19
webroot/rsrc/css/application/harbormaster/harbormaster.css
Normal file
19
webroot/rsrc/css/application/harbormaster/harbormaster.css
Normal file
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* @provides harbormaster-css
|
||||
*/
|
||||
|
||||
.harbormaster-artifact-io {
|
||||
margin: 0 0 0 8px;
|
||||
padding: 4px 8px;
|
||||
border-width: 1px 0 0 1px;
|
||||
border-style: solid;
|
||||
box-shadow: inset 2px 2px 1px rgba(0, 0, 0, 0.075);
|
||||
background: {$lightbluebackground};
|
||||
border-color: {$lightblueborder};
|
||||
}
|
||||
|
||||
.harbormaster-artifact-summary-header {
|
||||
font-weight: bold;
|
||||
margin-bottom: 2px;
|
||||
color: {$darkbluetext};
|
||||
}
|
Loading…
Reference in a new issue