diff --git a/src/applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php --- a/src/applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php +++ b/src/applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php @@ -9,18 +9,222 @@ public function getMethodSummary() { return pht( - 'Send a message about the status of a build target to Harbormaster, '. - 'notifying the application of build results in an external system.'); + 'Modify running builds, and report build results.'); } public function getMethodDescription() { + return pht(<<newSendingDocumentationBoxPage($viewer); + $pages[] = $this->newBuildsDocumentationBoxPage($viewer); + $pages[] = $this->newCommandsDocumentationBoxPage($viewer); + $pages[] = $this->newTargetsDocumentationBoxPage($viewer); + $pages[] = $this->newUnitDocumentationBoxPage($viewer); + $pages[] = $this->newLintDocumentationBoxPage($viewer); + + return $pages; + } + + private function newSendingDocumentationBoxPage(PhabricatorUser $viewer) { + $title = pht('Sending Messages'); + $content = pht(<< + + Object Type + PHID Example + Description + + + Harbormaster Buildable + `PHID-HMBB-...` + %s + + + Harbormaster Build + `PHID-HMBD-...` + %s + + + Harbormaster Build Target + `PHID-HMBT-...` + %s + + + +See below for specifics on sending messages to different object types. +EOREMARKUP + , + pht( + 'Buildables may receive control commands like "abort" and "restart". '. + 'Sending a control command to a Buildable is the same as sending it '. + 'to each Build for the Buildable.'), + pht( + 'Builds may receive control commands like "pause", "resume", "abort", '. + 'and "restart".'), + pht( + 'Build Targets may receive build status and result messages, like '. + '"pass" or "fail".')); + + $content = $this->newRemarkupDocumentationView($content); + + return $this->newDocumentationBoxPage($viewer, $title, $content) + ->setAnchor('sending') + ->setIconIcon('fa-envelope-o'); + } + + private function newBuildsDocumentationBoxPage(PhabricatorUser $viewer) { + $title = pht('Updating Builds'); + + $content = pht(<<newRemarkupDocumentationView($content); + + return $this->newDocumentationBoxPage($viewer, $title, $content) + ->setAnchor('builds') + ->setIconIcon('fa-cubes'); + } + + private function newCommandsDocumentationBoxPage(PhabricatorUser $viewer) { + $messages = HarbormasterBuildMessageTransaction::getAllMessages(); + + $rows = array(); + + $rows[] = ''; + $rows[] = ''.pht('Message Type').''; + $rows[] = ''.pht('Description').''; + $rows[] = ''; + + foreach ($messages as $message) { + $row = array(); + + $row[] = sprintf( + '`%s`', + $message->getHarbormasterBuildMessageType()); + + $row[] = sprintf( + '%s', + $message->getHarbormasterBuildMessageDescription()); + + $rows[] = sprintf( + '%s', + implode("\n", $row)); + } + + $message_table = sprintf( + '%s
', + implode("\n", $rows)); + + $title = pht('Control Commands'); + + $content = pht(<< + + Object Type + PHID Example + + Description + + + Harbormaster Buildable + `PHID-HMBB-...` + {icon check color=green} + Buildables may receive control commands. + + + Harbormaster Build + `PHID-HMBD-...` + {icon check color=green} + Builds may receive control commands. + + + Harbormaster Build Target + `PHID-HMBT-...` + {icon times color=red} + You may **NOT** send control commands to build targets. + + + +You can send these commands: + +%s + +To send a command message, specify the PHID of the object you would like to +receive the message using the `receiver` parameter, and specify the message +type using the `type` parameter. + +EOREMARKUP + , + $message_table); + + $content = $this->newRemarkupDocumentationView($content); + + return $this->newDocumentationBoxPage($viewer, $title, $content) + ->setAnchor('commands') + ->setIconIcon('fa-exclamation-triangle'); + } + + private function newTargetsDocumentationBoxPage(PhabricatorUser $viewer) { $messages = HarbormasterMessageType::getAllMessages(); - $head_type = pht('Constant'); - $head_desc = pht('Description'); - $head_key = pht('Key'); $head_type = pht('Type'); - $head_name = pht('Name'); + $head_desc = pht('Description'); $rows = array(); $rows[] = "| {$head_type} | {$head_desc} |"; @@ -31,6 +235,84 @@ } $message_table = implode("\n", $rows); + $content = pht(<< + + Object Type + PHID Example + + Description + + + Harbormaster Buildable + `PHID-HMBB-...` + {icon times color=red} + Buildables may **NOT** receive status or result messages. + + + Harbormaster Build + `PHID-HMBD-...` + {icon times color=red} + Builds may **NOT** receive status or result messages. + + + Harbormaster Build Target + `PHID-HMBT-...` + {icon check color=green} + Report build status and results to Build Targets. + + + +The simplest way to use this method to report build results is to call it once +after the build finishes with a `pass` or `fail` message. This will record the +build result, and continue the next step in the build if the build was waiting +for a result. + +When you send a status message about a build target, you can optionally include +detailed `lint` or `unit` results alongside the message. See below for details. + +If you want to report intermediate results but a build hasn't completed yet, +you can use the `work` message. This message doesn't have any direct effects, +but allows you to send additional data to update the progress of the build +target. The target will continue waiting for a completion message, but the UI +will update to show the progress which has been made. + +When sending a message to a build target to report the status or results of +a build, your message must include a `type` which describes the overall state +of the build. For example, use `pass` to tell Harbormaster that a build target +completed successfully. + +Supported message types are: + +%s + +EOREMARKUP + , + $message_table); + + $title = pht('Updating Build Targets'); + + $content = $this->newRemarkupDocumentationView($content); + + return $this->newDocumentationBoxPage($viewer, $title, $content) + ->setAnchor('targets') + ->setIconIcon('fa-bullseye'); + } + + private function newUnitDocumentationBoxPage(PhabricatorUser $viewer) { + $head_key = pht('Key'); + $head_desc = pht('Description'); + $head_name = pht('Name'); + $head_type = pht('Type'); + $rows = array(); $rows[] = "| {$head_key} | {$head_type} | {$head_desc} |"; $rows[] = '|-------------|--------------|--------------|'; @@ -55,6 +337,64 @@ } $result_table = implode("\n", $rows); + $valid_unit = array( + array( + 'name' => 'PassingTest', + 'result' => ArcanistUnitTestResult::RESULT_PASS, + ), + array( + 'name' => 'FailingTest', + 'result' => ArcanistUnitTestResult::RESULT_FAIL, + ), + ); + + $json = new PhutilJSON(); + $valid_unit = $json->encodeAsList($valid_unit); + + + $title = pht('Reporting Unit Results'); + + $content = pht(<<newRemarkupDocumentationView($content); + + return $this->newDocumentationBoxPage($viewer, $title, $content) + ->setAnchor('unit'); + } + + private function newLintDocumentationBoxPage(PhabricatorUser $viewer) { + + $head_key = pht('Key'); + $head_desc = pht('Description'); + $head_name = pht('Name'); + $head_type = pht('Type'); + $rows = array(); $rows[] = "| {$head_key} | {$head_type} | {$head_desc} |"; $rows[] = '|-------------|--------------|--------------|'; @@ -76,17 +416,6 @@ } $severity_table = implode("\n", $rows); - $valid_unit = array( - array( - 'name' => 'PassingTest', - 'result' => ArcanistUnitTestResult::RESULT_PASS, - ), - array( - 'name' => 'FailingTest', - 'result' => ArcanistUnitTestResult::RESULT_FAIL, - ), - ); - $valid_lint = array( array( 'name' => pht('Syntax Error'), @@ -109,104 +438,58 @@ ); $json = new PhutilJSON(); - $valid_unit = $json->encodeAsList($valid_unit); $valid_lint = $json->encodeAsList($valid_lint); - return pht( - "Send a message about the status of a build target to Harbormaster, ". - "notifying the application of build results in an external system.". - "\n\n". - "Sending Messages\n". - "================\n". - "If you run external builds, you can use this method to publish build ". - "results back into Harbormaster after the external system finishes work ". - "or as it makes progress.". - "\n\n". - "The simplest way to use this method is to call it once after the ". - "build finishes with a `pass` or `fail` message. This will record the ". - "build result, and continue the next step in the build if the build was ". - "waiting for a result.". - "\n\n". - "When you send a status message about a build target, you can ". - "optionally include detailed `lint` or `unit` results alongside the ". - "message. See below for details.". - "\n\n". - "If you want to report intermediate results but a build hasn't ". - "completed yet, you can use the `work` message. This message doesn't ". - "have any direct effects, but allows you to send additional data to ". - "update the progress of the build target. The target will continue ". - "waiting for a completion message, but the UI will update to show the ". - "progress which has been made.". - "\n\n". - "Message Types\n". - "=============\n". - "When you send Harbormaster a message, you must include a `type`, ". - "which describes the overall state of the build. For example, use ". - "`pass` to tell Harbormaster that a build completed successfully.". - "\n\n". - "Supported message types are:". - "\n\n". - "%s". - "\n\n". - "Unit Results\n". - "============\n". - "You can report test results alongside a message. The simplest way to ". - "do this is to report all the results alongside a `pass` or `fail` ". - "message, but you can also send a `work` message to report intermediate ". - "results.\n\n". - "To provide unit test results, pass a list of results in the `unit` ". - "parameter. Each result should be a dictionary with these keys:". - "\n\n". - "%s". - "\n\n". - "The `result` parameter recognizes these test results:". - "\n\n". - "%s". - "\n\n". - "This is a simple, valid value for the `unit` parameter. It reports ". - "one passing test and one failing test:\n\n". - "\n\n". - "```lang=json\n". - "%s". - "```". - "\n\n". - "Lint Results\n". - "============\n". - "Like unit test results, you can report lint results alongside a ". - "message. The `lint` parameter should contain results as a list of ". - "dictionaries with these keys:". - "\n\n". - "%s". - "\n\n". - "The `severity` parameter recognizes these severity levels:". - "\n\n". - "%s". - "\n\n". - "This is a simple, valid value for the `lint` parameter. It reports one ". - "error and one warning:". - "\n\n". - "```lang=json\n". - "%s". - "```". - "\n\n", - $message_table, - $unit_table, - $result_table, - $valid_unit, + $title = pht('Reporting Lint Results'); + $content = pht(<<newRemarkupDocumentationView($content); + + return $this->newDocumentationBoxPage($viewer, $title, $content) + ->setAnchor('lint'); } protected function defineParamTypes() { $messages = HarbormasterMessageType::getAllMessages(); + + $more_messages = HarbormasterBuildMessageTransaction::getAllMessages(); + $more_messages = mpull($more_messages, 'getHarbormasterBuildMessageType'); + + $messages = array_merge($messages, $more_messages); + $messages = array_unique($messages); + + sort($messages); + $type_const = $this->formatStringConstants($messages); return array( - 'buildTargetPHID' => 'required phid', + 'receiver' => 'required string|phid', 'type' => 'required '.$type_const, 'unit' => 'optional list', 'lint' => 'optional list', + 'buildTargetPHID' => 'deprecated optional phid', ); } @@ -215,19 +498,90 @@ } protected function execute(ConduitAPIRequest $request) { - $viewer = $request->getUser(); + $viewer = $request->getViewer(); + + $receiver_name = $request->getValue('receiver'); $build_target_phid = $request->getValue('buildTargetPHID'); + if ($build_target_phid !== null) { + if ($receiver_name === null) { + $receiver_name = $build_target_phid; + } else { + throw new Exception( + pht( + 'Call specifies both "receiver" and "buildTargetPHID". '. + 'When using the modern "receiver" parameter, omit the '. + 'deprecated "buildTargetPHID" parameter.')); + } + } + + if (!strlen($receiver_name)) { + throw new Exception( + pht( + 'Call omits required "receiver" parameter. Specify the PHID '. + 'of the object you want to send a message to.')); + } + $message_type = $request->getValue('type'); + if (!strlen($message_type)) { + throw new Exception( + pht( + 'Call omits required "type" parameter. Specify the type of '. + 'message you want to send.')); + } - $build_target = id(new HarbormasterBuildTargetQuery()) + $receiver_object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) - ->withPHIDs(array($build_target_phid)) + ->withNames(array($receiver_name)) ->executeOne(); - if (!$build_target) { - throw new Exception(pht('No such build target!')); + if (!$receiver_object) { + throw new Exception( + pht( + 'Unable to load object "%s" to receive message.', + $receiver_name)); + } + + $is_target = ($receiver_object instanceof HarbormasterBuildTarget); + if ($is_target) { + return $this->sendToTarget($request, $message_type, $receiver_object); + } + + if ($request->getValue('unit') !== null) { + throw new Exception( + pht( + 'Call includes "unit" parameter. This parameter must be omitted '. + 'when the receiver is not a Build Target.')); + } + + if ($request->getValue('lint') !== null) { + throw new Exception( + pht( + 'Call includes "lint" parameter. This parameter must be omitted '. + 'when the receiver is not a Build Target.')); + } + + $is_build = ($receiver_object instanceof HarbormasterBuild); + if ($is_build) { + return $this->sendToBuild($request, $message_type, $receiver_object); + } + + $is_buildable = ($receiver_object instanceof HarbormasterBuildable); + if ($is_buildable) { + return $this->sendToBuildable($request, $message_type, $receiver_object); } + throw new Exception( + pht( + 'Receiver object (of class "%s") is not a valid receiver.', + get_class($receiver_object))); + } + + private function sendToTarget( + ConduitAPIRequest $request, + $message_type, + HarbormasterBuildTarget $build_target) { + $viewer = $request->getViewer(); + $save = array(); $lint_messages = $request->getValue('lint', array()); @@ -270,4 +624,67 @@ return null; } + private function sendToBuild( + ConduitAPIRequest $request, + $message_type, + HarbormasterBuild $build) { + $viewer = $request->getViewer(); + + $xaction = + HarbormasterBuildMessageTransaction::getTransactionObjectForMessageType( + $message_type); + if (!$xaction) { + throw new Exception( + pht( + 'Message type "%s" is not supported.', + $message_type)); + } + + // NOTE: This is a slightly weaker check than we perform in the web UI. + // We allow API callers to send a "pause" message to a pausing build, + // for example, even though the message will have no effect. + $xaction->assertCanApplyMessage($viewer, $build); + + $build->sendMessage($viewer, $xaction->getHarbormasterBuildMessageType()); + } + + private function sendToBuildable( + ConduitAPIRequest $request, + $message_type, + HarbormasterBuildable $buildable) { + $viewer = $request->getViewer(); + + $xaction = + HarbormasterBuildMessageTransaction::getTransactionObjectForMessageType( + $message_type); + if (!$xaction) { + throw new Exception( + pht( + 'Message type "%s" is not supported.', + $message_type)); + } + + // Reload the Buildable to load Builds. + $buildable = id(new HarbormasterBuildableQuery()) + ->setViewer($viewer) + ->withIDs(array($buildable->getID())) + ->needBuilds(true) + ->executeOne(); + + $can_send = array(); + foreach ($buildable->getBuilds() as $build) { + if ($xaction->canApplyMessage($viewer, $build)) { + $can_send[] = $build; + } + } + + // NOTE: This doesn't actually apply a transaction to the Buildable, + // but that transaction is purely informational and should probably be + // implemented as a Message. + + foreach ($can_send as $build) { + $build->sendMessage($viewer, $xaction->getHarbormasterBuildMessageType()); + } + } + } diff --git a/src/applications/harbormaster/exception/HarbormasterRestartException.php b/src/applications/harbormaster/exception/HarbormasterRestartException.php --- a/src/applications/harbormaster/exception/HarbormasterRestartException.php +++ b/src/applications/harbormaster/exception/HarbormasterRestartException.php @@ -9,7 +9,11 @@ $this->setTitle($title); $this->appendParagraph($body); - parent::__construct($title); + parent::__construct( + pht( + '%s: %s', + $title, + $body)); } public function setTitle($title) { diff --git a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageAbortTransaction.php b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageAbortTransaction.php --- a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageAbortTransaction.php +++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageAbortTransaction.php @@ -27,6 +27,10 @@ 'Progress on this build will be discarded. Really abort build?'); } + public function getHarbormasterBuildMessageDescription() { + return pht('Abort the build, discarding progress.'); + } + public function newBuildableConfirmPromptTitle( array $builds, array $sendable) { diff --git a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessagePauseTransaction.php b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessagePauseTransaction.php --- a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessagePauseTransaction.php +++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessagePauseTransaction.php @@ -28,6 +28,11 @@ 'complete. You can resume the build later.'); } + + public function getHarbormasterBuildMessageDescription() { + return pht('Pause the build.'); + } + public function newBuildableConfirmPromptTitle( array $builds, array $sendable) { diff --git a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageRestartTransaction.php b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageRestartTransaction.php --- a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageRestartTransaction.php +++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageRestartTransaction.php @@ -28,6 +28,11 @@ 'Side effects of the build will occur again. Really restart build?'); } + + public function getHarbormasterBuildMessageDescription() { + return pht('Restart the build, discarding all progress.'); + } + public function newBuildableConfirmPromptTitle( array $builds, array $sendable) { diff --git a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageResumeTransaction.php b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageResumeTransaction.php --- a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageResumeTransaction.php +++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageResumeTransaction.php @@ -27,6 +27,10 @@ 'Work will continue on the build. Really resume?'); } + public function getHarbormasterBuildMessageDescription() { + return pht('Resume work on a previously paused build.'); + } + public function newBuildableConfirmPromptTitle( array $builds, array $sendable) { @@ -77,7 +81,7 @@ 'You can not resume a build that uses an autoplan.')); } - if (!$build->isPaused()) { + if (!$build->isPaused() && !$build->isPausing()) { throw new HarbormasterRestartException( pht('Unable to Resume Build'), pht( diff --git a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageTransaction.php b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageTransaction.php --- a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageTransaction.php +++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageTransaction.php @@ -8,6 +8,7 @@ } abstract public function getHarbormasterBuildMessageName(); + abstract public function getHarbormasterBuildMessageDescription(); abstract public function getHarbormasterBuildableMessageName(); abstract public function getHarbormasterBuildableMessageEffect(); @@ -42,12 +43,18 @@ ); } - final public static function getTransactionObjectForMessageType( - $message_type) { + final public static function getAllMessages() { $message_xactions = id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->execute(); + return $message_xactions; + } + + final public static function getTransactionObjectForMessageType( + $message_type) { + $message_xactions = self::getAllMessages(); + foreach ($message_xactions as $message_xaction) { $xaction_type = $message_xaction->getHarbormasterBuildMessageType(); if ($xaction_type === $message_type) {