Continuous Deployment to CPAN
Recently I was working on a refactor of one of my CPAN modules which, among other things, involved changing its name from Test::A8N
to the specific Test::Story
. Doing so made me think about the process I usually go through when I consider releasing a CPAN module.
First, let me explain something about myself: I don’t like tedious or repetitive tasks. I hate having to do the same thing over and over again, partly because I don’t want to waste my time, but mostly because inevitably one of the following will happen:
- I’ll forget a crucial step, and will screw something up;
- I’ll forget how to do it, and in my efforts to re-learn it I’ll screw something up;
- I won’t care enough to go through the effort, so something will get screwed up.
I expect you’re noticing a trend here. Really the only reason programmers come into this profession in the first place I suspect is because we’re just so bad at doing things the normal way, we have to automate everything we’ll either do poorly, lazily, or forget to do all together.
For those of us who are programmers, many times we’re so lazy that we won’t want to do the same thing within our programs more than once, so we abstract functionality into reusable modules. By that token the Perl community must be some of the most inventively lazy group of people, because CPAN is full of useful tidbits like that. Getting modules to CPAN requires a contributor to actually, you know, submit their project. And this is, in itself, a somewhat manual process.
I do all of my development in a version control repository, and I write a decent amount of unit tests to prove my functionality works. So once I come to the decision that a set of new features is worthy of a new release, this is the process I go through:
- Run “
perl Makefile.PL && make && make test
” to verify everything runs okay; - Run “
make dist
” to create the distribution tarball; - Noticing the version number is wrong, I go in and change it in the main Perl module;
- After running “
make dist
” again, I realize I forgot to change the README; - Potentially after another “
make dist
“, I’ll remember I’m supposed to update the Changes file to indicate what I’ve added; - It’s at this point I realize that I forgot to add new documentation to cover this new feature;
- I run “
make test
” again, this time with TEST_POD=1 set to ensure my documentation checks are run; - I’ll then try to remember what the command is for uploading a new CPAN module, which involves searching on CPAN for something related to “Upload”;
- Finding “
cpan-upload
“, I’ll have to look at its documentation to figure out how to use it; - I run “
cpan-upload
“, only to realize I forgot to set my PAUSE credentials in ~/.pause.
At this point, if I’m lucky, the upload will succeed. This process isn’t meant to be a negative reflection on CPAN, but rather on my own forgetfulness and need to automate.
Read on to find out how I managed to automate this part of my life as well.
Hudson, to the rescue!
I figure if I take the repetition and human-error out of releasing CPAN modules, I’ll be more likely to submit updates to CPAN and help everyone in the community to make better software. I’ve been using Hudson quite a bit lately, first to automate my iPhone application builds, and more recently to continuously test and deploy my web applications. Those two techniques are definitely on opposite ends of the easy-to-complicated spectrum, so I thought I’d throw something quick & dirty together to make my CPAN life a bit easier.
Setup Hudson
I’ve talked about Hudson a few times already, but I figured it couldn’t hurt to go over it again. If you don’t have Hudson installed and running already, go follow the Hudson installation instructions to get a running copy going. I run Hudson in a VirtualBox VM on my Mac Mini at home, which sits inside my Airport Extereme’s DMZ. This allows access to Hudson from elsewhere on the internet, most importantly from Github.
Once you have a running copy of Hudson, here are some of the plugins I recommend when building Perl-based packages:
- Git Plugin – Accessing code from within Git repositories.
- Github Plugin – While not strictly necessary, this creates a link within your Hudson job that points to your Github project.
- NCover Plugin – If you want to track the code coverage of your unit tests, this plugin lets you track the coverage of any system that generates HTML coverage reports, including
Devel::Cover
. - xUnit Plugin – Publishes the results of your unit tests, and tracks them over time.
This walkthrough assumes the above plugins are installed, but feel free to tweak this tutorial to your heart’s content.
Create a “free-style software project” with the name of your CPAN package, and enter in your Github project URL. As you can see from the screenshot, I’ve tied this project to a specific node since I have multiple Hudson slaves configured that have different configurations that won’t all work to build Perl modules.
Next, configure the Source Code Management section. All you should really need to enter in is the URL of your Git repository. I configured the necessary SSH keys to access Github from the user Hudson runs as. I’ve set up a separate Github user with its own SSH keys for Hudson, which makes it easy to disallow access at a later date if this system gets compromised.
Configure the build process
Enable the “Trigger builds remotely” feature, and plug in some sort of unique ID that can’t be guessed. I suggest running the “uuidgen
” Unix command to generate a unique number and plug that in. This allows you to remotely trigger builds without having to authenticate first.
Create a build script by selecting “Execute shell” from the “Add build step” menu. You can either paste the entire script into this text box, or you can save it as a script on your build server.
Setup the package dependancies
The important thing I’ve found out is that keeping module dependancies up-to-date on a build server is complicated at best. Additionally it’s hard to tell when dependancies on CPAN change what sorts of impact that would have on your module. So it’s best to pull down dependancies at the beginning of your build job.
set -ex
git clean -dfx
echo "Setup dependancies"
eval $(perl -Mlocal::lib=support)
wget -q -O - http://cpanmin.us | perl - --notest App::cpanminus
./support/bin/cpanm --notest
CPAN::Uploader Module::Install
TAP::Formatter::JUnit Package::DeprecationManager
./support/bin/cpanm --installdeps --notest .
The “set -ex
” ensures that any uncaught errors will cause the build script to fail, preventing it from going any further. Cleaning the build directory is also important, so that any files from a previous build run won’t be included in the current run.
This section also fetches Tatsuhiko Miyagawa’s wonderful App::cpanminus
, and uses it to install the dependancies of this build script, in addition to the dependancies of the package you’re building.
Setup and build
The following sets up a build output directory where logs and test output can be stored, as well as basic options for building test output, coverage options, etc.
echo "Setup environment"
export OUTPUT=$WORKSPACE/output
mkdir -p $OUTPUT
export AUTOMATED_TESTING=1
export PERL_MM_USE_DEFAULT=1
export PERL_TEST_HARNESS_DUMP_TAP=$OUTPUT
export HARNESS_PERL_SWITCHES=-MDevel::Cover=-silent,off,-summary,off
export HARNESS_OPTIONS=j5
export TEST_VERBOSE=1
export TEST_POD=1
echo "Build package"
perl Makefile.PL
make
Run the unit tests
Due to Hudson’s very Java-oriented nature, most of its plugins are geared towards Java projects. Fortunately there’s a TAP module for outputting JUnit-style output that Hudson can understand. Instead of typing “make test”, I opt to run the tests directly with “prove”, specifying the JUnit formatter.
echo "Test package"
prove --merge --shuffle --timer --blib
--formatter TAP::Formatter::JUnit >
$OUTPUT/tests.xml || echo "*** Failed test cases";
EXIT_STATUS=$?
echo "Make coverage"
(cd output; prove --nocolor --exec 'cat' $OUTPUT/t/*.t)
cover -select_re blib/lib -ignore_re '^t/' -ignore_re '/support/'
if [ $EXIT_STATUS -ne 0 ];
then
echo "Exiting with $EXIT_STATUS"
exit $EXIT_STATUS
fi
I find generating coverage, and actually watching it, is an important step that most people tend to ignore. It’s not only important to test your code, but it’s important to also verify that you’re testing what your package does. In fact you might even want to take this section a step further, and fail the build if the coverage drops below a certain threshold. As it is now, the last few lines in this section exits the build process if any of the unit tests failed.
Finally, you might want to enable some extra Hudson options to expose your Coverage output, your “make dist
” tarball output, and any email notification options to inform you of your build’s status.
Publish CPAN distribution to PAUSE
The “make dist” is easy enough to understand. However much more needs to happen after that point. I was trying to find a good way of determining when a build was ready to be submitted to CPAN. I know from personal experience I tend to only update the version number for a package when I’m ready to release it.
echo "Make distribution"
make dist
echo "Publish distribution to CPAN"
PACKAGE_NAME=$(perl -MYAML=LoadFile -e "print LoadFile('META.yml')->{name}")
PACKAGE_CLASS=$(echo $PACKAGE_NAME | sed -e 's/-/::/g')
NEW_VERSION=$(perl -MYAML=LoadFile -e "print LoadFile('META.yml')->{version}")
CUR_VERSION=$(wget -q -O - http://cpanmetadb.appspot.com/v1.0/package/$PACKAGE_NAME |
perl -MYAML=Load -e '$data=join("n",<>); print Load($data)->{version} if $data')
if [ -n $NEW_VERSION ] && [ -n $CUR_VERSION ] &&
[ $(echo "$NEW_VERSION > $CUR_VERSION" | bc) -eq 1 ];
then
./support/bin/cpan-upload $PACKAGE_NAME-*.tar.gz
fi
This block of code checks the name of the package you’re publishing, extracts the version number of the new build, and fetches the currently-available version on CPAN. If and only if the new build is newer than the version available on CPAN, it will attempt to publish the package to PAUSE. This assumes the “~/.pause
” config file that cpan-upload uses has been configured under the user your Hudson job runs as.
Putting it all together
Here’s the final result, for your copy-and-pasting pleasure:
I’m sure this script will evolve over time, but this is a good starting point for almost anyone. With any luck this will improve the quality as well as the frequency of my submissions to CPAN, and hopefully it can do the same for you too.
Extra credit: Minimum test coverage
I wanted to ensure a minimum amount of code quality, so therefore I added the following snippet to my personal build scripts, immediately before the “make dist” stage. It uses Devel::Cover
‘s classes to check the total coverage of your package, and will fail the build if you drop below a minimum baseline. I suggest raising the minimum-coverage threshold whenever you add more unit tests to make sure any new features added include tests to prove them.
MIN_COVERAGE=75
COVER_AMT=$(perl -MDevel::Cover::DB
-e 'Devel::Cover::DB->new(db => "cover_db")->print_summary' 2>/dev/null
| awk '/Total/ { print $7 }')
if [ $(echo "$COVER_AMT < $MIN_COVERAGE" | bc) -eq 1) ] ;
then
echo "*** ERROR: Unit test coverage is only ${COVER_AMT}%"
exit 2
fi