diff --git a/resources/sql/patches/20131107.buildlog.sql b/resources/sql/patches/20131107.buildlog.sql new file mode 100644 index 0000000000..e761b46c9a --- /dev/null +++ b/resources/sql/patches/20131107.buildlog.sql @@ -0,0 +1,26 @@ +CREATE TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlog ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + buildPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + buildStepPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + logSource VARCHAR(255) NULL COLLATE utf8_bin, + logType VARCHAR(255) NULL COLLATE utf8_bin, + duration INT UNSIGNED NULL, + live BOOLEAN NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + KEY `key_build` (buildPHID, buildStepPHID), + UNIQUE KEY `key_phid` (phid) +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_build +ADD COLUMN cancelRequested BOOLEAN NOT NULL; + +CREATE TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlogchunk ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + logID INT UNSIGNED NOT NULL COLLATE utf8_bin, + encoding VARCHAR(30) NOT NULL COLLATE utf8_bin, + size LONG NULL, + chunk LONGBLOB NOT NULL, + KEY `key_log` (logID) +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index d266852b22..0746bd8bb5 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -569,22 +569,22 @@ celerity_register_resource_map(array( ), '/rsrc/image/sprite-apps-X2.png' => array( - 'hash' => '68bbb3f409d0eb42d65dd94769813044', - 'uri' => '/res/68bbb3f4/rsrc/image/sprite-apps-X2.png', + 'hash' => '67e8a6bf2d7fbb0b7961f1a0dcf8592b', + 'uri' => '/res/67e8a6bf/rsrc/image/sprite-apps-X2.png', 'disk' => '/rsrc/image/sprite-apps-X2.png', 'type' => 'png', ), '/rsrc/image/sprite-apps-large-X2.png' => array( - 'hash' => '15368afbac0e1402c20f99f3166cdb11', - 'uri' => '/res/15368afb/rsrc/image/sprite-apps-large-X2.png', + 'hash' => '9bed1778022e2bd25d658842be54844d', + 'uri' => '/res/9bed1778/rsrc/image/sprite-apps-large-X2.png', 'disk' => '/rsrc/image/sprite-apps-large-X2.png', 'type' => 'png', ), '/rsrc/image/sprite-apps-large.png' => array( - 'hash' => 'b1f1de55803cf22eb3beb391fff17b04', - 'uri' => '/res/b1f1de55/rsrc/image/sprite-apps-large.png', + 'hash' => '518d6e5487c8d3758921ad85c1bb7d60', + 'uri' => '/res/518d6e54/rsrc/image/sprite-apps-large.png', 'disk' => '/rsrc/image/sprite-apps-large.png', 'type' => 'png', ), @@ -597,8 +597,8 @@ celerity_register_resource_map(array( ), '/rsrc/image/sprite-apps.png' => array( - 'hash' => 'bf7feaae848d44a461e63123c28e402f', - 'uri' => '/res/bf7feaae/rsrc/image/sprite-apps.png', + 'hash' => '9024ab95247f936f41c0b51c75e2e228', + 'uri' => '/res/9024ab95/rsrc/image/sprite-apps.png', 'disk' => '/rsrc/image/sprite-apps.png', 'type' => 'png', ), @@ -1197,7 +1197,7 @@ celerity_register_resource_map(array( ), 'herald-rule-editor' => array( - 'uri' => '/res/a561eb19/rsrc/js/application/herald/HeraldRuleEditor.js', + 'uri' => '/res/928275b4/rsrc/js/application/herald/HeraldRuleEditor.js', 'type' => 'js', 'requires' => array( @@ -4183,7 +4183,7 @@ celerity_register_resource_map(array( ), 'sprite-apps-css' => array( - 'uri' => '/res/37c55e75/rsrc/css/sprite-apps.css', + 'uri' => '/res/774f4bad/rsrc/css/sprite-apps.css', 'type' => 'css', 'requires' => array( @@ -4192,7 +4192,7 @@ celerity_register_resource_map(array( ), 'sprite-apps-large-css' => array( - 'uri' => '/res/8ddded36/rsrc/css/sprite-apps-large.css', + 'uri' => '/res/b547fab1/rsrc/css/sprite-apps-large.css', 'type' => 'css', 'requires' => array( @@ -4328,7 +4328,7 @@ celerity_register_resource_map(array( ), array( 'packages' => array( - 'f350af41' => + 'f0d63822' => array( 'name' => 'core.pkg.css', 'symbols' => @@ -4377,7 +4377,7 @@ celerity_register_resource_map(array( 41 => 'phabricator-tag-view-css', 42 => 'phui-list-view-css', ), - 'uri' => '/res/pkg/f350af41/core.pkg.css', + 'uri' => '/res/pkg/f0d63822/core.pkg.css', 'type' => 'css', ), '2c1dba03' => @@ -4569,15 +4569,15 @@ celerity_register_resource_map(array( ), 'reverse' => array( - 'aphront-dialog-view-css' => 'f350af41', - 'aphront-error-view-css' => 'f350af41', - 'aphront-list-filter-view-css' => 'f350af41', - 'aphront-pager-view-css' => 'f350af41', - 'aphront-panel-view-css' => 'f350af41', - 'aphront-table-view-css' => 'f350af41', - 'aphront-tokenizer-control-css' => 'f350af41', - 'aphront-tooltip-css' => 'f350af41', - 'aphront-typeahead-control-css' => 'f350af41', + 'aphront-dialog-view-css' => 'f0d63822', + 'aphront-error-view-css' => 'f0d63822', + 'aphront-list-filter-view-css' => 'f0d63822', + 'aphront-pager-view-css' => 'f0d63822', + 'aphront-panel-view-css' => 'f0d63822', + 'aphront-table-view-css' => 'f0d63822', + 'aphront-tokenizer-control-css' => 'f0d63822', + 'aphront-tooltip-css' => 'f0d63822', + 'aphront-typeahead-control-css' => 'f0d63822', 'differential-changeset-view-css' => '1084b12b', 'differential-core-view-css' => '1084b12b', 'differential-inline-comment-editor' => '5e9e5c4e', @@ -4591,7 +4591,7 @@ celerity_register_resource_map(array( 'differential-table-of-contents-css' => '1084b12b', 'diffusion-commit-view-css' => '7aa115b4', 'diffusion-icons-css' => '7aa115b4', - 'global-drag-and-drop-css' => 'f350af41', + 'global-drag-and-drop-css' => 'f0d63822', 'inline-comment-summary-css' => '1084b12b', 'javelin-aphlict' => '2c1dba03', 'javelin-behavior' => '3e3be199', @@ -4666,56 +4666,56 @@ celerity_register_resource_map(array( 'javelin-util' => '3e3be199', 'javelin-vector' => '3e3be199', 'javelin-workflow' => '3e3be199', - 'lightbox-attachment-css' => 'f350af41', + 'lightbox-attachment-css' => 'f0d63822', 'maniphest-task-summary-css' => '49898640', - 'phabricator-action-list-view-css' => 'f350af41', - 'phabricator-application-launch-view-css' => 'f350af41', + 'phabricator-action-list-view-css' => 'f0d63822', + 'phabricator-application-launch-view-css' => 'f0d63822', 'phabricator-busy' => '2c1dba03', 'phabricator-content-source-view-css' => '1084b12b', - 'phabricator-core-css' => 'f350af41', - 'phabricator-crumbs-view-css' => 'f350af41', + 'phabricator-core-css' => 'f0d63822', + 'phabricator-crumbs-view-css' => 'f0d63822', 'phabricator-drag-and-drop-file-upload' => '5e9e5c4e', 'phabricator-dropdown-menu' => '2c1dba03', 'phabricator-file-upload' => '2c1dba03', - 'phabricator-filetree-view-css' => 'f350af41', - 'phabricator-flag-css' => 'f350af41', + 'phabricator-filetree-view-css' => 'f0d63822', + 'phabricator-flag-css' => 'f0d63822', 'phabricator-hovercard' => '2c1dba03', - 'phabricator-jump-nav' => 'f350af41', + 'phabricator-jump-nav' => 'f0d63822', 'phabricator-keyboard-shortcut' => '2c1dba03', 'phabricator-keyboard-shortcut-manager' => '2c1dba03', - 'phabricator-main-menu-view' => 'f350af41', + 'phabricator-main-menu-view' => 'f0d63822', 'phabricator-menu-item' => '2c1dba03', - 'phabricator-nav-view-css' => 'f350af41', + 'phabricator-nav-view-css' => 'f0d63822', 'phabricator-notification' => '2c1dba03', - 'phabricator-notification-css' => 'f350af41', - 'phabricator-notification-menu-css' => 'f350af41', + 'phabricator-notification-css' => 'f0d63822', + 'phabricator-notification-menu-css' => 'f0d63822', 'phabricator-object-selector-css' => '1084b12b', 'phabricator-phtize' => '2c1dba03', 'phabricator-prefab' => '2c1dba03', 'phabricator-project-tag-css' => '49898640', - 'phabricator-remarkup-css' => 'f350af41', + 'phabricator-remarkup-css' => 'f0d63822', 'phabricator-shaped-request' => '5e9e5c4e', - 'phabricator-side-menu-view-css' => 'f350af41', - 'phabricator-standard-page-view' => 'f350af41', - 'phabricator-tag-view-css' => 'f350af41', + 'phabricator-side-menu-view-css' => 'f0d63822', + 'phabricator-standard-page-view' => 'f0d63822', + 'phabricator-tag-view-css' => 'f0d63822', 'phabricator-textareautils' => '2c1dba03', 'phabricator-tooltip' => '2c1dba03', - 'phabricator-transaction-view-css' => 'f350af41', - 'phabricator-zindex-css' => 'f350af41', - 'phui-button-css' => 'f350af41', - 'phui-form-css' => 'f350af41', - 'phui-form-view-css' => 'f350af41', - 'phui-header-view-css' => 'f350af41', - 'phui-icon-view-css' => 'f350af41', - 'phui-list-view-css' => 'f350af41', - 'phui-object-item-list-view-css' => 'f350af41', - 'phui-property-list-view-css' => 'f350af41', - 'phui-spacing-css' => 'f350af41', - 'sprite-apps-large-css' => 'f350af41', - 'sprite-gradient-css' => 'f350af41', - 'sprite-icons-css' => 'f350af41', - 'sprite-menu-css' => 'f350af41', - 'sprite-status-css' => 'f350af41', - 'syntax-highlighting-css' => 'f350af41', + 'phabricator-transaction-view-css' => 'f0d63822', + 'phabricator-zindex-css' => 'f0d63822', + 'phui-button-css' => 'f0d63822', + 'phui-form-css' => 'f0d63822', + 'phui-form-view-css' => 'f0d63822', + 'phui-header-view-css' => 'f0d63822', + 'phui-icon-view-css' => 'f0d63822', + 'phui-list-view-css' => 'f0d63822', + 'phui-object-item-list-view-css' => 'f0d63822', + 'phui-property-list-view-css' => 'f0d63822', + 'phui-spacing-css' => 'f0d63822', + 'sprite-apps-large-css' => 'f0d63822', + 'sprite-gradient-css' => 'f0d63822', + 'sprite-icons-css' => 'f0d63822', + 'sprite-menu-css' => 'f0d63822', + 'sprite-status-css' => 'f0d63822', + 'syntax-highlighting-css' => 'f0d63822', ), )); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index c6e75cd973..1d7911f388 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -652,9 +652,11 @@ phutil_register_library_map(array( 'FileReplyHandler' => 'applications/files/mail/FileReplyHandler.php', 'HarbormasterBuild' => 'applications/harbormaster/storage/build/HarbormasterBuild.php', 'HarbormasterBuildArtifact' => 'applications/harbormaster/storage/build/HarbormasterBuildArtifact.php', + 'HarbormasterBuildCancelController' => 'applications/harbormaster/controller/HarbormasterBuildCancelController.php', 'HarbormasterBuildItem' => 'applications/harbormaster/storage/build/HarbormasterBuildItem.php', 'HarbormasterBuildItemQuery' => 'applications/harbormaster/query/HarbormasterBuildItemQuery.php', 'HarbormasterBuildLog' => 'applications/harbormaster/storage/build/HarbormasterBuildLog.php', + 'HarbormasterBuildLogQuery' => 'applications/harbormaster/query/HarbormasterBuildLogQuery.php', 'HarbormasterBuildPlan' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php', 'HarbormasterBuildPlanEditor' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditor.php', 'HarbormasterBuildPlanQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanQuery.php', @@ -667,6 +669,7 @@ phutil_register_library_map(array( 'HarbormasterBuildStepQuery' => 'applications/harbormaster/query/HarbormasterBuildStepQuery.php', 'HarbormasterBuildTarget' => 'applications/harbormaster/storage/build/HarbormasterBuildTarget.php', 'HarbormasterBuildTargetQuery' => 'applications/harbormaster/query/HarbormasterBuildTargetQuery.php', + 'HarbormasterBuildViewController' => 'applications/harbormaster/controller/HarbormasterBuildViewController.php', 'HarbormasterBuildWorker' => 'applications/harbormaster/worker/HarbormasterBuildWorker.php', 'HarbormasterBuildable' => 'applications/harbormaster/storage/HarbormasterBuildable.php', 'HarbormasterBuildableApplyController' => 'applications/harbormaster/controller/HarbormasterBuildableApplyController.php', @@ -682,6 +685,7 @@ phutil_register_library_map(array( 'HarbormasterObject' => 'applications/harbormaster/storage/HarbormasterObject.php', 'HarbormasterPHIDTypeBuild' => 'applications/harbormaster/phid/HarbormasterPHIDTypeBuild.php', 'HarbormasterPHIDTypeBuildItem' => 'applications/harbormaster/phid/HarbormasterPHIDTypeBuildItem.php', + 'HarbormasterPHIDTypeBuildLog' => 'applications/harbormaster/phid/HarbormasterPHIDTypeBuildLog.php', 'HarbormasterPHIDTypeBuildPlan' => 'applications/harbormaster/phid/HarbormasterPHIDTypeBuildPlan.php', 'HarbormasterPHIDTypeBuildStep' => 'applications/harbormaster/phid/HarbormasterPHIDTypeBuildStep.php', 'HarbormasterPHIDTypeBuildTarget' => 'applications/harbormaster/phid/HarbormasterPHIDTypeBuildTarget.php', @@ -1392,6 +1396,7 @@ phutil_register_library_map(array( 'PhabricatorGlobalUploadTargetView' => 'applications/files/view/PhabricatorGlobalUploadTargetView.php', 'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php', 'PhabricatorHandleQuery' => 'applications/phid/query/PhabricatorHandleQuery.php', + 'PhabricatorHarbormasterConfigOptions' => 'applications/harbormaster/config/PhabricatorHarbormasterConfigOptions.php', 'PhabricatorHash' => 'infrastructure/util/PhabricatorHash.php', 'PhabricatorHashTestCase' => 'infrastructure/util/__tests__/PhabricatorHashTestCase.php', 'PhabricatorHelpController' => 'applications/help/controller/PhabricatorHelpController.php', @@ -2254,9 +2259,11 @@ phutil_register_library_map(array( 'ReleephStatusFieldSpecification' => 'applications/releeph/field/specification/ReleephStatusFieldSpecification.php', 'ReleephSummaryFieldSpecification' => 'applications/releeph/field/specification/ReleephSummaryFieldSpecification.php', 'ReleephUserView' => 'applications/releeph/view/user/ReleephUserView.php', + 'ShellLogView' => 'applications/harbormaster/view/ShellLogView.php', 'SleepBuildStepImplementation' => 'applications/harbormaster/step/SleepBuildStepImplementation.php', 'SlowvoteEmbedView' => 'applications/slowvote/view/SlowvoteEmbedView.php', 'SlowvoteRemarkupRule' => 'applications/slowvote/remarkup/SlowvoteRemarkupRule.php', + 'VariableBuildStepImplementation' => 'applications/harbormaster/step/VariableBuildStepImplementation.php', ), 'function' => array( @@ -2920,9 +2927,15 @@ phutil_register_library_map(array( 0 => 'HarbormasterDAO', 1 => 'PhabricatorPolicyInterface', ), + 'HarbormasterBuildCancelController' => 'HarbormasterController', 'HarbormasterBuildItem' => 'HarbormasterDAO', 'HarbormasterBuildItemQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', - 'HarbormasterBuildLog' => 'HarbormasterDAO', + 'HarbormasterBuildLog' => + array( + 0 => 'HarbormasterDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'HarbormasterBuildLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildPlan' => array( 0 => 'HarbormasterDAO', @@ -2944,6 +2957,7 @@ phutil_register_library_map(array( 'HarbormasterBuildStepQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildTarget' => 'HarbormasterDAO', 'HarbormasterBuildTargetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'HarbormasterBuildViewController' => 'HarbormasterController', 'HarbormasterBuildWorker' => 'PhabricatorWorker', 'HarbormasterBuildable' => array( @@ -2967,6 +2981,7 @@ phutil_register_library_map(array( 'HarbormasterObject' => 'HarbormasterDAO', 'HarbormasterPHIDTypeBuild' => 'PhabricatorPHIDType', 'HarbormasterPHIDTypeBuildItem' => 'PhabricatorPHIDType', + 'HarbormasterPHIDTypeBuildLog' => 'PhabricatorPHIDType', 'HarbormasterPHIDTypeBuildPlan' => 'PhabricatorPHIDType', 'HarbormasterPHIDTypeBuildStep' => 'PhabricatorPHIDType', 'HarbormasterPHIDTypeBuildTarget' => 'PhabricatorPHIDType', @@ -3776,6 +3791,7 @@ phutil_register_library_map(array( 'PhabricatorGlobalLock' => 'PhutilLock', 'PhabricatorGlobalUploadTargetView' => 'AphrontView', 'PhabricatorHandleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorHarbormasterConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorHashTestCase' => 'PhabricatorTestCase', 'PhabricatorHelpController' => 'PhabricatorController', 'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController', @@ -4783,8 +4799,10 @@ phutil_register_library_map(array( 'ReleephStatusFieldSpecification' => 'ReleephFieldSpecification', 'ReleephSummaryFieldSpecification' => 'ReleephFieldSpecification', 'ReleephUserView' => 'AphrontView', + 'ShellLogView' => 'AphrontView', 'SleepBuildStepImplementation' => 'BuildStepImplementation', 'SlowvoteEmbedView' => 'AphrontView', 'SlowvoteRemarkupRule' => 'PhabricatorRemarkupRuleObject', + 'VariableBuildStepImplementation' => 'BuildStepImplementation', ), )); diff --git a/src/applications/harbormaster/application/PhabricatorApplicationHarbormaster.php b/src/applications/harbormaster/application/PhabricatorApplicationHarbormaster.php index bf0d7c21ac..cbf9ed908e 100644 --- a/src/applications/harbormaster/application/PhabricatorApplicationHarbormaster.php +++ b/src/applications/harbormaster/application/PhabricatorApplicationHarbormaster.php @@ -51,6 +51,10 @@ final class PhabricatorApplicationHarbormaster extends PhabricatorApplication { 'edit/(?:(?P\d+)/)?' => 'HarbormasterStepEditController', 'delete/(?:(?P\d+)/)?' => 'HarbormasterStepDeleteController', ), + 'build/' => array( + '(?:(?P\d+)/)?' => 'HarbormasterBuildViewController', + 'cancel/(?:(?P\d+)/)?' => 'HarbormasterBuildCancelController', + ), 'plan/' => array( '(?:query/(?P[^/]+)/)?' => 'HarbormasterPlanListController', diff --git a/src/applications/harbormaster/config/PhabricatorHarbormasterConfigOptions.php b/src/applications/harbormaster/config/PhabricatorHarbormasterConfigOptions.php new file mode 100644 index 0000000000..fdfa27152e --- /dev/null +++ b/src/applications/harbormaster/config/PhabricatorHarbormasterConfigOptions.php @@ -0,0 +1,35 @@ +newOption( + 'harbormaster.temporary.hosts.whitelist', + 'list', + array()) + ->setSummary('Temporary configuration value.') + ->setLocked(true) + ->setDescription( + pht( + "This specifies a whitelist of remote hosts that the \"Run ". + "Remote Command\" may connect to. This is a temporary ". + "configuration option as Drydock is not yet available.". + "\n\n". + "**This configuration option will be removed in the future and ". + "your build configuration will no longer work when Drydock ". + "replaces this option. There is ABSOLUTELY NO SUPPORT for ". + "using this functionality!**")) + ); + } + +} diff --git a/src/applications/harbormaster/controller/HarbormasterBuildCancelController.php b/src/applications/harbormaster/controller/HarbormasterBuildCancelController.php new file mode 100644 index 0000000000..961d243be6 --- /dev/null +++ b/src/applications/harbormaster/controller/HarbormasterBuildCancelController.php @@ -0,0 +1,49 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $id = $this->id; + + $build = id(new HarbormasterBuildQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if ($build === null) { + return new Aphront404Response(); + } + + $build_uri = $this->getApplicationURI('/build/'.$build->getID()); + + if ($request->isDialogFormPost()) { + $build->setCancelRequested(true); + $build->save(); + + return id(new AphrontRedirectResponse())->setURI($build_uri); + } + + $dialog = new AphrontDialogView(); + $dialog->setTitle(pht('Really cancel build?')) + ->setUser($viewer) + ->addSubmitButton(pht('Cancel')) + ->addCancelButton($build_uri, pht('Don\'t Cancel')); + $dialog->appendChild( + phutil_tag( + 'p', + array(), + pht( + 'Really cancel this build?'))); + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} diff --git a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php new file mode 100644 index 0000000000..4eb5d39d3d --- /dev/null +++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php @@ -0,0 +1,218 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $id = $this->id; + + $build = id(new HarbormasterBuildQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$build) { + return new Aphront404Response(); + } + + $title = pht("Build %d", $id); + + $header = id(new PHUIHeaderView()) + ->setHeader($title) + ->setUser($viewer) + ->setPolicyObject($build); + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header); + + $actions = $this->buildActionList($build); + $this->buildPropertyLists($box, $build, $actions); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addCrumb( + id(new PhabricatorCrumbView()) + ->setName($title)); + + $logs = $this->buildLog($build); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + $logs + ), + array( + 'title' => $title, + 'device' => true, + )); + } + + private function buildLog(HarbormasterBuild $build) { + $request = $this->getRequest(); + $viewer = $request->getUser(); + $limit = $request->getInt('l', 25); + + $logs = id(new HarbormasterBuildLogQuery()) + ->setViewer($viewer) + ->withBuildPHIDs(array($build->getPHID())) + ->execute(); + + $log_boxes = array(); + foreach ($logs as $log) { + $start = 1; + $lines = preg_split("/\r\n|\r|\n/", $log->getLogText()); + if ($limit !== 0) { + $start = count($lines) - $limit; + if ($start >= 1) { + $lines = array_slice($lines, -$limit, $limit); + } else { + $start = 1; + } + } + $log_view = new ShellLogView(); + $log_view->setLines($lines); + $log_view->setStart($start); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht( + 'Build Log %d (%s - %s)', + $log->getID(), + $log->getLogSource(), + $log->getLogType())) + ->setSubheader($this->createLogHeader($build, $log)) + ->setUser($viewer); + + $log_boxes[] = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setForm($log_view); + } + + return $log_boxes; + } + + private function createLogHeader($build, $log) { + $request = $this->getRequest(); + $limit = $request->getInt('l', 25); + + $lines_25 = $this->getApplicationURI('/build/'.$build->getID().'/?l=25'); + $lines_50 = $this->getApplicationURI('/build/'.$build->getID().'/?l=50'); + $lines_100 = + $this->getApplicationURI('/build/'.$build->getID().'/?l=100'); + $lines_0 = $this->getApplicationURI('/build/'.$build->getID().'/?l=0'); + + $link_25 = phutil_tag('a', array('href' => $lines_25), pht('25')); + $link_50 = phutil_tag('a', array('href' => $lines_50), pht('50')); + $link_100 = phutil_tag('a', array('href' => $lines_100), pht('100')); + $link_0 = phutil_tag('a', array('href' => $lines_0), pht('Unlimited')); + + if ($limit === 25) { + $link_25 = phutil_tag('strong', array(), $link_25); + } else if ($limit === 50) { + $link_50 = phutil_tag('strong', array(), $link_50); + } else if ($limit === 100) { + $link_100 = phutil_tag('strong', array(), $link_100); + } else if ($limit === 0) { + $link_0 = phutil_tag('strong', array(), $link_0); + } + + return phutil_tag( + 'span', + array(), + array( + $link_25, + ' - ', + $link_50, + ' - ', + $link_100, + ' - ', + $link_0, + ' Lines')); + } + + private function buildActionList(HarbormasterBuild $build) { + $request = $this->getRequest(); + $viewer = $request->getUser(); + $id = $build->getID(); + + $list = id(new PhabricatorActionListView()) + ->setUser($viewer) + ->setObject($build) + ->setObjectURI("/build/{$id}"); + + $action = + id(new PhabricatorActionView()) + ->setName(pht('Cancel Build')) + ->setIcon('delete'); + switch ($build->getBuildStatus()) { + case HarbormasterBuild::STATUS_PENDING: + case HarbormasterBuild::STATUS_WAITING: + case HarbormasterBuild::STATUS_BUILDING: + $cancel_uri = $this->getApplicationURI('/build/cancel/'.$id.'/'); + $action + ->setHref($cancel_uri) + ->setWorkflow(true); + break; + default: + $action + ->setDisabled(true); + break; + } + $list->addAction($action); + + return $list; + } + + private function buildPropertyLists( + PHUIObjectBoxView $box, + HarbormasterBuild $build, + PhabricatorActionListView $actions) { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($build) + ->setActionList($actions); + $box->addPropertyList($properties); + + $properties->addProperty( + pht('Status'), + $this->getStatus($build)); + + } + + private function getStatus(HarbormasterBuild $build) { + if ($build->getCancelRequested()) { + return pht('Cancelling'); + } + switch ($build->getBuildStatus()) { + case HarbormasterBuild::STATUS_INACTIVE: + return pht('Inactive'); + case HarbormasterBuild::STATUS_PENDING: + return pht('Pending'); + case HarbormasterBuild::STATUS_WAITING: + return pht('Waiting on Resource'); + case HarbormasterBuild::STATUS_BUILDING: + return pht('Building'); + case HarbormasterBuild::STATUS_PASSED: + return pht('Passed'); + case HarbormasterBuild::STATUS_FAILED: + return pht('Failed'); + case HarbormasterBuild::STATUS_ERROR: + return pht('Unexpected Error'); + case HarbormasterBuild::STATUS_CANCELLED: + return pht('Cancelled'); + default: + return pht('Unknown'); + } + } + +} diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php index c1eb56496a..adae866dfe 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php @@ -34,38 +34,49 @@ final class HarbormasterBuildableViewController $build_list = id(new PHUIObjectItemListView()) ->setUser($viewer); foreach ($builds as $build) { + $view_uri = $this->getApplicationURI('/build/'.$build->getID().'/'); $item = id(new PHUIObjectItemView()) ->setObjectName(pht('Build %d', $build->getID())) - ->setHeader($build->getName()); - switch ($build->getBuildStatus()) { - case HarbormasterBuild::STATUS_INACTIVE: - $item->setBarColor('grey'); - $item->addAttribute(pht('Inactive')); - break; - case HarbormasterBuild::STATUS_PENDING: - $item->setBarColor('blue'); - $item->addAttribute(pht('Pending')); - break; - case HarbormasterBuild::STATUS_WAITING: - $item->setBarColor('blue'); - $item->addAttribute(pht('Waiting on Resource')); - break; - case HarbormasterBuild::STATUS_BUILDING: - $item->setBarColor('yellow'); - $item->addAttribute(pht('Building')); - break; - case HarbormasterBuild::STATUS_PASSED: - $item->setBarColor('green'); - $item->addAttribute(pht('Passed')); - break; - case HarbormasterBuild::STATUS_FAILED: - $item->setBarColor('red'); - $item->addAttribute(pht('Failed')); - break; - case HarbormasterBuild::STATUS_ERROR: - $item->setBarColor('red'); - $item->addAttribute(pht('Unexpected Error')); - break; + ->setHeader($build->getName()) + ->setHref($view_uri); + if ($build->getCancelRequested()) { + $item->setBarColor('black'); + $item->addAttribute(pht('Cancelling')); + } else { + switch ($build->getBuildStatus()) { + case HarbormasterBuild::STATUS_INACTIVE: + $item->setBarColor('grey'); + $item->addAttribute(pht('Inactive')); + break; + case HarbormasterBuild::STATUS_PENDING: + $item->setBarColor('blue'); + $item->addAttribute(pht('Pending')); + break; + case HarbormasterBuild::STATUS_WAITING: + $item->setBarColor('blue'); + $item->addAttribute(pht('Waiting on Resource')); + break; + case HarbormasterBuild::STATUS_BUILDING: + $item->setBarColor('yellow'); + $item->addAttribute(pht('Building')); + break; + case HarbormasterBuild::STATUS_PASSED: + $item->setBarColor('green'); + $item->addAttribute(pht('Passed')); + break; + case HarbormasterBuild::STATUS_FAILED: + $item->setBarColor('red'); + $item->addAttribute(pht('Failed')); + break; + case HarbormasterBuild::STATUS_ERROR: + $item->setBarColor('red'); + $item->addAttribute(pht('Unexpected Error')); + break; + case HarbormasterBuild::STATUS_CANCELLED: + $item->setBarColor('black'); + $item->addAttribute(pht('Cancelled')); + break; + } } $build_list->addItem($item); } diff --git a/src/applications/harbormaster/controller/HarbormasterStepEditController.php b/src/applications/harbormaster/controller/HarbormasterStepEditController.php index 81d3f0b2a8..db98d7e858 100644 --- a/src/applications/harbormaster/controller/HarbormasterStepEditController.php +++ b/src/applications/harbormaster/controller/HarbormasterStepEditController.php @@ -63,6 +63,11 @@ final class HarbormasterStepEditController $form = id(new AphrontFormView()) ->setUser($viewer); + $instructions = $implementation->getSettingRemarkupInstructions(); + if ($instructions !== null) { + $form->appendRemarkupInstructions($instructions); + } + // We need to render out all of the fields for the settings that // the implementation has. foreach ($implementation->getSettingDefinitions() as $name => $opt) { diff --git a/src/applications/harbormaster/phid/HarbormasterPHIDTypeBuildLog.php b/src/applications/harbormaster/phid/HarbormasterPHIDTypeBuildLog.php new file mode 100644 index 0000000000..385cbcaac6 --- /dev/null +++ b/src/applications/harbormaster/phid/HarbormasterPHIDTypeBuildLog.php @@ -0,0 +1,37 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $build_log = $objects[$phid]; + } + } + +} diff --git a/src/applications/harbormaster/query/HarbormasterBuildLogQuery.php b/src/applications/harbormaster/query/HarbormasterBuildLogQuery.php new file mode 100644 index 0000000000..000b267f9f --- /dev/null +++ b/src/applications/harbormaster/query/HarbormasterBuildLogQuery.php @@ -0,0 +1,98 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withBuildPHIDs(array $build_phids) { + $this->buildPHIDs = $build_phids; + return $this; + } + + protected function loadPage() { + $table = new HarbormasterBuildLog(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + return $table->loadAllFromArray($data); + } + + protected function willFilterPage(array $page) { + $builds = array(); + + $build_phids = array_filter(mpull($page, 'getBuildPHID')); + if ($build_phids) { + $builds = id(new HarbormasterBuildQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($build_phids) + ->setParentQuery($this) + ->execute(); + $builds = mpull($builds, null, 'getPHID'); + } + + foreach ($page as $key => $build_log) { + $build_phid = $build_log->getBuildPHID(); + if (empty($builds[$build_phid])) { + unset($page[$key]); + continue; + } + $build_log->attachBuild($builds[$build_phid]); + } + + return $page; + } + + private function buildWhereClause(AphrontDatabaseConnection $conn_r) { + $where = array(); + + if ($this->ids) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->buildPHIDs) { + $where[] = qsprintf( + $conn_r, + 'buildPHID IN (%Ls)', + $this->buildPHIDs); + } + + $where[] = $this->buildPagingClause($conn_r); + + return $this->formatWhereClause($where); + } + + public function getQueryApplicationClass() { + return 'PhabricatorApplicationHarbormaster'; + } + +} diff --git a/src/applications/harbormaster/step/BuildStepImplementation.php b/src/applications/harbormaster/step/BuildStepImplementation.php index 6a524f4e39..8e1df82abf 100644 --- a/src/applications/harbormaster/step/BuildStepImplementation.php +++ b/src/applications/harbormaster/step/BuildStepImplementation.php @@ -38,7 +38,9 @@ abstract class BuildStepImplementation { /** * Run the build step against the specified build. */ - abstract public function execute(HarbormasterBuild $build); + abstract public function execute( + HarbormasterBuild $build, + HarbormasterBuildStep $build_step); /** * Gets the settings for this build step. @@ -85,4 +87,11 @@ abstract class BuildStepImplementation { public function getSettingDefinitions() { return array(); } + + /** + * Return relevant setting instructions as Remarkup. + */ + public function getSettingRemarkupInstructions() { + return null; + } } diff --git a/src/applications/harbormaster/step/SleepBuildStepImplementation.php b/src/applications/harbormaster/step/SleepBuildStepImplementation.php index e80f559e29..0c51eaa881 100644 --- a/src/applications/harbormaster/step/SleepBuildStepImplementation.php +++ b/src/applications/harbormaster/step/SleepBuildStepImplementation.php @@ -16,7 +16,10 @@ final class SleepBuildStepImplementation extends BuildStepImplementation { return pht('Sleep for %s seconds.', $settings['seconds']); } - public function execute(HarbormasterBuild $build) { + public function execute( + HarbormasterBuild $build, + HarbormasterBuildStep $build_step) { + $settings = $this->getSettings(); sleep($settings['seconds']); diff --git a/src/applications/harbormaster/step/VariableBuildStepImplementation.php b/src/applications/harbormaster/step/VariableBuildStepImplementation.php new file mode 100644 index 0000000000..7f91518282 --- /dev/null +++ b/src/applications/harbormaster/step/VariableBuildStepImplementation.php @@ -0,0 +1,67 @@ + null, + 'commit' => null, + 'repository' => null, + 'vcs' => null, + 'uri' => null, + 'timestamp' => null); + + $buildable = $build->getBuildable(); + $object = $buildable->getBuildableObject(); + + $repo = null; + if ($object instanceof DifferentialRevision) { + $results['revision'] = $object->getID(); + $repo = $object->getRepository(); + } else if ($object instanceof PhabricatorRepositoryCommit) { + $results['commit'] = $object->getCommitIdentifier(); + $repo = $object->getRepository(); + } + + $results['repository'] = $repo->getCallsign(); + $results['vcs'] = $repo->getVersionControlSystem(); + $results['uri'] = $repo->getPublicRemoteURI(); + $results['timestamp'] = time(); + + return $results; + } + + public function mergeVariables(HarbormasterBuild $build, $string) { + $variables = $this->retrieveVariablesFromBuild($build); + foreach ($variables as $name => $value) { + if ($value === null) { + $value = ''; + } + $string = str_replace('${'.$name.'}', $value, $string); + } + return $string; + } + + public function getAvailableVariables() { + return array( + 'revision' => pht('The differential revision ID, if applicable.'), + 'commit' => pht('The commit identifier, if applicable.'), + 'repository' => pht('The callsign of the repository in Phabricator.'), + 'vcs' => pht('The version control system, either "svn", "hg" or "git".'), + 'uri' => pht('The URI to clone or checkout the repository from.'), + 'timestamp' => pht('The current UNIX timestamp.')); + } + + public function getSettingRemarkupInstructions() { + $text = ''; + $text .= pht('The following variables are available: ')."\n"; + $text .= "\n"; + foreach ($this->getAvailableVariables() as $name => $desc) { + $text .= ' - `'.$name.'`: '.$desc."\n"; + } + $text .= "\n"; + $text .= "Use `\${name}` to merge a variable into a setting."; + return $text; + } + +} diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index aa8e6c4f53..0d789d46d9 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -6,6 +6,7 @@ final class HarbormasterBuild extends HarbormasterDAO protected $buildablePHID; protected $buildPlanPHID; protected $buildStatus; + protected $cancelRequested; private $buildable = self::ATTACHABLE; private $buildPlan = self::ATTACHABLE; @@ -45,9 +46,15 @@ final class HarbormasterBuild extends HarbormasterDAO */ const STATUS_ERROR = 'error'; + /** + * The build has been cancelled. + */ + const STATUS_CANCELLED = 'cancelled'; + public static function initializeNewBuild(PhabricatorUser $actor) { return id(new HarbormasterBuild()) - ->setBuildStatus(self::STATUS_INACTIVE); + ->setBuildStatus(self::STATUS_INACTIVE) + ->setCancelRequested(false); } public function getConfiguration() { @@ -87,6 +94,37 @@ final class HarbormasterBuild extends HarbormasterDAO return $this->assertAttached($this->buildPlan); } + public function createLog( + HarbormasterBuildStep $build_step, + $log_source, + $log_type) { + + $log = HarbormasterBuildLog::initializeNewBuildLog($this, $build_step); + $log->setLogSource($log_source); + $log->setLogType($log_type); + $log->save(); + return $log; + } + + /** + * Checks for and handles build cancellation. If this method returns + * true, the caller should stop any current operations and return control + * as quickly as possible. + */ + public function checkForCancellation() { + // Here we load a copy of the current build and check whether + // the user requested cancellation. We can't do `reload()` here + // in case there are changes that have not yet been saved. + $copy = id(new HarbormasterBuild())->load($this->getID()); + if ($copy->getCancelRequested()) { + $this->setBuildStatus(HarbormasterBuild::STATUS_CANCELLED); + $this->setCancelRequested(false); + $this->save(); + return true; + } + return false; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php index 2e5d05d7eb..bd00dc5d6d 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php @@ -1,7 +1,206 @@ setBuildPHID($build->getPHID()) + ->setBuildStepPHID($build_step->getPHID()) + ->setDuration(null) + ->setLive(false); + } + + public function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + HarbormasterPHIDTypeBuildLog::TYPECONST); + } + + public function attachBuild(HarbormasterBuild $build) { + $this->build = $build; + return $this; + } + + public function getBuild() { + return $this->assertAttached($this->build); + } + + public function getName() { + return pht('Build Log'); + } + + public function attachBuildStep( + HarbormasterBuildStep $build_step = null) { + $this->buildStep = $build_step; + return $this; + } + + public function getBuildStep() { + return $this->assertAttached($this->buildStep); + } + + public function start() { + if ($this->getLive()) { + throw new Exception("Live logging has already started for this log."); + } + + $this->setLive(true); + $this->save(); + + return time(); + } + + public function append($content) { + if (!$this->getLive()) { + throw new Exception("Start logging before appending data to the log."); + } + if (strlen($content) === 0) { + return; + } + + // If the length of the content is greater than the chunk size limit, + // then we can never fit the content in a single record. We need to + // split our content out and call append on it for as many parts as there + // are to the content. + if (strlen($content) > self::CHUNK_BYTE_LIMIT) { + $current = $content; + while (strlen($current) > self::CHUNK_BYTE_LIMIT) { + $part = substr($current, 0, self::CHUNK_BYTE_LIMIT); + $current = substr($current, self::CHUNK_BYTE_LIMIT); + $this->append($part); + } + return; + } + + // Retrieve the size of last chunk from the DB for this log. If the + // chunk is over 500K, then we need to create a new log entry. + $conn = $this->establishConnection('w'); + $result = queryfx_all( + $conn, + 'SELECT id, size, encoding '. + 'FROM harbormaster_buildlogchunk '. + 'WHERE logID = %d '. + 'ORDER BY id DESC '. + 'LIMIT 1', + $this->getID()); + if (count($result) === 0 || + $result[0]["size"] + strlen($content) > self::CHUNK_BYTE_LIMIT || + $result[0]["encoding"] !== self::ENCODING_TEXT) { + + // We must insert a new chunk because the data we are appending + // won't fit into the existing one, or we don't have any existing + // chunk data. + queryfx( + $conn, + 'INSERT INTO harbormaster_buildlogchunk '. + '(logID, encoding, size, chunk) '. + 'VALUES '. + '(%d, %s, %d, %s)', + $this->getID(), + self::ENCODING_TEXT, + strlen($content), + $content); + } else { + // We have a resulting record that we can append our content onto. + queryfx( + $conn, + 'UPDATE harbormaster_buildlogchunk '. + 'SET chunk = CONCAT(chunk, %s), size = LENGTH(CONCAT(chunk, %s))'. + 'WHERE id = %d', + $content, + $content, + $result[0]["id"]); + } + } + + public function finalize($start = 0) { + if (!$this->getLive()) { + throw new Exception("Start logging before finalizing it."); + } + + // TODO: Encode the log contents in a gzipped format. + $this->reload(); + if ($start > 0) { + $this->setDuration(time() - $start); + } + $this->setLive(false); + $this->save(); + } + + public function getLogText() { + // TODO: This won't cope very well if we're pulling like a 700MB + // log file out of the DB. We should probably implement some sort + // of optional limit parameter so that when we're rendering out only + // 25 lines in the UI, we don't wastefully read in the whole log. + + // We have to read our content out of the database and stitch all of + // the log data back together. + $conn = $this->establishConnection('r'); + $result = queryfx_all( + $conn, + 'SELECT chunk '. + 'FROM harbormaster_buildlogchunk '. + 'WHERE logID = %d '. + 'ORDER BY id ASC', + $this->getID()); + + $content = ""; + foreach ($result as $row) { + $content .= $row["chunk"]; + } + return $content; + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + return $this->getBuild()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getBuild()->hasAutomaticCapability( + $capability, + $viewer); + } + + public function describeAutomaticCapability($capability) { + return pht( + 'Users must be able to see a build to view it\'s build log.'); + } - protected $buildItemPHID; } diff --git a/src/applications/harbormaster/view/ShellLogView.php b/src/applications/harbormaster/view/ShellLogView.php new file mode 100644 index 0000000000..db7f85548f --- /dev/null +++ b/src/applications/harbormaster/view/ShellLogView.php @@ -0,0 +1,101 @@ +start = $start; + return $this; + } + + public function setLimit($limit) { + $this->limit = $limit; + return $this; + } + + public function setLines(array $lines) { + $this->lines = $lines; + return $this; + } + + public function setHighlights(array $highlights) { + $this->highlights = array_fuse($highlights); + return $this; + } + + public function render() { + require_celerity_resource('phabricator-source-code-view-css'); + require_celerity_resource('syntax-highlighting-css'); + + Javelin::initBehavior('phabricator-oncopy', array()); + + $line_number = $this->start; + + $rows = array(); + foreach ($this->lines as $line) { + $hit_limit = $this->limit && + ($line_number == $this->limit) && + (count($this->lines) != $this->limit); + + if ($hit_limit) { + $content_number = ''; + $content_line = phutil_tag( + 'span', + array( + 'class' => 'c', + ), + pht('...')); + } else { + $content_number = $line_number; + $content_line = $line; + } + + $row_attributes = array(); + if (isset($this->highlights[$line_number])) { + $row_attributes['class'] = 'phabricator-source-highlight'; + } + + // TODO: Provide nice links. + + $rows[] = phutil_tag( + 'tr', + $row_attributes, + hsprintf( + '%s'. + '%s', + $content_number, + $content_line)); + + if ($hit_limit) { + break; + } + + $line_number++; + } + + $classes = array(); + $classes[] = 'phabricator-source-code-view'; + $classes[] = 'remarkup-code'; + $classes[] = 'PhabricatorMonospaced'; + + return phutil_tag( + 'div', + array( + 'class' => 'phabricator-source-code-container', + 'style' => 'background-color: black; color: white;' + ), + phutil_tag( + 'table', + array( + 'class' => implode(' ', $classes), + 'style' => 'background-color: black' + ), + phutil_implode_html('', $rows))); + } + +} diff --git a/src/applications/harbormaster/worker/HarbormasterBuildWorker.php b/src/applications/harbormaster/worker/HarbormasterBuildWorker.php index 0306c18d8f..2f8a31605d 100644 --- a/src/applications/harbormaster/worker/HarbormasterBuildWorker.php +++ b/src/applications/harbormaster/worker/HarbormasterBuildWorker.php @@ -25,6 +25,13 @@ final class HarbormasterBuildWorker extends PhabricatorWorker { pht('Invalid build ID "%s".', $id)); } + // It's possible for the user to request cancellation before + // a worker picks up a build. We check to see if the build + // is already cancelled, and return if it is. + if ($build->checkForCancellation()) { + return; + } + try { $build->setBuildStatus(HarbormasterBuild::STATUS_BUILDING); $build->save(); @@ -44,12 +51,21 @@ final class HarbormasterBuildWorker extends PhabricatorWorker { $build->setBuildStatus(HarbormasterBuild::STATUS_ERROR); break; } - $implementation->execute($build); + $implementation->execute($build, $step); if ($build->getBuildStatus() !== HarbormasterBuild::STATUS_BUILDING) { break; } + if ($build->checkForCancellation()) { + break; + } } + // Check to see if the user requested cancellation. If they did and + // we get to here, they might have either cancelled too late, or the + // step isn't cancellation aware. In either case we ignore the result + // and move to a cancelled state. + $build->checkForCancellation(); + // If we get to here, then the build has finished. Set it to passed // if no build step explicitly set the status. if ($build->getBuildStatus() === HarbormasterBuild::STATUS_BUILDING) { diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php index 6f5e699c98..08a4c21349 100644 --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -1744,6 +1744,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { 'type' => 'sql', 'name' => $this->getPatchPath('20131106.nuance-v0.sql'), ), + '20131107.buildlog.sql' => array( + 'type' => 'sql', + 'name' => $this->getPatchPath('20131107.buildlog.sql'), + ), ); } }