diff --git a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php index b0e3105d78..dc63b5fd95 100644 --- a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php +++ b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php @@ -1,169 +1,204 @@ key = $key; + $this->properties = $properties; + } + + public static function newBuildStatusObject($status) { + $spec = self::getBuildStatusSpec($status); + return new self($status, $spec); + } + + private function getProperty($key) { + if (!array_key_exists($key, $this->properties)) { + throw new Exception( + pht( + 'Attempting to access unknown build status property ("%s").', + $key)); + } + + return $this->properties[$key]; + } + + public function isBuilding() { + return $this->getProperty('isBuilding'); + } + + public function isPaused() { + return ($this->key === self::STATUS_PAUSED); + } + + public function isComplete() { + return $this->getProperty('isComplete'); + } + + public function isPassed() { + return ($this->key === self::STATUS_PASSED); + } + /** * Get a human readable name for a build status constant. * * @param const Build status constant. * @return string Human-readable name. */ public static function getBuildStatusName($status) { $spec = self::getBuildStatusSpec($status); - return idx($spec, 'name', pht('Unknown ("%s")', $status)); + return $spec['name']; } public static function getBuildStatusMap() { $specs = self::getBuildStatusSpecMap(); return ipull($specs, 'name'); } public static function getBuildStatusIcon($status) { $spec = self::getBuildStatusSpec($status); - return idx($spec, 'icon', 'fa-question-circle'); + return $spec['icon']; } public static function getBuildStatusColor($status) { $spec = self::getBuildStatusSpec($status); - return idx($spec, 'color', 'bluegrey'); + return $spec['color']; } public static function getBuildStatusANSIColor($status) { $spec = self::getBuildStatusSpec($status); - return idx($spec, 'color.ansi', 'magenta'); + return $spec['color.ansi']; } public static function getWaitingStatusConstants() { return array( self::STATUS_INACTIVE, self::STATUS_PENDING, ); } public static function getActiveStatusConstants() { return array( self::STATUS_BUILDING, self::STATUS_PAUSED, ); } public static function getCompletedStatusConstants() { return array( self::STATUS_PASSED, self::STATUS_FAILED, self::STATUS_ABORTED, self::STATUS_ERROR, self::STATUS_DEADLOCKED, ); } private static function getBuildStatusSpecMap() { return array( self::STATUS_INACTIVE => array( 'name' => pht('Inactive'), 'icon' => 'fa-circle-o', 'color' => 'dark', 'color.ansi' => 'yellow', + 'isBuilding' => false, + 'isComplete' => false, ), self::STATUS_PENDING => array( 'name' => pht('Pending'), 'icon' => 'fa-circle-o', 'color' => 'blue', 'color.ansi' => 'yellow', + 'isBuilding' => true, + 'isComplete' => false, ), self::STATUS_BUILDING => array( 'name' => pht('Building'), 'icon' => 'fa-chevron-circle-right', 'color' => 'blue', 'color.ansi' => 'yellow', + 'isBuilding' => true, + 'isComplete' => false, ), self::STATUS_PASSED => array( 'name' => pht('Passed'), 'icon' => 'fa-check-circle', 'color' => 'green', 'color.ansi' => 'green', + 'isBuilding' => false, + 'isComplete' => true, ), self::STATUS_FAILED => array( 'name' => pht('Failed'), 'icon' => 'fa-times-circle', 'color' => 'red', 'color.ansi' => 'red', + 'isBuilding' => false, + 'isComplete' => true, ), self::STATUS_ABORTED => array( 'name' => pht('Aborted'), 'icon' => 'fa-minus-circle', 'color' => 'red', 'color.ansi' => 'red', + 'isBuilding' => false, + 'isComplete' => true, ), self::STATUS_ERROR => array( 'name' => pht('Unexpected Error'), 'icon' => 'fa-minus-circle', 'color' => 'red', 'color.ansi' => 'red', + 'isBuilding' => false, + 'isComplete' => true, ), self::STATUS_PAUSED => array( 'name' => pht('Paused'), 'icon' => 'fa-minus-circle', 'color' => 'dark', 'color.ansi' => 'yellow', + 'isBuilding' => false, + 'isComplete' => false, ), self::STATUS_DEADLOCKED => array( 'name' => pht('Deadlocked'), 'icon' => 'fa-exclamation-circle', 'color' => 'red', 'color.ansi' => 'red', + 'isBuilding' => false, + 'isComplete' => true, ), ); } private static function getBuildStatusSpec($status) { - return idx(self::getBuildStatusSpecMap(), $status, array()); + $map = self::getBuildStatusSpecMap(); + if (isset($map[$status])) { + return $map[$status]; + } + + return array( + 'name' => pht('Unknown ("%s")', $status), + 'icon' => 'fa-question-circle', + 'color' => 'bluegrey', + 'color.ansi' => 'magenta', + 'isBuilding' => false, + 'isComplete' => false, + ); } } diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php index 6a47dae429..58e5cf8c49 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php @@ -1,351 +1,355 @@ getViewer(); $buildable = id(new HarbormasterBuildableQuery()) ->setViewer($viewer) ->withIDs(array($request->getURIData('id'))) ->executeOne(); if (!$buildable) { return new Aphront404Response(); } $id = $buildable->getID(); // Pull builds and build targets. $builds = id(new HarbormasterBuildQuery()) ->setViewer($viewer) ->withBuildablePHIDs(array($buildable->getPHID())) ->needBuildTargets(true) ->execute(); list($lint, $unit) = $this->renderLintAndUnit($buildable, $builds); $buildable->attachBuilds($builds); $object = $buildable->getBuildableObject(); $build_list = $this->buildBuildList($buildable); $title = pht('Buildable %d', $id); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($viewer) ->setPolicyObject($buildable) + ->setStatus( + $buildable->getStatusIcon(), + $buildable->getStatusColor(), + $buildable->getStatusDisplayName()) ->setHeaderIcon('fa-recycle'); $timeline = $this->buildTransactionTimeline( $buildable, new HarbormasterBuildableTransactionQuery()); $timeline->setShouldTerminate(true); $curtain = $this->buildCurtainView($buildable); $properties = $this->buildPropertyList($buildable); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($buildable->getMonogram()); $crumbs->setBorder(true); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn(array( $properties, $lint, $unit, $build_list, $timeline, )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild($view); } private function buildCurtainView(HarbormasterBuildable $buildable) { $viewer = $this->getViewer(); $id = $buildable->getID(); $curtain = $this->newCurtainView($buildable); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $buildable, PhabricatorPolicyCapability::CAN_EDIT); $can_restart = false; $can_resume = false; $can_pause = false; $can_abort = false; $command_restart = HarbormasterBuildCommand::COMMAND_RESTART; $command_resume = HarbormasterBuildCommand::COMMAND_RESUME; $command_pause = HarbormasterBuildCommand::COMMAND_PAUSE; $command_abort = HarbormasterBuildCommand::COMMAND_ABORT; 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; } } } $restart_uri = "buildable/{$id}/restart/"; $pause_uri = "buildable/{$id}/pause/"; $resume_uri = "buildable/{$id}/resume/"; $abort_uri = "buildable/{$id}/abort/"; $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-repeat') ->setName(pht('Restart All Builds')) ->setHref($this->getApplicationURI($restart_uri)) ->setWorkflow(true) ->setDisabled(!$can_restart || !$can_edit)); $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pause') ->setName(pht('Pause All Builds')) ->setHref($this->getApplicationURI($pause_uri)) ->setWorkflow(true) ->setDisabled(!$can_pause || !$can_edit)); $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-play') ->setName(pht('Resume All 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 All Builds')) ->setHref($this->getApplicationURI($abort_uri)) ->setWorkflow(true) ->setDisabled(!$can_abort || !$can_edit)); return $curtain; } private function buildPropertyList(HarbormasterBuildable $buildable) { $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer); $container_phid = $buildable->getContainerPHID(); $buildable_phid = $buildable->getBuildablePHID(); if ($container_phid) { $properties->addProperty( pht('Container'), $viewer->renderHandle($container_phid)); } $properties->addProperty( pht('Buildable'), $viewer->renderHandle($buildable_phid)); $properties->addProperty( pht('Origin'), $buildable->getIsManualBuildable() ? pht('Manual Buildable') : pht('Automatic Buildable')); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Properties')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($properties); } private function buildBuildList(HarbormasterBuildable $buildable) { $viewer = $this->getRequest()->getUser(); $build_list = id(new PHUIObjectItemListView()) ->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); 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())); } $targets = $build->getBuildTargets(); if ($targets) { $target_list = id(new PHUIStatusListView()); foreach ($targets as $target) { $status = $target->getTargetStatus(); $icon = HarbormasterBuildTarget::getBuildTargetStatusIcon($status); $color = HarbormasterBuildTarget::getBuildTargetStatusColor($status); $status_name = HarbormasterBuildTarget::getBuildTargetStatusName($status); $name = $target->getName(); $target_list->addItem( id(new PHUIStatusItemView()) ->setIcon($icon, $color, $status_name) ->setTarget(pht('Target %d', $target->getID())) ->setNote($name)); } $target_box = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_SMALL) ->appendChild($target_list); $item->appendChild($target_box); } $build_list->addItem($item); } $build_list->setFlush(true); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Builds')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($build_list); return $box; } private function renderLintAndUnit( HarbormasterBuildable $buildable, array $builds) { $viewer = $this->getViewer(); $targets = array(); foreach ($builds as $build) { foreach ($build->getBuildTargets() as $target) { $targets[] = $target; } } if (!$targets) { return; } $target_phids = mpull($targets, 'getPHID'); $lint_data = id(new HarbormasterBuildLintMessage())->loadAllWhere( 'buildTargetPHID IN (%Ls)', $target_phids); $unit_data = id(new HarbormasterBuildUnitMessage())->loadAllWhere( 'buildTargetPHID IN (%Ls)', $target_phids); if ($lint_data) { $lint_table = id(new HarbormasterLintPropertyView()) ->setUser($viewer) ->setLimit(10) ->setLintMessages($lint_data); $lint_href = $this->getApplicationURI('lint/'.$buildable->getID().'/'); $lint_header = id(new PHUIHeaderView()) ->setHeader(pht('Lint Messages')) ->addActionLink( id(new PHUIButtonView()) ->setTag('a') ->setHref($lint_href) ->setIcon('fa-list-ul') ->setText('View All')); $lint = id(new PHUIObjectBoxView()) ->setHeader($lint_header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($lint_table); } else { $lint = null; } if ($unit_data) { $unit = id(new HarbormasterUnitSummaryView()) ->setBuildable($buildable) ->setUnitMessages($unit_data) ->setShowViewAll(true) ->setLimit(5); } else { $unit = null; } return array($lint, $unit); } } diff --git a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php index 0616e77b4e..9c4112e810 100644 --- a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php +++ b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php @@ -1,582 +1,578 @@ forceBuildableUpdate = $force_buildable_update; return $this; } public function shouldForceBuildableUpdate() { return $this->forceBuildableUpdate; } public function queueNewBuildTarget(HarbormasterBuildTarget $target) { $this->newBuildTargets[] = $target; return $this; } public function getNewBuildTargets() { return $this->newBuildTargets; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setBuild(HarbormasterBuild $build) { $this->build = $build; return $this; } public function getBuild() { return $this->build; } public function continueBuild() { $build = $this->getBuild(); $lock_key = 'harbormaster.build:'.$build->getID(); $lock = PhabricatorGlobalLock::newLock($lock_key)->lock(15); $build->reload(); $old_status = $build->getBuildStatus(); try { $this->updateBuild($build); } catch (Exception $ex) { // If any exception is raised, the build is marked as a failure and the // exception is re-thrown (this ensures we don't leave builds in an // inconsistent state). $build->setBuildStatus(HarbormasterBuildStatus::STATUS_ERROR); $build->save(); $lock->unlock(); $this->releaseAllArtifacts($build); throw $ex; } $lock->unlock(); // NOTE: We queue new targets after releasing the lock so that in-process // execution via `bin/harbormaster` does not reenter the locked region. foreach ($this->getNewBuildTargets() as $target) { $task = PhabricatorWorker::scheduleTask( 'HarbormasterTargetWorker', array( 'targetID' => $target->getID(), ), array( 'objectPHID' => $target->getPHID(), )); } // If the build changed status, we might need to update the overall status // on the buildable. $new_status = $build->getBuildStatus(); if ($new_status != $old_status || $this->shouldForceBuildableUpdate()) { $this->updateBuildable($build->getBuildable()); } $this->releaseQueuedArtifacts(); // If we are no longer building for any reason, release all artifacts. if (!$build->isBuilding()) { $this->releaseAllArtifacts($build); } } private function updateBuild(HarbormasterBuild $build) { if ($build->isAborting()) { $this->releaseAllArtifacts($build); $build->setBuildStatus(HarbormasterBuildStatus::STATUS_ABORTED); $build->save(); } if (($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_PENDING) || ($build->isRestarting())) { $this->restartBuild($build); $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING); $build->save(); } if ($build->isResuming()) { $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING); $build->save(); } if ($build->isPausing() && !$build->isComplete()) { $build->setBuildStatus(HarbormasterBuildStatus::STATUS_PAUSED); $build->save(); } $build->deleteUnprocessedCommands(); if ($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_BUILDING) { $this->updateBuildSteps($build); } } private function restartBuild(HarbormasterBuild $build) { // We're restarting the build, so release all previous artifacts. $this->releaseAllArtifacts($build); // Increment the build generation counter on the build. $build->setBuildGeneration($build->getBuildGeneration() + 1); // Currently running targets should periodically check their build // generation (which won't have changed) against the build's generation. // If it is different, they will automatically stop what they're doing // and abort. // Previously we used to delete targets, logs and artifacts here. Instead // leave them around so users can view previous generations of this build. } private function updateBuildSteps(HarbormasterBuild $build) { $all_targets = id(new HarbormasterBuildTargetQuery()) ->setViewer($this->getViewer()) ->withBuildPHIDs(array($build->getPHID())) ->withBuildGenerations(array($build->getBuildGeneration())) ->execute(); $this->updateWaitingTargets($all_targets); $targets = mgroup($all_targets, 'getBuildStepPHID'); $steps = id(new HarbormasterBuildStepQuery()) ->setViewer($this->getViewer()) ->withBuildPlanPHIDs(array($build->getBuildPlan()->getPHID())) ->execute(); $steps = mpull($steps, null, 'getPHID'); // Identify steps which are in various states. $queued = array(); $underway = array(); $waiting = array(); $complete = array(); $failed = array(); foreach ($steps as $step) { $step_targets = idx($targets, $step->getPHID(), array()); if ($step_targets) { $is_queued = false; $is_underway = false; foreach ($step_targets as $target) { if ($target->isUnderway()) { $is_underway = true; break; } } $is_waiting = false; foreach ($step_targets as $target) { if ($target->isWaiting()) { $is_waiting = true; break; } } $is_complete = true; foreach ($step_targets as $target) { if (!$target->isComplete()) { $is_complete = false; break; } } $is_failed = false; foreach ($step_targets as $target) { if ($target->isFailed()) { $is_failed = true; break; } } } else { $is_queued = true; $is_underway = false; $is_waiting = false; $is_complete = false; $is_failed = false; } if ($is_queued) { $queued[$step->getPHID()] = true; } if ($is_underway) { $underway[$step->getPHID()] = true; } if ($is_waiting) { $waiting[$step->getPHID()] = true; } if ($is_complete) { $complete[$step->getPHID()] = true; } if ($is_failed) { $failed[$step->getPHID()] = true; } } // If any step failed, fail the whole build, then bail. if (count($failed)) { $build->setBuildStatus(HarbormasterBuildStatus::STATUS_FAILED); $build->save(); return; } // If every step is complete, we're done with this build. Mark it passed // and bail. if (count($complete) == count($steps)) { $build->setBuildStatus(HarbormasterBuildStatus::STATUS_PASSED); $build->save(); return; } // Release any artifacts which are not inputs to any remaining build // step. We're done with these, so something else is free to use them. $ongoing_phids = array_keys($queued + $waiting + $underway); $ongoing_steps = array_select_keys($steps, $ongoing_phids); $this->releaseUnusedArtifacts($all_targets, $ongoing_steps); // Identify all the steps which are ready to run (because all their // dependencies are complete). $runnable = array(); foreach ($steps as $step) { $dependencies = $step->getStepImplementation()->getDependencies($step); if (isset($queued[$step->getPHID()])) { $can_run = true; foreach ($dependencies as $dependency) { if (empty($complete[$dependency])) { $can_run = false; break; } } if ($can_run) { $runnable[] = $step; } } } if (!$runnable && !$waiting && !$underway) { // This means the build is deadlocked, and the user has configured // circular dependencies. $build->setBuildStatus(HarbormasterBuildStatus::STATUS_DEADLOCKED); $build->save(); return; } foreach ($runnable as $runnable_step) { $target = HarbormasterBuildTarget::initializeNewBuildTarget( $build, $runnable_step, $build->retrieveVariablesFromBuild()); $target->save(); $this->queueNewBuildTarget($target); } } /** * Release any artifacts which aren't used by any running or waiting steps. * * This releases artifacts as soon as they're no longer used. This can be * particularly relevant when a build uses multiple hosts since it returns * hosts to the pool more quickly. * * @param list Targets in the build. * @param list List of running and waiting steps. * @return void */ private function releaseUnusedArtifacts(array $targets, array $steps) { assert_instances_of($targets, 'HarbormasterBuildTarget'); assert_instances_of($steps, 'HarbormasterBuildStep'); if (!$targets || !$steps) { return; } $target_phids = mpull($targets, 'getPHID'); $artifacts = id(new HarbormasterBuildArtifactQuery()) ->setViewer($this->getViewer()) ->withBuildTargetPHIDs($target_phids) ->withIsReleased(false) ->execute(); if (!$artifacts) { return; } // Collect all the artifacts that remaining build steps accept as inputs. $must_keep = array(); foreach ($steps as $step) { $inputs = $step->getStepImplementation()->getArtifactInputs(); foreach ($inputs as $input) { $artifact_key = $input['key']; $must_keep[$artifact_key] = true; } } // Queue unreleased artifacts which no remaining step uses for immediate // release. foreach ($artifacts as $artifact) { $key = $artifact->getArtifactKey(); if (isset($must_keep[$key])) { continue; } $this->artifactReleaseQueue[] = $artifact; } } /** * Process messages which were sent to these targets, kicking applicable * targets out of "Waiting" and into either "Passed" or "Failed". * * @param list List of targets to process. * @return void */ private function updateWaitingTargets(array $targets) { assert_instances_of($targets, 'HarbormasterBuildTarget'); // We only care about messages for targets which are actually in a waiting // state. $waiting_targets = array(); foreach ($targets as $target) { if ($target->isWaiting()) { $waiting_targets[$target->getPHID()] = $target; } } if (!$waiting_targets) { return; } $messages = id(new HarbormasterBuildMessageQuery()) ->setViewer($this->getViewer()) ->withBuildTargetPHIDs(array_keys($waiting_targets)) ->withConsumed(false) ->execute(); foreach ($messages as $message) { $target = $waiting_targets[$message->getBuildTargetPHID()]; switch ($message->getType()) { case HarbormasterMessageType::MESSAGE_PASS: $new_status = HarbormasterBuildTarget::STATUS_PASSED; break; case HarbormasterMessageType::MESSAGE_FAIL: $new_status = HarbormasterBuildTarget::STATUS_FAILED; break; case HarbormasterMessageType::MESSAGE_WORK: default: $new_status = null; break; } if ($new_status !== null) { $message->setIsConsumed(true); $message->save(); $target->setTargetStatus($new_status); if ($target->isComplete()) { $target->setDateCompleted(PhabricatorTime::getNow()); } $target->save(); } } } /** * Update the overall status of the buildable this build is attached to. * * After a build changes state (for example, passes or fails) it may affect * the overall state of the associated buildable. Compute the new aggregate * state and save it on the buildable. * * @param HarbormasterBuild The buildable to update. * @return void */ private function updateBuildable(HarbormasterBuildable $buildable) { $viewer = $this->getViewer(); $lock_key = 'harbormaster.buildable:'.$buildable->getID(); $lock = PhabricatorGlobalLock::newLock($lock_key)->lock(15); $buildable = id(new HarbormasterBuildableQuery()) ->setViewer($viewer) ->withIDs(array($buildable->getID())) ->needBuilds(true) ->executeOne(); $all_pass = true; $any_fail = false; foreach ($buildable->getBuilds() as $build) { - if ($build->getBuildStatus() != HarbormasterBuildStatus::STATUS_PASSED) { + if (!$build->isPassed()) { $all_pass = false; } - if (in_array($build->getBuildStatus(), array( - HarbormasterBuildStatus::STATUS_FAILED, - HarbormasterBuildStatus::STATUS_ERROR, - HarbormasterBuildStatus::STATUS_DEADLOCKED, - ))) { + if ($build->isComplete() && !$build->isPassed()) { $any_fail = true; } } if ($any_fail) { $new_status = HarbormasterBuildable::STATUS_FAILED; } else if ($all_pass) { $new_status = HarbormasterBuildable::STATUS_PASSED; } else { $new_status = HarbormasterBuildable::STATUS_BUILDING; } $old_status = $buildable->getBuildableStatus(); $did_update = ($old_status != $new_status); if ($did_update) { $buildable->setBuildableStatus($new_status); $buildable->save(); } $lock->unlock(); // If we changed the buildable status, try to post a transaction to the // object about it. We can safely do this outside of the locked region. // NOTE: We only post transactions for automatic buildables, not for // manual ones: manual builds are test builds, whoever is doing tests // can look at the results themselves, and other users generally don't // care about the outcome. $should_publish = $did_update && $new_status != HarbormasterBuildable::STATUS_BUILDING && !$buildable->getIsManualBuildable(); if (!$should_publish) { return; } $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($buildable->getBuildablePHID())) ->executeOne(); if (!$object) { return; } $publish_phid = $object->getHarbormasterPublishablePHID(); if (!$publish_phid) { return; } if ($publish_phid === $object->getPHID()) { $publish = $object; } else { $publish = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($publish_phid)) ->executeOne(); if (!$publish) { return; } } if (!($publish instanceof PhabricatorApplicationTransactionInterface)) { return; } $template = $publish->getApplicationTransactionTemplate(); if (!$template) { return; } $template ->setTransactionType(PhabricatorTransactions::TYPE_BUILDABLE) ->setMetadataValue( 'harbormaster:buildablePHID', $buildable->getPHID()) ->setOldValue($old_status) ->setNewValue($new_status); $harbormaster_phid = id(new PhabricatorHarbormasterApplication()) ->getPHID(); $daemon_source = PhabricatorContentSource::newForSource( PhabricatorDaemonContentSource::SOURCECONST); $editor = $publish->getApplicationTransactionEditor() ->setActor($viewer) ->setActingAsPHID($harbormaster_phid) ->setContentSource($daemon_source) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $editor->applyTransactions( $publish->getApplicationTransactionObject(), array($template)); } private function releaseAllArtifacts(HarbormasterBuild $build) { $targets = id(new HarbormasterBuildTargetQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuildPHIDs(array($build->getPHID())) ->withBuildGenerations(array($build->getBuildGeneration())) ->execute(); if (count($targets) === 0) { return; } $target_phids = mpull($targets, 'getPHID'); $artifacts = id(new HarbormasterBuildArtifactQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuildTargetPHIDs($target_phids) ->withIsReleased(false) ->execute(); foreach ($artifacts as $artifact) { $artifact->releaseArtifact(); } } private function releaseQueuedArtifacts() { foreach ($this->artifactReleaseQueue as $key => $artifact) { $artifact->releaseArtifact(); unset($this->artifactReleaseQueue[$key]); } } } diff --git a/src/applications/harbormaster/storage/HarbormasterBuildable.php b/src/applications/harbormaster/storage/HarbormasterBuildable.php index 9761fb287f..89c13b8b41 100644 --- a/src/applications/harbormaster/storage/HarbormasterBuildable.php +++ b/src/applications/harbormaster/storage/HarbormasterBuildable.php @@ -1,333 +1,345 @@ pht('Building'), self::STATUS_PASSED => pht('Passed'), self::STATUS_FAILED => pht('Failed'), ); } public static function getBuildableStatusIcon($status) { switch ($status) { case self::STATUS_BUILDING: return PHUIStatusItemView::ICON_RIGHT; case self::STATUS_PASSED: return PHUIStatusItemView::ICON_ACCEPT; case self::STATUS_FAILED: return PHUIStatusItemView::ICON_REJECT; default: return PHUIStatusItemView::ICON_QUESTION; } } public static function getBuildableStatusColor($status) { switch ($status) { case self::STATUS_BUILDING: return 'blue'; case self::STATUS_PASSED: return 'green'; case self::STATUS_FAILED: return 'red'; default: return 'bluegrey'; } } + public function getStatusIcon() { + return self::getBuildableStatusIcon($this->getBuildableStatus()); + } + + public function getStatusDisplayName() { + return self::getBuildableStatusName($this->getBuildableStatus()); + } + + public function getStatusColor() { + return self::getBuildableStatusColor($this->getBuildableStatus()); + } + public static function initializeNewBuildable(PhabricatorUser $actor) { return id(new HarbormasterBuildable()) ->setIsManualBuildable(0) ->setBuildableStatus(self::STATUS_BUILDING); } public function getMonogram() { return 'B'.$this->getID(); } public function getURI() { return '/'.$this->getMonogram(); } /** * Returns an existing buildable for the object's PHID or creates a * new buildable implicitly if needed. */ public static function createOrLoadExisting( PhabricatorUser $actor, $buildable_object_phid, $container_object_phid) { $buildable = id(new HarbormasterBuildableQuery()) ->setViewer($actor) ->withBuildablePHIDs(array($buildable_object_phid)) ->withManualBuildables(false) ->setLimit(1) ->executeOne(); if ($buildable) { return $buildable; } $buildable = self::initializeNewBuildable($actor) ->setBuildablePHID($buildable_object_phid) ->setContainerPHID($container_object_phid); $buildable->save(); return $buildable; } /** * Start builds for a given buildable. * * @param phid PHID of the object to build. * @param phid Container PHID for the buildable. * @param list List of builds to perform. * @return void */ public static function applyBuildPlans( $phid, $container_phid, array $requests) { assert_instances_of($requests, 'HarbormasterBuildRequest'); if (!$requests) { return; } // Skip all of this logic if the Harbormaster application // isn't currently installed. $harbormaster_app = 'PhabricatorHarbormasterApplication'; if (!PhabricatorApplication::isClassInstalled($harbormaster_app)) { return; } $viewer = PhabricatorUser::getOmnipotentUser(); $buildable = self::createOrLoadExisting( $viewer, $phid, $container_phid); $plan_phids = mpull($requests, 'getBuildPlanPHID'); $plans = id(new HarbormasterBuildPlanQuery()) ->setViewer($viewer) ->withPHIDs($plan_phids) ->execute(); $plans = mpull($plans, null, 'getPHID'); foreach ($requests as $request) { $plan_phid = $request->getBuildPlanPHID(); $plan = idx($plans, $plan_phid); if (!$plan) { throw new Exception( pht( 'Failed to load build plan ("%s").', $plan_phid)); } if ($plan->isDisabled()) { // TODO: This should be communicated more clearly -- maybe we should // create the build but set the status to "disabled" or "derelict". continue; } $parameters = $request->getBuildParameters(); $buildable->applyPlan($plan, $parameters, $request->getInitiatorPHID()); } } public function applyPlan( HarbormasterBuildPlan $plan, array $parameters, $initiator_phid) { $viewer = PhabricatorUser::getOmnipotentUser(); $build = HarbormasterBuild::initializeNewBuild($viewer) ->setBuildablePHID($this->getPHID()) ->setBuildPlanPHID($plan->getPHID()) ->setBuildParameters($parameters) ->setBuildStatus(HarbormasterBuildStatus::STATUS_PENDING); if ($initiator_phid) { $build->setInitiatorPHID($initiator_phid); } $auto_key = $plan->getPlanAutoKey(); if ($auto_key) { $build->setPlanAutoKey($auto_key); } $build->save(); PhabricatorWorker::scheduleTask( 'HarbormasterBuildWorker', array( 'buildID' => $build->getID(), ), array( 'objectPHID' => $build->getPHID(), )); return $build; } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'containerPHID' => 'phid?', 'buildableStatus' => 'text32', 'isManualBuildable' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_buildable' => array( 'columns' => array('buildablePHID'), ), 'key_container' => array( 'columns' => array('containerPHID'), ), 'key_manual' => array( 'columns' => array('isManualBuildable'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterBuildablePHIDType::TYPECONST); } public function attachBuildableObject($buildable_object) { $this->buildableObject = $buildable_object; return $this; } public function getBuildableObject() { return $this->assertAttached($this->buildableObject); } public function attachContainerObject($container_object) { $this->containerObject = $container_object; return $this; } public function getContainerObject() { return $this->assertAttached($this->containerObject); } public function attachBuilds(array $builds) { assert_instances_of($builds, 'HarbormasterBuild'); $this->builds = $builds; return $this; } public function getBuilds() { return $this->assertAttached($this->builds); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HarbormasterBuildableTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new HarbormasterBuildableTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getBuildableObject()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBuildableObject()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('A buildable inherits policies from the underlying object.'); } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildableDisplayPHID() { return $this->getBuildableObject()->getHarbormasterBuildableDisplayPHID(); } public function getHarbormasterBuildablePHID() { // NOTE: This is essentially just for convenience, as it allows you create // a copy of a buildable by specifying `B123` without bothering to go // look up the underlying object. return $this->getBuildablePHID(); } public function getHarbormasterContainerPHID() { return $this->getContainerPHID(); } public function getHarbormasterPublishablePHID() { return $this->getBuildableObject()->getHarbormasterPublishablePHID(); } public function getBuildVariables() { return array(); } public function getAvailableBuildVariables() { return array(); } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index 92d4293913..d43b19d71f 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -1,453 +1,458 @@ setBuildStatus(HarbormasterBuildStatus::STATUS_INACTIVE) ->setBuildGeneration(0); } public function delete() { $this->openTransaction(); $this->deleteUnprocessedCommands(); $result = parent::delete(); $this->saveTransaction(); return $result; } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'buildParameters' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'buildStatus' => 'text32', 'buildGeneration' => 'uint32', 'planAutoKey' => 'text32?', 'initiatorPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_buildable' => array( 'columns' => array('buildablePHID'), ), 'key_plan' => array( 'columns' => array('buildPlanPHID'), ), 'key_status' => array( 'columns' => array('buildStatus'), ), 'key_planautokey' => array( 'columns' => array('buildablePHID', 'planAutoKey'), 'unique' => true, ), 'key_initiator' => array( 'columns' => array('initiatorPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterBuildPHIDType::TYPECONST); } public function attachBuildable(HarbormasterBuildable $buildable) { $this->buildable = $buildable; return $this; } public function getBuildable() { return $this->assertAttached($this->buildable); } public function getName() { if ($this->getBuildPlan()) { return $this->getBuildPlan()->getName(); } return pht('Build'); } public function attachBuildPlan( HarbormasterBuildPlan $build_plan = null) { $this->buildPlan = $build_plan; return $this; } public function getBuildPlan() { return $this->assertAttached($this->buildPlan); } public function getBuildTargets() { return $this->assertAttached($this->buildTargets); } public function attachBuildTargets(array $targets) { $this->buildTargets = $targets; return $this; } public function isBuilding() { - return - $this->getBuildStatus() === HarbormasterBuildStatus::STATUS_PENDING || - $this->getBuildStatus() === HarbormasterBuildStatus::STATUS_BUILDING; + return $this->getBuildStatusObject()->isBuilding(); } public function isAutobuild() { return ($this->getPlanAutoKey() !== null); } public function retrieveVariablesFromBuild() { $results = array( 'buildable.diff' => null, 'buildable.revision' => null, 'buildable.commit' => null, 'repository.callsign' => null, 'repository.phid' => null, 'repository.vcs' => null, 'repository.uri' => null, 'step.timestamp' => null, 'build.id' => null, 'initiator.phid' => null, ); foreach ($this->getBuildParameters() as $key => $value) { $results['build/'.$key] = $value; } $buildable = $this->getBuildable(); $object = $buildable->getBuildableObject(); $object_variables = $object->getBuildVariables(); $results = $object_variables + $results; $results['step.timestamp'] = time(); $results['build.id'] = $this->getID(); $results['initiator.phid'] = $this->getInitiatorPHID(); return $results; } public static function getAvailableBuildVariables() { $objects = id(new PhutilClassMapQuery()) ->setAncestorClass('HarbormasterBuildableInterface') ->execute(); $variables = array(); $variables[] = array( 'step.timestamp' => pht('The current UNIX timestamp.'), 'build.id' => pht('The ID of the current build.'), 'target.phid' => pht('The PHID of the current build target.'), 'initiator.phid' => pht( 'The PHID of the user or Object that initiated the build, '. 'if applicable.'), ); foreach ($objects as $object) { $variables[] = $object->getAvailableBuildVariables(); } $variables = array_mergev($variables); return $variables; } public function isComplete() { - return in_array( - $this->getBuildStatus(), - HarbormasterBuildStatus::getCompletedStatusConstants()); + return $this->getBuildStatusObject()->isComplete(); } public function isPaused() { - return ($this->getBuildStatus() == HarbormasterBuildStatus::STATUS_PAUSED); + return $this->getBuildStatusObject()->isPaused(); + } + + public function isPassed() { + return $this->getBuildStatusObject()->isPassed(); } public function getURI() { $id = $this->getID(); return "/harbormaster/build/{$id}/"; } + protected function getBuildStatusObject() { + $status_key = $this->getBuildStatus(); + return HarbormasterBuildStatus::newBuildStatusObject($status_key); + } + /* -( Build Commands )----------------------------------------------------- */ private function getUnprocessedCommands() { return $this->assertAttached($this->unprocessedCommands); } public function attachUnprocessedCommands(array $commands) { $this->unprocessedCommands = $commands; return $this; } public function canRestartBuild() { if ($this->isAutobuild()) { return false; } return !$this->isRestarting(); } public function canPauseBuild() { if ($this->isAutobuild()) { return false; } return !$this->isComplete() && !$this->isPaused() && !$this->isPausing(); } public function canAbortBuild() { if ($this->isAutobuild()) { return false; } return !$this->isComplete(); } public function canResumeBuild() { if ($this->isAutobuild()) { return false; } return $this->isPaused() && !$this->isResuming(); } public function isPausing() { $is_pausing = false; foreach ($this->getUnprocessedCommands() as $command_object) { $command = $command_object->getCommand(); switch ($command) { case HarbormasterBuildCommand::COMMAND_PAUSE: $is_pausing = true; break; case HarbormasterBuildCommand::COMMAND_RESUME: case HarbormasterBuildCommand::COMMAND_RESTART: $is_pausing = false; break; case HarbormasterBuildCommand::COMMAND_ABORT: $is_pausing = true; break; } } return $is_pausing; } public function isResuming() { $is_resuming = false; foreach ($this->getUnprocessedCommands() as $command_object) { $command = $command_object->getCommand(); switch ($command) { case HarbormasterBuildCommand::COMMAND_RESTART: case HarbormasterBuildCommand::COMMAND_RESUME: $is_resuming = true; break; case HarbormasterBuildCommand::COMMAND_PAUSE: $is_resuming = false; break; case HarbormasterBuildCommand::COMMAND_ABORT: $is_resuming = false; break; } } return $is_resuming; } public function isRestarting() { $is_restarting = false; foreach ($this->getUnprocessedCommands() as $command_object) { $command = $command_object->getCommand(); switch ($command) { case HarbormasterBuildCommand::COMMAND_RESTART: $is_restarting = true; break; } } return $is_restarting; } public function isAborting() { $is_aborting = false; foreach ($this->getUnprocessedCommands() as $command_object) { $command = $command_object->getCommand(); switch ($command) { case HarbormasterBuildCommand::COMMAND_ABORT: $is_aborting = true; break; } } return $is_aborting; } public function deleteUnprocessedCommands() { foreach ($this->getUnprocessedCommands() as $key => $command_object) { $command_object->delete(); unset($this->unprocessedCommands[$key]); } 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) { $need_edit = false; switch ($command) { case HarbormasterBuildCommand::COMMAND_RESTART: break; case HarbormasterBuildCommand::COMMAND_PAUSE: case HarbormasterBuildCommand::COMMAND_RESUME: case HarbormasterBuildCommand::COMMAND_ABORT: $need_edit = true; 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, $this->getBuildPlan(), PhabricatorPolicyCapability::CAN_EDIT); } } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HarbormasterBuildTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new HarbormasterBuildTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getBuildable()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBuildable()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('A build inherits policies from its buildable.'); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('buildablePHID') ->setType('phid') ->setDescription(pht('PHID of the object this build is building.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('buildPlanPHID') ->setType('phid') ->setDescription(pht('PHID of the build plan being run.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('buildStatus') ->setType('map') ->setDescription(pht('The current status of this build.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('initiatorPHID') ->setType('phid') ->setDescription(pht('The person (or thing) that started this build.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of this build.')), ); } public function getFieldValuesForConduit() { $status = $this->getBuildStatus(); return array( 'buildablePHID' => $this->getBuildablePHID(), 'buildPlanPHID' => $this->getBuildPlanPHID(), 'buildStatus' => array( 'value' => $status, 'name' => HarbormasterBuildStatus::getBuildStatusName($status), 'color.ansi' => HarbormasterBuildStatus::getBuildStatusANSIColor($status), ), 'initiatorPHID' => nonempty($this->getInitiatorPHID(), null), 'name' => $this->getName(), ); } public function getConduitSearchAttachments() { return array( id(new HarbormasterQueryBuildsSearchEngineAttachment()) ->setAttachmentKey('querybuilds'), ); } }