Deployment hook in XL Deploy
Now that we've migrated to TFS 2013 we now have Team Rooms available for all the projects. TFS provides a number of default events such as work item state changes and builds that have completed. Because we use XL Deploy to handle our automated deployments I wanted to include those events to the team room as well.
XL Deploy has a nice REST API but unfortunately there is no way to register a hook when a deployment completes. Fortunately XL Deploy has a way to add rules that can execute extra steps at various points in a deployment. I've used this to add a step to the end of a deployment plan that uses a little Jython script to call a remote REST API whenever the deployment completes.
Adding a rule
XL Deploy stores it's rules under the $XLDEPLOYPATH/server/ext
folder in the xl-rules.xml file and this is where we'll add the necessary configuration to run our script.
The rule definition looks like
<rule name="DeploymentSucceeded" scope="post-plan">
<steps>
<jython>
<order>99999</order>
<description>Execute application deployed hook</description>
<script-path>hooks/application-deployed.py</script-path>
<jython-context>
<application expression="true">context.deployedApplication.name</application>
<environment expression="true">context.deployedApplication.environment.name</environment>
<version expression="true">context.deployedApplication.version.name</version>
<hooksDict expression="true">context.repository.read("Environments/Hooks")</hooksDict>
</jython-context>
</jython>
</steps>
</rule>
I've set the rule scope to be post-plan
to make sure that this step is only added after the validation and planning stages have completed. The rule adds a new jython step that instructs XL Deploy to call the script defined in script-path
and is a relative path starting from $XLDEPLOYPATH/server/ext
on the XL Deploy server.
To provide the script with some useful information you can define a number of parameters in the jython-context
. These parameters will be made available as variables in the jython script. In this example I pass in the application, environment and version names. The hooksDict
parameter is pointing to a dictionary under Environments/Hooks
in the XL Deploy repository and it contains the URL of the REST API we want to call.
! Before you add this to your xl-rules.xml be sure to make a backup first!
Depending on the configuration of your XL Deploy instance you may need to restart the server before the rule is activated.
The Hooks dictionary
To be able to configure the URL that the script will be calling I've added a dictionary under the Environments folder in the XL Deploy repository. This dictionary contains the following settings:
Key | Value |
---|---|
host | localhost:8000 |
deployment-complete-url | /$application/$environment/$version/$username |
The $application
, $environment
, $version
and $username
placeholders will be replaced by the Jython script later on. If you leave host
empty the script won't run which makes it an easy way to disable the hook alltogether.
Creating the script
Now that we have the rule in place we need to act whenever the step is triggered. The script is actually fairly simple, it collects the variables, figures out the current username running the deployment and calls a REST service:
import httplib
from string import Template
if hooksDict != null and "host" in hooksDict.entries and "deployment-complete-url" in hooksDict.entries:
username = context.task.username
context.logOutput("Calling deployment-completed hook with: " + username + " deployed application " + application + " (" + version + ") to " + environment)
s = Template(hooksDict.entries["deployment-complete-url"])
url = s.substitute(application=application, version=version, environment=environment, username=username)
conn = httplib.HTTPConnection(hooksDict.entries["host"])
conn.request("GET", url)
r1 = conn.getresponse()
conn.close()
! Note that Jython is a Python derived language and is white-space sensitive.
In the script you can see that the deployment-complete-url
is retrieved from the dictionary and the placeholders are replaced with the actual values. This allows a bit of flexibility when defining a REST service endpoint.
Creating the REST service
To demonstrate that this all works I put together a simple NodeJS app (this is pushing the term app) to receive the request that the Jython script makes. It will simply log the request URL to a file and return "OK":
var http = require("http");
var fs = require("fs");
var server = http.createServer(function(req, res) {
fs.appendFile("/tmp/completed.log", req.url + "\n", function(err) {
if(err) {
res.writeHead(500, { "Content-Type": "text/plain" });
res.end(err);
}
else {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("OK\r\n");
}
});
});
server.listen(8000, '127.0.0.1');
This service is started by running:
sauron@localhost> node restservice.js
Testing the deployment complete hook
To test the rule and the actual script I created the following in a local XL Deploy instance:
- Application: TestApp
- Package: TestPackage (without anything in it)
- Environment: Production
I also added the Hooks dictionary with the settings as described above.
On the deployment tab simply drag the package TestPackage in the left box and the environment Production in the right box and click Execute. Because there isn't actually anything in the package or the environment the deployment will start immediately and you should see sometthing like this:
And when the execution is complete you should see this:
If everything went OK then the file /tmp/completed.log
(or wherever you've put it) should show:
/TestApp/Production/TestPackage/admin
Wrap up
While extending XL Deploy this way works and provides a nice and simple way to achieve what I wanted I'm not entirely convinced this is the best way to do this. It may be that a plugin is actually better suited and could be more powerfull but that is something I need to look at in the future.
In the meantime, this hook now allows me to post messages to the TFS team room whenever someone deploys an application with XL Deploy!