Thursday, 17 April 2008

Hooking a Growl publisher plugin into Xinc

This week I finally had the time to setup Xinc, PHP's shiny new Continuous Integration(CI) server, on my MacBook and started to fiddle with it's publisher plugins and plugin architecture. While still down with 'tool envy' influenza I recently came across some nice and inspiring blog posts from the Ruby/Rails camp, showing how to employ Growl as an useful and fun feedback radiator for test/spec results. Since then the idea of building a Growl publisher plugin for Xinc was travelling my mind repeatedly, so the following post will break this circle and show a possible approach to build such a plugin, which can be used to notify the build result for continuously integrated projects and thereby provide an on-point/immediate feedback.

Building the Growl publisher plugin

Xinc comes with an elegant plugin system, allowing you to easily role your own one. The following code shows the plugin class and is the place where the actual Growl notifications are executed/raised by calling the growlnotify CLI. The images in the public growl() method, used to accent the two main build statuses(failure and success) in the Growl notification, are borrowed from this blog post and should be located in the $HOME/Pictures directory.
<?php
/**
* A Growl publisher plugin for Xinc
*
* @package Xinc.Plugin
* @author Raphael Stolt
* @version 2.0
* @copyright 2008 Raphael Stolt, Constance
* @license http://www.gnu.org/copyleft/lgpl.html GNU/LGPL, see license.php
*/
require_once 'Xinc/Plugin/Base.php';
require_once 'Xinc/Plugin/Repos/Publisher/Growl/Task.php';

class Xinc_Plugin_Repos_Publisher_Growl extends Xinc_Plugin_Base
{
public function validate()
{
return true;
}
public function getTaskDefinitions()
{
return array(new Xinc_Plugin_Repos_Publisher_Growl_Task($this));
}
private function _sendGrowlNotification($message, $image, $name)
{
$command = "growlnotify -w -m '{$message}' "
. "-n '{$name}' "
. "-p 2 --image {$image}";

Xinc_Logger::getInstance()->info('Executed growlnotify command: ' . $command);

exec($command, $response, $return);

if ($return === 0) {
return true;
}
return false;
}
public function growl(Xinc_Project &$project, $message, $buildstatus, $name = 'Xinc')
{
if ($buildstatus === Xinc_Build_Interface::PASSED) {
$image = '$HOME/Pictures/pass.png';
$buildstatus = 'PASSED';
} elseif ($buildstatus === Xinc_Build_Interface::FAILED) {
$image = '$HOME/Pictures/fail.png';
$buildstatus = 'FAILED';
}

$project->info('Executing Growl publisher with content '
."\nMessage: " . $message
."\nBuildstatus: " . $buildstatus
."\nImage: " . $image);

return $this->_sendGrowlNotification($message, $image, $name);
}
}
The next big code chunk shows the task definition of the plugin and is the place where the publisher plugin name and is acceptable and required attributes are programmaticly defined, as you can see the Growl task currently only takes a required message attribute which can be set for the Growl publisher within the Xinc project configuration file.
<?php
/**
* A Growl publisher plugin task for Xinc
*
* @package Xinc.Plugin
* @author Raphael Stolt
* @version 2.0
* @copyright 2008 Raphael Stolt, Constance
* @license http://www.gnu.org/copyleft/lgpl.html GNU/LGPL, see license.php
*/
require_once 'Xinc/Plugin/Repos/Publisher/AbstractTask.php';

class Xinc_Plugin_Repos_Publisher_Growl_Task extends Xinc_Plugin_Repos_Publisher_AbstractTask
{
private $_message;

public function setMessage($message)
{
$this->_message = $message;
}
public function getName()
{
return 'growl';
}
private function _isGrowlnotifyAvailable()
{
exec('growlnotify -v', $reponse, $return);

if ($return === 0) {
return true;
}
return false;
}
public function validateTask()
{
if (!$this->_isGrowlnotifyAvailable()) {
$message = 'The growlnotify command seems to be not available '
. 'on this system';
Xinc_Logger::getInstance()->error($message);
throw new RuntimeException($message);
}

if (!isset($this->_message)) {
$message = 'Element publisher/growl - required attribute '
. '\'message\' is not set';
throw new Xinc_Exception_MalformedConfig($message);
}
return true;
}
public function publish(Xinc_Build_Interface &$build)
{
$statusBefore = $build->getStatus();
$res = $this->_plugin->growl($build->getProject(), $this->_message, $build->getStatus());
if (!$res && $statusBefore == Xinc_Build_Interface::PASSED ) {
/**
* Status was PASSED, but now the publish process made it fail
*/
$build->setStatus(Xinc_Build_Interface::FAILED);
}
}
}

Hooking the Growl publisher plugin into the build system

To make the just crafted Growl publisher plugin available to the Xinc build system you have to add the plugin file and class name/path to the /etc/xinc/system.xml file. After restarting the Xinc server the Growl publisher should be available to use and also be listed in the section of the /var/log/xinc.log file stating all registered plugins.

The next listing shows the Growl publisher plugin added to the aforementioned XML file.
<?xml version="1.0" encoding="UTF-8"?>
<xinc>
<configuration>
<setting name="loglevel" value="2"/>
<setting name="timezone" value="Europe/Berlin"/>
</configuration>
<plugins>
<plugin filename="Xinc/Plugin/Repos/ModificationSet.php" classname="Xinc_Plugin_Repos_ModificationSet"/>
...
<plugin filename="Xinc/Plugin/Repos/Publisher/Growl.php" classname="Xinc_Plugin_Repos_Publisher_Growl"/>
...
<plugin filename="Xinc/Contrib/Warko/Plugin/ModificationSet/SvnTag.php"
classname="Xinc_Contrib_Warko_Plugin_ModificationSet_SvnTag"/>
</plugins>

<engines>
<engine classname="Xinc_Engine_Sunrise" filename="Xinc/Engine/Sunrise.php" default="default"/>
</engines>
</xinc>

Putting the Xinc Growl publisher plugin to action

Now that the Growl publisher plugin is available it can be used in the build cycle. The following Xinc project configuration file shows the utilization of the Growl publisher plugin in both of the main publisher realms onsuccess and onfailure.
<?xml version="1.0" encoding="UTF-8"?>
<xinc engine="Sunrise">
<project name="recordshelf">
<configuration>
<setting name="loglevel" value="2"/>
</configuration>
<property name="dir" value="${projectdir}/${project.name}" />
<property name="build.failure.message" value="Build for project ${project.name} failed!"/>
<schedule interval="180" />
<modificationset>
<svn directory="${dir}" update="true" />
</modificationset>
<builders>
<phingbuilder buildfile="${dir}/build.xml" target="main"/>
</builders>
<publishers>
<onfailure>
<email to="example@example.com" subject="[${project.name}] Build failure"
message="${build.failure.message}" />
<growl message="${build.failure.message}" />
</onfailure>
<onsuccess>
<email to="example@example.com" subject="[${project.name}] Build success"
message="Build for project was successful" />
<growl message="Build for project ${project.name} was successful." />
</onsuccess>
</publishers>
</project>
</xinc>
The next two images are finally showing the Growl notifications for a successful and a failed build. To minimize the notification noise it's a good and common practice to only 'ring the alarm' for failed builds, which can be achieved by using the Growl publisher only in the onfailure publisher realm of the Xinc project configuration file. Nuff talk, happy Xincing!

Growl notification for a successful build

Growl notification for failed build

No comments: