@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator

On Harbormaster build plans, show which Herald rules trigger builds

Summary:
Ref T13258. Provide an easy way to find rules which trigger a particular build plan from the build plan page.

The implementation here ends up a little messy: we can't just search for `actionType = 'build' AND targetPHID = '<build plan PHID>'` since the field is a blob of JSON.

Instead, make rules indexable and write a "build plan is affected by rule actions" edge when indexing rules, then search on that edge.

For now, only "Run Build Plan: ..." rules actually write this edge, since I think (?) that it doesn't really have meaningful values for other edge types today. Maybe "Call Webhooks", and you could get a link from a hook to rules that trigger it? Reasonable to do in the future.

Things end up a little bit rough overall, but I think this panel is pretty useful to add to the Build Plan page.

This index needs to be rebuilt with `bin/search index --type HeraldRule`. I'll call this out in the changelog but I'm not planning to explicitly migrate or add an activity, since this is only really important for larger installs and they probably (?) read the changelog. As rules are edited over time, this will converge to the right behavior even if you don't rebuild the index.

Test Plan: {F6260095}

Reviewers: amckinley

Reviewed By: amckinley

Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam

Maniphest Tasks: T13258

Differential Revision: https://secure.phabricator.com/D20259

+264 -40
+7
src/__phutil_library_map__.php
··· 1531 1531 'HeraldRemarkupFieldValue' => 'applications/herald/value/HeraldRemarkupFieldValue.php', 1532 1532 'HeraldRemarkupRule' => 'applications/herald/remarkup/HeraldRemarkupRule.php', 1533 1533 'HeraldRule' => 'applications/herald/storage/HeraldRule.php', 1534 + 'HeraldRuleActionAffectsObjectEdgeType' => 'applications/herald/edge/HeraldRuleActionAffectsObjectEdgeType.php', 1534 1535 'HeraldRuleAdapter' => 'applications/herald/adapter/HeraldRuleAdapter.php', 1535 1536 'HeraldRuleAdapterField' => 'applications/herald/field/rule/HeraldRuleAdapterField.php', 1536 1537 'HeraldRuleController' => 'applications/herald/controller/HeraldRuleController.php', ··· 1540 1541 'HeraldRuleEditor' => 'applications/herald/editor/HeraldRuleEditor.php', 1541 1542 'HeraldRuleField' => 'applications/herald/field/rule/HeraldRuleField.php', 1542 1543 'HeraldRuleFieldGroup' => 'applications/herald/field/rule/HeraldRuleFieldGroup.php', 1544 + 'HeraldRuleIndexEngineExtension' => 'applications/herald/engineextension/HeraldRuleIndexEngineExtension.php', 1543 1545 'HeraldRuleListController' => 'applications/herald/controller/HeraldRuleListController.php', 1546 + 'HeraldRuleListView' => 'applications/herald/view/HeraldRuleListView.php', 1544 1547 'HeraldRuleNameTransaction' => 'applications/herald/xaction/HeraldRuleNameTransaction.php', 1545 1548 'HeraldRulePHIDType' => 'applications/herald/phid/HeraldRulePHIDType.php', 1546 1549 'HeraldRuleQuery' => 'applications/herald/query/HeraldRuleQuery.php', ··· 7189 7192 'PhabricatorFlaggableInterface', 7190 7193 'PhabricatorPolicyInterface', 7191 7194 'PhabricatorDestructibleInterface', 7195 + 'PhabricatorIndexableInterface', 7192 7196 'PhabricatorSubscribableInterface', 7193 7197 ), 7198 + 'HeraldRuleActionAffectsObjectEdgeType' => 'PhabricatorEdgeType', 7194 7199 'HeraldRuleAdapter' => 'HeraldAdapter', 7195 7200 'HeraldRuleAdapterField' => 'HeraldRuleField', 7196 7201 'HeraldRuleController' => 'HeraldController', ··· 7200 7205 'HeraldRuleEditor' => 'PhabricatorApplicationTransactionEditor', 7201 7206 'HeraldRuleField' => 'HeraldField', 7202 7207 'HeraldRuleFieldGroup' => 'HeraldFieldGroup', 7208 + 'HeraldRuleIndexEngineExtension' => 'PhabricatorIndexEngineExtension', 7203 7209 'HeraldRuleListController' => 'HeraldController', 7210 + 'HeraldRuleListView' => 'AphrontView', 7204 7211 'HeraldRuleNameTransaction' => 'HeraldRuleTransactionType', 7205 7212 'HeraldRulePHIDType' => 'PhabricatorPHIDType', 7206 7213 'HeraldRuleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+38
src/applications/harbormaster/controller/HarbormasterPlanViewController.php
··· 62 62 63 63 $builds_view = $this->newBuildsView($plan); 64 64 $options_view = $this->newOptionsView($plan); 65 + $rules_view = $this->newRulesView($plan); 65 66 66 67 $timeline = $this->buildTransactionTimeline( 67 68 $plan, ··· 76 77 $error, 77 78 $step_list, 78 79 $options_view, 80 + $rules_view, 79 81 $builds_view, 80 82 $timeline, 81 83 )); ··· 486 488 ->appendChild($list); 487 489 } 488 490 491 + private function newRulesView(HarbormasterBuildPlan $plan) { 492 + $viewer = $this->getViewer(); 493 + 494 + $rules = id(new HeraldRuleQuery()) 495 + ->setViewer($viewer) 496 + ->withDisabled(false) 497 + ->withAffectedObjectPHIDs(array($plan->getPHID())) 498 + ->needValidateAuthors(true) 499 + ->execute(); 500 + 501 + $list = id(new HeraldRuleListView()) 502 + ->setViewer($viewer) 503 + ->setRules($rules) 504 + ->newObjectList(); 505 + 506 + $list->setNoDataString(pht('No active Herald rules trigger this build.')); 507 + 508 + $more_href = new PhutilURI( 509 + '/herald/', 510 + array('affectedPHID' => $plan->getPHID())); 511 + 512 + $more_link = id(new PHUIButtonView()) 513 + ->setTag('a') 514 + ->setIcon('fa-list-ul') 515 + ->setText(pht('View All Rules')) 516 + ->setHref($more_href); 517 + 518 + $header = id(new PHUIHeaderView()) 519 + ->setHeader(pht('Run By Herald Rules')) 520 + ->addActionLink($more_link); 521 + 522 + return id(new PHUIObjectBoxView()) 523 + ->setHeader($header) 524 + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 525 + ->appendChild($list); 526 + } 489 527 490 528 private function newOptionsView(HarbormasterBuildPlan $plan) { 491 529 $viewer = $this->getViewer();
+5
src/applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php
··· 91 91 'Run build plans: %s.', 92 92 $this->renderHandleList($value)); 93 93 } 94 + 95 + public function getPHIDsAffectedByAction(HeraldActionRecord $record) { 96 + return $record->getTarget(); 97 + } 98 + 94 99 }
+4
src/applications/herald/action/HeraldAction.php
··· 401 401 return null; 402 402 } 403 403 404 + public function getPHIDsAffectedByAction(HeraldActionRecord $record) { 405 + return array(); 406 + } 407 + 404 408 }
+8
src/applications/herald/edge/HeraldRuleActionAffectsObjectEdgeType.php
··· 1 + <?php 2 + 3 + final class HeraldRuleActionAffectsObjectEdgeType 4 + extends PhabricatorEdgeType { 5 + 6 + const EDGECONST = 69; 7 + 8 + }
+4
src/applications/herald/editor/HeraldRuleEditor.php
··· 74 74 return $body; 75 75 } 76 76 77 + protected function supportsSearch() { 78 + return true; 79 + } 80 + 77 81 }
+92
src/applications/herald/engineextension/HeraldRuleIndexEngineExtension.php
··· 1 + <?php 2 + 3 + final class HeraldRuleIndexEngineExtension 4 + extends PhabricatorIndexEngineExtension { 5 + 6 + const EXTENSIONKEY = 'herald.actions'; 7 + 8 + public function getExtensionName() { 9 + return pht('Herald Actions'); 10 + } 11 + 12 + public function shouldIndexObject($object) { 13 + if (!($object instanceof HeraldRule)) { 14 + return false; 15 + } 16 + 17 + return true; 18 + } 19 + 20 + public function indexObject( 21 + PhabricatorIndexEngine $engine, 22 + $object) { 23 + 24 + $edge_type = HeraldRuleActionAffectsObjectEdgeType::EDGECONST; 25 + 26 + $old_edges = PhabricatorEdgeQuery::loadDestinationPHIDs( 27 + $object->getPHID(), 28 + $edge_type); 29 + $old_edges = array_fuse($old_edges); 30 + 31 + $new_edges = $this->getPHIDsAffectedByActions($object); 32 + $new_edges = array_fuse($new_edges); 33 + 34 + $add_edges = array_diff_key($new_edges, $old_edges); 35 + $rem_edges = array_diff_key($old_edges, $new_edges); 36 + 37 + if (!$add_edges && !$rem_edges) { 38 + return; 39 + } 40 + 41 + $editor = new PhabricatorEdgeEditor(); 42 + 43 + foreach ($add_edges as $phid) { 44 + $editor->addEdge($object->getPHID(), $edge_type, $phid); 45 + } 46 + 47 + foreach ($rem_edges as $phid) { 48 + $editor->removeEdge($object->getPHID(), $edge_type, $phid); 49 + } 50 + 51 + $editor->save(); 52 + } 53 + 54 + public function getIndexVersion($object) { 55 + $phids = $this->getPHIDsAffectedByActions($object); 56 + sort($phids); 57 + $phids = implode(':', $phids); 58 + return PhabricatorHash::digestForIndex($phids); 59 + } 60 + 61 + private function getPHIDsAffectedByActions(HeraldRule $rule) { 62 + $viewer = $this->getViewer(); 63 + 64 + $rule = id(new HeraldRuleQuery()) 65 + ->setViewer($viewer) 66 + ->withIDs(array($rule->getID())) 67 + ->needConditionsAndActions(true) 68 + ->executeOne(); 69 + if (!$rule) { 70 + return array(); 71 + } 72 + 73 + $phids = array(); 74 + 75 + $actions = HeraldAction::getAllActions(); 76 + foreach ($rule->getActions() as $action_record) { 77 + $action = idx($actions, $action_record->getAction()); 78 + 79 + if (!$action) { 80 + continue; 81 + } 82 + 83 + foreach ($action->getPHIDsAffectedByAction($action_record) as $phid) { 84 + $phids[] = $phid; 85 + } 86 + } 87 + 88 + $phids = array_fuse($phids); 89 + return array_keys($phids); 90 + } 91 + 92 + }
+28
src/applications/herald/query/HeraldRuleQuery.php
··· 11 11 private $active; 12 12 private $datasourceQuery; 13 13 private $triggerObjectPHIDs; 14 + private $affectedObjectPHIDs; 14 15 15 16 private $needConditionsAndActions; 16 17 private $needAppliedToPHIDs; ··· 58 59 59 60 public function withTriggerObjectPHIDs(array $phids) { 60 61 $this->triggerObjectPHIDs = $phids; 62 + return $this; 63 + } 64 + 65 + public function withAffectedObjectPHIDs(array $phids) { 66 + $this->affectedObjectPHIDs = $phids; 61 67 return $this; 62 68 } 63 69 ··· 261 267 $this->triggerObjectPHIDs); 262 268 } 263 269 270 + if ($this->affectedObjectPHIDs !== null) { 271 + $where[] = qsprintf( 272 + $conn, 273 + 'edge_affects.dst IN (%Ls)', 274 + $this->affectedObjectPHIDs); 275 + } 276 + 264 277 return $where; 278 + } 279 + 280 + protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { 281 + $joins = parent::buildJoinClauseParts($conn); 282 + 283 + if ($this->affectedObjectPHIDs !== null) { 284 + $joins[] = qsprintf( 285 + $conn, 286 + 'JOIN %T edge_affects ON rule.phid = edge_affects.src 287 + AND edge_affects.type = %d', 288 + PhabricatorEdgeConfig::TABLE_NAME_EDGE, 289 + HeraldRuleActionAffectsObjectEdgeType::EDGECONST); 290 + } 291 + 292 + return $joins; 265 293 } 266 294 267 295 private function validateRuleAuthors(array $rules) {
+12 -40
src/applications/herald/query/HeraldRuleSearchEngine.php
··· 55 55 pht('(Show All)'), 56 56 pht('Show Only Disabled Rules'), 57 57 pht('Show Only Enabled Rules')), 58 + id(new PhabricatorPHIDsSearchField()) 59 + ->setLabel(pht('Affected Objects')) 60 + ->setKey('affectedPHIDs') 61 + ->setAliases(array('affectedPHID')), 58 62 ); 59 63 } 60 64 ··· 79 83 80 84 if ($map['active'] !== null) { 81 85 $query->withActive($map['active']); 86 + } 87 + 88 + if ($map['affectedPHIDs']) { 89 + $query->withAffectedObjectPHIDs($map['affectedPHIDs']); 82 90 } 83 91 84 92 return $query; ··· 127 135 PhabricatorSavedQuery $query, 128 136 array $handles) { 129 137 assert_instances_of($rules, 'HeraldRule'); 130 - 131 138 $viewer = $this->requireViewer(); 132 - $handles = $viewer->loadHandles(mpull($rules, 'getAuthorPHID')); 133 - 134 - $content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer); 135 - 136 - $list = id(new PHUIObjectItemListView()) 137 - ->setUser($viewer); 138 - foreach ($rules as $rule) { 139 - $monogram = $rule->getMonogram(); 140 139 141 - $item = id(new PHUIObjectItemView()) 142 - ->setObjectName($monogram) 143 - ->setHeader($rule->getName()) 144 - ->setHref("/{$monogram}"); 145 - 146 - if ($rule->isPersonalRule()) { 147 - $item->addIcon('fa-user', pht('Personal Rule')); 148 - $item->addByline( 149 - pht( 150 - 'Authored by %s', 151 - $handles[$rule->getAuthorPHID()]->renderLink())); 152 - } else if ($rule->isObjectRule()) { 153 - $item->addIcon('fa-briefcase', pht('Object Rule')); 154 - } else { 155 - $item->addIcon('fa-globe', pht('Global Rule')); 156 - } 157 - 158 - if ($rule->getIsDisabled()) { 159 - $item->setDisabled(true); 160 - $item->addIcon('fa-lock grey', pht('Disabled')); 161 - } else if (!$rule->hasValidAuthor()) { 162 - $item->setDisabled(true); 163 - $item->addIcon('fa-user grey', pht('Author Not Active')); 164 - } 165 - 166 - $content_type_name = idx($content_type_map, $rule->getContentType()); 167 - $item->addAttribute(pht('Affects: %s', $content_type_name)); 168 - 169 - $list->addItem($item); 170 - } 140 + $list = id(new HeraldRuleListView()) 141 + ->setViewer($viewer) 142 + ->setRules($rules) 143 + ->newObjectList(); 171 144 172 145 $result = new PhabricatorApplicationSearchResultView(); 173 146 $result->setObjectList($list); 174 147 $result->setNoDataString(pht('No rules found.')); 175 148 176 149 return $result; 177 - 178 150 } 179 151 180 152 protected function getNewUserBody() {
+1
src/applications/herald/storage/HeraldRule.php
··· 6 6 PhabricatorFlaggableInterface, 7 7 PhabricatorPolicyInterface, 8 8 PhabricatorDestructibleInterface, 9 + PhabricatorIndexableInterface, 9 10 PhabricatorSubscribableInterface { 10 11 11 12 const TABLE_RULE_APPLIED = 'herald_ruleapplied';
+65
src/applications/herald/view/HeraldRuleListView.php
··· 1 + <?php 2 + 3 + final class HeraldRuleListView 4 + extends AphrontView { 5 + 6 + private $rules; 7 + 8 + public function setRules(array $rules) { 9 + assert_instances_of($rules, 'HeraldRule'); 10 + $this->rules = $rules; 11 + return $this; 12 + } 13 + 14 + public function render() { 15 + return $this->newObjectList(); 16 + } 17 + 18 + public function newObjectList() { 19 + $viewer = $this->getViewer(); 20 + $rules = $this->rules; 21 + 22 + $handles = $viewer->loadHandles(mpull($rules, 'getAuthorPHID')); 23 + 24 + $content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer); 25 + 26 + $list = id(new PHUIObjectItemListView()) 27 + ->setViewer($viewer); 28 + foreach ($rules as $rule) { 29 + $monogram = $rule->getMonogram(); 30 + 31 + $item = id(new PHUIObjectItemView()) 32 + ->setObjectName($monogram) 33 + ->setHeader($rule->getName()) 34 + ->setHref($rule->getURI()); 35 + 36 + if ($rule->isPersonalRule()) { 37 + $item->addIcon('fa-user', pht('Personal Rule')); 38 + $item->addByline( 39 + pht( 40 + 'Authored by %s', 41 + $handles[$rule->getAuthorPHID()]->renderLink())); 42 + } else if ($rule->isObjectRule()) { 43 + $item->addIcon('fa-briefcase', pht('Object Rule')); 44 + } else { 45 + $item->addIcon('fa-globe', pht('Global Rule')); 46 + } 47 + 48 + if ($rule->getIsDisabled()) { 49 + $item->setDisabled(true); 50 + $item->addIcon('fa-lock grey', pht('Disabled')); 51 + } else if (!$rule->hasValidAuthor()) { 52 + $item->setDisabled(true); 53 + $item->addIcon('fa-user grey', pht('Author Not Active')); 54 + } 55 + 56 + $content_type_name = idx($content_type_map, $rule->getContentType()); 57 + $item->addAttribute(pht('Affects: %s', $content_type_name)); 58 + 59 + $list->addItem($item); 60 + } 61 + 62 + return $list; 63 + } 64 + 65 + }