Skip to content

Commit 6995e28

Browse files
committed
Add support for automatically add milestone
1 parent 22eb1af commit 6995e28

File tree

7 files changed

+298
-1
lines changed

7 files changed

+298
-1
lines changed

config/github.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ services:
1515
class: App\Subscriber\NeedsReviewNewPRSubscriber
1616
public: true
1717

18+
app.subscriber.milestone_new_pr_subscriber:
19+
class: App\Subscriber\MilestoneNewPRSubscriber
20+
public: true
21+
1822
app.subscriber.bug_label_new_issue_subscriber:
1923
class: App\Subscriber\BugLabelNewIssueSubscriber
2024
public: true
@@ -37,6 +41,7 @@ parameters:
3741
- app.subscriber.needs_review_new_pr_subscriber
3842
- app.subscriber.bug_label_new_issue_subscriber
3943
- app.subscriber.auto_label_pr_from_content_subscriber
44+
- app.subscriber.milestone_new_pr_subscriber
4045
# secret: change_me
4146

4247
symfony/symfony-docs:
@@ -47,6 +52,7 @@ parameters:
4752
- app.subscriber.needs_review_new_pr_subscriber
4853
- app.subscriber.bug_label_new_issue_subscriber
4954
- app.subscriber.auto_label_pr_from_content_subscriber
55+
- app.subscriber.milestone_new_pr_subscriber
5056
# secret: %symfony_docs_secret%
5157

5258
# used in a functional test
@@ -58,3 +64,4 @@ parameters:
5864
- app.subscriber.needs_review_new_pr_subscriber
5965
- app.subscriber.bug_label_new_issue_subscriber
6066
- app.subscriber.auto_label_pr_from_content_subscriber
67+
- app.subscriber.milestone_new_pr_subscriber

config/services.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,15 @@ services:
3737
Github\Api\Issue\Labels:
3838
factory: ['@Github\Api\Issue', labels]
3939

40+
Github\Api\Issue\Milestones:
41+
factory: ['@Github\Api\Issue', milestones]
42+
4043
App\Issues\GitHub\CachedLabelsApi: ~
4144

4245
App\Issues\GitHub\GitHubStatusApi: ~
4346

47+
App\Issues\GitHub\MilestonesApi: ~
48+
4449
App\Issues\StatusApi:
4550
alias: App\Issues\GitHub\GitHubStatusApi
4651

src/Issues/GitHub/MilestonesApi.php

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
namespace App\Issues\GitHub;
4+
5+
use App\Repository\Repository;
6+
use Github\Api\Issue;
7+
use Github\Api\Issue\Labels;
8+
use Github\Api\Issue\Milestones;
9+
10+
/**
11+
*
12+
* @author Tobias Nyholm <[email protected]>
13+
*/
14+
class MilestonesApi
15+
{
16+
/**
17+
* @var Milestones
18+
*/
19+
private $milestonesApi;
20+
21+
/**
22+
* @var Issue
23+
*/
24+
private $issuesApi;
25+
26+
/**
27+
* @var string[][]
28+
*/
29+
private $cache = [];
30+
31+
public function __construct(Milestones $milestonesApi, Issue $issuesApi)
32+
{
33+
$this->milestonesApi = $milestonesApi;
34+
$this->issuesApi = $issuesApi;
35+
}
36+
37+
private function getMilestones(Repository $repository)
38+
{
39+
$key = $this->getCacheKey($repository);
40+
if (!isset($this->cache[$key])) {
41+
$this->cache[$key] = [];
42+
43+
$milestones = $this->milestonesApi->all($repository->getVendor(), $repository->getName());
44+
45+
foreach ($milestones as $milestone) {
46+
$this->cache[$key][] = $milestone;
47+
}
48+
}
49+
50+
return $this->cache[$key];
51+
}
52+
53+
public function updateMilestone(Repository $repository, int $issueNumber, string $milestoneName)
54+
{
55+
$milestoneNumber = null;
56+
foreach ($this->getMilestones($repository) as $milestone) {
57+
if ($milestone['name'] === $milestoneName) {
58+
$milestoneNumber = $milestone['number'];
59+
}
60+
}
61+
62+
if ($milestoneNumber === null) {
63+
throw new \LogicException(\sprintf('Milestone "%s" does not exist', $milestoneName));
64+
}
65+
66+
$this->issuesApi->update($repository->getVendor(), $repository->getName(), $issueNumber, [
67+
'milestone' => $milestoneNumber,
68+
]);
69+
}
70+
71+
/**
72+
* @return bool
73+
*/
74+
public function exits(Repository $repository, string $milestoneName)
75+
{
76+
foreach ($this->getMilestones($repository) as $milestone) {
77+
if ($milestone['name'] === $milestoneName) {
78+
return true;
79+
}
80+
}
81+
82+
return false;
83+
}
84+
85+
private function getCacheKey(Repository $repository)
86+
{
87+
return sprintf('%s_%s', $repository->getVendor(), $repository->getName());
88+
}
89+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace App\Subscriber;
4+
5+
use App\Event\GitHubEvent;
6+
use App\GitHubEvents;
7+
use App\Issues\GitHub\CachedMilestonesApi;
8+
use App\Issues\GitHub\MilestonesApi;
9+
use App\Issues\Status;
10+
use App\Issues\StatusApi;
11+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
12+
13+
/**
14+
* @author Tobias Nyholm <[email protected]>
15+
*/
16+
class MilestoneNewPRSubscriber implements EventSubscriberInterface
17+
{
18+
private $milestonesApi;
19+
20+
public function __construct(MilestonesApi $milestonesApi)
21+
{
22+
$this->milestonesApi = $milestonesApi;
23+
}
24+
25+
/**
26+
* Adds a "Needs Review" label to new PRs.
27+
*
28+
* @param GitHubEvent $event
29+
*/
30+
public function onPullRequest(GitHubEvent $event)
31+
{
32+
$data = $event->getData();
33+
$repository = $event->getRepository();
34+
if ('opened' !== $action = $data['action']) {
35+
return;
36+
}
37+
38+
$targetBranch = $data['pull_request']['base']['ref'];
39+
if ($targetBranch === $data['repository']['default_branch']) {
40+
return;
41+
}
42+
43+
if (!$this->milestonesApi->exits($repository, $targetBranch)) {
44+
return;
45+
}
46+
47+
$pullRequestNumber = $data['pull_request']['number'];
48+
$this->milestonesApi->updateMilestone($repository, $pullRequestNumber, $targetBranch);
49+
50+
$event->setResponseData(array(
51+
'pull_request' => $pullRequestNumber,
52+
'milestone' => $targetBranch,
53+
));
54+
}
55+
56+
public static function getSubscribedEvents()
57+
{
58+
return array(
59+
GitHubEvents::PULL_REQUEST => 'onPullRequest',
60+
);
61+
}
62+
}

tests/Controller/WebhookControllerTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ public function getTests()
5858
'pull_request.opened.json',
5959
['pull_request' => 3, 'status_change' => 'needs_review', 'pr_labels' => ['Bug']],
6060
],
61+
'On pull request opened with target branch' => [
62+
'pull_request',
63+
'pull_request.opened_target_branch.json',
64+
['pull_request' => 3, 'status_change' => 'needs_review', 'pr_labels' => ['Bug']],
65+
],
6166
'On issue labeled bug' => [
6267
'issues',
6368
'issues.labeled.bug.json',
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
namespace App\Tests\Subscriber;
4+
5+
use App\Event\GitHubEvent;
6+
use App\GitHubEvents;
7+
use App\Issues\GitHub\MilestonesApi;
8+
use App\Issues\Status;
9+
use App\Repository\Repository;
10+
use App\Subscriber\MilestoneNewPRSubscriber;
11+
use App\Subscriber\NeedsReviewNewPRSubscriber;
12+
use PHPUnit\Framework\TestCase;
13+
use Symfony\Component\EventDispatcher\EventDispatcher;
14+
use App\Issues\StatusApi;
15+
16+
class MilestoneNewPRSubscriberTest extends TestCase
17+
{
18+
private $subscriber;
19+
20+
private $milestonesApi;
21+
22+
private $repository;
23+
24+
/**
25+
* @var EventDispatcher
26+
*/
27+
private $dispatcher;
28+
29+
protected function setUp()
30+
{
31+
$this->milestonesApi = $this->createMock(MilestonesApi::class);
32+
$this->subscriber = new MilestoneNewPRSubscriber($this->milestonesApi);
33+
$this->repository = new Repository('nyholm', 'symfony', [], null);
34+
35+
$this->dispatcher = new EventDispatcher();
36+
$this->dispatcher->addSubscriber($this->subscriber);
37+
}
38+
39+
public function testOnPullRequestOpen()
40+
{
41+
$this->milestonesApi->expects($this->once())
42+
->method('exits')
43+
->with($this->repository, '4.4')
44+
->willReturn(true);
45+
46+
$this->milestonesApi->expects($this->once())
47+
->method('updateMilestone')
48+
->with($this->repository, 1234, '4.4');
49+
50+
$event = new GitHubEvent([
51+
'action' => 'opened',
52+
'pull_request' => [
53+
'number' => 1234,
54+
'base' => [ 'ref' => '4.4' ]
55+
],
56+
'repository' => [
57+
'default_branch' => 'master'
58+
]
59+
], $this->repository);
60+
61+
$this->dispatcher->dispatch($event, GitHubEvents::PULL_REQUEST);
62+
$responseData = $event->getResponseData();
63+
64+
$this->assertCount(2, $responseData);
65+
$this->assertSame(1234, $responseData['pull_request']);
66+
$this->assertSame('4.4', $responseData['milestone']);
67+
}
68+
69+
public function testOnPullRequestOpenDefaultBranch()
70+
{
71+
$this->milestonesApi->expects($this->never())
72+
->method('updateMilestone');
73+
74+
$event = new GitHubEvent([
75+
'action' => 'opened',
76+
'pull_request' => [
77+
'number' => 1234,
78+
'base' => [ 'ref' => 'master' ]
79+
],
80+
'repository' => [
81+
'default_branch' => 'master'
82+
]
83+
], $this->repository);
84+
85+
$this->dispatcher->dispatch($event, GitHubEvents::PULL_REQUEST);
86+
$responseData = $event->getResponseData();
87+
$this->assertEmpty($responseData);
88+
}
89+
90+
public function testOnPullRequestOpenMilestoneNotExist()
91+
{
92+
$this->milestonesApi->expects($this->once())
93+
->method('exits')
94+
->with($this->repository, '4.4')
95+
->willReturn(false);
96+
97+
$this->milestonesApi->expects($this->never())
98+
->method('updateMilestone');
99+
100+
$event = new GitHubEvent([
101+
'action' => 'opened',
102+
'pull_request' => [
103+
'number' => 1234,
104+
'base' => [ 'ref' => '4.4' ]
105+
],
106+
'repository' => [
107+
'default_branch' => 'master'
108+
]
109+
], $this->repository);
110+
111+
$this->dispatcher->dispatch($event, GitHubEvents::PULL_REQUEST);
112+
$responseData = $event->getResponseData();
113+
$this->assertEmpty($responseData);
114+
}
115+
116+
public function testOnPullRequestNotOpen()
117+
{
118+
$this->milestonesApi->expects($this->never())
119+
->method('updateMilestone');
120+
121+
$event = new GitHubEvent([
122+
'action' => 'close',
123+
], $this->repository);
124+
125+
$this->dispatcher->dispatch($event, GitHubEvents::PULL_REQUEST);
126+
$responseData = $event->getResponseData();
127+
$this->assertEmpty($responseData);
128+
}
129+
}

tests/webhook_examples/pull_request.opened_target_branch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"number": 5,
1313
"state": "open",
1414
"locked": false,
15-
"title": "Demo",
15+
"title": "Test Target branch",
1616
"user": {
1717
"login": "Nyholm",
1818
"id": 1275206,

0 commit comments

Comments
 (0)