1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-01-03 19:31:02 +01:00

Modularize almost all Harbormaster build message workflows and UI/UX

Summary: Ref T13072. Push nearly all Harbormaster build message logic into the new per-message transaction classes.

Test Plan:
  - Issued every message to Buildables.
  - Issued every message to Builds.
  - Looked at a big pile of error messages, couldn't find any typos.
  - Grepped for affected symbols, etc.
  - Ran `bin/harbormaster restart ...`.

Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam

Maniphest Tasks: T13072

Differential Revision: https://secure.phabricator.com/D21691
This commit is contained in:
epriestley 2021-07-15 16:08:26 -07:00
parent 8bbee92139
commit 1212dc5fbe
13 changed files with 724 additions and 695 deletions

View file

@ -221,8 +221,8 @@ final class HarbormasterBuildStatus extends Phobject {
),
self::STATUS_PAUSED => array(
'name' => pht('Paused'),
'icon' => 'fa-minus-circle',
'color' => 'dark',
'icon' => 'fa-pause',
'color' => 'yellow',
'color.ansi' => 'yellow',
'isBuilding' => false,
'isComplete' => false,

View file

@ -22,24 +22,13 @@ final class HarbormasterBuildActionController
return new Aphront404Response();
}
switch ($action) {
case HarbormasterBuildCommand::COMMAND_RESTART:
$can_issue = $build->canRestartBuild();
break;
case HarbormasterBuildCommand::COMMAND_PAUSE:
$can_issue = $build->canPauseBuild();
break;
case HarbormasterBuildCommand::COMMAND_RESUME:
$can_issue = $build->canResumeBuild();
break;
case HarbormasterBuildCommand::COMMAND_ABORT:
$can_issue = $build->canAbortBuild();
break;
default:
return new Aphront400Response();
}
$xaction =
HarbormasterBuildMessageTransaction::getTransactionObjectForMessageType(
$action);
$build->assertCanIssueCommand($viewer, $action);
if (!$xaction) {
return new Aphront404Response();
}
switch ($via) {
case 'buildable':
@ -50,100 +39,29 @@ final class HarbormasterBuildActionController
break;
}
if ($request->isDialogFormPost() && $can_issue) {
$build->sendMessage($viewer, $action);
try {
$xaction->assertCanSendMessage($viewer, $build);
} catch (HarbormasterRestartException $ex) {
return $this->newDialog()
->setTitle($ex->getTitle())
->appendChild($ex->getBody())
->addCancelButton($return_uri);
}
if ($request->isDialogFormPost()) {
$build->sendMessage($viewer, $xaction->getHarbormasterBuildMessageType());
return id(new AphrontRedirectResponse())->setURI($return_uri);
}
switch ($action) {
case HarbormasterBuildCommand::COMMAND_RESTART:
if ($can_issue) {
$title = pht('Really restart build?');
$body = pht(
'Progress on this build will be discarded and the build will '.
'restart. Side effects of the build will occur again. Really '.
'restart build?');
$submit = pht('Restart Build');
} else {
try {
$build->assertCanRestartBuild();
throw new Exception(pht('Expected to be unable to restart build.'));
} catch (HarbormasterRestartException $ex) {
$title = $ex->getTitle();
$body = $ex->getBody();
}
}
break;
case HarbormasterBuildCommand::COMMAND_ABORT:
if ($can_issue) {
$title = pht('Really abort build?');
$body = pht(
'Progress on this build will be discarded. Really '.
'abort build?');
$submit = pht('Abort Build');
} else {
$title = pht('Unable to Abort Build');
$body = pht('You can not abort this build.');
}
break;
case HarbormasterBuildCommand::COMMAND_PAUSE:
if ($can_issue) {
$title = pht('Really pause build?');
$body = pht(
'If you pause this build, work will halt once the current steps '.
'complete. You can resume the build later.');
$submit = pht('Pause Build');
} else {
$title = pht('Unable to Pause Build');
if ($build->isComplete()) {
$body = pht(
'This build is already complete. You can not pause a completed '.
'build.');
} else if ($build->isPaused()) {
$body = pht(
'This build is already paused. You can not pause a build which '.
'has already been paused.');
} else if ($build->isPausing()) {
$body = pht(
'This build is already pausing. You can not reissue a pause '.
'command to a pausing build.');
} else {
$body = pht(
'This build can not be paused.');
}
}
break;
case HarbormasterBuildCommand::COMMAND_RESUME:
if ($can_issue) {
$title = pht('Really resume build?');
$body = pht(
'Work will continue on the build. Really resume?');
$submit = pht('Resume Build');
} else {
$title = pht('Unable to Resume Build');
if ($build->isResuming()) {
$body = pht(
'This build is already resuming. You can not reissue a resume '.
'command to a resuming build.');
} else if (!$build->isPaused()) {
$body = pht(
'This build is not paused. You can only resume a paused '.
'build.');
}
}
break;
}
$title = $xaction->newConfirmPromptTitle();
$body = $xaction->newConfirmPromptBody();
$submit = $xaction->getHarbormasterBuildMessageName();
$dialog = $this->newDialog()
return $this->newDialog()
->setTitle($title)
->appendChild($body)
->addCancelButton($return_uri);
if ($can_issue) {
$dialog->addSubmitButton($submit);
}
return $dialog;
->addCancelButton($return_uri)
->addSubmitButton($submit);
}
}

View file

@ -533,64 +533,32 @@ final class HarbormasterBuildViewController
$curtain = $this->newCurtainView($build);
$can_restart =
$build->canRestartBuild() &&
$build->canIssueCommand(
$viewer,
HarbormasterBuildCommand::COMMAND_RESTART);
$messages = array(
new HarbormasterBuildMessageRestartTransaction(),
new HarbormasterBuildMessagePauseTransaction(),
new HarbormasterBuildMessageResumeTransaction(),
new HarbormasterBuildMessageAbortTransaction(),
);
$can_pause =
$build->canPauseBuild() &&
$build->canIssueCommand(
$viewer,
HarbormasterBuildCommand::COMMAND_PAUSE);
foreach ($messages as $message) {
$can_send = $message->canSendMessage($viewer, $build);
$can_resume =
$build->canResumeBuild() &&
$build->canIssueCommand(
$viewer,
HarbormasterBuildCommand::COMMAND_RESUME);
$message_uri = urisprintf(
'/build/%s/%d/',
$message->getHarbormasterBuildMessageType(),
$id);
$message_uri = $this->getApplicationURI($message_uri);
$can_abort =
$build->canAbortBuild() &&
$build->canIssueCommand(
$viewer,
HarbormasterBuildCommand::COMMAND_ABORT);
$action = id(new PhabricatorActionView())
->setName($message->getHarbormasterBuildMessageName())
->setIcon($message->getIcon())
->setHref($message_uri)
->setDisabled(!$can_send)
->setWorkflow(true);
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Restart Build'))
->setIcon('fa-repeat')
->setHref($this->getApplicationURI('/build/restart/'.$id.'/'))
->setDisabled(!$can_restart)
->setWorkflow(true));
if ($build->canResumeBuild()) {
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Resume Build'))
->setIcon('fa-play')
->setHref($this->getApplicationURI('/build/resume/'.$id.'/'))
->setDisabled(!$can_resume)
->setWorkflow(true));
} else {
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Pause Build'))
->setIcon('fa-pause')
->setHref($this->getApplicationURI('/build/pause/'.$id.'/'))
->setDisabled(!$can_pause)
->setWorkflow(true));
$curtain->addAction($action);
}
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Abort Build'))
->setIcon('fa-exclamation-triangle')
->setHref($this->getApplicationURI('/build/abort/'.$id.'/'))
->setDisabled(!$can_abort)
->setWorkflow(true));
return $curtain;
}

View file

@ -22,54 +22,89 @@ final class HarbormasterBuildableActionController
return new Aphront404Response();
}
$issuable = array();
$builds = $buildable->getBuilds();
foreach ($builds as $key => $build) {
switch ($action) {
case HarbormasterBuildCommand::COMMAND_RESTART:
if ($build->canRestartBuild()) {
$issuable[$key] = $build;
}
break;
case HarbormasterBuildCommand::COMMAND_PAUSE:
if ($build->canPauseBuild()) {
$issuable[$key] = $build;
}
break;
case HarbormasterBuildCommand::COMMAND_RESUME:
if ($build->canResumeBuild()) {
$issuable[$key] = $build;
}
break;
case HarbormasterBuildCommand::COMMAND_ABORT:
if ($build->canAbortBuild()) {
$issuable[$key] = $build;
}
break;
default:
return new Aphront400Response();
}
}
$restricted = false;
foreach ($issuable as $key => $build) {
if (!$build->canIssueCommand($viewer, $action)) {
$restricted = true;
unset($issuable[$key]);
}
}
$building = false;
foreach ($issuable as $key => $build) {
if ($build->isBuilding()) {
$building = true;
break;
}
$message =
HarbormasterBuildMessageTransaction::getTransactionObjectForMessageType(
$action);
if (!$message) {
return new Aphront404Response();
}
$return_uri = '/'.$buildable->getMonogram();
if ($request->isDialogFormPost() && $issuable) {
// See T13348. Actions may apply to only a subset of builds, so give the
// user a preview of what will happen.
$can_send = array();
$rows = array();
$builds = $buildable->getBuilds();
foreach ($builds as $key => $build) {
$exception = null;
try {
$message->assertCanSendMessage($viewer, $build);
$can_send[$key] = $build;
} catch (HarbormasterRestartException $ex) {
$exception = $ex;
}
if (!$exception) {
$icon_icon = $message->getIcon();
$icon_color = 'green';
$title = $message->getHarbormasterBuildMessageName();
$body = $message->getHarbormasterBuildableMessageEffect();
} else {
$icon_icon = 'fa-times';
$icon_color = 'red';
$title = $ex->getTitle();
$body = $ex->getBody();
}
$icon = id(new PHUIIconView())
->setIcon($icon_icon)
->setColor($icon_color);
$build_name = phutil_tag(
'a',
array(
'href' => $build->getURI(),
'target' => '_blank',
),
pht('%s %s', $build->getObjectName(), $build->getName()));
$rows[] = array(
$icon,
$build_name,
$title,
$body,
);
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
null,
pht('Build'),
pht('Action'),
pht('Details'),
))
->setColumnClasses(
array(
null,
null,
'pri',
'wide',
));
$table = phutil_tag(
'div',
array(
'class' => 'mlt mlb',
),
$table);
if ($request->isDialogFormPost() && $can_send) {
$editor = id(new HarbormasterBuildableTransactionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
@ -88,233 +123,65 @@ final class HarbormasterBuildableActionController
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
foreach ($issuable as $build) {
$xaction = id(new HarbormasterBuildTransaction())
->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND)
->setNewValue($action);
$build_editor->applyTransactions($build, array($xaction));
foreach ($can_send as $build) {
$build->sendMessage(
$viewer,
$message->getHarbormasterBuildMessageType());
}
return id(new AphrontRedirectResponse())->setURI($return_uri);
}
$width = AphrontDialogView::WIDTH_DEFAULT;
switch ($action) {
case HarbormasterBuildCommand::COMMAND_RESTART:
// See T13348. The "Restart Builds" action may restart only a subset
// of builds, so show the user a preview of which builds will actually
// restart.
$body = array();
if ($issuable) {
$title = pht('Restart Builds');
$submit = pht('Restart Builds');
if (!$builds) {
$title = pht('No Builds');
$body = pht(
'This buildable has no builds, so you can not issue any commands.');
} else {
$title = pht('Unable to Restart Builds');
}
if ($can_send) {
$title = $message->newBuildableConfirmPromptTitle(
$builds,
$can_send);
if ($builds) {
$width = AphrontDialogView::WIDTH_FORM;
$body[] = pht('Builds for this buildable:');
$rows = array();
foreach ($builds as $key => $build) {
if (isset($issuable[$key])) {
$icon = id(new PHUIIconView())
->setIcon('fa-repeat green');
$build_note = pht('Will Restart');
$body = $message->newBuildableConfirmPromptBody(
$builds,
$can_send);
} else {
$icon = null;
try {
$build->assertCanRestartBuild();
} catch (HarbormasterRestartException $ex) {
$icon = id(new PHUIIconView())
->setIcon('fa-times red');
$build_note = pht(
'%s: %s',
phutil_tag('strong', array(), pht('Not Restartable')),
$ex->getTitle());
$title = pht('Unable to Send Command');
$body = pht(
'You can not send this command to any of the current builds '.
'for this buildable.');
}
if (!$icon) {
try {
$build->assertCanIssueCommand($viewer, $action);
} catch (PhabricatorPolicyException $ex) {
$icon = id(new PHUIIconView())
->setIcon('fa-lock red');
$build_note = pht(
'%s: %s',
phutil_tag('strong', array(), pht('Not Restartable')),
pht('You do not have permission to restart this build.'));
}
}
if (!$icon) {
$icon = id(new PHUIIconView())
->setIcon('fa-times red');
$build_note = pht('Will Not Restart');
}
}
$build_name = phutil_tag(
'a',
array(
'href' => $build->getURI(),
'target' => '_blank',
),
pht('%s %s', $build->getObjectName(), $build->getName()));
$rows[] = array(
$icon,
$build_name,
$build_note,
$body = array(
pht('Builds for this buildable:'),
$table,
$body,
);
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
null,
pht('Build'),
pht('Action'),
))
->setColumnClasses(
array(
null,
'pri',
'wide',
));
$table = phutil_tag(
'div',
array(
'class' => 'mlt mlb',
),
$table);
$body[] = $table;
}
if ($issuable) {
$warnings = array();
if ($restricted) {
$warnings[] = pht(
'You only have permission to restart some builds.');
}
if ($building) {
$warnings[] = pht(
'Progress on running builds will be discarded.');
}
$warnings[] = pht(
'When a build is restarted, side effects associated with '.
'the build may occur again.');
$warnings = $message->newBuildableConfirmPromptWarnings(
$builds,
$can_send);
if ($warnings) {
$body[] = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors($warnings);
$body[] = pht('Really restart builds?');
} else {
if ($restricted) {
$body[] = pht('You do not have permission to restart any builds.');
} else {
$body[] = pht('No builds can be restarted.');
}
}
break;
case HarbormasterBuildCommand::COMMAND_PAUSE:
if ($issuable) {
$title = pht('Really pause builds?');
$submit = $message->getHarbormasterBuildableMessageName();
if ($restricted) {
$body = pht(
'You only have permission to pause some builds. Once the '.
'current steps complete, work will halt on builds you have '.
'permission to pause. You can resume the builds later.');
} else {
$body = pht(
'If you pause all builds, work will halt once the current steps '.
'complete. You can resume the builds later.');
}
$submit = pht('Pause Builds');
} else {
$title = pht('Unable to Pause Builds');
if ($restricted) {
$body = pht('You do not have permission to pause any builds.');
} else {
$body = pht('No builds can be paused.');
}
}
break;
case HarbormasterBuildCommand::COMMAND_ABORT:
if ($issuable) {
$title = pht('Really abort builds?');
if ($restricted) {
$body = pht(
'You only have permission to abort some builds. Work will '.
'halt immediately on builds you have permission to abort. '.
'Progress will be discarded, and builds must be completely '.
'restarted if you want them to complete.');
} else {
$body = pht(
'If you abort all builds, work will halt immediately. Work '.
'will be discarded, and builds must be completely restarted.');
}
$submit = pht('Abort Builds');
} else {
$title = pht('Unable to Abort Builds');
if ($restricted) {
$body = pht('You do not have permission to abort any builds.');
} else {
$body = pht('No builds can be aborted.');
}
}
break;
case HarbormasterBuildCommand::COMMAND_RESUME:
if ($issuable) {
$title = pht('Really resume builds?');
if ($restricted) {
$body = pht(
'You only have permission to resume some builds. Work will '.
'continue on builds you have permission to resume.');
} else {
$body = pht('Work will continue on all builds. Really resume?');
}
$submit = pht('Resume Builds');
} else {
$title = pht('Unable to Resume Builds');
if ($restricted) {
$body = pht('You do not have permission to resume any builds.');
} else {
$body = pht('No builds can be resumed.');
}
}
break;
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setWidth($width)
$dialog = $this->newDialog()
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle($title)
->appendChild($body)
->addCancelButton($return_uri);
if ($issuable) {
if ($can_send) {
$dialog->addSubmitButton($submit);
}
return id(new AphrontDialogResponse())->setDialog($dialog);
return $dialog;
}
}

View file

@ -87,75 +87,39 @@ final class HarbormasterBuildableViewController
$buildable,
PhabricatorPolicyCapability::CAN_EDIT);
$can_restart = false;
$can_resume = false;
$can_pause = false;
$can_abort = false;
$messages = array(
new HarbormasterBuildMessageRestartTransaction(),
new HarbormasterBuildMessagePauseTransaction(),
new HarbormasterBuildMessageResumeTransaction(),
new HarbormasterBuildMessageAbortTransaction(),
);
$command_restart = HarbormasterBuildCommand::COMMAND_RESTART;
$command_resume = HarbormasterBuildCommand::COMMAND_RESUME;
$command_pause = HarbormasterBuildCommand::COMMAND_PAUSE;
$command_abort = HarbormasterBuildCommand::COMMAND_ABORT;
foreach ($messages as $message) {
// Messages are enabled if they can be sent to at least one build.
$can_send = false;
foreach ($buildable->getBuilds() as $build) {
if ($build->canRestartBuild()) {
if ($build->canIssueCommand($viewer, $command_restart)) {
$can_restart = true;
}
}
if ($build->canResumeBuild()) {
if ($build->canIssueCommand($viewer, $command_resume)) {
$can_resume = true;
}
}
if ($build->canPauseBuild()) {
if ($build->canIssueCommand($viewer, $command_pause)) {
$can_pause = true;
}
}
if ($build->canAbortBuild()) {
if ($build->canIssueCommand($viewer, $command_abort)) {
$can_abort = true;
}
$can_send = $message->canSendMessage($viewer, $build);
if ($can_send) {
break;
}
}
$restart_uri = "buildable/{$id}/restart/";
$pause_uri = "buildable/{$id}/pause/";
$resume_uri = "buildable/{$id}/resume/";
$abort_uri = "buildable/{$id}/abort/";
$message_uri = urisprintf(
'/buildable/%d/%s/',
$id,
$message->getHarbormasterBuildMessageType());
$message_uri = $this->getApplicationURI($message_uri);
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-repeat')
->setName(pht('Restart Builds'))
->setHref($this->getApplicationURI($restart_uri))
->setWorkflow(true)
->setDisabled(!$can_restart || !$can_edit));
$action = id(new PhabricatorActionView())
->setName($message->getHarbormasterBuildableMessageName())
->setIcon($message->getIcon())
->setHref($message_uri)
->setDisabled(!$can_send || !$can_edit)
->setWorkflow(true);
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pause')
->setName(pht('Pause Builds'))
->setHref($this->getApplicationURI($pause_uri))
->setWorkflow(true)
->setDisabled(!$can_pause || !$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-play')
->setName(pht('Resume Builds'))
->setHref($this->getApplicationURI($resume_uri))
->setWorkflow(true)
->setDisabled(!$can_resume || !$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-exclamation-triangle')
->setName(pht('Abort Builds'))
->setHref($this->getApplicationURI($abort_uri))
->setWorkflow(true)
->setDisabled(!$can_abort || !$can_edit));
$curtain->addAction($action);
}
return $curtain;
}
@ -198,56 +162,17 @@ final class HarbormasterBuildableViewController
->setUser($viewer);
foreach ($buildable->getBuilds() as $build) {
$view_uri = $this->getApplicationURI('/build/'.$build->getID().'/');
$item = id(new PHUIObjectItemView())
->setObjectName(pht('Build %d', $build->getID()))
->setHeader($build->getName())
->setHref($view_uri);
$status = $build->getBuildStatus();
$status_color = HarbormasterBuildStatus::getBuildStatusColor($status);
$status_name = HarbormasterBuildStatus::getBuildStatusName($status);
$item->setStatusIcon('fa-dot-circle-o '.$status_color, $status_name);
$item->addAttribute($status_name);
$status = $build->getBuildPendingStatusObject();
if ($build->isRestarting()) {
$item->addIcon('fa-repeat', pht('Restarting'));
} else if ($build->isPausing()) {
$item->addIcon('fa-pause', pht('Pausing'));
} else if ($build->isResuming()) {
$item->addIcon('fa-play', pht('Resuming'));
}
$build_id = $build->getID();
$restart_uri = "build/restart/{$build_id}/buildable/";
$resume_uri = "build/resume/{$build_id}/buildable/";
$pause_uri = "build/pause/{$build_id}/buildable/";
$abort_uri = "build/abort/{$build_id}/buildable/";
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-repeat')
->setName(pht('Restart'))
->setHref($this->getApplicationURI($restart_uri))
->setWorkflow(true)
->setDisabled(!$build->canRestartBuild()));
if ($build->canResumeBuild()) {
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-play')
->setName(pht('Resume'))
->setHref($this->getApplicationURI($resume_uri))
->setWorkflow(true));
} else {
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-pause')
->setName(pht('Pause'))
->setHref($this->getApplicationURI($pause_uri))
->setWorkflow(true)
->setDisabled(!$build->canPauseBuild()));
}
$item->setStatusIcon(
$status->getIconIcon().' '.$status->getIconColor(),
$status->getName());
$targets = $build->getBuildTargets();

View file

@ -30,4 +30,13 @@ final class HarbormasterRestartException extends Exception {
return $this->body;
}
public function newDisplayString() {
$title = $this->getTitle();
$body = $this->getBody();
$body = implode("\n\n", $body);
return pht('%s: %s', $title, $body);
}
}

View file

@ -61,21 +61,24 @@ final class HarbormasterManagementRestartWorkflow
throw new ArcanistUserAbortException();
}
$message = new HarbormasterBuildMessageRestartTransaction();
foreach ($builds as $build) {
$this->logInfo(
pht('RESTARTING'),
pht('Build %d: %s', $build->getID(), $build->getName()));
if (!$build->canRestartBuild()) {
try {
$message->assertCanSendMessage($viewer, $build);
} catch (HarbormasterRestartException $ex) {
$this->logWarn(
pht('INVALID'),
pht('Build can not be restarted.'));
continue;
$ex->newDisplayString());
}
$build->sendMessage(
$viewer,
HarbormasterBuildCommand::COMMAND_RESTART);
$message->getHarbormasterBuildMessageType());
$this->logOkay(
pht('QUEUED'),

View file

@ -315,107 +315,6 @@ final class HarbormasterBuild extends HarbormasterDAO
return $this;
}
public function canRestartBuild() {
try {
$this->assertCanRestartBuild();
return true;
} catch (HarbormasterRestartException $ex) {
return false;
}
}
public function assertCanRestartBuild() {
if ($this->isAutobuild()) {
throw new HarbormasterRestartException(
pht('Can Not Restart Autobuild'),
pht(
'This build can not be restarted because it is an automatic '.
'build.'));
}
$restartable = HarbormasterBuildPlanBehavior::BEHAVIOR_RESTARTABLE;
$plan = $this->getBuildPlan();
// See T13526. Users who can't see the "BuildPlan" can end up here with
// no object. This is highly questionable.
if (!$plan) {
throw new HarbormasterRestartException(
pht('No Build Plan Permission'),
pht(
'You can not restart this build because you do not have '.
'permission to access the build plan.'));
}
$option = HarbormasterBuildPlanBehavior::getBehavior($restartable)
->getPlanOption($plan);
$option_key = $option->getKey();
$never_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_NEVER;
$is_never = ($option_key === $never_restartable);
if ($is_never) {
throw new HarbormasterRestartException(
pht('Build Plan Prevents Restart'),
pht(
'This build can not be restarted because the build plan is '.
'configured to prevent the build from restarting.'));
}
$failed_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_IF_FAILED;
$is_failed = ($option_key === $failed_restartable);
if ($is_failed) {
if (!$this->isFailed()) {
throw new HarbormasterRestartException(
pht('Only Restartable if Failed'),
pht(
'This build can not be restarted because the build plan is '.
'configured to prevent the build from restarting unless it '.
'has failed, and it has not failed.'));
}
}
if ($this->isRestarting()) {
throw new HarbormasterRestartException(
pht('Already Restarting'),
pht(
'This build is already restarting. You can not reissue a restart '.
'command to a restarting build.'));
}
}
public function canPauseBuild() {
if ($this->isAutobuild()) {
return false;
}
return !$this->isComplete() &&
!$this->isPaused() &&
!$this->isPausing() &&
!$this->isRestarting() &&
!$this->isAborting();
}
public function canAbortBuild() {
if ($this->isAutobuild()) {
return false;
}
return
!$this->isComplete() &&
!$this->isAborting();
}
public function canResumeBuild() {
if ($this->isAutobuild()) {
return false;
}
return
$this->isPaused() &&
!$this->isResuming() &&
!$this->isRestarting() &&
!$this->isAborting();
}
public function isPausing() {
return $this->getBuildPendingStatusObject()->isPausing();
}
@ -451,51 +350,6 @@ final class HarbormasterBuild extends HarbormasterDAO
return $this;
}
public function canIssueCommand(PhabricatorUser $viewer, $command) {
try {
$this->assertCanIssueCommand($viewer, $command);
return true;
} catch (Exception $ex) {
return false;
}
}
public function assertCanIssueCommand(PhabricatorUser $viewer, $command) {
$plan = $this->getBuildPlan();
// See T13526. Users without permission to access the build plan can
// currently end up here with no "BuildPlan" object.
if (!$plan) {
return false;
}
$need_edit = true;
switch ($command) {
case HarbormasterBuildCommand::COMMAND_RESTART:
case HarbormasterBuildCommand::COMMAND_PAUSE:
case HarbormasterBuildCommand::COMMAND_RESUME:
case HarbormasterBuildCommand::COMMAND_ABORT:
if ($plan->canRunWithoutEditCapability()) {
$need_edit = false;
}
break;
default:
throw new Exception(
pht(
'Invalid Harbormaster build command "%s".',
$command));
}
// Issuing these commands requires that you be able to edit the build, to
// prevent enemy engineers from sabotaging your builds. See T9614.
if ($need_edit) {
PhabricatorPolicyFilter::requireCapability(
$viewer,
$plan,
PhabricatorPolicyCapability::CAN_EDIT);
}
}
public function sendMessage(PhabricatorUser $viewer, $message_type) {
HarbormasterBuildMessage::initializeNewMessage($viewer)
->setReceiverPHID($this->getPHID())

View file

@ -4,9 +4,51 @@ final class HarbormasterBuildMessageAbortTransaction
extends HarbormasterBuildMessageTransaction {
const TRANSACTIONTYPE = 'message/abort';
const MESSAGETYPE = 'abort';
public function getMessageType() {
return 'abort';
public function getHarbormasterBuildMessageName() {
return pht('Abort Build');
}
public function getHarbormasterBuildableMessageName() {
return pht('Abort Builds');
}
public function newConfirmPromptTitle() {
return pht('Really abort build?');
}
public function getHarbormasterBuildableMessageEffect() {
return pht('Build will abort.');
}
public function newConfirmPromptBody() {
return pht(
'Progress on this build will be discarded. Really abort build?');
}
public function newBuildableConfirmPromptTitle(
array $builds,
array $sendable) {
return pht(
'Really abort %s build(s)?',
phutil_count($builds));
}
public function newBuildableConfirmPromptBody(
array $builds,
array $sendable) {
if (count($sendable) === count($builds)) {
return pht(
'If you abort all builds, work will halt immediately. Work '.
'will be discarded, and builds must be completely restarted.');
} else {
return pht(
'You can only abort some builds. Work will halt immediately on '.
'builds you can abort. Progress will be discarded, and builds must '.
'be completely restarted if you want them to complete.');
}
}
public function getTitle() {
@ -37,5 +79,35 @@ final class HarbormasterBuildMessageAbortTransaction
$build->releaseAllArtifacts($actor);
}
protected function newCanApplyMessageAssertion(
PhabricatorUser $viewer,
HarbormasterBuild $build) {
if ($build->isAutobuild()) {
throw new HarbormasterRestartException(
pht('Unable to Abort Build'),
pht(
'You can not abort a build that uses an autoplan.'));
}
if ($build->isComplete()) {
throw new HarbormasterRestartException(
pht('Unable to Abort Build'),
pht(
'You can not abort this biuld because it is already complete.'));
}
}
protected function newCanSendMessageAssertion(
PhabricatorUser $viewer,
HarbormasterBuild $build) {
if ($build->isAborting()) {
throw new HarbormasterRestartException(
pht('Unable to Abort Build'),
pht(
'You can not abort this build because it is already aborting.'));
}
}
}

View file

@ -4,9 +4,52 @@ final class HarbormasterBuildMessagePauseTransaction
extends HarbormasterBuildMessageTransaction {
const TRANSACTIONTYPE = 'message/pause';
const MESSAGETYPE = 'pause';
public function getMessageType() {
return 'pause';
public function getHarbormasterBuildMessageName() {
return pht('Pause Build');
}
public function getHarbormasterBuildableMessageName() {
return pht('Pause Builds');
}
public function newConfirmPromptTitle() {
return pht('Really pause build?');
}
public function getHarbormasterBuildableMessageEffect() {
return pht('Build will pause.');
}
public function newConfirmPromptBody() {
return pht(
'If you pause this build, work will halt once the current steps '.
'complete. You can resume the build later.');
}
public function newBuildableConfirmPromptTitle(
array $builds,
array $sendable) {
return pht(
'Really pause %s build(s)?',
phutil_count($builds));
}
public function newBuildableConfirmPromptBody(
array $builds,
array $sendable) {
if (count($sendable) === count($builds)) {
return pht(
'If you pause all builds, work will halt once the current steps '.
'complete. You can resume the builds later.');
} else {
return pht(
'You can only pause some builds. Once the current steps complete, '.
'work will halt on builds you can pause. You can resume the builds '.
'later.');
}
}
public function getTitle() {
@ -30,4 +73,49 @@ final class HarbormasterBuildMessagePauseTransaction
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_PAUSED);
}
protected function newCanApplyMessageAssertion(
PhabricatorUser $viewer,
HarbormasterBuild $build) {
if ($build->isAutobuild()) {
throw new HarbormasterRestartException(
pht('Unable to Pause Build'),
pht('You can not pause a build that uses an autoplan.'));
}
if ($build->isPaused()) {
throw new HarbormasterRestartException(
pht('Unable to Pause Build'),
pht('You can not pause this build because it is already paused.'));
}
if ($build->isComplete()) {
throw new HarbormasterRestartException(
pht('Unable to Pause Build'),
pht('You can not pause this build because it has already completed.'));
}
}
protected function newCanSendMessageAssertion(
PhabricatorUser $viewer,
HarbormasterBuild $build) {
if ($build->isPausing()) {
throw new HarbormasterRestartException(
pht('Unable to Pause Build'),
pht('You can not pause this build because it is already pausing.'));
}
if ($build->isRestarting()) {
throw new HarbormasterRestartException(
pht('Unable to Pause Build'),
pht('You can not pause this build because it is already restarting.'));
}
if ($build->isAborting()) {
throw new HarbormasterRestartException(
pht('Unable to Pause Build'),
pht('You can not pause this build because it is already aborting.'));
}
}
}

View file

@ -4,9 +4,77 @@ final class HarbormasterBuildMessageRestartTransaction
extends HarbormasterBuildMessageTransaction {
const TRANSACTIONTYPE = 'message/restart';
const MESSAGETYPE = 'restart';
public function getMessageType() {
return 'restart';
public function getHarbormasterBuildMessageName() {
return pht('Restart Build');
}
public function getHarbormasterBuildableMessageName() {
return pht('Restart Builds');
}
public function getHarbormasterBuildableMessageEffect() {
return pht('Build will restart.');
}
public function newConfirmPromptTitle() {
return pht('Really restart build?');
}
public function newConfirmPromptBody() {
return pht(
'Progress on this build will be discarded and the build will restart. '.
'Side effects of the build will occur again. Really restart build?');
}
public function newBuildableConfirmPromptTitle(
array $builds,
array $sendable) {
return pht(
'Really restart %s build(s)?',
phutil_count($builds));
}
public function newBuildableConfirmPromptBody(
array $builds,
array $sendable) {
if (count($sendable) === count($builds)) {
return pht(
'All builds will restart.');
} else {
return pht(
'You can only restart some builds.');
}
}
public function newBuildableConfirmPromptWarnings(
array $builds,
array $sendable) {
$building = false;
foreach ($sendable as $build) {
if ($build->isBuilding()) {
$building = true;
break;
}
}
$warnings = array();
if ($building) {
$warnings[] = pht(
'Progress on running builds will be discarded.');
}
if ($sendable) {
$warnings[] = pht(
'When a build is restarted, side effects associated with '.
'the build may occur again.');
}
return $warnings;
}
public function getTitle() {
@ -16,7 +84,7 @@ final class HarbormasterBuildMessageRestartTransaction
}
public function getIcon() {
return 'fa-backward';
return 'fa-repeat';
}
public function applyInternalEffects($object, $value) {
@ -27,4 +95,72 @@ final class HarbormasterBuildMessageRestartTransaction
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING);
}
protected function newCanApplyMessageAssertion(
PhabricatorUser $viewer,
HarbormasterBuild $build) {
if ($build->isAutobuild()) {
throw new HarbormasterRestartException(
pht('Can Not Restart Autobuild'),
pht(
'This build can not be restarted because it is an automatic '.
'build.'));
}
$restartable = HarbormasterBuildPlanBehavior::BEHAVIOR_RESTARTABLE;
$plan = $build->getBuildPlan();
// See T13526. Users who can't see the "BuildPlan" can end up here with
// no object. This is highly questionable.
if (!$plan) {
throw new HarbormasterRestartException(
pht('No Build Plan Permission'),
pht(
'You can not restart this build because you do not have '.
'permission to access the build plan.'));
}
$option = HarbormasterBuildPlanBehavior::getBehavior($restartable)
->getPlanOption($plan);
$option_key = $option->getKey();
$never_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_NEVER;
$is_never = ($option_key === $never_restartable);
if ($is_never) {
throw new HarbormasterRestartException(
pht('Build Plan Prevents Restart'),
pht(
'This build can not be restarted because the build plan is '.
'configured to prevent the build from restarting.'));
}
$failed_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_IF_FAILED;
$is_failed = ($option_key === $failed_restartable);
if ($is_failed) {
if (!$this->isFailed()) {
throw new HarbormasterRestartException(
pht('Only Restartable if Failed'),
pht(
'This build can not be restarted because the build plan is '.
'configured to prevent the build from restarting unless it '.
'has failed, and it has not failed.'));
}
}
}
protected function newCanSendMessageAssertion(
PhabricatorUser $viewer,
HarbormasterBuild $build) {
if ($build->isRestarting()) {
throw new HarbormasterRestartException(
pht('Already Restarting'),
pht(
'This build is already restarting. You can not reissue a restart '.
'command to a restarting build.'));
}
}
}

View file

@ -4,9 +4,49 @@ final class HarbormasterBuildMessageResumeTransaction
extends HarbormasterBuildMessageTransaction {
const TRANSACTIONTYPE = 'message/resume';
const MESSAGETYPE = 'resume';
public function getMessageType() {
return 'resume';
public function getHarbormasterBuildMessageName() {
return pht('Resume Build');
}
public function getHarbormasterBuildableMessageName() {
return pht('Resume Builds');
}
public function getHarbormasterBuildableMessageEffect() {
return pht('Build will resume.');
}
public function newConfirmPromptTitle() {
return pht('Really resume build?');
}
public function newConfirmPromptBody() {
return pht(
'Work will continue on the build. Really resume?');
}
public function newBuildableConfirmPromptTitle(
array $builds,
array $sendable) {
return pht(
'Really resume %s build(s)?',
phutil_count($builds));
}
public function newBuildableConfirmPromptBody(
array $builds,
array $sendable) {
if (count($sendable) === count($builds)) {
return pht(
'Work will continue on all builds. Really resume?');
} else {
return pht(
'You can only resume some builds. Work will continue on builds '.
'you have permission to resume.');
}
}
public function getTitle() {
@ -26,4 +66,50 @@ final class HarbormasterBuildMessageResumeTransaction
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING);
}
protected function newCanApplyMessageAssertion(
PhabricatorUser $viewer,
HarbormasterBuild $build) {
if ($build->isAutobuild()) {
throw new HarbormasterRestartException(
pht('Unable to Resume Build'),
pht(
'You can not resume a build that uses an autoplan.'));
}
if (!$build->isPaused()) {
throw new HarbormasterRestartException(
pht('Unable to Resume Build'),
pht(
'You can not resume this build because it is not paused. You can '.
'only resume a paused build.'));
}
}
protected function newCanSendMessageAssertion(
PhabricatorUser $viewer,
HarbormasterBuild $build) {
if ($build->isResuming()) {
throw new HarbormasterRestartException(
pht('Unable to Resume Build'),
pht(
'You can not resume this build beacuse it is already resuming.'));
}
if ($build->isRestarting()) {
throw new HarbormasterRestartException(
pht('Unable to Resume Build'),
pht('You can not resume this build because it is already restarting.'));
}
if ($build->isAborting()) {
throw new HarbormasterRestartException(
pht('Unable to Resume Build'),
pht('You can not resume this build because it is already aborting.'));
}
}
}

View file

@ -3,6 +3,31 @@
abstract class HarbormasterBuildMessageTransaction
extends HarbormasterBuildTransactionType {
final public function getHarbormasterBuildMessageType() {
return $this->getPhobjectClassConstant('MESSAGETYPE');
}
abstract public function getHarbormasterBuildMessageName();
abstract public function getHarbormasterBuildableMessageName();
abstract public function getHarbormasterBuildableMessageEffect();
abstract public function newConfirmPromptTitle();
abstract public function newConfirmPromptBody();
abstract public function newBuildableConfirmPromptTitle(
array $builds,
array $sendable);
abstract public function newBuildableConfirmPromptBody(
array $builds,
array $sendable);
public function newBuildableConfirmPromptWarnings(
array $builds,
array $sendable) {
return array();
}
final public function generateOldValue($object) {
return null;
}
@ -17,32 +42,110 @@ abstract class HarbormasterBuildMessageTransaction
);
}
final public static function getTransactionTypeForMessageType($message_type) {
final public static function getTransactionObjectForMessageType(
$message_type) {
$message_xactions = id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->execute();
foreach ($message_xactions as $message_xaction) {
if ($message_xaction->getMessageType() === $message_type) {
return $message_xaction->getTransactionTypeConstant();
$xaction_type = $message_xaction->getHarbormasterBuildMessageType();
if ($xaction_type === $message_type) {
return $message_xaction;
}
}
return null;
}
abstract public function getMessageType();
final public static function getTransactionTypeForMessageType($message_type) {
$message_xaction = self::getTransactionObjectForMessageType($message_type);
public function validateTransactions($object, array $xactions) {
$errors = array();
// TODO: Restore logic that tests if the command can issue without causing
// anything to lapse into an invalid state. This should not be the same
// as the logic which powers the web UI: for example, if an "abort" is
// queued we want to disable "Abort" in the web UI, but should obviously
// process it here.
return $errors;
if ($message_xaction) {
return $message_xaction->getTransactionTypeConstant();
}
return null;
}
final public function getTransactionHasEffect($object, $old, $new) {
return $this->canApplyMessage($this->getActor(), $object);
}
final public function canApplyMessage(
PhabricatorUser $viewer,
HarbormasterBuild $build) {
try {
$this->assertCanApplyMessage($viewer, $build);
return true;
} catch (HarbormasterRestartException $ex) {
return false;
}
}
final public function canSendMessage(
PhabricatorUser $viewer,
HarbormasterBuild $build) {
try {
$this->assertCanSendMessage($viewer, $build);
return true;
} catch (HarbormasterRestartException $ex) {
return false;
}
}
final public function assertCanApplyMessage(
PhabricatorUser $viewer,
HarbormasterBuild $build) {
$this->newCanApplyMessageAssertion($viewer, $build);
}
final public function assertCanSendMessage(
PhabricatorUser $viewer,
HarbormasterBuild $build) {
$plan = $build->getBuildPlan();
// See T13526. Users without permission to access the build plan can
// currently end up here with no "BuildPlan" object.
if (!$plan) {
throw new HarbormasterRestartException(
pht('No Build Plan Permission'),
pht(
'You can not issue this command because you do not have '.
'permission to access the build plan for this build.'));
}
// Issuing these commands requires that you be able to edit the build, to
// prevent enemy engineers from sabotaging your builds. See T9614.
if (!$plan->canRunWithoutEditCapability()) {
try {
PhabricatorPolicyFilter::requireCapability(
$viewer,
$plan,
PhabricatorPolicyCapability::CAN_EDIT);
} catch (PhabricatorPolicyException $ex) {
throw new HarbormasterRestartException(
pht('Insufficent Build Plan Permission'),
pht(
'The build plan for this build is configured to prevent '.
'users who can not edit it from issuing commands to the '.
'build, and you do not have permission to edit the build '.
'plan.'));
}
}
$this->newCanSendMessageAssertion($viewer, $build);
$this->assertCanApplyMessage($viewer, $build);
}
abstract protected function newCanSendMessageAssertion(
PhabricatorUser $viewer,
HarbormasterBuild $build);
abstract protected function newCanApplyMessageAssertion(
PhabricatorUser $viewer,
HarbormasterBuild $build);
}