Publish Doorkeeper object stories to JIRA
Summary: Ref T3687. Publish stories into JIRA. These need some voicing fixes, which maybe involves straightening out the feed code. For example, they're voiced in-context ("updated this revision") when they should be voiced out-of-context ("updated D123"). Generally, this is similar to the Asana stuff but a lot simpler since we don't need to do any state management. Test Plan: {F57366} Reviewers: btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T3687 Differential Revision: https://secure.phabricator.com/D6892
This commit is contained in:
parent
470bb4931b
commit
a40861e5c6
|
@ -553,6 +553,7 @@ phutil_register_library_map(array(
|
||||||
'DoorkeeperExternalObjectQuery' => 'applications/doorkeeper/query/DoorkeeperExternalObjectQuery.php',
|
'DoorkeeperExternalObjectQuery' => 'applications/doorkeeper/query/DoorkeeperExternalObjectQuery.php',
|
||||||
'DoorkeeperFeedStoryPublisher' => 'applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php',
|
'DoorkeeperFeedStoryPublisher' => 'applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php',
|
||||||
'DoorkeeperFeedWorkerAsana' => 'applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php',
|
'DoorkeeperFeedWorkerAsana' => 'applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php',
|
||||||
|
'DoorkeeperFeedWorkerJIRA' => 'applications/doorkeeper/worker/DoorkeeperFeedWorkerJIRA.php',
|
||||||
'DoorkeeperImportEngine' => 'applications/doorkeeper/engine/DoorkeeperImportEngine.php',
|
'DoorkeeperImportEngine' => 'applications/doorkeeper/engine/DoorkeeperImportEngine.php',
|
||||||
'DoorkeeperObjectRef' => 'applications/doorkeeper/engine/DoorkeeperObjectRef.php',
|
'DoorkeeperObjectRef' => 'applications/doorkeeper/engine/DoorkeeperObjectRef.php',
|
||||||
'DoorkeeperRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php',
|
'DoorkeeperRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php',
|
||||||
|
@ -2599,6 +2600,7 @@ phutil_register_library_map(array(
|
||||||
),
|
),
|
||||||
'DoorkeeperExternalObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
'DoorkeeperExternalObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||||
'DoorkeeperFeedWorkerAsana' => 'FeedPushWorker',
|
'DoorkeeperFeedWorkerAsana' => 'FeedPushWorker',
|
||||||
|
'DoorkeeperFeedWorkerJIRA' => 'FeedPushWorker',
|
||||||
'DoorkeeperImportEngine' => 'Phobject',
|
'DoorkeeperImportEngine' => 'Phobject',
|
||||||
'DoorkeeperObjectRef' => 'Phobject',
|
'DoorkeeperObjectRef' => 'Phobject',
|
||||||
'DoorkeeperRemarkupRule' => 'PhutilRemarkupRule',
|
'DoorkeeperRemarkupRule' => 'PhutilRemarkupRule',
|
||||||
|
|
193
src/applications/doorkeeper/worker/DoorkeeperFeedWorkerJIRA.php
Normal file
193
src/applications/doorkeeper/worker/DoorkeeperFeedWorkerJIRA.php
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class DoorkeeperFeedWorkerJIRA extends FeedPushWorker {
|
||||||
|
|
||||||
|
private $provider;
|
||||||
|
private $publisher;
|
||||||
|
private $workspaceID;
|
||||||
|
private $feedStory;
|
||||||
|
private $storyObject;
|
||||||
|
|
||||||
|
private function getProvider() {
|
||||||
|
if (!$this->provider) {
|
||||||
|
$provider = PhabricatorAuthProviderOAuth1JIRA::getJIRAProvider();
|
||||||
|
if (!$provider) {
|
||||||
|
throw new PhabricatorWorkerPermanentFailureException(
|
||||||
|
'No JIRA provider configured.');
|
||||||
|
}
|
||||||
|
$this->provider = $provider;
|
||||||
|
}
|
||||||
|
return $this->provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getFeedStory() {
|
||||||
|
if (!$this->feedStory) {
|
||||||
|
$story = $this->loadFeedStory();
|
||||||
|
$this->feedStory = $story;
|
||||||
|
}
|
||||||
|
return $this->feedStory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getViewer() {
|
||||||
|
return PhabricatorUser::getOmnipotentUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPublisher() {
|
||||||
|
return $this->publisher;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getStoryObject() {
|
||||||
|
if (!$this->storyObject) {
|
||||||
|
$story = $this->getFeedStory();
|
||||||
|
try {
|
||||||
|
$object = $story->getPrimaryObject();
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
throw new PhabricatorWorkerPermanentFailureException(
|
||||||
|
$ex->getMessage());
|
||||||
|
}
|
||||||
|
$this->storyObject = $object;
|
||||||
|
}
|
||||||
|
return $this->storyObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doWork() {
|
||||||
|
$story = $this->getFeedStory();
|
||||||
|
$data = $story->getStoryData();
|
||||||
|
|
||||||
|
$viewer = $this->getViewer();
|
||||||
|
$provider = $this->getProvider();
|
||||||
|
|
||||||
|
$object = $this->getStoryObject();
|
||||||
|
$src_phid = $object->getPHID();
|
||||||
|
|
||||||
|
$chronological_key = $story->getChronologicalKey();
|
||||||
|
|
||||||
|
$publishers = id(new PhutilSymbolLoader())
|
||||||
|
->setAncestorClass('DoorkeeperFeedStoryPublisher')
|
||||||
|
->loadObjects();
|
||||||
|
foreach ($publishers as $publisher) {
|
||||||
|
if ($publisher->canPublishStory($story, $object)) {
|
||||||
|
$publisher
|
||||||
|
->setViewer($viewer)
|
||||||
|
->setFeedStory($story);
|
||||||
|
|
||||||
|
$object = $publisher->willPublishStory($object);
|
||||||
|
$this->storyObject = $object;
|
||||||
|
|
||||||
|
$this->publisher = $publisher;
|
||||||
|
$this->log("Using publisher '%s'.\n", get_class($publisher));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->publisher) {
|
||||||
|
$this->log("Story is about an unsupported object type.\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$jira_issue_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
|
||||||
|
$object->getPHID(),
|
||||||
|
PhabricatorEdgeConfig::TYPE_PHOB_HAS_JIRAISSUE);
|
||||||
|
if (!$jira_issue_phids) {
|
||||||
|
$this->log("Story is about an object with no linked JIRA issues.\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$xobjs = id(new DoorkeeperExternalObjectQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withPHIDs($jira_issue_phids)
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
if (!$xobjs) {
|
||||||
|
$this->log("Story object has no corresponding external JIRA objects.\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$try_users = $this->findUsersToPossess();
|
||||||
|
if (!$try_users) {
|
||||||
|
$this->log("No users to act on linked JIRA objects.\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$story_text = $publisher->getStoryText($object);
|
||||||
|
|
||||||
|
$xobjs = mgroup($xobjs, 'getApplicationDomain');
|
||||||
|
foreach ($xobjs as $domain => $xobj_list) {
|
||||||
|
$accounts = id(new PhabricatorExternalAccountQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withUserPHIDs($try_users)
|
||||||
|
->withAccountTypes(array($provider->getProviderType()))
|
||||||
|
->withAccountDomains(array($domain))
|
||||||
|
->execute();
|
||||||
|
// Reorder accounts in the original order.
|
||||||
|
// TODO: This needs to be adjusted if/when we allow you to link multiple
|
||||||
|
// accounts.
|
||||||
|
$accounts = mpull($accounts, null, 'getUserPHID');
|
||||||
|
$accounts = array_select_keys($accounts, $try_users);
|
||||||
|
|
||||||
|
foreach ($xobj_list as $xobj) {
|
||||||
|
foreach ($accounts as $account) {
|
||||||
|
try {
|
||||||
|
$provider->newJIRAFuture(
|
||||||
|
$account,
|
||||||
|
'rest/api/2/issue/'.$xobj->getObjectID().'/comment',
|
||||||
|
'POST',
|
||||||
|
array(
|
||||||
|
'body' => $story_text,
|
||||||
|
))->resolveJSON();
|
||||||
|
break;
|
||||||
|
} catch (HTTPFutureResponseStatus $ex) {
|
||||||
|
phlog($ex);
|
||||||
|
$this->log(
|
||||||
|
"Failed to update object %s using user %s.\n",
|
||||||
|
$xobj->getObjectID(),
|
||||||
|
$account->getUserPHID());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMaximumRetryCount() {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getWaitBeforeRetry(PhabricatorWorkerTask $task) {
|
||||||
|
$count = $task->getFailureCount();
|
||||||
|
return (5 * 60) * pow(8, $count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findUsersToPossess() {
|
||||||
|
$object = $this->getStoryObject();
|
||||||
|
$publisher = $this->getPublisher();
|
||||||
|
$data = $this->getFeedStory()->getStoryData();
|
||||||
|
|
||||||
|
// Figure out all the users related to the object. Users go into one of
|
||||||
|
// four buckets. For JIRA integration, we don't care about which bucket
|
||||||
|
// a user is in, since we just want to publish an update to linked objects.
|
||||||
|
|
||||||
|
$owner_phid = $publisher->getOwnerPHID($object);
|
||||||
|
$active_phids = $publisher->getActiveUserPHIDs($object);
|
||||||
|
$passive_phids = $publisher->getPassiveUserPHIDs($object);
|
||||||
|
$follow_phids = $publisher->getCCUserPHIDs($object);
|
||||||
|
|
||||||
|
$all_phids = array_merge(
|
||||||
|
array($owner_phid),
|
||||||
|
$active_phids,
|
||||||
|
$passive_phids,
|
||||||
|
$follow_phids);
|
||||||
|
$all_phids = array_unique(array_filter($all_phids));
|
||||||
|
|
||||||
|
// Even if the actor isn't a reviewer, etc., try to use their account so
|
||||||
|
// we can post in the correct voice. If we miss, we'll try all the other
|
||||||
|
// related users.
|
||||||
|
|
||||||
|
$try_users = array_merge(
|
||||||
|
array($data->getAuthorPHID()),
|
||||||
|
$all_phids);
|
||||||
|
$try_users = array_filter($try_users);
|
||||||
|
|
||||||
|
return $try_users;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -23,6 +23,14 @@ final class FeedPublisherWorker extends FeedPushWorker {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (PhabricatorAuthProviderOAuth1JIRA::getJIRAProvider()) {
|
||||||
|
PhabricatorWorker::scheduleTask(
|
||||||
|
'DoorkeeperFeedWorkerJIRA',
|
||||||
|
array(
|
||||||
|
'key' => $story->getChronologicalKey(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue