Commit ab8e2ac9 authored by MikeyMJCO's avatar MikeyMJCO

Merge remote-tracking branch 'michael/60_auto-publish-gitlab-books'

parents 325b5795 f9adf985
...@@ -10,8 +10,18 @@ services: ...@@ -10,8 +10,18 @@ services:
arguments: arguments:
- %books_dir% - %books_dir%
github.hook.processor: webhook.processor:
class: AppBundle\Utils\GitHubHookProcessor class: AppBundle\Utils\WebhookProcessor
arguments:
-
- '@github.webhook_handler'
- '@gitlab.webbook_handler'
github.webhook_handler:
class: AppBundle\Utils\WebhookAdapters\GithubHandler
gitlab.webbook_handler:
class: AppBundle\Utils\WebhookAdapters\GitlabHandler
mkdocs: mkdocs:
class: AppBundle\Utils\MkDocs class: AppBundle\Utils\MkDocs
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
namespace AppBundle\Controller; namespace AppBundle\Controller;
use AppBundle\Model\WebhookEvent;
use AppBundle\Utils\Publisher; use AppBundle\Utils\Publisher;
use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
...@@ -56,50 +57,46 @@ class PublishController extends Controller { ...@@ -56,50 +57,46 @@ class PublishController extends Controller {
* @Route("/admin/listen") * @Route("/admin/listen")
*/ */
public function ListenAction(Request $request) { public function ListenAction(Request $request) {
$body = $request->getContent();
$event = $request->headers->get('X-GitHub-Event'); $processor = $this->get('webhook.processor');
$processor = $this->get('github.hook.processor'); $event = $processor->process($request);
try {
$hookData = $processor->process($event, json_decode($body));
}
catch (\Exception $e) {
$response = "CRITICAL - Skipping the publishing process due to the "
. "following reason: " . $e->getMessage();
return new Response($response, 200);
}
$library = $this->get('library'); $library = $this->get('library');
$identifiers = $library->getIdentifiersByRepo($processor->repo); $identifiers = $library->getIdentifiersByRepo($event->getRepo());
if ($identifiers) {
$this->publisher = $this->get('publisher'); if (!$identifiers) {
foreach ($identifiers as $identifier) { $msg = "CRITICAL - No books found which match " . $event->getRepo();
$fullIdentifier = "{$identifier}/{$processor->branch}";
$this->publisher->publish($fullIdentifier); return new Response($msg, Response::HTTP_BAD_REQUEST);
$this->sendEmail($fullIdentifier, $hookData);
}
$response = $this->publisher->getMessagesInPlainText();
} }
else {
$response = "CRITICAL - No books found which match {$processor->repo}"; $this->publisher = $this->get('publisher');
foreach ($identifiers as $identifier) {
$fullIdentifier = sprintf('%s/%s', $identifier, $event->getBranch());
$this->publisher->publish($fullIdentifier);
$this->sendEmail($fullIdentifier, $event);
} }
$response = $this->publisher->getMessagesInPlainText();
return new Response($response, 200); return new Response($response);
} }
/** /**
* Send notification emails after publishing * Send notification emails after publishing
* *
* @param string $identifier * @param string $identifier
* @param array $hookData * @param WebhookEvent $event
*/ */
private function sendEmail(string $identifier, $hookData) { private function sendEmail(string $identifier, WebhookEvent $event) {
/** /**
* Array of strings for email addresses that should receive the * Array of strings for email addresses that should receive the
* notification email. If none are specified, then the email will be sent to * notification email. If none are specified, then the email will be sent to
* all addresses set in the book's yaml configuration * all addresses set in the book's yaml configuration
*/ */
$extraRecipients = $hookData['recipients']; $extraRecipients = $event->getNotificationRecipients();
$commits = $hookData['commits']; $commits = $event->getCommits();
$library = $this->get('library'); $library = $this->get('library');
$messages = $this->get('publisher')->getMessages(); $messages = $this->get('publisher')->getMessages();
$parts = $library::parseIdentifier($identifier); $parts = $library::parseIdentifier($identifier);
......
<?php
namespace AppBundle\Model;
class WebhookEvent {
const SOURCE_GITHUB = 'Github';
const SOURCE_GITLAB = 'Gitlab';
/**
* @var string
* Where did the hook original, e.g. github
*/
protected $source = '';
/**
* @var string
* What sort of event is it, for example "push"
*/
protected $type = '';
/**
* @var string
* The repo this event was fired from
*/
protected $repo = '';
/**
* @var string
* The branch the event was fired from
*/
protected $branch = '';
/**
* @var \stdClass[]
* An array of commits in the event, each commit will have properties "ID",
* "author" and "message"
*/
protected $commits = [];
/**
* @return string
*/
public function getSource(): string {
return $this->source;
}
/**
* @param string $source
* @return $this
*/
public function setSource(string $source) {
$this->source = $source;
return $this;
}
/**
* @return string
*/
public function getType(): string {
return $this->type;
}
/**
* @param string $type
* @return $this
*/
public function setType(string $type) {
$this->type = $type;
return $this;
}
/**
* @return string
*/
public function getRepo(): string {
return $this->repo;
}
/**
* @param string $repo
* @return $this
*/
public function setRepo(string $repo) {
$this->repo = $repo;
return $this;
}
/**
* @return string
*/
public function getBranch(): string {
return $this->branch;
}
/**
* @param string $branch
* @return $this
*/
public function setBranch(string $branch) {
$this->branch = $branch;
return $this;
}
/**
* @param array $commit
*/
public function addCommit(array $commit) {
$this->commits[] = $commit;
}
/**
* @param \stdClass[] $commits
*/
public function setCommits(array $commits) {
$this->commits = $commits;
}
/**
* @return \stdClass[]
*/
public function getCommits() {
return $this->commits;
}
/**
* @return array
*/
public function getCommitMessages() {
return array_column($this->getCommits(), 'message');
}
/**
* @return array
* Email address of all those who should be notified about the event
*/
public function getNotificationRecipients() {
$recipients = [];
foreach ($this->getCommits() as $commit) {
$author = $commit->author ?? NULL;
if (property_exists($author, 'email')) {
$recipients[] = $author->email;
}
}
// filter out mails that start with "do not reply"
$recipients = preg_grep('/^(donot|no)reply@/', $recipients, PREG_GREP_INVERT);
return array_unique($recipients);
}
}
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
{% for commit in commits %} {% for commit in commits %}
<li> <li>
<strong><a href="{{ commit.url }}">{{ commit.id }}</a></strong> | <strong><a href="{{ commit.url }}">{{ commit.id }}</a></strong> |
{{ commit.message }} | {{ commit.author.username }} {{ commit.message }} | {{ commit.author.name }}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
......
<?php
namespace AppBundle\Utils;
class GitHubHookProcessor {
/**
* @var string the URL for the repository
*/
public $repo;
/**
* @var string the name of the branch to publish
*/
public $branch;
/**
* Process a GitHub webhook
*
* @param string $event
* e.g. 'pull_request', 'push'
*
* @param mixed $payload
* An object given by json_decode()
*
* @return array
*
* @throws \Exception
*/
public function process($event, $payload) {
if (empty($payload)) {
throw new \Exception("No payload data supplied");
}
if (empty($event)) {
throw new \Exception("Unable to determine webhook event type from "
. "request headers");
}
if ($event != 'push') {
throw new \Exception("Webhook event type is not 'push'");
}
return $this->getDetailsFromPush($payload);
}
/**
* Use a "push" event to figure out what branch and repo we are talking
* about, and the also work out what emails we should send.
*
* @param mixed $payload
* An object given by json_decode()
*
* @return array
*
* @throws \Exception
*/
protected function getDetailsFromPush($payload) {
$this->branch = preg_replace("#.*/(.*)#", "$1", $payload->ref);
if (empty($this->branch)) {
throw new \Exception("Unable to determine branch from payload data");
}
$this->repo = $payload->repository->html_url;
if (empty($this->repo)) {
throw new \Exception("Unable to determine repository from payload data");
}
$recipients = [];
foreach ($payload->commits as $commit) {
$this->addRecipients($recipients, $commit->author->email);
$this->addRecipients($recipients, $commit->committer->email);
}
return [
'commits' => $payload->commits,
'recipients' => $recipients
];
}
/**
* Adds one or more email recipients, and makes sure all recipients are
* kept unique
*
* @param array $new
* Array of strings for emails of people to notify
* @param array $existing
* Existing recipients so far
* @return array
* Unique array of recipients
*/
protected function addRecipients($existing, $new) {
if (!is_array($new)) {
$new = array($new);
}
// remove any email addresses begins with "donotreply@" or "noreply"
$new = preg_grep('/^(donot|no)reply@/', $new, PREG_GREP_INVERT);
return array_unique(array_merge($existing, $new));
}
}
<?php
namespace AppBundle\Utils\WebhookAdapters;
use AppBundle\Model\WebhookEvent;
use Symfony\Component\HttpFoundation\Request;
class GithubHandler implements WebhookHandler {
/**
* @inheritdoc
*/
public function handle(Request $request): WebhookEvent {
$event = new WebhookEvent();
$event->setType($request->headers->get('X-GitHub-Event'));
$event->setSource($event::SOURCE_GITHUB);
$this->getDetailsFromPush($event, $request->getContent());
return $event;
}
/**
* @inheritdoc
*/
public function canHandle(Request $request): bool {
return $request->headers->has('X-Github-Event');
}
/**
* Use a "push" event to figure out what branch and repo we are talking
* about, and the also work out what emails we should send.
*
* @param WebhookEvent $event
* @param string $payload
*
* @return WebhookEvent
*
* @throws \Exception
*/
protected function getDetailsFromPush(WebhookEvent $event, $payload) {
$payload = json_decode($payload);
if (empty($payload)) {
throw new \Exception('Could not decode webhook body');
}
$branch = basename($payload->ref);;
if (empty($branch)) {
throw new \Exception("Unable to determine branch from payload data");
}
$repo = $payload->repository->html_url;
if (empty($repo)) {
throw new \Exception("Unable to determine repository from payload data");
}
$event->setRepo($repo);
$event->setBranch($branch);
$event->setCommits($payload->commits);
return $event;
}
}
<?php
namespace AppBundle\Utils\WebhookAdapters;
use AppBundle\Model\WebhookEvent;
use Symfony\Component\HttpFoundation\Request;
class GitlabHandler implements WebhookHandler {
/**
* @inheritdoc
*/
public function handle(Request $request): WebhookEvent {
$event = new WebhookEvent();
$event->setType($this->getEventType($request));
$event->setSource($event::SOURCE_GITLAB);
$this->getDetailsFromPush($event, $request->getContent());
return $event;
}
/**
* @inheritdoc
*/
public function canHandle(Request $request): bool {
return $request->headers->has('X-Gitlab-Event');
}
/**
* Use a "push" event to figure out what branch and repo we are talking
* about, and the also work out what emails we should send.
*
* @param WebhookEvent $event
* @param string $payload
*
* @return WebhookEvent
*
* @throws \Exception
*/
protected function getDetailsFromPush(WebhookEvent $event, $payload) {
$payload = json_decode($payload);
if (empty($payload)) {
throw new \Exception('Could not decode webhook body');
}
$branch = basename($payload->ref);;
if (empty($branch)) {
throw new \Exception("Unable to determine branch from payload data");
}
$repo = $payload->repository->homepage;
if (empty($repo)) {
throw new \Exception("Unable to determine repository from payload data");
}
$event->setRepo($repo);
$event->setBranch($branch);
$event->setCommits($payload->commits);
return $event;
}
/**
* @param Request $request
* @return string
* @throws \Exception
*/
protected function getEventType(Request $request) : string {
switch ($request->headers->get('X-Gitlab-Event')) {
case 'Push Hook':
return self::EVENT_PUSH;
default:
throw new \Exception('Unrecognized webhook event type');
}
}
}
<?php
namespace AppBundle\Utils\WebhookAdapters;
use AppBundle\Model\WebhookEvent;
use Symfony\Component\HttpFoundation\Request;
interface WebhookHandler {
const EVENT_PUSH = 'push';
/**
* Turns a request into a webhook event
*
* @param Request $request
* @return WebhookEvent
*/
public function handle(Request $request) : WebhookEvent;
/**
* Decides whether or not this adapter can handle the incoming request
*
* @param Request $request
* @return bool
*/
public function canHandle(Request $request) : bool;
}
<?php
namespace AppBundle\Utils;
use AppBundle\Model\WebhookEvent;
use AppBundle\Utils\WebhookAdapters\WebhookHandler;
use Symfony\Component\HttpFoundation\Request;
class WebhookProcessor {
/**
* @var WebhookHandler[]
*/
protected $handlers = [];
/**
* @param WebhookHandler[] $handlers
*/
public function __construct(array $handlers) {
$this->handlers = $handlers;
}
/**
* Process a webhook
*
* @param Request $request
*
* @return WebhookEvent
*
* @throws \Exception
*/
public function process(Request $request) {
$event = null;
foreach ($this->handlers as $handler) {
if ($handler->canHandle($request)) {
$event = $handler->handle($request);
break;
}
}
if (!$event) {
throw new \Exception('Could not handle webhook event');
}
if ($event->getType() != 'push') {
throw new \Exception("Webhook event type is not 'push'");
}
return $event;
}
}
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
namespace AppBundle\Tests\Controller; namespace AppBundle\Tests\Controller;
use AppBundle\Model\Library;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Bundle\SwiftmailerBundle\DataCollector\MessageDataCollector; use Symfony\Bundle\SwiftmailerBundle\DataCollector\MessageDataCollector;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
...@@ -15,10 +16,13 @@ class PublishControllerTest extends WebTestCase { ...@@ -15,10 +16,13 @@ class PublishControllerTest extends WebTestCase {
$client = static::createClient(); $client = static::createClient();
$client->enableProfiler(); $client->enableProfiler();
$hookBody = $this->getGithubRequestBody(); $hookBody = $this->getTestBookRequestBody();
$headers = $this->getHeaders(); $headers = $this->getHeaders();
$endpoint = '/admin/listen'; $endpoint = '/admin/listen';
$testLibrary = new Library(__DIR__ . '/../Files/books');
$client->getContainer()->set('library', $testLibrary);
$client->request('POST', $endpoint, [], [], $headers, $hookBody); $client->request('POST', $endpoint, [], [], $headers, $hookBody);
$statusCode = $client->getResponse()->getStatusCode(); $statusCode = $client->getResponse()->getStatusCode();
...@@ -41,8 +45,8 @@ class PublishControllerTest extends WebTestCase { ...@@ -41,8 +45,8 @@ class PublishControllerTest extends WebTestCase {
/** /**
* @return string * @return string
*/ */
private function getGithubRequestBody(): string { private function getTestBookRequestBody(): string {
return file_get_contents(__DIR__ . '/../Files/webhook-push-sample.json'); return file_get_contents(__DIR__ . '/../Files/webhook-gitlab-push-test-book.json');
} }
/** /**
...@@ -50,7 +54,7 @@ class PublishControllerTest extends WebTestCase { ...@@ -50,7 +54,7 @@ class PublishControllerTest extends WebTestCase {
*/ */
private function getHeaders(): array { private function getHeaders(): array {
$headers = [ $headers = [
'HTTP_X-GitHub-Event' => 'push', // prefix required for non-standard 'HTTP_X-GitLab-Event' => 'Push Hook', // prefix required for non-standard
'Content-Type' => 'application/json' 'Content-Type' => 'application/json'
]; ];