Skip to content

Commit f6aa788

Browse files
authored
Merge pull request #87 from Nyholm/milestone
Add support for automatically add milestone
2 parents a8ad401 + 9176670 commit f6aa788

File tree

8 files changed

+785
-0
lines changed

8 files changed

+785
-0
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: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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+
* @author Tobias Nyholm <[email protected]>
12+
*/
13+
class MilestonesApi
14+
{
15+
/**
16+
* @var Milestones
17+
*/
18+
private $milestonesApi;
19+
20+
/**
21+
* @var Issue
22+
*/
23+
private $issuesApi;
24+
25+
/**
26+
* @var string[][]
27+
*/
28+
private $cache = [];
29+
30+
public function __construct(Milestones $milestonesApi, Issue $issuesApi)
31+
{
32+
$this->milestonesApi = $milestonesApi;
33+
$this->issuesApi = $issuesApi;
34+
}
35+
36+
private function getMilestones(Repository $repository)
37+
{
38+
$key = $this->getCacheKey($repository);
39+
if (!isset($this->cache[$key])) {
40+
$this->cache[$key] = [];
41+
42+
$milestones = $this->milestonesApi->all($repository->getVendor(), $repository->getName());
43+
44+
foreach ($milestones as $milestone) {
45+
$this->cache[$key][] = $milestone;
46+
}
47+
}
48+
49+
return $this->cache[$key];
50+
}
51+
52+
public function updateMilestone(Repository $repository, int $issueNumber, string $milestoneName)
53+
{
54+
$milestoneNumber = null;
55+
foreach ($this->getMilestones($repository) as $milestone) {
56+
if ($milestone['name'] === $milestoneName) {
57+
$milestoneNumber = $milestone['number'];
58+
}
59+
}
60+
61+
if ($milestoneNumber === null) {
62+
throw new \LogicException(\sprintf('Milestone "%s" does not exist', $milestoneName));
63+
}
64+
65+
$this->issuesApi->update($repository->getVendor(), $repository->getName(), $issueNumber, [
66+
'milestone' => $milestoneNumber,
67+
]);
68+
}
69+
70+
/**
71+
* @return bool
72+
*/
73+
public function exists(Repository $repository, string $milestoneName)
74+
{
75+
foreach ($this->getMilestones($repository) as $milestone) {
76+
if ($milestone['name'] === $milestoneName) {
77+
return true;
78+
}
79+
}
80+
81+
return false;
82+
}
83+
84+
private function getCacheKey(Repository $repository)
85+
{
86+
return sprintf('%s_%s', $repository->getVendor(), $repository->getName());
87+
}
88+
}
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+
* Sets milestone on PRs that target non-default branch.
27+
*
28+
* @param GitHubEvent $event
29+
*/
30+
public function onPullRequest(GitHubEvent $event)
31+
{
32+
$data = $event->getData();
33+
$repository = $event->getRepository();
34+
if ('opened' !== $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->exists($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' => 5, 'status_change' => 'needs_review', 'pr_labels' => ['Bug'], 'milestone'=>'4.4'],
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('exists')
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('exists')
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+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Request URL: http://carson.knpuniversity.com/webhooks/github
2+
Request method: POST
3+
Accept: */*
4+
content-type: application/x-www-form-urlencoded
5+
User-Agent: GitHub-Hookshot/b1c41a3
6+
X-GitHub-Delivery: d9c13b80-17ae-11eb-8ab5-293f6833841d
7+
X-GitHub-Event: pull_request
8+
X-GitHub-Hook-ID: 141546673

0 commit comments

Comments
 (0)