Skip to content

Commit af3bb2e

Browse files
committed
Merge pull request #34 from carsonbot/auto_label_prs
Auto label PR's based on content
2 parents 0f4ef34 + 2e7ba8e commit af3bb2e

File tree

8 files changed

+289
-5
lines changed

8 files changed

+289
-5
lines changed

app/config/github.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ services:
1515
arguments:
1616
- '@app.status_api'
1717

18+
app.subscriber.auto_label_pr_from_content_subscriber:
19+
class: AppBundle\Subscriber\AutoLabelPRFromContentSubscriber
20+
arguments:
21+
- '@app.github.cached_labels_api'
22+
1823
parameters:
1924
# point to the main symfony repositories
2025
repositories:
@@ -23,7 +28,9 @@ parameters:
2328
- app.subscriber.status_change_by_comment_subscriber
2429
- app.subscriber.needs_review_new_pr_subscriber
2530
- app.subscriber.bug_label_new_issue_subscriber
31+
- app.subscriber.auto_label_pr_from_content_subscriber
2632
# secret: change_me
33+
2734
symfony/symfony-docs:
2835
subscribers:
2936
- app.subscriber.status_change_by_comment_subscriber
@@ -36,3 +43,4 @@ parameters:
3643
- app.subscriber.status_change_by_comment_subscriber
3744
- app.subscriber.needs_review_new_pr_subscriber
3845
- app.subscriber.bug_label_new_issue_subscriber
46+
- app.subscriber.auto_label_pr_from_content_subscriber

src/AppBundle/Issues/GitHub/CachedLabelsApi.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ public function removeIssueLabel($issueNumber, $label, Repository $repository)
8787
}
8888
}
8989

90+
public function addIssueLabels($issueNumber, array $labels, Repository $repository)
91+
{
92+
foreach ($labels as $label) {
93+
$this->addIssueLabel($issueNumber, $label, $repository);
94+
}
95+
}
96+
9097
private function getCacheKey($issueNumber, Repository $repository)
9198
{
9299
return sprintf('%s_%s_%s', $issueNumber, $repository->getVendor(), $repository->getName());
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php
2+
3+
namespace AppBundle\Subscriber;
4+
5+
use AppBundle\Event\GitHubEvent;
6+
use AppBundle\GitHubEvents;
7+
use AppBundle\Issues\GitHub\CachedLabelsApi;
8+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
9+
10+
/**
11+
* Looks at new pull requests and auto-labels based on text.
12+
*/
13+
class AutoLabelPRFromContentSubscriber implements EventSubscriberInterface
14+
{
15+
private $labelsApi;
16+
17+
public function __construct(CachedLabelsApi $labelsApi)
18+
{
19+
$this->labelsApi = $labelsApi;
20+
}
21+
22+
/**
23+
* @param GitHubEvent $event
24+
*/
25+
public function onPullRequest(GitHubEvent $event)
26+
{
27+
$data = $event->getData();
28+
if ('opened' !== $action = $data['action']) {
29+
$event->setResponseData(array('unsupported_action' => $action));
30+
31+
return;
32+
}
33+
34+
$prNumber = $data['pull_request']['number'];
35+
$prTitle = $data['pull_request']['title'];
36+
$prBody = $data['pull_request']['body'];
37+
$prLabels = array();
38+
39+
// the PR title usually contains one or more labels
40+
foreach ($this->extractLabels($prTitle) as $label) {
41+
$prLabels[] = $label;
42+
}
43+
44+
// the PR body usually indicates if this is a Bug, Feature, BC Break or Deprecation
45+
if (preg_match('/\|\s*Bug fix\?\s*\|\s*yes\s*/i', $prBody, $matches)) {
46+
$prLabels[] = 'Bug';
47+
}
48+
if (preg_match('/\|\s*New feature\?\s*\|\s*yes\s*/i', $prBody, $matches)) {
49+
$prLabels[] = 'Feature';
50+
}
51+
if (preg_match('/\|\s*BC breaks\?\s*\|\s*yes\s*/i', $prBody, $matches)) {
52+
$prLabels[] = 'BC Break';
53+
}
54+
if (preg_match('/\|\s*Deprecations\?\s*\|\s*yes\s*/i', $prBody, $matches)) {
55+
$prLabels[] = 'Deprecation';
56+
}
57+
58+
$this->labelsApi->addIssueLabels($prNumber, $prLabels, $event->getRepository());
59+
60+
$event->setResponseData(array(
61+
'pull_request' => $prNumber,
62+
'pr_labels' => $prLabels,
63+
));
64+
}
65+
66+
private function extractLabels($prTitle)
67+
{
68+
$labels = array();
69+
70+
// e.g. "[PropertyAccess] [RFC] [WIP] Allow custom methods on property accesses"
71+
if (preg_match_all('/\[(?P<labels>.+)\]/U', $prTitle, $matches)) {
72+
// creates a key=>val array, but the key is lowercased
73+
$validLabels = array_combine(
74+
array_map(function($s) {
75+
return strtolower($s);
76+
}, $this->getValidLabels()),
77+
$this->getValidLabels()
78+
);
79+
80+
foreach ($matches['labels'] as $label) {
81+
// check case-insensitively, but the apply the correctly-cased label
82+
if (isset($validLabels[strtolower($label)])) {
83+
$labels[] = $validLabels[strtolower($label)];
84+
}
85+
}
86+
}
87+
88+
return $labels;
89+
}
90+
91+
/**
92+
* TODO: get valid labels from the repository via GitHub API.
93+
*/
94+
private function getValidLabels()
95+
{
96+
return array(
97+
'Asset', 'BC Break', 'BrowserKit', 'Bug', 'Cache', 'ClassLoader',
98+
'Config', 'Console', 'Critical', 'CssSelector', 'Debug', 'DebugBundle',
99+
'DependencyInjection', 'Deprecation', 'Doctrine', 'DoctrineBridge',
100+
'DomCrawler', 'Drupal related', 'DX', 'Easy Pick', 'Enhancement',
101+
'EventDispatcher', 'ExpressionLanguage', 'Feature', 'Filesystem',
102+
'Finder', 'Form', 'FrameworkBundle', 'HttpFoundation', 'HttpKernel',
103+
'Intl', 'Ldap', 'Locale', 'MonologBridge', 'OptionsResolver',
104+
'PhpUnitBridge', 'Process', 'PropertyAccess', 'PropertyInfo', 'Ready',
105+
'RFC', 'Routing', 'Security', 'SecurityBundle', 'Serializer',
106+
'Stopwatch', 'Templating', 'Translator', 'TwigBridge', 'TwigBundle',
107+
'Unconfirmed', 'Validator', 'VarDumper', 'WebProfilerBundle', 'Yaml',
108+
);
109+
}
110+
111+
public static function getSubscribedEvents()
112+
{
113+
return array(
114+
GitHubEvents::PULL_REQUEST => 'onPullRequest',
115+
);
116+
}
117+
}

src/AppBundle/Tests/Controller/WebhookControllerTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public function getTests()
3434
'On pull request opened' => array(
3535
'pull_request',
3636
'pull_request.opened.json',
37-
array('pull_request' => 3, 'status_change' => 'needs_review'),
37+
array('pull_request' => 3, 'status_change' => 'needs_review', 'pr_labels' => array('Bug')),
3838
),
3939
'On issue labeled "bug"' => array(
4040
'issues',
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?php
2+
3+
namespace AppBundle\Tests\Subscriber;
4+
5+
use AppBundle\Event\GitHubEvent;
6+
use AppBundle\GitHubEvents;
7+
use AppBundle\Repository\Repository;
8+
use AppBundle\Subscriber\AutoLabelPRFromContentSubscriber;
9+
use Symfony\Component\EventDispatcher\EventDispatcher;
10+
11+
class AutoLabelPRFromContentSubscriberTest extends \PHPUnit_Framework_TestCase
12+
{
13+
private $autoLabelSubscriber;
14+
15+
private $labelsApi;
16+
17+
private $repository;
18+
19+
/**
20+
* @var EventDispatcher
21+
*/
22+
private static $dispatcher;
23+
24+
public static function setUpBeforeClass()
25+
{
26+
self::$dispatcher = new EventDispatcher();
27+
}
28+
29+
protected function setUp()
30+
{
31+
$this->labelsApi = $this->getMockBuilder('AppBundle\Issues\GitHub\CachedLabelsApi')
32+
->disableOriginalConstructor()
33+
->getMock();
34+
$this->autoLabelSubscriber = new AutoLabelPRFromContentSubscriber($this->labelsApi);
35+
$this->repository = new Repository('weaverryan', 'symfony', [], null);
36+
37+
self::$dispatcher->addSubscriber($this->autoLabelSubscriber);
38+
}
39+
40+
/**
41+
* @dataProvider getPRTests
42+
*/
43+
public function testAutoLabel($prTitle, $prBody, array $expectedNewLabels)
44+
{
45+
$this->labelsApi->expects($this->once())
46+
->method('addIssueLabels')
47+
->with(1234, $expectedNewLabels, $this->repository)
48+
->willReturn(null);
49+
50+
$event = new GitHubEvent(array(
51+
'action' => 'opened',
52+
'pull_request' => array(
53+
'number' => 1234,
54+
'title' => $prTitle,
55+
'body' => $prBody,
56+
),
57+
), $this->repository);
58+
59+
self::$dispatcher->dispatch(GitHubEvents::PULL_REQUEST, $event);
60+
61+
$responseData = $event->getResponseData();
62+
63+
$this->assertCount(2, $responseData);
64+
$this->assertSame(1234, $responseData['pull_request']);
65+
$this->assertSame($expectedNewLabels, $responseData['pr_labels']);
66+
}
67+
68+
public function getPRTests()
69+
{
70+
$tests = [];
71+
72+
// nothing added automatically
73+
$tests[] = [
74+
'Cool new Feature!',
75+
<<<EOF
76+
FOO
77+
EOF
78+
,
79+
[],
80+
];
81+
82+
$tests[] = [
83+
'I am fixing a bug!',
84+
<<<EOF
85+
Well hi cool peeps!
86+
87+
| Q | A
88+
| ------------- | ---
89+
| Branch? | master
90+
| Bug fix? | yes
91+
| New feature? | no
92+
| BC breaks? | no
93+
| Deprecations? | no
94+
| Tests pass? | yes
95+
| Fixed tickets | n/a
96+
| License | MIT
97+
| Doc PR | n/a
98+
EOF
99+
,
100+
['Bug'],
101+
];
102+
103+
$tests[] = [
104+
'Using all the auto-labels!',
105+
<<<EOF
106+
Well hi cool peeps!
107+
108+
| Q | A
109+
| ------------- | ---
110+
| Branch? | master
111+
| Bug fix? | yes
112+
| New feature? | yes
113+
| BC breaks? | yes
114+
| Deprecations? | yes
115+
| Tests pass? | yes
116+
| Fixed tickets | n/a
117+
| License | MIT
118+
| Doc PR | n/a
119+
EOF
120+
,
121+
['Bug', 'Feature', 'BC Break', 'Deprecation'],
122+
];
123+
124+
$tests[] = [
125+
'case-insensitive!',
126+
<<<EOF
127+
Well hi cool peeps!
128+
129+
| Q | A
130+
| ------------- | ---
131+
| Branch? | master
132+
| Bug fix? | YeS
133+
| New feature? | yEs
134+
| BC breaks? | yES
135+
| Deprecations? | yeS
136+
| Tests pass? | yes
137+
| Fixed tickets | n/a
138+
| License | MIT
139+
| Doc PR | n/a
140+
EOF
141+
,
142+
['Bug', 'Feature', 'BC Break', 'Deprecation'],
143+
];
144+
145+
$tests[] = [
146+
'[Asset][bc Break][Fake] Extracting from title [Bug]',
147+
<<<EOF
148+
EOF
149+
,
150+
['Asset', 'BC Break', 'Bug'],
151+
];
152+
153+
return $tests;
154+
}
155+
}

src/AppBundle/Tests/Subscriber/BugLabelNewIssueSubscriberTest.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
use AppBundle\Issues\Status;
88
use AppBundle\Repository\Repository;
99
use AppBundle\Subscriber\BugLabelNewIssueSubscriber;
10-
use AppBundle\Subscriber\NeedsReviewNewPRSubscriber;
11-
use AppBundle\Subscriber\StatusChangeByCommentSubscriber;
1210
use Symfony\Component\EventDispatcher\EventDispatcher;
1311

1412
class BugLabelNewIssueSubscriberTest extends \PHPUnit_Framework_TestCase

src/AppBundle/Tests/Subscriber/NeedsReviewNewPRSubscriberTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use AppBundle\Issues\Status;
88
use AppBundle\Repository\Repository;
99
use AppBundle\Subscriber\NeedsReviewNewPRSubscriber;
10-
use AppBundle\Subscriber\StatusChangeByCommentSubscriber;
1110
use Symfony\Component\EventDispatcher\EventDispatcher;
1211

1312
class NeedsReviewNewPRSubscriberTest extends \PHPUnit_Framework_TestCase

src/AppBundle/Tests/webhook_examples/pull_request.opened.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"type": "User",
3232
"site_admin": false
3333
},
34-
"body": "Testing PR",
34+
"body": "Testing PR - | Bug fix? | yes",
3535
"created_at": "2015-06-28T15:20:57Z",
3636
"updated_at": "2015-06-28T15:20:57Z",
3737
"closed_at": null,

0 commit comments

Comments
 (0)