Page MenuHomePhabricator

D20220.diff
No OneTemporary

D20220.diff

diff --git a/resources/sql/autopatches/20190226.harbor.01.planprops.sql b/resources/sql/autopatches/20190226.harbor.01.planprops.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20190226.harbor.01.planprops.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildplan
+ ADD properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20190226.harbor.02.planvalue.sql b/resources/sql/autopatches/20190226.harbor.02.planvalue.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20190226.harbor.02.planvalue.sql
@@ -0,0 +1,2 @@
+UPDATE {$NAMESPACE}_harbormaster.harbormaster_buildplan
+ SET properties = '{}' WHERE properties = '';
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
@@ -1328,6 +1328,9 @@
'HarbormasterBuildMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildMessageQuery.php',
'HarbormasterBuildPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPHIDType.php',
'HarbormasterBuildPlan' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php',
+ 'HarbormasterBuildPlanBehavior' => 'applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php',
+ 'HarbormasterBuildPlanBehaviorOption' => 'applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php',
+ 'HarbormasterBuildPlanBehaviorTransaction' => 'applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php',
'HarbormasterBuildPlanDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildPlanDatasource.php',
'HarbormasterBuildPlanDefaultEditCapability' => 'applications/harbormaster/capability/HarbormasterBuildPlanDefaultEditCapability.php',
'HarbormasterBuildPlanDefaultViewCapability' => 'applications/harbormaster/capability/HarbormasterBuildPlanDefaultViewCapability.php',
@@ -1424,6 +1427,7 @@
'HarbormasterMessageType' => 'applications/harbormaster/engine/HarbormasterMessageType.php',
'HarbormasterObject' => 'applications/harbormaster/storage/HarbormasterObject.php',
'HarbormasterOtherBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterOtherBuildStepGroup.php',
+ 'HarbormasterPlanBehaviorController' => 'applications/harbormaster/controller/HarbormasterPlanBehaviorController.php',
'HarbormasterPlanController' => 'applications/harbormaster/controller/HarbormasterPlanController.php',
'HarbormasterPlanDisableController' => 'applications/harbormaster/controller/HarbormasterPlanDisableController.php',
'HarbormasterPlanEditController' => 'applications/harbormaster/controller/HarbormasterPlanEditController.php',
@@ -6942,6 +6946,9 @@
'PhabricatorConduitResultInterface',
'PhabricatorProjectInterface',
),
+ 'HarbormasterBuildPlanBehavior' => 'Phobject',
+ 'HarbormasterBuildPlanBehaviorOption' => 'Phobject',
+ 'HarbormasterBuildPlanBehaviorTransaction' => 'HarbormasterBuildPlanTransactionType',
'HarbormasterBuildPlanDatasource' => 'PhabricatorTypeaheadDatasource',
'HarbormasterBuildPlanDefaultEditCapability' => 'PhabricatorPolicyCapability',
'HarbormasterBuildPlanDefaultViewCapability' => 'PhabricatorPolicyCapability',
@@ -7057,6 +7064,7 @@
'HarbormasterMessageType' => 'Phobject',
'HarbormasterObject' => 'HarbormasterDAO',
'HarbormasterOtherBuildStepGroup' => 'HarbormasterBuildStepGroup',
+ 'HarbormasterPlanBehaviorController' => 'HarbormasterPlanController',
'HarbormasterPlanController' => 'HarbormasterController',
'HarbormasterPlanDisableController' => 'HarbormasterPlanController',
'HarbormasterPlanEditController' => 'HarbormasterPlanController',
diff --git a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php
--- a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php
+++ b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php
@@ -83,6 +83,8 @@
=> 'HarbormasterPlanEditController',
'order/(?:(?P<id>\d+)/)?' => 'HarbormasterPlanOrderController',
'disable/(?P<id>\d+)/' => 'HarbormasterPlanDisableController',
+ 'behavior/(?P<id>\d+)/(?P<behaviorKey>[^/]+)/' =>
+ 'HarbormasterPlanBehaviorController',
'run/(?P<id>\d+)/' => 'HarbormasterPlanRunController',
'(?P<id>\d+)/' => 'HarbormasterPlanViewController',
),
diff --git a/src/applications/harbormaster/controller/HarbormasterPlanBehaviorController.php b/src/applications/harbormaster/controller/HarbormasterPlanBehaviorController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/controller/HarbormasterPlanBehaviorController.php
@@ -0,0 +1,92 @@
+<?php
+
+final class HarbormasterPlanBehaviorController
+ extends HarbormasterPlanController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+
+ $plan = id(new HarbormasterBuildPlanQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($request->getURIData('id')))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ if (!$plan) {
+ return new Aphront404Response();
+ }
+
+ $behavior_key = $request->getURIData('behaviorKey');
+ $metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey();
+
+ $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
+ $behavior = idx($behaviors, $behavior_key);
+ if (!$behavior) {
+ return new Aphront404Response();
+ }
+
+ $plan_uri = $plan->getURI();
+
+ $v_option = $behavior->getPlanOption($plan)->getKey();
+ if ($request->isFormPost()) {
+ $v_option = $request->getStr('option');
+
+ $xactions = array();
+
+ $xactions[] = id(new HarbormasterBuildPlanTransaction())
+ ->setTransactionType(
+ HarbormasterBuildPlanBehaviorTransaction::TRANSACTIONTYPE)
+ ->setMetadataValue($metadata_key, $behavior_key)
+ ->setNewValue($v_option);
+
+ $editor = id(new HarbormasterBuildPlanEditor())
+ ->setActor($viewer)
+ ->setContinueOnNoEffect(true)
+ ->setContinueOnMissingFields(true)
+ ->setContentSourceFromRequest($request);
+
+ $editor->applyTransactions($plan, $xactions);
+
+ return id(new AphrontRedirectResponse())->setURI($plan_uri);
+ }
+
+ $select_control = id(new AphrontFormRadioButtonControl())
+ ->setName('option')
+ ->setValue($v_option)
+ ->setLabel(pht('Option'));
+
+ foreach ($behavior->getOptions() as $option) {
+ $icon = id(new PHUIIconView())
+ ->setIcon($option->getIcon());
+
+ $select_control->addButton(
+ $option->getKey(),
+ array(
+ $icon,
+ ' ',
+ $option->getName(),
+ ),
+ $option->getDescription());
+ }
+
+ $form = id(new AphrontFormView())
+ ->setViewer($viewer)
+ ->appendInstructions(
+ pht(
+ 'Choose a build plan behavior for "%s".',
+ phutil_tag('strong', array(), $behavior->getName())))
+ ->appendRemarkupInstructions($behavior->getEditInstructions())
+ ->appendControl($select_control);
+
+ return $this->newDialog()
+ ->setTitle(pht('Edit Behavior: %s', $behavior->getName()))
+ ->appendForm($form)
+ ->setWidth(AphrontDialogView::WIDTH_FORM)
+ ->addSubmitButton(pht('Save Changes'))
+ ->addCancelButton($plan_uri);
+ }
+
+}
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
@@ -61,6 +61,7 @@
}
$builds_view = $this->newBuildsView($plan);
+ $options_view = $this->newOptionsView($plan);
$timeline = $this->buildTransactionTimeline(
$plan,
@@ -74,6 +75,7 @@
array(
$error,
$step_list,
+ $options_view,
$builds_view,
$timeline,
));
@@ -484,4 +486,75 @@
->appendChild($list);
}
+
+ private function newOptionsView(HarbormasterBuildPlan $plan) {
+ $viewer = $this->getViewer();
+
+ $can_edit = PhabricatorPolicyFilter::hasCapability(
+ $viewer,
+ $plan,
+ PhabricatorPolicyCapability::CAN_EDIT);
+
+ $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
+
+ $rows = array();
+ foreach ($behaviors as $behavior) {
+ $option = $behavior->getPlanOption($plan);
+
+ $icon = $option->getIcon();
+ $icon = id(new PHUIIconView())->setIcon($icon);
+
+ $edit_uri = new PhutilURI(
+ $this->getApplicationURI(
+ urisprintf(
+ 'plan/behavior/%d/%s/',
+ $plan->getID(),
+ $behavior->getKey())));
+
+ $edit_button = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setColor(PHUIButtonView::GREY)
+ ->setSize(PHUIButtonView::SMALL)
+ ->setDisabled(!$can_edit)
+ ->setWorkflow(true)
+ ->setText(pht('Edit'))
+ ->setHref($edit_uri);
+
+ $rows[] = array(
+ $icon,
+ $behavior->getName(),
+ $option->getName(),
+ $option->getDescription(),
+ $edit_button,
+ );
+ }
+
+ $table = id(new AphrontTableView($rows))
+ ->setHeaders(
+ array(
+ null,
+ pht('Name'),
+ pht('Behavior'),
+ pht('Details'),
+ null,
+ ))
+ ->setColumnClasses(
+ array(
+ null,
+ 'pri',
+ null,
+ 'wide',
+ null,
+ ));
+
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader(pht('Plan Behaviors'));
+
+ return id(new PHUIObjectBoxView())
+ ->setHeader($header)
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->setTable($table);
+ }
+
}
diff --git a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php
--- a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php
+++ b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php
@@ -77,7 +77,7 @@
}
protected function buildCustomEditFields($object) {
- return array(
+ $fields = array(
id(new PhabricatorTextEditField())
->setKey('name')
->setLabel(pht('Name'))
@@ -89,6 +89,36 @@
->setConduitTypeDescription(pht('New plan name.'))
->setValue($object->getName()),
);
+
+
+ $metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey();
+
+ $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
+ foreach ($behaviors as $behavior) {
+ $key = $behavior->getKey();
+
+ // Get the raw key off the object so that we don't reset stuff to
+ // default values by mistake if a behavior goes missing somehow.
+ $storage_key = HarbormasterBuildPlanBehavior::getStorageKeyForBehaviorKey(
+ $key);
+ $behavior_option = $object->getPlanProperty($storage_key);
+
+ if (!strlen($behavior_option)) {
+ $behavior_option = $behavior->getPlanOption($object)->getKey();
+ }
+
+ $fields[] = id(new PhabricatorSelectEditField())
+ ->setIsFormField(false)
+ ->setKey(sprintf('behavior.%s', $behavior->getKey()))
+ ->setMetadataValue($metadata_key, $behavior->getKey())
+ ->setLabel(pht('Behavior: %s', $behavior->getName()))
+ ->setTransactionType(
+ HarbormasterBuildPlanBehaviorTransaction::TRANSACTIONTYPE)
+ ->setValue($behavior_option)
+ ->setOptions($behavior->getOptionMap());
+ }
+
+ return $fields;
}
}
diff --git a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php
@@ -0,0 +1,348 @@
+<?php
+
+final class HarbormasterBuildPlanBehavior
+ extends Phobject {
+
+ private $key;
+ private $name;
+ private $options;
+ private $defaultKey;
+ private $editInstructions;
+
+ public function setKey($key) {
+ $this->key = $key;
+ return $this;
+ }
+
+ public function getKey() {
+ return $this->key;
+ }
+
+ public function setName($name) {
+ $this->name = $name;
+ return $this;
+ }
+
+ public function getName() {
+ return $this->name;
+ }
+
+ public function setEditInstructions($edit_instructions) {
+ $this->editInstructions = $edit_instructions;
+ return $this;
+ }
+
+ public function getEditInstructions() {
+ return $this->editInstructions;
+ }
+
+ public function getOptionMap() {
+ return mpull($this->options, 'getName', 'getKey');
+ }
+
+ public function setOptions(array $options) {
+ assert_instances_of($options, 'HarbormasterBuildPlanBehaviorOption');
+
+ $key_map = array();
+ $default = null;
+
+ foreach ($options as $option) {
+ $key = $option->getKey();
+
+ if (isset($key_map[$key])) {
+ throw new Exception(
+ pht(
+ 'Multiple behavior options (for behavior "%s") have the same '.
+ 'key ("%s"). Each option must have a unique key.',
+ $this->getKey(),
+ $key));
+ }
+ $key_map[$key] = true;
+
+ if ($option->getIsDefault()) {
+ if ($default === null) {
+ $default = $key;
+ } else {
+ throw new Exception(
+ pht(
+ 'Multiple behavior options (for behavior "%s") are marked as '.
+ 'default options ("%s" and "%s"). Exactly one option must be '.
+ 'marked as the default option.',
+ $this->getKey(),
+ $default,
+ $key));
+ }
+ }
+ }
+
+ if ($default === null) {
+ throw new Exception(
+ pht(
+ 'No behavior option is marked as the default option (for '.
+ 'behavior "%s"). Exactly one option must be marked as the '.
+ 'default option.',
+ $this->getKey()));
+ }
+
+ $this->options = mpull($options, null, 'getKey');
+ $this->defaultKey = $default;
+
+ return $this;
+ }
+
+ public function getOptions() {
+ return $this->options;
+ }
+
+ public function getPlanOption(HarbormasterBuildPlan $plan) {
+ $behavior_key = $this->getKey();
+ $storage_key = self::getStorageKeyForBehaviorKey($behavior_key);
+
+ $plan_value = $plan->getPlanProperty($storage_key);
+ if (isset($this->options[$plan_value])) {
+ return $this->options[$plan_value];
+ }
+
+ return idx($this->options, $this->defaultKey);
+ }
+
+ public static function getTransactionMetadataKey() {
+ return 'behavior-key';
+ }
+
+ public static function getStorageKeyForBehaviorKey($behavior_key) {
+ return sprintf('behavior.%s', $behavior_key);
+ }
+
+ public static function newPlanBehaviors() {
+ $draft_options = array(
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey('always')
+ ->setIcon('fa-check-circle-o green')
+ ->setName(pht('Always'))
+ ->setIsDefault(true)
+ ->setDescription(
+ pht(
+ 'Revisions are not sent for review until the build completes, '.
+ 'and are returned to the author for updates if the build fails.')),
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey('building')
+ ->setIcon('fa-pause-circle-o yellow')
+ ->setName(pht('If Building'))
+ ->setDescription(
+ pht(
+ 'Revisions are not sent for review until the build completes, '.
+ 'but they will be sent for review even if it fails.')),
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey('never')
+ ->setIcon('fa-circle-o red')
+ ->setName(pht('Never'))
+ ->setDescription(
+ pht(
+ 'Revisions are sent for review regardless of the status of the '.
+ 'build.')),
+ );
+
+ $land_options = array(
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey('always')
+ ->setIcon('fa-check-circle-o green')
+ ->setName(pht('Always'))
+ ->setIsDefault(true)
+ ->setDescription(
+ pht(
+ '"arc land" warns if the build is still running or has '.
+ 'failed.')),
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey('building')
+ ->setIcon('fa-pause-circle-o yellow')
+ ->setName(pht('If Building'))
+ ->setDescription(
+ pht(
+ '"arc land" warns if the build is still running, but ignores '.
+ 'the build if it has failed.')),
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey('complete')
+ ->setIcon('fa-dot-circle-o yellow')
+ ->setName(pht('If Complete'))
+ ->setDescription(
+ pht(
+ '"arc land" warns if the build has failed, but ignores the '.
+ 'build if it is still running.')),
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey('never')
+ ->setIcon('fa-circle-o red')
+ ->setName(pht('Never'))
+ ->setDescription(
+ pht(
+ '"arc land" never warns that the build is still running or '.
+ 'has failed.')),
+ );
+
+ $aggregate_options = array(
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey('always')
+ ->setIcon('fa-check-circle-o green')
+ ->setName(pht('Always'))
+ ->setIsDefault(true)
+ ->setDescription(
+ pht(
+ 'The buildable waits for the build, and fails if the '.
+ 'build fails.')),
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey('building')
+ ->setIcon('fa-pause-circle-o yellow')
+ ->setName(pht('If Building'))
+ ->setDescription(
+ pht(
+ 'The buildable waits for the build, but does not fail '.
+ 'if the build fails.')),
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey('never')
+ ->setIcon('fa-circle-o red')
+ ->setName(pht('Never'))
+ ->setDescription(
+ pht(
+ 'The buildable does not wait for the build.')),
+ );
+
+ $restart_options = array(
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey('always')
+ ->setIcon('fa-repeat green')
+ ->setName(pht('Always'))
+ ->setIsDefault(true)
+ ->setDescription(
+ pht('The build may be restarted.')),
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey('never')
+ ->setIcon('fa-times red')
+ ->setName(pht('Never'))
+ ->setDescription(
+ pht('The build may not be restarted.')),
+ );
+
+ $run_options = array(
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey('edit')
+ ->setIcon('fa-pencil green')
+ ->setName(pht('If Editable'))
+ ->setIsDefault(true)
+ ->setDescription(
+ pht('Only users who can edit the plan can run it manually.')),
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey('view')
+ ->setIcon('fa-exclamation-triangle yellow')
+ ->setName(pht('If Viewable'))
+ ->setDescription(
+ pht(
+ 'Any user who can view the plan can run it manually.')),
+ );
+
+ $behaviors = array(
+ id(new self())
+ ->setKey('hold-drafts')
+ ->setName(pht('Hold Drafts'))
+ ->setEditInstructions(
+ pht(
+ 'When users create revisions in Differential, the default '.
+ 'behavior is to hold them in the "Draft" state until all builds '.
+ 'pass. Once builds pass, the revisions promote and are sent for '.
+ 'review, which notifies reviewers.'.
+ "\n\n".
+ 'The general intent of this workflow is to make sure reviewers '.
+ 'are only spending time on review once changes survive automated '.
+ 'tests. If a change does not pass tests, it usually is not '.
+ 'really ready for review.'.
+ "\n\n".
+ 'If you want to promote revisions out of "Draft" before builds '.
+ 'pass, or promote revisions even when builds fail, you can '.
+ 'change the promotion behavior. This may be useful if you have '.
+ 'very long-running builds, or some builds which are not very '.
+ 'important.'.
+ "\n\n".
+ 'Users may always use "Request Review" to promote a "Draft" '.
+ 'revision, even if builds have failed or are still in progress.'))
+ ->setOptions($draft_options),
+ id(new self())
+ ->setKey('arc-land')
+ ->setName(pht('Warn When Landing'))
+ ->setEditInstructions(
+ pht(
+ 'When a user attempts to `arc land` a revision and that revision '.
+ 'has ongoing or failed builds, the default behavior of `arc` is '.
+ 'to warn them about those builds and give them a chance to '.
+ 'reconsider: they may want to wait for ongoing builds to '.
+ 'complete, or fix failed builds before landing the change.'.
+ "\n\n".
+ 'If you do not want to warn users about this build, you can '.
+ 'change the warning behavior. This may be useful if the build '.
+ 'takes a long time to run (so you do not expect users to wait '.
+ 'for it) or the outcome is not important.'.
+ "\n\n".
+ 'This warning is only advisory. Users may always elect to ignore '.
+ 'this warning and continue, even if builds have failed.'))
+ ->setOptions($land_options),
+ id(new self())
+ ->setKey('buildable')
+ ->setEditInstructions(
+ pht(
+ 'The overall state of a buildable (like a commit or revision) is '.
+ 'normally the aggregation of the individual states of all builds '.
+ 'that have run against it.'.
+ "\n\n".
+ 'Buildables are "building" until all builds pass (which changes '.
+ 'them to "pass"), or any build fails (which changes them to '.
+ '"fail").'.
+ "\n\n".
+ 'You can change this behavior if you do not want to wait for this '.
+ 'build, or do not care if it fails.'))
+ ->setName(pht('Affects Buildable'))
+ ->setOptions($aggregate_options),
+ id(new self())
+ ->setKey('restartable')
+ ->setEditInstructions(
+ pht(
+ 'Usually, builds may be restarted. This may be useful if you '.
+ 'suspect a build has failed for environmental or circumstantial '.
+ 'reasons unrelated to the actual code, and want to give it '.
+ 'another chance at glory.'.
+ "\n\n".
+ 'If you want to prevent a build from being restarted, you can '.
+ 'change the behavior here. This may be useful to prevent '.
+ 'accidents where a build with a dangerous side effect (like '.
+ 'deployment) is restarted improperly.'))
+ ->setName(pht('Restartable'))
+ ->setOptions($restart_options),
+ id(new self())
+ ->setKey('runnable')
+ ->setEditInstructions(
+ pht(
+ 'To run a build manually, you normally must have permission to '.
+ 'edit the related build plan. If you would prefer that anyone who '.
+ 'can see the build plan be able to run and restart the build, you '.
+ 'can change the behavior here.'.
+ "\n\n".
+ 'Note that this affects both {nav Run Plan Manually} and '.
+ '{nav Restart Build}, since the two actions are largely '.
+ 'equivalent.'.
+ "\n\n".
+ 'WARNING: This may be unsafe, particularly if the build has '.
+ 'side effects like deployment.'.
+ "\n\n".
+ 'If you weaken this policy, an attacker with control of an '.
+ 'account that has "Can View" permission but not "Can Edit" '.
+ 'permission can manually run this build against any old version '.
+ 'of the code, including versions with known security issues.'.
+ "\n\n".
+ 'If running the build has a side effect like deploying code, '.
+ 'they can force deployment of a vulnerable version and then '.
+ 'escalate into an attack against the deployed service.'))
+ ->setName(pht('Runnable'))
+ ->setOptions($run_options),
+ );
+
+ return mpull($behaviors, null, 'getKey');
+ }
+
+}
diff --git a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php
@@ -0,0 +1,57 @@
+<?php
+
+final class HarbormasterBuildPlanBehaviorOption
+ extends Phobject {
+
+ private $name;
+ private $key;
+ private $icon;
+ private $description;
+ private $isDefault;
+
+ public function setName($name) {
+ $this->name = $name;
+ return $this;
+ }
+
+ public function getName() {
+ return $this->name;
+ }
+
+ public function setKey($key) {
+ $this->key = $key;
+ return $this;
+ }
+
+ public function getKey() {
+ return $this->key;
+ }
+
+ public function setDescription($description) {
+ $this->description = $description;
+ return $this;
+ }
+
+ public function getDescription() {
+ return $this->description;
+ }
+
+ public function setIsDefault($is_default) {
+ $this->isDefault = $is_default;
+ return $this;
+ }
+
+ public function getIsDefault() {
+ return $this->isDefault;
+ }
+
+ public function setIcon($icon) {
+ $this->icon = $icon;
+ return $this;
+ }
+
+ public function getIcon() {
+ return $this->icon;
+ }
+
+}
diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php
--- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php
+++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php
@@ -17,6 +17,7 @@
protected $planAutoKey;
protected $viewPolicy;
protected $editPolicy;
+ protected $properties = array();
const STATUS_ACTIVE = 'active';
const STATUS_DISABLED = 'disabled';
@@ -45,6 +46,9 @@
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
+ self::CONFIG_SERIALIZATION => array(
+ 'properties' => self::SERIALIZATION_JSON,
+ ),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort128',
'planStatus' => 'text32',
@@ -94,6 +98,15 @@
return pht('Plan %d', $this->getID());
}
+ public function getPlanProperty($key, $default = null) {
+ return idx($this->properties, $key, $default);
+ }
+
+ public function setPlanProperty($key, $value) {
+ $this->properties[$key] = $value;
+ return $this;
+ }
+
/* -( Autoplans )---------------------------------------------------------- */
diff --git a/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php
@@ -0,0 +1,127 @@
+<?php
+
+final class HarbormasterBuildPlanBehaviorTransaction
+ extends HarbormasterBuildPlanTransactionType {
+
+ const TRANSACTIONTYPE = 'behavior';
+
+ public function generateOldValue($object) {
+ $behavior = $this->getBehavior();
+ return $behavior->getPlanOption($object)->getKey();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $key = $this->getStorageKey();
+ return $object->setPlanProperty($key, $value);
+ }
+
+ public function getTitle() {
+ $old_value = $this->getOldValue();
+ $new_value = $this->getNewValue();
+
+ $behavior = $this->getBehavior();
+ if ($behavior) {
+ $behavior_name = $behavior->getName();
+
+ $options = $behavior->getOptions();
+ if (isset($options[$old_value])) {
+ $old_value = $options[$old_value]->getName();
+ }
+
+ if (isset($options[$new_value])) {
+ $new_value = $options[$new_value]->getName();
+ }
+ } else {
+ $behavior_name = $this->getBehaviorKey();
+ }
+
+ return pht(
+ '%s changed the %s behavior for this plan from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderValue($behavior_name),
+ $this->renderValue($old_value),
+ $this->renderValue($new_value));
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
+ $behaviors = mpull($behaviors, null, 'getKey');
+
+ foreach ($xactions as $xaction) {
+ $key = $this->getBehaviorKeyForTransaction($xaction);
+
+ if (!isset($behaviors[$key])) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'No behavior with key "%s" exists. Valid keys are: %s.',
+ $key,
+ implode(', ', array_keys($behaviors))),
+ $xaction);
+ continue;
+ }
+
+ $behavior = $behaviors[$key];
+ $options = $behavior->getOptions();
+
+ $storage_key = HarbormasterBuildPlanBehavior::getStorageKeyForBehaviorKey(
+ $key);
+ $old = $object->getPlanProperty($storage_key);
+ $new = $xaction->getNewValue();
+
+ if ($old === $new) {
+ continue;
+ }
+
+ if (!isset($options[$new])) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Value "%s" is not a valid option for behavior "%s". Valid '.
+ 'options are: %s.',
+ $new,
+ $key,
+ implode(', ', array_keys($options))),
+ $xaction);
+ continue;
+ }
+ }
+
+ return $errors;
+ }
+
+ public function getTransactionTypeForConduit($xaction) {
+ return 'behavior';
+ }
+
+ public function getFieldValuesForConduit($xaction, $data) {
+ return array(
+ 'key' => $this->getBehaviorKeyForTransaction($xaction),
+ 'old' => $xaction->getOldValue(),
+ 'new' => $xaction->getNewValue(),
+ );
+ }
+
+ private function getBehaviorKeyForTransaction(
+ PhabricatorApplicationTransaction $xaction) {
+ $metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey();
+ return $xaction->getMetadataValue($metadata_key);
+ }
+
+ private function getBehaviorKey() {
+ $metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey();
+ return $this->getMetadataValue($metadata_key);
+ }
+
+ private function getBehavior() {
+ $behavior_key = $this->getBehaviorKey();
+ $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
+ return idx($behaviors, $behavior_key);
+ }
+
+ private function getStorageKey() {
+ return HarbormasterBuildPlanBehavior::getStorageKeyForBehaviorKey(
+ $this->getBehaviorKey());
+ }
+
+}

File Metadata

Mime Type
text/plain
Expires
Sun, May 12, 4:31 AM (3 w, 4 d ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/sa/67/5zokcr4s3ruvl2br
Default Alt Text
D20220.diff (31 KB)

Event Timeline