Skip to content

Commit 668496b

Browse files
committed
Initial work - parsing through the issue comment, partially-functional webhook endpoint
1 parent 2a1a3b4 commit 668496b

12 files changed

+615
-38
lines changed

app/config/parameters.yml.dist

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ parameters:
1717

1818
# A secret key that's used to generate certain security-related tokens
1919
secret: ThisTokenIsNotSoSecretChangeIt
20+
21+
# token used to update labels on the repository, etc
22+
github_token: XXXX

app/config/services.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ parameters:
44
# parameter_name: value
55

66
services:
7-
# service_name:
8-
# class: AppBundle\Directory\ClassName
9-
# arguments: ["@another_service_name", "plain_value", "%parameter_name%"]
7+
app.github.status_manager:
8+
class: AppBundle\GitHub\StatusManager
9+
arguments: []

src/AppBundle/Controller/DefaultController.php

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
namespace AppBundle\Controller;
4+
5+
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
6+
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
7+
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
8+
use Symfony\Component\HttpFoundation\JsonResponse;
9+
use Symfony\Component\HttpFoundation\Request;
10+
11+
class WebhookController extends Controller
12+
{
13+
/**
14+
* @Route("/webhooks/github", name="webhooks_github")
15+
* @Method("POST")
16+
*/
17+
public function githubAction(Request $request)
18+
{
19+
$data = json_decode($request->getContent(), true);
20+
if ($data === null) {
21+
throw new \Exception('Invalid JSON body!');
22+
}
23+
24+
$event = $request->headers->get('X-Github-Event');
25+
26+
switch ($event) {
27+
case 'issue_comment':
28+
$responseData = $this->handleIssueCommentEvent($data);
29+
break;
30+
default:
31+
$responseData = [
32+
'unsupported_event' => $event
33+
];
34+
}
35+
36+
return new JsonResponse($responseData);
37+
38+
// 1 read in what event they have
39+
// 2 perform some action
40+
// 3 return JSON
41+
42+
// log something to the database?
43+
}
44+
45+
private function handleIssueCommentEvent(array $data)
46+
{
47+
$commentText = $data['comment']['body'];
48+
$issueNumber = $data['issue']['number'];
49+
50+
$newStatus = $this->get('app.github.status_manager')
51+
->getStatusChangeFromComment($commentText);
52+
53+
// todo - send this status back to GitHub
54+
55+
return [
56+
'issue' => $issueNumber,
57+
'status_change' => $newStatus,
58+
];
59+
}
60+
61+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
namespace AppBundle\GitHub;
4+
5+
class StatusManager
6+
{
7+
const STATUS_NEEDS_REVIEW = 'needs_review';
8+
const STATUS_NEEDS_WORK = 'needs_work';
9+
const STATUS_WORKS_FOR_ME = 'works_for_me';
10+
const STATUS_REVIEWED = 'reviewed';
11+
12+
private static $triggerWords = [
13+
self::STATUS_NEEDS_REVIEW => ['needs review'],
14+
self::STATUS_NEEDS_WORK => ['needs work'],
15+
self::STATUS_WORKS_FOR_ME => ['works for me'],
16+
self::STATUS_REVIEWED => ['reviewed'],
17+
];
18+
19+
/**
20+
* Parses the text of the comment and looks for keywords to see
21+
* if this should cause any status change.
22+
*
23+
* Returns the status that this comment is causing or null of there
24+
* should be no status change.
25+
*
26+
* @param $comment
27+
* @return string|null
28+
*/
29+
public function getStatusChangeFromComment($comment)
30+
{
31+
// 1) Find the last "status:"
32+
$statusPosition = $this->findStatusPosition($comment);
33+
34+
if ($statusPosition === false) {
35+
return null;
36+
}
37+
38+
// get what comes *after* status:, with spaces trimmed
39+
// now, the status string "needs review" should be at the 0 character
40+
$statusString = trim(substr($comment, $statusPosition));
41+
42+
$newStatus = null;
43+
foreach (self::$triggerWords as $status => $triggerWords) {
44+
foreach ($triggerWords as $triggerWord) {
45+
// status should be right at the beginning of the string
46+
if (stripos($statusString, $triggerWord) === 0) {
47+
// don't return immediately - we use the last status
48+
// in the rare case there are multiple
49+
$newStatus = $status;
50+
}
51+
}
52+
}
53+
54+
return $newStatus;
55+
}
56+
57+
/**
58+
* Finds the position where the status string will start - e.g.
59+
* for "Status: Needs review", this would return the position
60+
* that points to the "N" in "Needs".
61+
*
62+
* If there are multiple "Status:" in the string, this returns the
63+
* final one.
64+
*
65+
* This takes into account possible formatting (e.g. **Status**: )
66+
*
67+
* Returns the position or false if none was found.
68+
*
69+
* @param string $comment
70+
* @return boolean|integer
71+
*/
72+
private function findStatusPosition($comment)
73+
{
74+
$formats = ['status:', '*status*:', '**status**:'];
75+
76+
foreach ($formats as $format) {
77+
$lastStatusPosition = strripos($comment, $format);
78+
79+
if ($lastStatusPosition !== false) {
80+
return $lastStatusPosition + strlen($format);
81+
}
82+
}
83+
84+
return false;
85+
}
86+
}

src/AppBundle/Tests/Controller/DefaultControllerTest.php

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace AppBundle\Tests\Controller;
4+
5+
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
6+
7+
class WebhookControllerTest extends WebTestCase
8+
{
9+
public function testIssueComment()
10+
{
11+
$client = $this->createClient();
12+
$body = file_get_contents(__DIR__.'/../webhook_examples/issue_comment.created.json');
13+
$client->request('POST', '/webhooks/github', array(), array(), array('HTTP_X-Github-Event' => 'issue_comment'), $body);
14+
$response = $client->getResponse();
15+
16+
$responseData = json_decode($response->getContent(), true);
17+
$this->assertEquals(200, $response->getStatusCode());
18+
// a weak sanity check that we went down "the right path" in the controller
19+
$this->assertEquals($responseData['status_change'], 'needs_review');
20+
$this->assertEquals($responseData['issue'], 1);
21+
}
22+
23+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
namespace AppBundle\Tests\GitHub;
4+
5+
use AppBundle\GitHub\StatusManager;
6+
7+
class StatusManagerTest extends \PHPUnit_Framework_TestCase
8+
{
9+
/**
10+
* @dataProvider getCommentsForStatusChange
11+
*/
12+
public function testGetStatusChangeFromComment($comment, $expectedStatus)
13+
{
14+
$statusManager = new StatusManager();
15+
$actualStatus = $statusManager->getStatusChangeFromComment($comment);
16+
17+
$this->assertEquals(
18+
$expectedStatus,
19+
$actualStatus,
20+
sprintf('Comment "%s" did not result in the status "%s"', $comment, $expectedStatus)
21+
);
22+
}
23+
24+
public function getCommentsForStatusChange()
25+
{
26+
$tests = [];
27+
$tests[] = array(
28+
'Have a great day!',
29+
null
30+
);
31+
// basic tests for status change
32+
$tests[] = array(
33+
'Status: needs review',
34+
StatusManager::STATUS_NEEDS_REVIEW
35+
);
36+
$tests[] = array(
37+
'Status: needs work',
38+
StatusManager::STATUS_NEEDS_WORK
39+
);
40+
$tests[] = array(
41+
'Status: works for me!',
42+
StatusManager::STATUS_WORKS_FOR_ME
43+
);
44+
$tests[] = array(
45+
'Status: reviewed',
46+
StatusManager::STATUS_REVIEWED
47+
);
48+
49+
// play with different formatting
50+
$tests[] = array(
51+
'STATUS: REVIEWED',
52+
StatusManager::STATUS_REVIEWED
53+
);
54+
$tests = [];
55+
$tests[] = array(
56+
'**Status**: reviewed',
57+
StatusManager::STATUS_REVIEWED
58+
);
59+
return $tests;
60+
// missing the colon - so we do NOT read this
61+
$tests[] = array(
62+
'Status reviewed',
63+
null,
64+
);
65+
$tests[] = array(
66+
'Status:reviewed',
67+
StatusManager::STATUS_REVIEWED
68+
);
69+
$tests[] = array(
70+
'Status: reviewed',
71+
StatusManager::STATUS_REVIEWED
72+
);
73+
74+
// multiple matches - use the last one
75+
$tests[] = array(
76+
"Status: needs review \r\n that is what the issue *was* marked as. Now it should be Status: reviewed",
77+
StatusManager::STATUS_REVIEWED
78+
);
79+
// "needs review" does not come directly after status: , so there is no status change
80+
$tests[] = array(
81+
'Here is my status: I\'m really happy! I realize this needs review, but I\'m, having too much fun Googling cats!',
82+
null
83+
);
84+
85+
return $tests;
86+
}
87+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Total-Route-Time: 0
2+
Content-Type: application/json
3+
Host: requestb.in
4+
X-Github-Event: issue_comment
5+
Connection: close
6+
X-Request-Id: 44783e91-c72d-4941-988d-5d5ba59e9531
7+
Accept: */*
8+
Content-Length: 8301
9+
X-Github-Delivery: 65cd0080-1d93-11e5-92c8-651e67aa022e
10+
User-Agent: GitHub-Hookshot/2ee22c1
11+
Connect-Time: 0
12+
Via: 1.1 vegur

0 commit comments

Comments
 (0)