Commit f7261a9d authored by Sean Madsen's avatar Sean Madsen

refactor - publishing via GitHub webhooks

parent 633649bc
<p><strong>Note from <a href="{{ app.request.getUriForPath('/') }}">{{ app.request.getUriForPath('/') }}</a>:<br />Published '{{ branch }}' branch of '
{{ book }}' in '{{ lang }}'.</strong>
</p>
<p>Please check the publish log for any errors:</p>
<ul>
{% for message in messages %}
<li>
<strong>{{ message.label }}:</strong>
{{ message.content|raw }}
</li>
{% endfor %}
</ul>
......@@ -12,9 +12,6 @@ services:
github.hook.processor:
class: AppBundle\Utils\GitHubHookProcessor
arguments:
- '@publisher'
- '@library'
publisher:
class: AppBundle\Utils\Publisher
......
......@@ -8,31 +8,39 @@ use Symfony\Component\HttpFoundation\Request;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use AppBundle\Model\Library;
/**
* @TODO Make the errors that will occur if the slugs don't pass the regexp
* more obvious to the user
*/
class PublishController extends Controller {
/**
*
* @var \AppBundle\Utils\Publisher
*/
private $publisher;
/**
*
* @var bool TRUE if the book was published without any errors
*/
private $publishSuccess;
/**
* @Route("/admin/publish{identifier}" , requirements={"identifier": ".*"})
*/
public function PublishAction(Request $request, $identifier) {
/** @var \AppBundle\Utils\Publisher $publisher */
$publisher = $this->get('publisher');
$this->publisher = $this->get('publisher');
$bookSlug = Library::parseIdentifier($identifier)['bookSlug'];
if ($bookSlug) {
$publisher->publish($identifier);
$this->publishSuccess = $this->publisher->publish($identifier);
}
else {
$publisher->addMessage('INFO', "Publish action called without a book "
$this->publisher->addMessage('INFO', "Publish action called without a book "
. "specified, thus attempting to publish all books.");
$publisher->addMessage('CRITICAL', "Publishing all books it not "
$this->publisher->addMessage('CRITICAL', "Publishing all books it not "
. "supported through the web interface because it has the potential "
. "to really slow down the server. If you want to publish all books "
. "you can run 'docs:publish' from the command line interface.");
}
$content['messages'] = $publisher->getMessages();
$content['messages'] = $this->publisher->getMessages();
return $this->render('AppBundle:Publish:publish.html.twig', $content);
}
......@@ -42,33 +50,59 @@ class PublishController extends Controller {
public function ListenAction(Request $request) {
$body = $request->getContent();
$event = $request->headers->get('X-GitHub-Event');
$payload = json_decode($body);
$processor = $this->get('github.hook.processor');
$processor->process($event, $payload);
if (!$processor->published) {
return new Response('Something went wrong during publishing.', 200); // @TODO Add more appropriate error code
try {
$processor->process($event, json_decode($body));
}
$messages = $processor->getMessages();
$subject = $processor->getSubject();
$recipients = $processor->getRecipients();
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');
$identifiers = $library->getIdentifiersByRepo($processor->repo);
if ($identifiers) {
$this->publisher = $this->get('publisher');
foreach ($identifiers as $identifier) {
$this->publisher->publish("{$identifier}/{$processor->branch}");
$this->sendEmail($processor->recipients);
}
$response = $this->publisher->getMessagesInPlainText();
}
else {
$response = "CRITICAL - No books found which match {$processor->repo}";
}
return new Response($response, 200);
}
/**
* Send notification emails after publishing
*
* @param array $extraRecipients Array of strings for email addresses that
* should receive the notification email. If
* non are specified, then the email will be
* sent to all addresses set in the book's yaml
* configuration.
*/
private function sendEmail($extraRecipients = array()) {
$subject = $this->publisher->status;
$recipients = array_unique(array_merge(
$extraRecipients,
$this->publisher->language->watchers));
$mail = \Swift_Message::newInstance()
->setSubject("[CiviCRM docs] $subject")
->setFrom('no-reply@civicrm.org')
->setSubject("$subject")
->setFrom('no-reply@civicrm.org', "CiviCRM docs")
->setTo($recipients)
->setBody(
$this->renderView('Emails/notify.html.twig',
$this->renderView('AppBundle:Emails:notify.html.twig',
array(
'branch' => $processor->publisher->branch,
'book' => $processor->publisher->book,
'lang' => $processor->publisher->lang,
'messages' => $processor->publisher->getMessages(),
'publishURLBase' => $this->publisher->publishURLBase,
'status' => $this->publisher->status,
'messages' => $this->publisher->messages,
)
), 'text/html'
);
$this->get('mailer')->send($mail);
return new Response($subject, 200);
}
}
......@@ -89,7 +89,7 @@ class Book {
"static",
);
if (in_array($this->slug, $illegalBookSlugs)) {
throw new Exception("Book slug is '{$this->slug}' but this word is "
throw new \Exception("Book slug is '{$this->slug}' but this word is "
. "reserved in order to maintain functionality within this app. "
. "Reserved words are: " . implode(", ", $illegalBookSlugs));
}
......
......@@ -23,6 +23,14 @@ class Language {
*/
public $code;
/**
*
* @var array email addresses for people who would like to receive a
* notification any time a version within this language is
* published
*/
public $watchers = array();
/**
* Initialize a language with values in it.
*
......@@ -33,6 +41,11 @@ class Language {
$this->code = $code;
$this->repo = $yaml['repo'];
$this->setupVersions($yaml);
if (isset($yaml['watchers'])) {
foreach ($yaml['watchers'] as $watcher) {
$this->watchers[] = $watcher;
}
}
}
/**
......@@ -82,7 +95,7 @@ class Language {
private function validateCode() {
if (!LocaleTools::codeIsValid($this->code)) {
throw new Exception("Language code '{$this->code}' is not a valid "
throw new \Exception("Language code '{$this->code}' is not a valid "
. "ISO 639-1 code.");
}
}
......@@ -119,7 +132,7 @@ class Language {
$duplicateDescriptors
= array_diff_assoc($descriptors, array_unique($descriptors));
if ($duplicateDescriptors) {
throw new Exception(
throw new \Exception(
"Duplicate descriptors '" . implode(", ", $duplicateDescriptors)
. "' found for the versions defined within language '{$this->code}'");
}
......
......@@ -99,6 +99,32 @@ class Library {
return $chosen;
}
/**
* See which books/languages are using a given repository.
*
* @param string $repoURL
*
* @return array of strings to identify each occurence of a book/language
* which matches the specified repository. Example return:
* ["mybook/en", "mybook/es"]
* Note that it's rare for a repository to map to multiple
* identifiers. In most cases the return will be an array with
* a single element.
* Note also that the return identifier only has the book slug
* and the language code, not the branch name.
*/
public function getIdentifiersByRepo($repoURL) {
$identifiers = array();
foreach ($this->books as $book) {
foreach ($book->languages as $language) {
if ($language->repo == $repoURL) {
$identifiers[] = "$book->slug/$language->code";
}
}
}
return $identifiers;
}
/**
* Parses an identifier into components we can use to identify a book
*
......
<p><strong>Note from
<a href="{{ publishURLBase }}">{{ publishURLBase }}</a>:<br />
{{ status }}</strong>
</p>
<p>Please check the publish log for any errors:</p>
<ul>
{% for message in messages %}
<li><strong>{{ message.label }}:</strong> {{ message.content|raw }}</li>
{% endfor %}
</ul>
<p>You are receiving this message becuase either (a) you have made commits
included in the changes currently being published, or (b) your email address
is set for notification in this book's repository.</p>
<p>If you have questions, please contact the CiviCRM documentation working
group here:<br />
<a href='https://chat.civicrm.org/civicrm/channels/documentation'>
https://chat.civicrm.org/civicrm/channels/documentation</a></p>
......@@ -4,65 +4,61 @@ namespace AppBundle\Utils;
class GitHubHookProcessor {
protected $messages = array();
protected $recipients = array();
public $published = FALSE;
/**
* @var array of strings for email addresses of people to notify
*/
public $recipients = array();
/**
* @var string the URL for the repository
*/
public $repo;
/**
* @var string the name of the branch to publish
*/
public $branch;
/**
*
*/
public function __construct() {
public function __construct($publisher, $bookLoader) {
$this->publisher = $publisher;
$this->books = $bookLoader->find();
}
/**
*
* @param string $event (e.g. 'pull_request', 'push')
* @param mixed $payload An object given by json_decode()
*/
public function process($event, $payload) {
//The getDetailsFrom functions work out what branch and repo we are talking
//about, and the also work out what emails we should send.
switch ($event) {
case 'pull_request':
$this->getDetailsFromPullRequest($payload);
break;
case 'push':
$this->getDetailsFromPush($payload);
break;
if ($event == 'pull_request') {
$this->getDetailsFromPullRequest($payload);
}
foreach ($this->books as $bookName => $bookConfig) {
foreach ($bookConfig['langs'] as $bookLang => $bookLangConfig) {
if ($bookLangConfig['repo'] == $this->repo) {
$this->book = $bookName;
$this->lang = $bookLang;
$config = $bookLangConfig;
break 2;
}
}
elseif ($event == 'push') {
$this->getDetailsFromPush($payload);
}
// If the book (in a specific language) wants additional people to be
// notified on each publication, they can be added in the book yml
// definition and will get added here
if (isset($config['notify'])) {
foreach ($config['notify'] as $recipient) {
$this->recipients[] = $recipient;
}
if (!$this->branch) {
throw new \Exception("Unable to determine branch from payload data");
}
if (!$this->repo) {
throw new \Exception("Unable to determine repository from payload data");
}
$this->published = $this->publisher->publish(
$this->book, $this->lang, $this->branch);
$this->messages = $this->publisher->getMessages();
$this->subject = "Published '{$this->branch}' branch of '{$this->book}'"
. "in '{$this->lang}'";
$this->recipients = array_unique($this->recipients);
}
/**
* Use a pull request 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()
*/
public function getDetailsFromPullRequest($payload) {
//Only continue if this pull request is closed and merged
if ($payload->action != 'closed' OR !$payload->pull_request->merged) {
return;
if ($payload->action != 'closed') {
throw new \Exception("Pull request is not closed");
}
if (!$payload->pull_request->merged) {
throw new \Exception("Pull request is not merged");
}
//Work out what book, language and branch to publish
$this->branch = $payload->pull_request->base->ref;
$this->repo = $payload->repository->html_url;
......@@ -78,31 +74,37 @@ class GitHubHookProcessor {
$this->commits = json_decode(curl_exec($ch));
foreach ($this->commits as $commit) {
$this->recipients[] = $commit->commit->author->email;
$this->recipients[] = $commit->commit->committer->email;
$this->addRecipients($commit->commit->author->email);
$this->addRecipients($commit->commit->committer->email);
}
}
/**
* Use a pull request 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()
*/
public function getDetailsFromPush($payload) {
$this->branch = preg_replace("/.*\/(.*)/", "$1", $payload->ref);
$this->branch = preg_replace("#.*/(.*)#", "$1", $payload->ref);
$this->repo = $payload->repository->html_url;
foreach ($payload->commits as $commit) {
$this->recipients[] = $commit->author->email;
$this->recipients[] = $commit->committer->email;
$this->addRecipients($commit->author->email);
$this->addRecipients($commit->committer->email);
}
}
public function getMessages() {
return $this->messages;
}
public function getRecipients() {
return $this->recipients;
}
public function getSubject() {
return $this->subject;
/**
* Adds one or more email recipients, and makes sure all recipients are
* kept unique
*
* @param array $recipients Array of strings for emails of people to notify
*/
public function addRecipients($recipients) {
if (!is_array($recipients)){
$recipients = array($recipients);
}
$this->recipients = array_unique(array_merge($this->recipients, $recipients));
}
}
......@@ -39,7 +39,24 @@ class Publisher {
* @var array Messages with key as a string to represent message type and
* value as a string with the message content
*/
public $messages;
public $messages = array();
/**
*
* @var string A simple description of the status of the publishing operation
*/
public $status = "Book not published";
/**
* @var string the identifier passed in when calling publish()
*/
private $suppliedIdentifier;
/**
*
* @var string (e.g. "user/en/4.6", "dev/en/master")
*/
public $fullIdentifier;
/**
* @var string The domain name of the site (e.g. "https://docs.civicrm.org")
......@@ -124,10 +141,10 @@ class Publisher {
* @return boolean TRUE if success
*/
private function initializeLocations() {
$this->publishURLFull = "{$this->publishURLBase}/{$this->book->slug}/"
. "{$this->language->code}/{$this->version->branch}";
$this->publishPath = "{$this->publishPathRoot}/{$this->book->slug}/"
. "{$this->language->code}/{$this->version->branch}";
$this->fullIdentifier = "{$this->book->slug}/{$this->language->code}/"
. "{$this->version->branch}";
$this->publishURLFull = "{$this->publishURLBase}/{$this->fullIdentifier}";
$this->publishPath = "{$this->publishPathRoot}/{$this->fullIdentifier}";
$this->repoURL = $this->language->repo;
$this->repoPath = $this->repoPathRoot . "/{$this->book->slug}/"
. "{$this->language->code}";
......@@ -386,26 +403,34 @@ class Publisher {
* @return bool TRUE if all books were published, FALSE if there were any
* errors while publishing any of the books
*/
public function publish($identifier = NULL) {
public function publish($identifier = "") {
$this->suppliedIdentifier = $identifier;
$this->addMessage('NOTICE', "PUBLISHING $identifier");
$parts = Library::parseIdentifier($identifier);
$bookSlug = $parts['bookSlug'];
$languageCode = $parts['languageCode'];
$versionDescriptor = $parts['versionDescriptor'];
if ($versionDescriptor) {
return
$success =
$this->initializeBook($bookSlug) &&
$this->initializeLanguage($languageCode) &&
$this->publishVersion($versionDescriptor);
}
elseif ($languageCode) {
return
$success =
$this->initializeBook($bookSlug) &&
$this->publishLanguage($languageCode);
}
elseif ($bookSlug) {
return $this->publishBook($bookSlug);
$success = $this->publishBook($bookSlug);
}
else {
$success = $this->publishLibrary();
}
if ($success) {
$this->setStatus('success');
}
return $this->publishLibrary();
return $success;
}
/**
......@@ -479,12 +504,37 @@ class Publisher {
}
/**
* @param string $label should be 'INFO', 'WARNING', or 'CRITICAL'
* Set the publishing status based on available info
*
* @param string $type Should be either "failure" or "success"
*/
private function setStatus($type) {
$phrase = $this->suppliedIdentifier;
if ($this->book) {
$phrase = $this->book->name;
if ($this->language) {
$phrase .= " / {$this->language->englishName()}";
if ($this->version) {
$phrase .= " / {$this->version->name}";
}
}
}
if ($type == 'failure') {
$this->status = "Errors trying to publish: $phrase";
}
elseif ($type == 'success') {
$this->status = "Published: $phrase";
}
}
/**
* @param string $label should be 'NOTICE', 'INFO', 'WARNING', or 'CRITICAL'
* @param string $content
*/
public function addMessage($label, $content) {
$this->messages[] = array('label' => $label, 'content' => $content);
$this->logger->addRecord($this->logger->toMonologLevel($label), $content);
$this->setStatus('failure'); // this gets set to 'success' when we're done
}
/**
......@@ -494,6 +544,17 @@ class Publisher {
return $this->messages;
}
/**
* @return string all messages as lines in one big string
*/
public function getMessagesInPlainText() {
$text = '';
foreach ($this->messages as $message) {
$text = "{$text}{$message['label']} - {$message['content']}\n";
}
return $text;
}
/**
* Deletes all stored messages
*/
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment