Phing task to create an Unfuddle message

Another day, another new Phing task; again integrating with project management software - this time the excellent Unfuddle.

I’ve been playing with Unfuddle for a few days now and it’s very impressive. You get SVN and git hosting as well as superb issue tracking. It also supports simple project messages (which are displayed on the project dashboard) and so-called notebooks which are essentially project wikis that can be used to house documentation and manuals. One great feature of integrated project management software is the ability to merge news from a variety of sources (SVN commits, changes in ticket status, changes to notebooks) onto a single page that provides a snapshot of the latest activity on a project. Unfuddle does this on each project dashboard, where the latest messages are displayed along side news of the latest SVN and issue activity. The task detailed below provides a means for Phing to automatically add information to this dashboard page by creating a new message.

This extension is very similar to my previous Phing task (for updating a Twitter status), making use of the cURL library to POST XML to Unfuddle. In this case though, the Unfuddle API for creating a message offers a few extra options such as categorising your messages. The task supports the following attributes:

  Unfuddle Name   Message   Description                                           Default   Required
  --------------- --------- ----------------------------------------------------- --------- ----------
  subdomain       String    Subdomain of Unfuddle account
                            (eg. 'example' from <http://example.unfuddle.com>).   n/a       Yes
  projectid       Integer   Project id (eg. 123
                            from <http://example.unfuddle.com/projects/123/>).    n/a       Yes
  username        String    Username.                                             n/a       Yes
  password        String    Password.                                             n/a       Yes
  title           String    Message title.                                        n/a       Yes
  body            String    Message body.                                         ''        No
  categoryid      Integer   The category id of the message.                       ''        No
  categoryids     String    A comma-separated list of category ids (eg. 1,2,3).   ''        No
  checkreturn     Boolean   Whether to check the return code of the request,
                            throws a BuildException the update files.             false     No

The only thing to note here is that you can choose whether you specify a single category id or a collection - it wouldn’t make sense to specify both these attributes.

An example build.xml using this task would be:

<?xml version="1.0" ?>
<project name="Example Unfuddle update" basedir="." default="message">
    <tstamp>
        <format property="build.time" pattern="%Y-%m-%d %H:%I" />
    </tstamp>
    <taskdef name="unfuddlemessage" classname="phing.tasks.my.UnfuddleMessageTask" />
    <target name="message">
        <unfuddlemessage
            subdomain="example"
            projectid="12345"
            username="exampleuser"
            password="password"
            title="Deploying to live site at ${build.time}"
            body=""
            categoryid="4" />
    </target>
</project>

This simply creates a new Unfuddle message with the time of the last build. This is an overly simplified example - see my previous post for a sample parameterised deployment target that would allow a dynamic message to be created by different targets within the deployment file.

The source code for TwitterUpdateTask.php is as follows (with docblocks stripped out for brevity):

<?php
require_once "phing/Task.php";
class UnfuddleMessageTask extends Task
{
    const URL_TEMPLATE_UPDATE = 'http://%s.unfuddle.com/api/v1/projects/%d/messages';

    // Twitter response codes
    const HTTP_RESPONSE_OK                  = 200;
    const HTTP_RESPONSE_CREATED             = 201;
    const HTTP_RESPONSE_BAD_REQUEST         = 400;
    const HTTP_RESPONSE_BAD_CREDENTIALS     = 401;
    const HTTP_RESPONSE_BAD_URL             = 404;
    const HTTP_RESPONSE_METHOD_NOT_ALLOWED  = 405;
    const HTTP_RESPONSE_SERVER_ERROR        = 500;
    const HTTP_RESPONSE_BAD_GATEWAY         = 502;
    const HTTP_RESPONSE_SERVICE_UNAVAILABLE = 503;

    private static $responseMessages = array(
        self::HTTP_RESPONSE_BAD_REQUEST         => 'Bad request - you may have exceeded the rate limit',
        self::HTTP_RESPONSE_BAD_CREDENTIALS     => 'Your username and password did not authenticate',
        self::HTTP_RESPONSE_BAD_URL             => 'The Unfuddle URL is invalid',
        self::HTTP_RESPONSE_METHOD_NOT_ALLOWED  => 'The specified HTTP verb is not allowed',
        self::HTTP_RESPONSE_SERVER_ERROR        => 'There is a problem with the Unfuddle server',
        self::HTTP_RESPONSE_BAD_GATEWAY         => 'Unfuddle is either down or being upgraded',
        self::HTTP_RESPONSE_SERVICE_UNAVAILABLE => 'Unfuddle servers are refusing request',
    );

    private $subdomain;
    private $projectId;
    private $username;
    private $password;
    private $title;
    private $body;
    private $categoryIds;
    private $checkReturn = false;

    public function setSubdomain($subdomain)
    {
        $this->subdomain = $subdomain;
    }
    public function setProjectId($projectId)
    {
        $this->projectId = (int)$projectId;
    }
    public function setUsername($username)
    {
        $this->username = $username;
    }
    public function setPassword($password)
    {
        $this->password = $password;
    }
    public function setTitle($title)
    {
        $this->title = $title;
    }
    public function setBody($body)
    {
        $this->body = $body;
    }
    public function setCategoryId($categoryId)
    {
        $this->categoryIds = array((int)$categoryId);
    }
    public function setCategoryIds($categoryIdList)
    {
        $this->categoryIds = explode(",", $categoryIdList);
    }
    public function setCheckReturn($checkReturn)
    {
        $this->checkReturn = (boolean)$checkReturn;
    }

    public function init()
    {
        if (!extension_loaded('curl')) {
            throw new BuildException("Cannot update Unfuddle", "The cURL extension is not installed");
        }
    }
    public function main()
    {
        $this->validateProperties();

        $curlHandle = curl_init();
        curl_setopt($curlHandle, CURLOPT_URL, $this->getUpdateUrl());
        curl_setopt($curlHandle, CURLOPT_USERPWD, "$this->username:$this->password");
        curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curlHandle, CURLOPT_HTTPHEADER, array('Accept: application/xml', 'Content-type: application/xml'));
        curl_setopt($curlHandle, CURLOPT_POST, true);
        curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $this->getRequestBodyXml());
        $responseData = curl_exec($curlHandle);
        $responseCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
        $errorCode    = curl_errno($curlHandle);
        $errorMessage = curl_error($curlHandle);
        curl_close($curlHandle);

        if (0 != $errorCode) {
            throw new BuildException("cURL error ($errorCode): $errorMessage");
        }
        $this->handleResponseCode((int)$responseCode);
    }
    private function validateProperties()
    {
        if (!$this->subdomain) {
            throw new BuildException("You must specify a subdomain");
        }
        if (!$this->projectId) {
            throw new BuildException("You must specify a project id");
        }
        if (!$this->username || !$this->password) {
            throw new BuildException("You must specify an Unfuddle username and password");
        }
        if (!$this->title) {
            throw new BuildException("You must specify a message title");
        }
    }
    private function getUpdateUrl()
    {
        return sprintf(self::URL_TEMPLATE_UPDATE, $this->subdomain, $this->projectId);
    }
    private function getRequestBodyXml()
    {
        $xmlWriter = new XMLWriter();
        $xmlWriter->openMemory();
        $xmlWriter->startElement('message');
        $xmlWriter->writeElement('title', $this->title);
        $xmlWriter->writeElement('body', $this->body);

        if ($this->categoryIds) {
            $xmlWriter->startElement('categories');
            foreach ($this->categoryIds as $categoryId) {
                $xmlWriter->startElement('category');
                $xmlWriter->writeAttribute('id', "$categoryId");
                $xmlWriter->endElement();
            }
            $xmlWriter->endElement();
        }
        $xmlWriter->endElement();
        return $xmlWriter->flush();
    }
    private function handleResponseCode($code)
    {
        if ($code == self::HTTP_RESPONSE_CREATED) {
            $this->log("New Unfuddle message posted: '$this->title'", Project::MSG_INFO);
            return;
        }
        if (array_key_exists($code, self::$responseMessages)) {
            $this->handleFailedUpdate(self::$responseMessages[$code]);
        } else {
            $this->handleFailedUpdate("Unrecognised HTTP response code '$code' from Unfuddle");
        }
    }
    private function handleFailedUpdate($failureMessage)
    {
        if (true === $this->checkReturn) {
            throw new BuildException($failureMessage);
        }
        $this->log("New Unfuddle message unsuccessful: $failureMessage", Project::MSG_WARN);
    }
}

The fully documented source and associated example build.xml file are available to download: UnfuddleMessageTask.zip (2.6kb)

——————

Something wrong? Suggest an improvement or add a comment (see article history)
Tagged with: phing
Filed in: tips

Previous: Phing task to update Twitter status
Next: Current pet project: Command-Line-Fu

Copyright © 2005-2023 David Winterbottom
Content licensed under CC BY-NC-SA 4.0.