Skip to content

Added support for GitHub reviews #58

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Sep 28, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/config/github.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,19 @@ services:
arguments:
- '@app.github.cached_labels_api'

app.subscriber.status_change_by_review_subscriber:
class: AppBundle\Subscriber\StatusChangeByReviewSubscriber
arguments:
- '@app.status_api'
- '@logger'

parameters:
# point to the main symfony repositories
repositories:
symfony/symfony:
subscribers:
- app.subscriber.status_change_by_comment_subscriber
- app.subscriber.status_change_by_review_subscriber
- app.subscriber.needs_review_new_pr_subscriber
- app.subscriber.bug_label_new_issue_subscriber
- app.subscriber.auto_label_pr_from_content_subscriber
Expand All @@ -41,6 +48,7 @@ parameters:
subscribers:
- app.subscriber.status_change_by_comment_subscriber
- app.subscriber.status_change_on_push_subscriber
- app.subscriber.status_change_by_review_subscriber
- app.subscriber.needs_review_new_pr_subscriber
- app.subscriber.bug_label_new_issue_subscriber
- app.subscriber.auto_label_pr_from_content_subscriber
Expand All @@ -51,6 +59,7 @@ parameters:
subscribers:
- app.subscriber.status_change_by_comment_subscriber
- app.subscriber.status_change_on_push_subscriber
- app.subscriber.status_change_by_review_subscriber
- app.subscriber.needs_review_new_pr_subscriber
- app.subscriber.bug_label_new_issue_subscriber
- app.subscriber.auto_label_pr_from_content_subscriber
3 changes: 3 additions & 0 deletions src/AppBundle/GitHubEvents.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ final class GitHubEvents
/** @Event('\AppBundle\Event\GitHubEvent') */
const PR_REVIEW_COMMENT = 'github.pull_request_review_comment';

/** @Event('\AppBundle\Event\GithubEvent') */
const PULL_REQUEST_REVIEW = 'github.pull_request_review';

/** @Event('\AppBundle\Event\GitHubEvent') */
const PULL_REQUEST = 'github.pull_request';

Expand Down
52 changes: 52 additions & 0 deletions src/AppBundle/Subscriber/AbstractStatusChangeSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace AppBundle\Subscriber;

use AppBundle\Issues\Status;
use AppBundle\Issues\StatusApi;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

abstract class AbstractStatusChangeSubscriber implements EventSubscriberInterface
{
protected static $triggerWordToStatus = [
'needs review' => Status::NEEDS_REVIEW,
'needs work' => Status::NEEDS_WORK,
'works for me' => Status::WORKS_FOR_ME,
'reviewed' => Status::REVIEWED,
];

protected $statusApi;

public function __construct(StatusApi $statusApi)
{
$this->statusApi = $statusApi;
}

/**
* Parses the text and looks for keywords to see if this should cause any
* status change.
*
* @param string $body
*
* @return null|string
*/
protected function parseStatusFromText($body)
{
$triggerWord = implode('|', array_keys(static::$triggerWordToStatus));
$formatting = '[\\s\\*]*';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI you don't have to escape special characters in a character class, except - if it's not at the end and to be used as literal.

// Match first character after "status:"
// Case insensitive ("i"), ignores formatting with "*" before or after the ":"
$pattern = "~(?=\n|^)${formatting}status${formatting}:${formatting}[\"']?($triggerWord)[\"']?${formatting}[.!]?${formatting}(?<=\r\n|\n|$)~i";

if (preg_match_all($pattern, $body, $matches)) {
// Second subpattern = first status character
return static::$triggerWordToStatus[strtolower(end($matches[1]))];
}
}

private function checkUserIsAllowedToReview(array $data)
{
return $data['issue']['user']['login'] !== $data['comment']['user']['login'];
}
}

45 changes: 12 additions & 33 deletions src/AppBundle/Subscriber/StatusChangeByCommentSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,14 @@
use AppBundle\Issues\Status;
use AppBundle\Issues\StatusApi;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class StatusChangeByCommentSubscriber implements EventSubscriberInterface
class StatusChangeByCommentSubscriber extends AbstractStatusChangeSubscriber
{
private static $triggerWordToStatus = [
'needs review' => Status::NEEDS_REVIEW,
'needs work' => Status::NEEDS_WORK,
'works for me' => Status::WORKS_FOR_ME,
'reviewed' => Status::REVIEWED,
];

private $statusApi;
private $logger;

public function __construct(StatusApi $statusApi, LoggerInterface $logger)
{
$this->statusApi = $statusApi;
parent::__construct($statusApi);
$this->logger = $logger;
}

Expand All @@ -37,36 +28,24 @@ public function onIssueComment(GitHubEvent $event)
{
$data = $event->getData();
$repository = $event->getRepository();
$newStatus = null;
$issueNumber = $data['issue']['number'];
$newStatus = $this->parseStatusFromText($data['comment']['body']);

$triggerWord = implode('|', array_keys(static::$triggerWordToStatus));
$formatting = '[\\s\\*]*';
// Match first character after "status:"
// Case insensitive ("i"), ignores formatting with "*" before or after the ":"
$pattern = "~(?=\n|^)${formatting}status${formatting}:${formatting}[\"']?($triggerWord)[\"']?${formatting}[.!]?${formatting}(?<=\r\n|\n|$)~i";

if (preg_match_all($pattern, $data['comment']['body'], $matches)) {
// Second subpattern = first status character
$newStatus = static::$triggerWordToStatus[strtolower(end($matches[1]))];

if (Status::REVIEWED === $newStatus && false === $this->checkUserIsAllowedToReview($data)) {
$event->setResponseData(array(
'issue' => $issueNumber,
'status_change' => null,
));

return;
}

$this->logger->debug(sprintf('Setting issue number %s to status %s', $issueNumber, $newStatus));
$this->statusApi->setIssueStatus($issueNumber, $newStatus, $repository);
if (Status::REVIEWED === $newStatus && false === $this->checkUserIsAllowedToReview($data)) {
$newStatus = null;
}

$event->setResponseData(array(
'issue' => $issueNumber,
'status_change' => $newStatus,
));

if (null === $newStatus) {
return;
}

$this->logger->debug(sprintf('Setting issue number %s to status %s', $issueNumber, $newStatus));
$this->statusApi->setIssueStatus($issueNumber, $newStatus, $repository);
}

public static function getSubscribedEvents()
Expand Down
115 changes: 115 additions & 0 deletions src/AppBundle/Subscriber/StatusChangeByReviewSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

namespace AppBundle\Subscriber;

use AppBundle\Event\GitHubEvent;
use AppBundle\GitHubEvents;
use AppBundle\Issues\Status;
use AppBundle\Issues\StatusApi;
use Psr\Log\LoggerInterface;

/**
* Changes the status when a new review is submitted.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
class StatusChangeByReviewSubscriber extends AbstractStatusChangeSubscriber
{
private $logger;

public function __construct(StatusApi $statusApi, LoggerInterface $logger)
{
parent::__construct($statusApi);
$this->logger = $logger;
}

/**
* Sets the status based on the review state (approved/changes requested)
* or the review body (using the Status: keyword).
*
* @param GithubEvent $event
*/
public function onReview(GitHubEvent $event)
{
$data = $event->getData();
if ('submitted' !== $data['action']) {
$event->setResponseData(array('unsupported_action' => $data['action']));

return;
}

$repository = $event->getRepository();
$pullRequestNumber = $data['pull_request']['number'];
$newStatus = null;

// Set status based on review state
switch (strtolower($data['review']['state'])) {
Copy link
Contributor Author

@wouterj wouterj Mar 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitHub docs are a bit vague on this: REST api docs shows state in all uppercase, while the webhooks docs show it in lowercase. Just to be sure, I've added this strtolower() call.

case 'approved':
$newStatus = Status::REVIEWED;

break;
case 'changes_requested':
$newStatus = Status::NEEDS_WORK;

break;
default:
$newStatus = $this->parseStatusFromText($data['review']['body']);

if (Status::REVIEWED === $newStatus && false === $this->checkUserIsAllowedToReview($data)) {
$newStatus = null;
}
}

$event->setResponseData(array(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about moving the beginning of the method and those line in the abstract listener too, it could call an abstract method to extract the status from the payload, wdyt?

'pull_request' => $pullRequestNumber,
'status_change' => $newStatus,
));

if (null === $newStatus) {
return;
}

$this->logger->debug(sprintf('Setting issue number %s to status %s', $pullRequestNumber, $newStatus));
$this->statusApi->setIssueStatus($pullRequestNumber, $newStatus, $repository);
}

/**
* Sets the status to needs review when a review is requested.
*
* @param GithubEvent $event
*/
public function onReviewRequested(GithubEvent $event)
{
$data = $event->getData();
if ('review_requested' !== $data['action']) {
$event->setResponseData(array('unsupported_action' => $data['action']));

return;
}

$repository = $event->getRepository();
$pullRequestNumber = $data['pull_request']['number'];
$newStatus = Status::NEEDS_REVIEW;

$this->logger->debug(sprintf('Setting issue number %s to status %s', $pullRequestNumber, $newStatus));
$this->statusApi->setIssueStatus($pullRequestNumber, $newStatus, $repository);

$event->setResponseData(array(
'pull_request' => $pullRequestNumber,
'status_change' => $newStatus,
));
}

public static function getSubscribedEvents()
{
return array(
GitHubEvents::PULL_REQUEST_REVIEW => 'onReview',
GitHubEvents::PULL_REQUEST => 'onReviewRequested',
);
}

private function checkUserIsAllowedToReview(array $data)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is also defined in AbstractStatusChangeSubscriber, and check implies it will throw exception or something. By Symfony conventions it should be isUserIsAllowedToReview 👍

{
return $data['pull_request']['user']['login'] !== $data['review']['user']['login'];
}
}
Loading