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:
arguments:
- %books_dir%
github.hook.processor:
class: AppBundle\Utils\GitHubHookProcessor
webhook.processor:
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:
class: AppBundle\Utils\MkDocs
......
......@@ -2,6 +2,7 @@
namespace AppBundle\Controller;
use AppBundle\Model\WebhookEvent;
use AppBundle\Utils\Publisher;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
......@@ -56,50 +57,46 @@ class PublishController extends Controller {
* @Route("/admin/listen")
*/
public function ListenAction(Request $request) {
$body = $request->getContent();
$event = $request->headers->get('X-GitHub-Event');
$processor = $this->get('github.hook.processor');
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);
}
$processor = $this->get('webhook.processor');
$event = $processor->process($request);
$library = $this->get('library');
$identifiers = $library->getIdentifiersByRepo($processor->repo);
if ($identifiers) {
$this->publisher = $this->get('publisher');
foreach ($identifiers as $identifier) {
$fullIdentifier = "{$identifier}/{$processor->branch}";
$this->publisher->publish($fullIdentifier);
$this->sendEmail($fullIdentifier, $hookData);
}
$response = $this->publisher->getMessagesInPlainText();
$identifiers = $library->getIdentifiersByRepo($event->getRepo());
if (!$identifiers) {
$msg = "CRITICAL - No books found which match " . $event->getRepo();
return new Response($msg, Response::HTTP_BAD_REQUEST);
}
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
*
* @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
* notification email. If none are specified, then the email will be sent to
* all addresses set in the book's yaml configuration
*/
$extraRecipients = $hookData['recipients'];
$commits = $hookData['commits'];
$extraRecipients = $event->getNotificationRecipients();
$commits = $event->getCommits();
$library = $this->get('library');
$messages = $this->get('publisher')->getMessages();
$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 @@
{% for commit in commits %}
<li>
<strong><a href="{{ commit.url }}">{{ commit.id }}</a></strong> |
{{ commit.message }} | {{ commit.author.username }}
{{ commit.message }} | {{ commit.author.name }}
</li>
{% endfor %}
</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 @@
namespace AppBundle\Tests\Controller;
use AppBundle\Model\Library;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Bundle\SwiftmailerBundle\DataCollector\MessageDataCollector;
use Symfony\Component\HttpFoundation\Response;
......@@ -15,10 +16,13 @@ class PublishControllerTest extends WebTestCase {
$client = static::createClient();
$client->enableProfiler();
$hookBody = $this->getGithubRequestBody();
$hookBody = $this->getTestBookRequestBody();
$headers = $this->getHeaders();
$endpoint = '/admin/listen';
$testLibrary = new Library(__DIR__ . '/../Files/books');
$client->getContainer()->set('library', $testLibrary);
$client->request('POST', $endpoint, [], [], $headers, $hookBody);
$statusCode = $client->getResponse()->getStatusCode();
......@@ -41,8 +45,8 @@ class PublishControllerTest extends WebTestCase {
/**
* @return string
*/
private function getGithubRequestBody(): string {
return file_get_contents(__DIR__ . '/../Files/webhook-push-sample.json');
private function getTestBookRequestBody(): string {
return file_get_contents(__DIR__ . '/../Files/webhook-gitlab-push-test-book.json');
}
/**
......@@ -50,7 +54,7 @@ class PublishControllerTest extends WebTestCase {
*/
private function getHeaders(): array {
$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'
];
......
name: TestBook
description: Real repo, but used for testing publishing
langs:
en:
repo: 'https://lab.civicrm.org/seanmadsen/docs-test-book'
......@@ -39,7 +39,7 @@
"id": "c337788155114d32a585be201c7c9a96bcb2ba9a",
"tree_id": "c10d6a99bb2413c68db1afb2e56239ae7f673ffe",
"distinct": true,
"message": "Merge pull request #409 from meusselea/build_profile_doc_imporvemet\n\nTidy up hook_civicrm_buildForm Documentation and remove comments from…",
"message": "Merge pull request #409 from meusselea/build_profile_doc_improvement",
"timestamp": "2017-10-09T20:24:31+01:00",
"url": "https://github.com/civicrm/civicrm-dev-docs/commit/c337788155114d32a585be201c7c9a96bcb2ba9a",
"author": {
......@@ -67,7 +67,7 @@
"id": "c337788155114d32a585be201c7c9a96bcb2ba9a",
"tree_id": "c10d6a99bb2413c68db1afb2e56239ae7f673ffe",
"distinct": true,
"message": "Merge pull request #409 from meusselea/build_profile_doc_imporvemet\n\nTidy up hook_civicrm_buildForm Documentation and remove comments from…",
"message": "Merge pull request #409 from meusselea/build_profile_doc_improvement",
"timestamp": "2017-10-09T20:24:31+01:00",
"url": "https://github.com/civicrm/civicrm-dev-docs/commit/c337788155114d32a585be201c7c9a96bcb2ba9a",
"author": {
......
{
"object_kind": "push",
"before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
"after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"ref": "refs/heads/master",
"checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"user_id": 4,
"user_name": "John Smith",
"user_username": "jsmith",
"user_email": "john@example.com",
"user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
"project_id": 15,
"project":{
"name":"Diaspora",
"description":"",
"web_url":"http://example.com/mike/diaspora",
"avatar_url":null,
"git_ssh_url":"git@example.com:mike/diaspora.git",
"git_http_url":"http://example.com/mike/diaspora.git",
"namespace":"Mike",
"visibility_level":0,
"path_with_namespace":"mike/diaspora",
"default_branch":"master",
"homepage":"http://example.com/mike/diaspora",
"url":"git@example.com:mike/diaspora.git",
"ssh_url":"git@example.com:mike/diaspora.git",
"http_url":"http://example.com/mike/diaspora.git"
},
"repository":{
"name": "Diaspora",
"url": "git@example.com:mike/diaspora.git",
"description": "",
"homepage": "http://example.com/mike/diaspora",
"git_http_url":"http://example.com/mike/diaspora.git",
"git_ssh_url":"git@example.com:mike/diaspora.git",
"visibility_level":0
},
"commits": [
{
"id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
"message": "Update Catalan translation to e38cb41.",
"timestamp": "2011-12-12T14:27:31+02:00",
"url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
"author": {
"name": "Jordi Mallach",
"email": "jordi@softcatala.org"
},
"added": ["CHANGELOG"],
"modified": ["app/controller/application.rb"],
"removed": []
},
{
"id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"message": "fixed readme",
"timestamp": "2012-01-03T23:36:29+02:00",
"url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"author": {
"name": "GitLab dev user",
"email": "gitlabdev@dv6700.(none)"
},