Appending nodes in XML files with xmlpeek and xmlpoke using NAnt
First post of the year and hopefully this is something useful. I think it is.
I'm currently doing a major automation-overhaul to our projects, trying to streamline everything. Part of this involves doing automated deployments of the projects to a location (say a file or web server) where a QA person can come along later and with the click of a button they can just launch an installer for any build of an application. This is very much structured like the JetBrains Nightly Build page you see here, but 100% automated for all projects using NAnt.
A lofty goal? Perhaps.
Anywho, the time to update the builds and page has come and I went hunting to see how I could do it (without having to write a silly little console app or something). To start with (and I'm not done here, but this works and is a model anyone can use) we have a basic XML file:
<?xml version="1.0" encoding="utf-8"?>
<builds>
<build>
<date>01/03/2008 15:03:41</date>
<build>0</build>
</build>
</builds>
This contains build information for the project and will be transformed using XSLT into something pretty (and useful once I add more attributes).
The challenge is that we want to append an XML node to this file during the deployment process, which is kicked off by CruiseControl.NET. Sounds easy huh. There are a few ways to do this. First, I could write that console app or something and have it update some file. Or maybe it would even write to a database. Or... no, that's getting too complicated. The next thought was to use the ability to write C# code in NAnt scripts, but then that started to get ugly real fast and more maintenance than I wanted.
Then I turned to xmlpoke. This little NAnt task let's you replace a node (or nodes) in an XML file. Trouble is that's what it's designed to do. Replace a node or property. Not append one. After about 15 minutes of Googling (my patience is pretty thin for finding an answer on the 3rd page of Google) I realized xmlpoke wasn't going to be good enough for this. Someone had come up with xmlpoke2 which did exactly what I wanted (appended data to an XML file), but to date it hasn't made it into the core or even NAntContrib.
After looking at the XML file I realized I might be able to use xmlpeek (read some XML from a file) and combine it with xmlpoke (modifying it on the way out) and write it back to the file. Maybe not the most elegant solution, but I think it's pretty nifty and it gets the job done.
First we have our XML file (above) so I created a target in NAnt to handle the update to the XML file:
<target name="publish-report" description="add the version deployed to an existing xml file">
</target>
Step 1 - Use xmlpeek to read in the entire XML node tree containing the current builds:
<!-- read in all the builds for rewriting -->
<property name="xmlnodes" value=""/>
<xmlpeek xpath="//builds" file="c:\autobuild.xml" property="xmlnodes"></xmlpeek>
Step 2 - Modify it by appending a new node with the new build info and saving it into a new property:
<!-- modify the node by adding a new one to it -->
<property name="newnode" value="<build><date>${datetime::now()}</date><build>${CCNetLabel}</build></build>" />
<property name="xmlnodes" value="${xmlnodes}${newnode}" />
Step 3 - Write it back out to the original XML file replacing the entire XML tree using xmlpoke:
<!-- rewrite it back out to the xml file using xmlpeek -->
<xmlpoke file="c:\autobuild.xml" xpath="//builds" value="${xmlnodes}" />
The result. Here's the updated XML file after running NAnt with our target task (and faking out the CCNetLabel that would usually get set by CruiseControl via a command line definition):
tools\nant\nant.exe publish-report -D:CCNetLabel=1
NAnt 0.85 (Build 0.85.2478.0; release; 14/10/2006)
Copyright (C) 2001-2006 Gerry Shaw
http://nant.sourceforge.net
Buildfile: file:///C:/development/common/Library/Common/Common.build
Target framework: Microsoft .NET Framework 2.0
Target(s) specified: publish-report
publish-report:
[xmlpeek] Peeking at 'c:\autobuild.xml' with XPath expression '//builds'.
[xmlpeek] Found '1' nodes with the XPath expression '//builds'.
[xmlpoke] Found '1' nodes matching XPath expression '//builds'.
BUILD SUCCEEDED
Total time: 0.2 seconds.
<?xml version="1.0" encoding="utf-8"?>
<builds>
<build>
<date>01/03/2008 15:03:41</date>
<build>0</build>
</build>
<build>
<date>01/03/2008 15:30:07</date>
<build>1</build>
</build>
</builds>
Now I have a continuously growing XML file with all my build numbers in them. Of course there's more info to add here like where to get the file and such but the concept works and I think it's a half decent compromise (to having to write my own task or more script). The cool thing is that you can even use it against a file like this:
<?xml version="1.0" encoding="utf-8"?>
<builds>
</builds>
This lets you start from scratch for new projects and start with build 1 (which will come from CruiseControl.NET). If the file didn't exist at all, you could even use the echo task or something like it to create the file, then update it with the task info above. Is it bullet proof? Hardly. It should work though and gives me the automation I want.
Well, I'm done for the day. That was a worthwhile hour to build this. Now I just have to go off and add in all the extra goop and hook it up to our builds.
Enjoy!