Wednesday, December 31, 2008

Perl SOAP Interface to Jira

For reasons that are lost in the mists of time (read: decisions made before I worked here), my company uses a lot of Object-Oriented Perl, particularly for our test infrastructure. One of the little corners of this infrastructure is a utility we call "fetchlogs". It takes some input, optionally creates or updates a bug in the defect tracking system, copies all the test logs from various machines to the appropriate directory, and puts a link to those logs into the bug.

Long story short, I wanted to update fetchlogs to create bugs in Jira instead of our current defect tracking system. I had a hard time with the documentation, so I thought I'd share. Here's the perl module that actually does the work:


##
# Perl interface to Jira defect tracking system
#
# @synopsis
#
# use Jira;
# $jira = Jira->new();
# $jira->addComment(issue => "QA-1",
# comment => "this is a comment");
#
# @description
#
# C provides an object oriented interface to the
# defect tracking system (Jira). It can be used to create issues,
# add comments, etc.
#
# $Id: $
##
package Jira;

use FindBin;

use strict;
use warnings;
use Carp;
use Data::Dumper;
use SOAP::Lite;
use Storable qw(dclone);

##
# @paramList{new}
my %properties
= (
# @ple The Jira host.
dhost => $ENV{PRSVP_HOST} || "jira",
# @ple Port of the Jira Daemon
dport => 8080,
# @ple Should this print the result of requests to STDOUT?
verbose => 0,
# @ple Jira user
jiraUser => "user",
# @ple Jira password
jiraPasswd => "password",
);
##


######################################################################
# Creates a C object.
#
# @params{new}
##
sub new {
my $invocant = shift;
my $class = ref($invocant) || $invocant;

my $self = {
# Clone %properties so original isn't modified
%{ dclone(\%properties) },
@_,
};
return bless $self, $class;
}

######################################################################
# Issue a request to the Jira server and get a response.
#
# @param cmd The name of the command being sent
# @param checkResult Whether or not to check the result of the command
# @param params The parameters of that command
#
# @return result of the command
##
sub _request {
my ($self, $cmd, $checkResult, $params) = @_;
my $soap = SOAP::Lite->proxy("http://$dhost:$dport/rpc/soap/jirasoapservice-v2?wsdl");
my $auth = $soap->login($jiraUser, $jiraPasswd);
my $doThis;
if ( $cmd eq "addComment") {
$doThis = $soap->$cmd($auth->result(),$params->{'issue'},$params->{'comment_obj'});
} elsif ( $cmd eq "getComponents") {
$doThis = $soap->$cmd($auth->result(),$params->{'project'});
} elsif ($cmd eq "createIssue") {
$doThis = $soap->$cmd($auth->result(),$params->{'issueDef'});
}
if ( $doThis->faultcode ) { # whoops something went wrong
croak("Error running command: $cmd\nGot: " . $doThis->faultstring);
}
return $doThis;
}

######################################################################
# Add a comment to an existing issue.
#
# @param params{issue} The id of the issue to add (e.g., QA-1)
# @param params{comment} The comment to add (text only)
##
sub addComment {
my ($self, %params) = @_;
$params{issue} ||= '';
$params{comment} ||= '';
my %issue;
$issue{issue} = $params{issue};
$issue{comment_obj} ||= SOAP::Data->type('RemoteComment' => {'body' => $params{comment}});
my $result = $self->_request('addComment', 1, \%issue);
return $result;
}

######################################################################
# Create an issue
#
# @param params{project} The Jira project name (e.g., QA)
# @param params{component} The component of the project (e.g., "Tools")
# @param params{summary} The title or summary of the issue in text
# @param params{description} A more verbose description of the issue.
# @param params{reporter} The Jira username who is reporting the issue.
# @param params{assignee} The Jira username who is assigned to the issue.
# @param params{priority} The priority of the issue (1-5, 1 is highest).
##
sub createIssue {
my ($self, %params) = @_;
$params{project} ||= '';
$params{component} ||= '';
$params{summary} ||= '';
$params{description} ||= '';
$params{reporter} ||= 'user';
$params{assignee} ||= 'user';
$params{priority} ||= '4'; # Default to "minor"

my %issue;
my $components = $self->getComponentList(project=>$params{project});
my @compList;
foreach my $component (@{$components->result()}) {
if ( $component->{'name'} eq $params{component} ) {
$component->{'id'} = SOAP::Data->type(string=>$component->{'id'});
push(@compList, $component);
last;
}
}
my $issueDef = {
assignee => SOAP::Data->type(string => $params{assignee}),
reporter => SOAP::Data->type(string => $params{reporter}),
summary => SOAP::Data->type(string => $params{summary}),
description => SOAP::Data->type(string => $params{description}),
priority => SOAP::Data->type(string => $params{priority}),
type => SOAP::Data->type(string => 1),
project => SOAP::Data->type(string => $params{project}),
components => SOAP::Data->type('impl:ArrayOf_tns1_RemoteComponent' => \@compList),
};
$issue{issueDef} = $issueDef;
my $issue = $self->_request('createIssue', 1, \%issue);
my $issueNum = $issue->result()->{'key'};
return $issueNum;
}

######################################################################
# Given a project, get the components in that project
#
# @param params{project} The Jira project name (e.g., QA)
##
sub getComponentList {
my ($self, %params) = @_;
$params{project} ||= '';
my %issue;
$issue{project} ||= SOAP::Data->type(string=>params{project});
my $componentList = $self->_request('getComponents', 1, \%issue);
return $componentList;
}

1;

There are a number of gotchas here:
  • I don't have an elegant way to handle the different parameters expected for each command, so my _request routine has a big case statement in it. Not pretty, but functional.
  • It took me a shockingly long time to figure out that the issue number was returned as $issue->result()->{'key'}, but it is. You can get at other parameters this way, too (for example $issue->result()->{'summary'}).
  • In createIssue I tried just passing in the elements rather than using issueDef, but I wound up with all sorts of null pointer exceptions.
  • From here it should (emphasis on "should") just be a matter of launder-rinse-repeat to add other calls for updateIssue, closeIssue, etc.
Many thanks to Google and about 20 posters in various forums for providing clues on how to get this to work. Here's hoping it helps someone else stuck in a similar boat.



EDIT 01/13/2009:
We've added a way to get version numbers.

Here's the method:

######################################################################
# Given a project, get all the versions in that project
#
# @param params{project} The Jira project name (e.g., QA)
##
sub _getVersionList {
my ($self, %params) = assertMinArgs(1, @_);
$params{project} ||= '';
my %issue;
$issue{project} ||= SOAP::Data->type(string=>$params{project});
my $versionList = $self->_request('getVersions', 1, \%issue);
foreach my $version (@{$versionList->result()}) {
print "$version->{'id'} $version->{'name'}\n";
}
return $versionList;
}

######################################################################
# Given a project and a version, get the ID of that version
#
# @param params{project} The Jira project name (e.g., "QA")
# @param params{version} The version of the project (e.g., "4.2.1")
##
sub _getVersionId {
my ($self, %params) = assertMinArgs(2, @_);
$params{project} ||= '';
$params{version} ||= '';
my $versions = $self->_getVersionList(project=>$params{project});
my @versionList;
foreach my $version (@{$versions->result()}) {
if ( $version->{'name'} eq $params{version} ) {
$version->{'id'} = SOAP::Data->type(string=>$version->{'id'});
push(@versionList, {'id' => $version->{'id'}});
last;
}
}
print "Version? -> $versionList[0]\n";
print Dumper @versionList;
my $numVersions = scalar @versionList;
print "We found $numVersions elements\n";
@versionList = SOAP::Data->type('Array' => @versionList) -> attr( {'soapenc:arrayType' => "ns1.RemoteVersion[$numVersions]"});
return @versionList;
}


And here's the call:

my @versionList = $self->_getVersionId(project=> $params{project},
version => $params{version});

Then just do "affectsVersions=>\@versionList" wherever you need it.

2 comments:

  1. Could you please provide perl script on linking issues.

    ReplyDelete
  2. Unfortunately, there's no support in the Jira Perl SOAP interface for linking issues. You can in theory use a Jelly script, like this person:
    http://forums.atlassian.com/message.jspa?messageID=257344441

    but I haven't tried it.

    ReplyDelete