File Coverage

blib/lib/JIRA/Client/Automated.pm
Criterion Covered Total %
statement 174 365 47.6
branch 33 86 38.3
condition 17 67 25.3
subroutine 26 51 50.9
pod 26 26 100.0
total 276 595 46.3


line stmt bran cond sub pod time code
1 2     2   227821 use strict;
  2         20  
  2         47  
2 2     2   8 use warnings;
  2         3  
  2         37  
3 2     2   27 use 5.010;
  2         6  
4              
5             package JIRA::Client::Automated;
6             $JIRA::Client::Automated::VERSION = '1.9';
7             =head1 NAME
8              
9             JIRA::Client::Automated - A JIRA REST Client for automated scripts
10              
11             =head1 VERSION
12              
13             version 1.9
14              
15             =head1 SYNOPSIS
16              
17             use JIRA::Client::Automated;
18              
19             my $jira = JIRA::Client::Automated->new($url, $user, $password);
20              
21             # If your JIRA instance does not use username/password for authorization
22             my $jira = JIRA::Client::Automated->new($url);
23              
24             my $jira_ua = $jira->ua(); # to add in a proxy
25              
26             $jira->trace(1); # enable tracing of requests and responses
27              
28             # The simplest way to create an issue
29             my $issue = $jira->create_issue($project, $type, $summary, $description);
30              
31             # The simplest way to create a subtask
32             my $subtask = $jira->create_subtask($project, $summary, $description, $parent_key);
33              
34             # A complex but flexible way to create a new issue, story, task or subtask
35             # if you know Jira issue hash structure well.
36             my $issue = $jira->create({
37             # Jira issue 'fields' hash
38             project => {
39             key => $project,
40             },
41             issuetype => {
42             name => $type, # "Bug", "Task", "Sub-task", etc.
43             },
44             summary => $summary,
45             description => $description,
46             parent => { # only required for a subtask
47             key => $parent_key,
48             },
49             ...
50             });
51              
52              
53             my $search_results = $jira->search_issues($jql, 1, 100); # query should be a single string of JQL
54             my @issues = $jira->all_search_results($jql, 1000); # query should be a single string of JQL
55              
56             my $issue = $jira->get_issue($key);
57              
58             $jira->update_issue($key, $update_hash); # update_hash is { field => value, ... }
59             $jira->create_comment($key, $text);
60             $jira->attach_file_to_issue($key, $filename);
61              
62             $jira->transition_issue($key, $transition, $transition_hash); # transition_hash is { field => value, ... }
63              
64             $jira->close_issue($key, $resolve, $comment); # resolve is the resolution value
65             $jira->delete_issue($key);
66              
67             $jira->add_issue_watchers($key, $watcher1, ......);
68             $jira->add_issue_labels($key, $label1, ......);
69              
70              
71             =head1 DESCRIPTION
72              
73             JIRA::Client::Automated is an adapter between any automated system and JIRA's
74             REST API. This module is explicitly designed to easily create and close issues
75             within a JIRA instance via automated scripts.
76              
77             For example, if you run nightly batch jobs, you can use JIRA::Client::Automated
78             to have those jobs automatically create issues in JIRA for you when the script
79             runs into errors. You can attach error log files to the issues and then they'll
80             be waiting in someone's open issues list when they arrive at work the next day.
81              
82             If you want to avoid creating the same issue more than once you can search JIRA
83             for it first, only creating it if it doesn't exist. If it does already exist
84             you can add a comment or a new error log to that issue.
85              
86             =head1 WORKING WITH JIRA
87             6
88             Atlassian has made a very complete REST API for recent (> 5.0) versions of
89             JIRA. By virtue of being complete it is also somewhat large and a little
90             complex for the beginner. Reading their tutorials is *highly* recommended
91             before you start making hashes to update or transition issues.
92              
93             L
94              
95             This module was designed for the JIRA 5.2.11 REST API, as of March 2013, but it
96             works fine with JIRA 6.0 as well. Your mileage may vary with future versions.
97              
98             =head1 JIRA ISSUE HASH FORMAT
99              
100             When you work with an issue in JIRA's REST API, it gives you a JSON file that
101             follows this spec:
102              
103             L
104              
105             JIRA::Client::Automated tries to be nice to you and not make you deal directly
106             with JSON. When you create a new issue, you can pass in just the pieces you
107             want and L will transform them to JSON for you. The same for
108             closing and deleting issues.
109              
110             Updating and transitioning issues is more complex. Each JIRA installation will
111             have different fields available for each issue type and transition screen and
112             only you will know what they are. So in those cases you'll need to pass in an
113             "update_hash" which will be transformed to the proper JSON by the method.
114              
115             An update_hash looks like this:
116              
117             { field1 => value, field2 => value2, ...}
118              
119             For example:
120              
121             {
122             host_id => "example.com",
123             { resolution => { name => "Resolved" } }
124             }
125              
126             If you do not read JIRA's documentation about their JSON format you will hurt
127             yourself banging your head against your desk in frustration the first few times
128             you try to use L. Please RTFM.
129              
130             Note that even though JIRA requires JSON, JIRA::Client::Automated will
131             helpfully translate it to and from regular hashes for you. You only pass hashes
132             to JIRA::Client::Automated, not direct JSON.
133              
134             I recommend connecting to your JIRA server and calling L with a
135             key you know exists and then dump the result. That'll get you started.
136              
137             =head1 METHODS
138              
139             =cut
140              
141 2     2   498 use JSON;
  2         8068  
  2         8  
142 2     2   1303 use LWP::UserAgent;
  2         52729  
  2         58  
143 2     2   13 use HTTP::Request;
  2         3  
  2         47  
144 2     2   798 use HTTP::Request::Common qw(GET POST PUT DELETE);
  2         3511  
  2         113  
145 2     2   728 use LWP::Protocol::https;
  2         146303  
  2         78  
146 2     2   12 use Carp;
  2         4  
  2         92  
147 2     2   855 use Data::Dump qw(pp);
  2         8434  
  2         8085  
148              
149             =head2 new
150              
151             my $jira = JIRA::Client::Automated->new($url, $user, $password);
152              
153             Create a new JIRA::Client::Automated object by passing in the following:
154              
155             =over 3
156              
157             =item 1.
158              
159             URL for the JIRA server, such as "http://example.atlassian.net/"
160              
161             =item 2.
162              
163             Username to use to login to the JIRA server
164              
165             =item 3.
166              
167             Password for that user
168              
169             =back
170              
171             All three parameters are required if your JIRA instance uses basic
172             authorization, for which JIRA::Client::Automated must connect to the
173             JIRA instance using I username and password. You may want to set up a
174             special "auto" or "batch" username to use just for use by scripts.
175              
176             If you are using Google Account integration, the username and password to use
177             are the ones you set up at the very beginning of the registration process and
178             then never used again because Google logged you in.
179              
180             If you have other ways of authorization, like GSSAPI based authorization, do
181             not provide username or password.
182              
183             my $jira = JIRA::Client::Automated->new($url);
184              
185             =cut
186              
187             sub new {
188 1     1 1 103 my ($class, $url, $user, $password) = @_;
189              
190 1 50 33     7 unless (defined $url && $url) {
191 0         0 croak "Need to specify url to access JIRA";
192             }
193 1   33     4 my $no_user_pwd = !(defined $user || defined $password);
194 1 50 33     12 unless ($no_user_pwd || defined $user && $user && defined $password && $password) {
      33        
      33        
      33        
195 0         0 croak "Need to either specify both user and password, or provide none of them";
196             }
197              
198 1 50       5 unless ($url =~ m{/$}) {
199 1         8 $url .= '/';
200             }
201              
202             # make sure we have a usable API URL
203 1         2 my $auth_url = $url;
204 1 50       3 unless ($auth_url =~ m{/rest/api/}) {
205 1         3 $auth_url .= '/rest/api/latest/';
206             }
207 1 50       5 unless ($auth_url =~ m{/$}) {
208 0         0 $auth_url .= '/';
209             }
210 1         4 $auth_url =~ s{//}{/}g;
211 1         4 $auth_url =~ s{:/}{://};
212              
213 1 50       6 if ($auth_url !~ m|https?://|) {
214 0         0 croak "URL for JIRA must be absolute, including 'http://' or 'https://'";
215             }
216              
217 1         5 my $self = { url => $url, auth_url => $auth_url, user => $user, password => $password };
218 1         2 bless $self, $class;
219              
220             # cached UserAgent for talking to JIRA
221 1         7 $self->{_ua} = LWP::UserAgent->new();
222              
223             # cached JSON object for handling conversions
224 1         2410 $self->{_json} = JSON->new->utf8()->allow_nonref;
225              
226 1         4 return $self;
227             }
228              
229              
230             =head2 ua
231              
232             my $ua = $jira->ua();
233              
234             Returns the L object used to connect to the JIRA instance.
235             Typically used to setup proxies or make other customizations to the UserAgent.
236             For example:
237              
238             my $ua = $jira->ua();
239             $ua->env_proxy();
240             $ua->ssl_opts(...);
241             $ua->conn_cache( LWP::ConnCache->new() );
242              
243             =cut
244              
245             sub ua {
246 0     0 1 0 my $self = shift;
247 0 0       0 $self->{_ua} = shift if @_;
248 0         0 return $self->{_ua};
249             }
250              
251              
252             =head2 trace
253              
254             $jira->trace(1); # enable
255             $jira->trace(0); # disable
256             $trace = $jira->trace;
257              
258             When tracing is enabled each request and response is logged using carp.
259              
260             =cut
261              
262             sub trace {
263 16     16 1 22 my $self = shift;
264 16 50       32 $self->{_trace} = shift if @_;
265 16         38 return $self->{_trace};
266             }
267              
268              
269             sub _handle_error_response {
270 0     0   0 my ($self, $response, $request) = @_;
271              
272 0         0 my $msg = $response->status_line;
273 0 0       0 if ($response->decoded_content) {
274 0 0       0 if ($response->content_type eq 'application/json') {
275 0         0 $msg .= pp($self->{_json}->decode($response->decoded_content));
276             } else {
277 0         0 $msg .= $response->decoded_content;
278             }
279             }
280              
281 0         0 $msg .= "\n\nfor request:\n";
282 0 0       0 if ($request->decoded_content) {
283 0 0       0 if ($request->content_type eq 'application/json') {
284 0         0 $msg .= pp($self->{_json}->decode($request->decoded_content));
285             } else {
286 0         0 $msg .= $request->decoded_content;
287             }
288             }
289              
290 0         0 croak sprintf "Unable to %s %s: %s",
291             $request->method, $request->uri->path, $msg;
292             }
293              
294              
295             sub _perform_request {
296 8     8   17 my ($self, $request, $handlers) = @_;
297              
298 8 50 33     36 if ((defined $self->{user}) && (defined $self->{password})) {
299 8         24 $request->authorization_basic($self->{user}, $self->{password});
300             }
301              
302 8 50       1523 if ($self->trace) {
303 0   0     0 carp sprintf "request %s %s: %s",
304             $request->method, $request->uri->path, $request->decoded_content//'';
305             }
306              
307 8         24 my $response = $self->{_ua}->request($request);
308              
309 8 50       3749 if ($self->trace) {
310 0   0     0 carp sprintf "response %s: %s",
311             $response->status_line, $response->decoded_content//'';
312             }
313              
314 8 50       19 return $response if $response->is_success();
315              
316 0   0     0 $handlers ||= {};
317             my $handler = $handlers->{ $response->code } || sub {
318 0     0   0 return $self->_handle_error_response($response, $request);
319 0   0     0 };
320              
321 0         0 return $handler->($response, $request, $self);
322             }
323              
324              
325             =head2 create
326              
327             my $issue = $jira->create({
328             # Jira issue 'fields' hash
329             project => {
330             key => $project,
331             },
332             issuetype => {
333             name => $type, # "Bug", "Task", "SubTask", etc.
334             },
335             summary => $summary,
336             description => $description,
337             parent => { # only required for a subtask
338             key => $parent_key,
339             },
340             ...
341             });
342              
343             Creating a new issue, story, task, subtask, etc.
344              
345             Returns a hash containing only the basic information about the new issue, or
346             dies if there is an error. The hash looks like:
347              
348             {
349             id => 24066,
350             key => "TEST-57",
351             self => "https://example.atlassian.net/rest/api/latest/issue/24066"
352             }
353              
354             See also L
355              
356             =cut
357              
358             sub _issue_type_meta {
359 2     2   4 my ($self, $project, $issuetype) = @_;
360              
361 2         13 my $uri = "$self->{auth_url}issue/createmeta?projectKeys=${project}&expand=projects.issuetypes.fields";
362              
363 2         8 my $request = GET $uri,
364             Content_Type => 'application/json';
365              
366 2         6306 my $response = $self->_perform_request($request);
367              
368 2         26 my $meta = $self->{_json}->decode($response->decoded_content());
369 2         409 foreach my $p (@{$meta->{projects}}) {
  2         7  
370 2 50       7 if ($p->{key} eq $project) {
371 2         3 foreach my $i (@{$p->{issuetypes}}) {
  2         4  
372 2 50 66     12 return %{$i->{fields}} if $i->{name} eq $issuetype and exists $i->{fields};
  1         12  
373             }
374             }
375             }
376              
377 1         30 return;
378             }
379              
380             sub _custom_field_conversion_map {
381 2     2   5 my ($self, $project, $issuetype) = @_;
382              
383 2         5 my %custom_field_meta = $self->_issue_type_meta($project, $issuetype);
384              
385 2         4 my %custom_field_conversion_map;
386 2         9 while (my ($cf, $meta) = each %custom_field_meta) {
387 13 100       31 if ($cf =~ /^customfield_/) {
388 4         6 my $english_name = $meta->{name};
389 4         6 $custom_field_conversion_map{english_to_customfield}{$english_name} = $cf;
390 4         6 $custom_field_conversion_map{customfield_to_english}{$cf} = $english_name;
391 4         11 $custom_field_conversion_map{meta}{$cf} = $meta;
392             }
393             }
394              
395 2         16 return \%custom_field_conversion_map;
396             }
397              
398             sub _convert_to_custom_field_name {
399 18     18   24 my ($self, $project, $issuetype, $field_name) = @_;
400              
401 18   66     41 $self->{custom_field_conversion_map}{$project}{$issuetype} ||= $self->_custom_field_conversion_map($project, $issuetype);
402              
403 18   66     52 return $self->{custom_field_conversion_map}{$project}{$issuetype}{english_to_customfield}{$field_name} || $field_name;
404             }
405              
406             sub _convert_to_custom_field_value {
407 17     17   28 my ($self, $project, $issuetype, $field_name, $field_value) = @_;
408              
409 17   33     31 $self->{custom_field_conversion_map}{$project}{$issuetype} ||= $self->_custom_field_conversion_map($project, $issuetype);
410              
411 17         24 my $custom_field_name = $self->{custom_field_conversion_map}{$project}{$issuetype}{english_to_customfield}{$field_name};
412 17 100       23 if ($custom_field_name) { # If it's a custom field...
413 4         6 my $custom_field_defn = $self->{custom_field_conversion_map}{$project}{$issuetype}{meta}{$custom_field_name};
414 4         7 my $custom_field_name = $custom_field_defn->{custom_field_name};
415              
416 4 50       7 if (exists $custom_field_defn->{allowedValues}) {
417 4         4 my %custom_values = map { $_->{value} => $_ } @{$custom_field_defn->{allowedValues}};
  60         105  
  4         8  
418 4 50       13 my $custom_field_id = $custom_values{$field_value}{id} or die "Cannot find custom field value for $field_name value $field_value";
419 4         18 return { id => $custom_field_id };
420             } else {
421 0         0 return $field_value;
422             }
423             } else {
424             # It's a regular field, just pass it through
425 13         25 return $field_value;
426             }
427             }
428              
429             sub _convert_to_customfields {
430 4     4   9 my ($self, $project, $issuetype, $fields) = @_;
431              
432 4         5 my $converted_fields;
433 4         14 while (my ($name, $value) = each %$fields) {
434 16         29 my $converted_name = $self->_convert_to_custom_field_name($project, $issuetype, $name);
435 16         21 my $converted_value;
436 16 100       29 if (ref $value eq 'ARRAY') {
437 2         5 $converted_value = [ map { $self->_convert_to_custom_field_value($project, $issuetype, $name, $_) } @$value ];
  3         6  
438             } else {
439 14         24 $converted_value = $self->_convert_to_custom_field_value($project, $issuetype, $name, $value);
440             }
441 16         47 $converted_fields->{$converted_name} = $converted_value;
442             }
443              
444 4         9 return $converted_fields;
445             }
446              
447             sub _convert_update_to_customfields {
448 1     1   13 my ($self, $project, $issuetype, $update) = @_;
449              
450 1         2 my $converted_update_verb_hash;
451 1 50       2 if( $update ){
452 1         3 foreach my $hkey ( keys %$update ){
453 2         5 my $ckey = $self->_convert_to_custom_field_name($project, $issuetype, $hkey);
454 2         4 $converted_update_verb_hash->{$ckey} = $update->{$hkey};
455             }
456             }
457              
458 1         3 return $converted_update_verb_hash;
459             }
460              
461             sub _issuetype_custom_fieldlist {
462 2     2   5 my ($self, $project, $issuetype) = @_;
463              
464 2   33     8 $self->{custom_field_conversion_map}{$project}{$issuetype} ||= $self->_custom_field_conversion_map($project, $issuetype);
465              
466 2         3 return keys %{$self->{custom_field_conversion_map}{$project}{$issuetype}{customfield_to_english}};
  2         9  
467             }
468              
469             sub _convert_from_custom_field_name {
470 8     8   13 my ($self, $project, $issuetype, $field_name) = @_;
471              
472 8   33     17 $self->{custom_field_conversion_map}{$project}{$issuetype} ||= $self->_custom_field_conversion_map($project, $issuetype);
473              
474 8   33     18 return $self->{custom_field_conversion_map}{$project}{$issuetype}{customfield_to_english}{$field_name} || $field_name;
475             }
476              
477             sub _convert_from_customfields {
478 2     2   5 my ($self, $project, $issuetype, $fields) = @_;
479              
480             # Built-in fields
481             my $converted_fields = { map {
482 2 100       9 if ($_ !~ /^customfield_/) {
  24         40  
483 10         20 ($_ => $fields->{$_})
484             } else {
485             ()
486 14         20 }
487             } keys %$fields };
488              
489             # And the custom fields. For some reason, JIRA seems to give me a
490             # list of *all* possible custom fields, not just ones relevant to
491             # this issuetype
492 2         7 for my $cfname ($self->_issuetype_custom_fieldlist($project, $issuetype)) {
493              
494 8         14 my $english_name = $self->_convert_from_custom_field_name($project, $issuetype, $cfname);
495 8 50       17 if (exists $fields->{$cfname}) {
496 8         10 my $value = $fields->{$cfname};
497 8         9 my $converted_value;
498 8 100       17 if (ref $value eq 'ARRAY') {
    100          
499 2 50       5 $converted_value = [ map { ref $_ eq 'HASH' ? $_->{value} : $_ } @$value ];
  6         41  
500             } elsif (ref $value eq 'HASH') {
501 4         7 $converted_value = $value->{value};
502             } else {
503 2         3 $converted_value = $value;
504             }
505 8         19 $converted_fields->{$english_name} = $converted_value;
506             } else {
507 0         0 $converted_fields->{$english_name} = undef;
508             }
509             }
510              
511 2         4 return $converted_fields;
512             }
513              
514             sub create {
515 3     3 1 6 my ($self, $fields) = @_;
516              
517 3         6 my $project = $fields->{project}{key};
518 3         4 my $issuetype = $fields->{issuetype}{name};
519 3         8 my $issue = { fields => $self->_convert_to_customfields( $project, $issuetype, $fields ) };
520              
521 3         23 my $issue_json = $self->{_json}->encode($issue);
522 3         8 my $uri = "$self->{auth_url}issue/";
523              
524 3         10 my $request = POST $uri,
525             Content_Type => 'application/json',
526             Content => $issue_json;
527              
528 3         841 my $response = $self->_perform_request($request);
529              
530 3         28 my $new_issue = $self->{_json}->decode($response->decoded_content());
531              
532 3         278 return $new_issue;
533             }
534              
535              
536             =head2 create_issue
537              
538             my $issue = $jira->create_issue($project, $type, $summary, $description, $fields);
539              
540             Creating a new issue requires the project key, type ("Bug", "Task", etc.), and
541             a summary and description.
542              
543             The optional $fields parameter can be used to pass a reference to a hash of
544             extra fields to be set when the issue is created, which avoids the need for a
545             separate L call. For example:
546              
547             $jira->create_issue($project, $type, $summary, $description, {
548             labels => [ "foo", "bar" ]
549             });
550              
551             This method calls L and return the same hash reference that it does.
552              
553             =cut
554              
555             sub create_issue {
556 3     3 1 29285 my ($self, $project, $type, $summary, $description, $fields) = @_;
557              
558             my $create_fields = {
559 3 50       6 %{ $fields || {} },
  3         19  
560             summary => $summary,
561             description => $description,
562             issuetype => { name => $type, },
563             project => { key => $project, },
564             };
565              
566 3         8 return $self->create($create_fields);
567             }
568              
569              
570             =head2 create_subtask
571              
572             my $subtask = $jira->create_subtask($project, $summary, $description, $parent_key);
573             # or with optional subtask type
574             my $subtask = $jira->create_subtask($project, $summary, $description, $parent_key, 'sub-task');
575              
576             Creating a subtask. If your JIRA instance does not call subtasks "Sub-task" or
577             "sub-task", then you will need to pass in your subtask type.
578              
579             This method calls L and return the same hash reference that it does.
580              
581             =cut
582              
583             sub create_subtask {
584 0     0 1 0 my ($self, $project, $summary, $description, $parent_key, $type) = @_;
585              
586             # validate fields
587 0 0       0 die "parent_key required" unless $parent_key;
588 0   0     0 $type ||= 'Sub-task';
589              
590 0         0 my $fields = {
591             project => { key => $project, },
592             issuetype => { name => $type },
593             summary => $summary,
594             description => $description,
595             parent => { key => $parent_key},
596             };
597              
598 0         0 return $self->create($fields);
599             }
600              
601              
602             =head2 update_issue
603              
604             $jira->update_issue($key, $field_update_hash, $update_verb_hash);
605              
606             There are two ways to express the updates you want to make to an issue.
607              
608             For simple changes you pass $field_update_hash as a reference to a hash of
609             field_name => new_value pairs. For example:
610              
611             $jira->update_issue($key, { summary => $new_summary });
612              
613             That works for simple fields, but there are some, like comments, that can't be
614             updated in this way. For them you need to use $update_verb_hash.
615              
616             The $update_verb_hash parameter allow you to express a series of specific
617             operations (verbs) to be performed on each field. For example:
618              
619             $jira->update_issue($key, undef, {
620             labels => [ { remove => "test" }, { add => "another" } ],
621             comments => [ { remove => { id => 10001 } } ]
622             });
623              
624             The two forms of update can be combined in a single call.
625              
626             For more information see:
627              
628             L
629              
630             =cut
631              
632             sub update_issue {
633 1     1 1 10981 my ($self, $key, $field_update_hash, $update_verb_hash) = @_;
634            
635 1         4 my $cur_issue = $self->get_issue( $key );
636 1         4 my $project = $cur_issue->{fields}{project}{key};
637 1         3 my $issuetype = $cur_issue->{fields}{issuetype}{name};
638            
639 1         2 my $issue = {};
640 1 50       8 $issue->{fields} = $self->_convert_to_customfields($project, $issuetype, $field_update_hash) if $field_update_hash;
641 1 50       5 $issue->{update} = $self->_convert_update_to_customfields($project, $issuetype, $update_verb_hash) if $update_verb_hash;
642              
643 1         9 my $issue_json = $self->{_json}->encode($issue);
644 1         3 my $uri = "$self->{auth_url}issue/$key";
645              
646 1         6 my $request = PUT $uri,
647             Content_Type => 'application/json',
648             Content => $issue_json;
649              
650 1         278 my $response = $self->_perform_request($request);
651              
652 1         19 return $key;
653             }
654              
655              
656             =head2 get_issue
657              
658             my $issue = $jira->get_issue($key);
659              
660             Returns details for any issue, given its key. This call returns a hash
661             containing the information for the issue in JIRA's format. See L
662             HASH FORMAT"> for details.
663              
664             =cut
665              
666             sub get_issue {
667 2     2 1 11146 my ($self, $key) = @_;
668 2         6 my $uri = "$self->{auth_url}issue/$key";
669              
670 2         7 my $request = GET $uri, Content_Type => 'application/json';
671              
672 2         309 my $response = $self->_perform_request($request);
673              
674 2         22 my $new_issue = $self->{_json}->decode($response->decoded_content());
675              
676 2         230 my $project = $new_issue->{fields}{project}{key};
677 2         5 my $issuetype = $new_issue->{fields}{issuetype}{name};
678 2         7 my $english_fields = $self->_convert_from_customfields( $project, $issuetype, $new_issue->{fields} );
679 2         10 $new_issue->{fields} = $english_fields;
680              
681 2         11 return $new_issue;
682             }
683              
684              
685             sub _get_transitions {
686 0     0     my ($self, $key) = @_;
687 0           my $uri = "$self->{auth_url}issue/$key/transitions";
688              
689 0           my $request = GET $uri, Content_Type => 'application/json';
690              
691 0           my $response = $self->_perform_request($request);
692              
693 0           my $transitions = $self->{_json}->decode($response->decoded_content())->{transitions};
694              
695 0           return $transitions;
696             }
697              
698              
699             # Each issue could have a different workflow and therefore a different
700             # transition id for 'Close Issue', so we have to look it up every time.
701             #
702             # Also, since transition names can be freely edited ('Close', 'Close it!')
703             # we also match against the destination status name, which is much more
704             # likely to remain stable ('Closed'). This is low risk because transition
705             # names are verbs and status names are nouns, so a clash is very unlikely,
706             # or if they are the same the effect is the same ('Open').
707             #
708             # We also allow the transition names to be specified as an array of names
709             # in which case the first one that matches either a transition or status is used.
710             # This makes it easier for scripts to handle the migration of names
711             # by allowing current and new names to be used so the later change in JIRA
712             # config doesn't cause any breakage.
713              
714             sub _get_transition_id {
715 0     0     my ($self, $key, $t_name) = @_;
716              
717 0           my $transitions = $self->_get_transitions($key);
718              
719 0           my %trans_names = map { $_->{name} => $_ } @$transitions;
  0            
720 0           my %status_names = map { $_->{to}{name} => $_ } @$transitions;
  0            
721              
722 0 0         my @names = (ref $t_name) ? @$t_name : ($t_name);
723 0   0       my @trans = map { $trans_names{$_} // $status_names{$_} } @names; # // is incompatible with perl <= 5.8
  0            
724 0           my $tran = (grep { defined } @trans)[0]; # use the first defined one
  0            
725              
726 0 0         if (not defined $tran) {
727 0           my @trans2status = map { "'$_->{name}' (to '$_->{to}{name}')" } @$transitions;
  0            
728             croak sprintf "%s has no transition or reachable status called %s (available transitions: %s)",
729             $key,
730 0   0       join(", ", map { "'$_'" } @names),
  0            
731             join(", ", sort @trans2status) || '';
732             }
733              
734 0 0         return $tran->{id} unless wantarray;
735 0           return ($tran->{id}, $tran);
736             }
737              
738              
739             =head2 transition_issue
740              
741             $jira->transition_issue($key, $transition);
742             $jira->transition_issue($key, $transition, $update_hash);
743              
744             Transitioning an issue is what happens when you click the button that says
745             "Resolve Issue" or "Start Progress" on it. Doing this from code is harder, but
746             JIRA::Client::Automated makes it as easy as possible.
747              
748             You pass this method the issue key, the name of the transition or the target
749             status (spacing and capitalization matter), and an optional update_hash
750             containing any fields that you want to update.
751              
752             =head3 Specifying The Transition
753              
754             The provided $transition name is first matched against the available
755             transitions for the $key issue ('Start Progress', 'Close Issue').
756             If there's no match then the names is matched against the available target
757             status names ('Open', 'Closed'). You can use whichever is most appropriate.
758             For example, in your configuration the transition names might vary between
759             different kinds of projects but the status names might be the same.
760             In which case scripts that are meant to work across multiple projects
761             might prefer to use the status names.
762              
763             The $transition parameter can also be specified as a reference to an array of
764             names. In this case the first one that matches either a transition name or
765             status name is used. This makes it easier for scripts to work across multiple
766             kinds of projects and/or handle the migration of names by allowing current and
767             future names to be used, so the later change in JIRA config doesn't cause any
768             breakage.
769              
770             =head3 Specifying Updates
771              
772             If you have required fields on the transition screen (such as "Resolution" for
773             the "Resolve Issue" screen), you must pass those fields in as part of the
774             update_hash or you will get an error from the server. See L
775             FORMAT"> for the format of the update_hash.
776              
777             (Note: it appears that in some obscure cases missing required fields may cause the
778             transition to fail I causing an error from the server. For example
779             a field that's required but isn't configured to appear on the transition screen.)
780              
781             The $update_hash is a combination of the $field_update_hash and $update_verb_hash
782             parameters used by the L method. Like this:
783              
784             $update_hash = {
785             fields => $field_update_hash,
786             update => $update_verb_hash
787             };
788              
789             You can use it to express both simple field settings and more complex update
790             operations. For example:
791              
792             $jira->transition_issue($key, $transition, {
793             fields => { summary => $new_summary },
794             update => {
795             labels => [ { remove => "test" }, { add => "another" } ],
796             comments => [ { remove => { id => 10001 } } ]
797             }
798             });
799              
800             =cut
801              
802             sub transition_issue {
803 0     0 1   my ($self, $key, $t_name, $t_hash) = @_;
804              
805 0           my $t_id = $self->_get_transition_id($key, $t_name);
806 0           $$t_hash{transition} = { id => $t_id };
807              
808 0           my $t_json = $self->{_json}->encode($t_hash);
809 0           my $uri = "$self->{auth_url}issue/$key/transitions";
810              
811 0           my $request = POST $uri,
812             Content_Type => 'application/json',
813             Content => $t_json;
814              
815 0           my $response = $self->_perform_request($request);
816              
817 0           return $key;
818             }
819              
820              
821             =head2 close_issue
822              
823             $jira->close_issue($key);
824             $jira->close_issue($key, $resolve);
825             $jira->close_issue($key, $resolve, $comment);
826             $jira->close_issue($key, $resolve, $comment, $update_hash);
827             $jira->close_issue($key, $resolve, $comment, $update_hash, $operation);
828              
829              
830             Pass in the resolution reason and an optional comment to close an issue. Using
831             this method requires that the issue is is a status where it can use the "Close
832             Issue" transition (or other one, specified by $operation).
833             If not, you will get an error from the server.
834              
835             Resolution ("Fixed", "Won't Fix", etc.) is only required if the issue hasn't
836             already been resolved in an earlier transition. If you try to resolve an issue
837             twice, you will get an error.
838              
839             If you do not supply a comment, the default value is "Issue closed by script".
840              
841             The $update_hash can be used to set or edit the values of other fields.
842              
843             The $operation parameter can be used to specify the closing transition type. This
844             can be useful when your JIRA configuration uses nonstandard or localized
845             transition and status names, e.g.
846              
847             use utf8;
848             $jira->close_issue($key, $resolve, $comment, $update_hash, "Done");
849              
850             See L for more details.
851              
852             This method is a wrapper for L.
853              
854             =cut
855              
856             sub close_issue {
857 0     0 1   my ($self, $key, $resolve, $comment, $update_hash, $operation) = @_;
858              
859 0   0       $comment //= 'Issue closed by script'; # // is incompatible with perl <= 5.8
860 0   0       $operation //= [ 'Close Issue', 'Close', 'Closed' ];
861              
862 0   0       $update_hash ||= {};
863              
864 0           push @{$update_hash->{update}{comment}}, {
  0            
865             add => { body => $comment }
866             };
867              
868 0 0         $update_hash->{fields}{resolution} = { name => $resolve }
869             if $resolve;
870              
871 0           return $self->transition_issue($key, $operation, $update_hash);
872             }
873              
874              
875             =head2 delete_issue
876              
877             $jira->delete_issue($key);
878              
879             Deleting issues is for testing your JIRA code. In real situations you almost
880             always want to close unwanted issues with an "Oops!" resolution instead.
881              
882             =cut
883              
884             sub delete_issue {
885 0     0 1   my ($self, $key) = @_;
886              
887 0           my $uri = "$self->{auth_url}issue/$key";
888              
889 0           my $request = DELETE $uri;
890              
891 0           my $response = $self->_perform_request($request);
892              
893 0           return $key;
894             }
895              
896              
897             =head2 create_comment
898              
899             $jira->create_comment($key, $text);
900              
901             You may use any valid JIRA markup in comment text. (This is handy for tables of
902             values explaining why something in the database is wrong.) Note that comments
903             are all created by the user you used to create your JIRA::Client::Automated
904             object, so you'll see that name often.
905              
906             =cut
907              
908             sub create_comment {
909 0     0 1   my ($self, $key, $text) = @_;
910              
911 0           my $comment = { body => $text };
912              
913 0           my $comment_json = $self->{_json}->encode($comment);
914 0           my $uri = "$self->{auth_url}issue/$key/comment";
915              
916 0           my $request = POST $uri,
917             Content_Type => 'application/json',
918             Content => $comment_json;
919              
920 0           my $response = $self->_perform_request($request);
921              
922 0           my $new_comment = $self->{_json}->decode($response->decoded_content());
923              
924 0           return $new_comment;
925             }
926              
927              
928             =head2 search_issues
929              
930             my @search_results = $jira->search_issues($jql, 1, 100, $fields);
931              
932             You've used JQL before, when you did an "Advanced Search" in the JIRA web
933             interface. That's the only way to search via the REST API.
934              
935             This is a paged method. Pass in the starting result number and number of
936             results per page and it will return issues a page at a time. If you know you
937             want all of the results, you can use L instead.
938              
939             Optional parameter $fields is the arrayref containing the list of fields to be returned.
940              
941             This method returns a hashref containing up to five values:
942              
943             =over 3
944              
945             =item 1.
946              
947             total => total number of results
948              
949             =item 2.
950              
951             start => result number for the first result
952              
953             =item 3.
954              
955             max => maximum number of results per page
956              
957             =item 4.
958              
959             issues => an arrayref containing the actual found issues
960              
961             =item 5.
962              
963             errors => an arrayref containing error messages
964              
965             =back
966              
967             For example, to page through all results C<$max> at a time:
968              
969             my (@all_results, @issues);
970             do {
971             $results = $self->search_issues($jql, $start, $max);
972             if ($results->{errors}) {
973             die join "\n", @{$results->{errors}};
974             }
975             @issues = @{$results->{issues}};
976             push @all_results, @issues;
977             $start += $max;
978             } until (scalar(@issues) < $max);
979              
980             (Or just use L instead.)
981              
982             =cut
983              
984             # This is a paged method. You pass in the starting number and max to retrieve and it returns those and the total
985             # number of hits. To get the next page, call search_issues() again with the start value = start + max, until total
986             # < max
987             # Note: if $max is > 1000 (set by jira.search.views.default.max in
988             # http://jira.example.com/secure/admin/ViewSystemInfo.jspa) then it'll be truncated to 1000 anyway.
989             sub search_issues {
990 0     0 1   my ($self, $jql, $start, $max, $fields) = @_;
991              
992 0   0       $fields ||= ['*navigable'];
993 0           my $query = {
994             jql => $jql,
995             startAt => $start,
996             maxResults => $max,
997             fields => $fields,
998             };
999              
1000 0           my $query_json = $self->{_json}->encode($query);
1001 0           my $uri = "$self->{auth_url}search/";
1002              
1003 0           my $request = POST $uri,
1004             Content_Type => 'application/json',
1005             Content => $query_json;
1006              
1007             my $response = $self->_perform_request($request, {
1008             400 => sub { # pass-thru 400 responses for us to deal with below
1009 0     0     my ($response, $request, $self) = @_;
1010 0           return $response;
1011             },
1012 0           });
1013              
1014 0 0         if ($response->code == 400) {
1015 0           my $error_msg = $self->{_json}->decode($response->decoded_content());
1016 0           return { total => 0, errors => $error_msg->{errorMessages} };
1017             }
1018              
1019 0           my $results = $self->{_json}->decode($response->decoded_content());
1020              
1021             return {
1022             total => $$results{total},
1023             start => $$results{startAt},
1024             max => $$results{maxResults},
1025 0           issues => $$results{issues} };
1026             }
1027              
1028              
1029             =head2 all_search_results
1030              
1031             my @issues = $jira->all_search_results($jql, 1000);
1032              
1033             Like L, but returns all the results as an array of issues.
1034             You can specify the maximum number to return, but no matter what, it can't
1035             return more than the value of jira.search.views.default.max for your JIRA
1036             installation.
1037              
1038             =cut
1039              
1040             sub all_search_results {
1041 0     0 1   my ($self, $jql, $max) = @_;
1042              
1043 0           my $start = 0;
1044 0   0       $max //= 100; # is a param for testing ; // is incompatible with perl <= 5.8
1045 0           my $total = 0;
1046 0           my (@all_results, @issues, $results);
1047              
1048 0           do {
1049 0           $results = $self->search_issues($jql, $start, $max);
1050 0 0         if ($results->{errors}) {
1051 0           die join "\n", @{ $results->{errors} };
  0            
1052             }
1053 0           @issues = @{ $results->{issues} };
  0            
1054 0           push @all_results, @issues;
1055 0           $start += $max;
1056             } until (scalar(@issues) < $max);
1057              
1058 0           return @all_results;
1059             }
1060              
1061             =head2 get_issue_comments
1062              
1063             $jira->get_issue_comments($key);
1064              
1065             Returns arryref of all comments to the given issue.
1066              
1067             =cut
1068              
1069             sub get_issue_comments {
1070 0     0 1   my ($self, $key) = @_;
1071 0           my $uri = "$self->{auth_url}issue/$key/comment";
1072 0           my $request = GET $uri;
1073 0           my $response = $self->_perform_request($request);
1074 0           my $content = $self->{_json}->decode($response->decoded_content());
1075              
1076             # dereference to get just the comments arrayref
1077 0           my $comments = $content->{comments};
1078 0           return $comments;
1079             }
1080              
1081             =head2 attach_file_to_issue
1082              
1083             $jira->attach_file_to_issue($key, $filename);
1084              
1085             This method does not let you attach a comment to the issue at the same time.
1086             You'll need to call L for that.
1087              
1088             Watch out for file permissions! If the user running the script does not have
1089             permission to read the file it is trying to upload, you'll get weird errors.
1090              
1091             =cut
1092              
1093             sub attach_file_to_issue {
1094 0     0 1   my ($self, $key, $filename) = @_;
1095              
1096 0           my $uri = "$self->{auth_url}issue/$key/attachments";
1097              
1098 0           my $request = POST $uri,
1099             Content_Type => 'form-data',
1100             'X-Atlassian-Token' => 'nocheck', # required by JIRA XSRF protection
1101             Content => [file => [$filename],];
1102              
1103 0           my $response = $self->_perform_request($request);
1104              
1105 0           my $new_attachment = $self->{_json}->decode($response->decoded_content());
1106              
1107 0           return $new_attachment;
1108             }
1109              
1110              
1111             =head2 make_browse_url
1112              
1113             my $url = $jira->make_browse_url($key);
1114              
1115             A helper method to return the "C<.../browse/$key>" url for the issue.
1116             It's handy to make emails containing lists of bugs easier to create.
1117              
1118             This just appends the key to the URL for the JIRA server so that you can click
1119             on it and go directly to that issue.
1120              
1121             =cut
1122              
1123             sub make_browse_url {
1124 0     0 1   my ($self, $key) = @_;
1125             # use url + browse + key to synthesize URL
1126 0           my $url = $self->{url};
1127 0           $url =~ s/\/rest\/api\/.*//;
1128 0 0         $url .= '/' unless $url =~ m{/$};
1129 0           return $url . 'browse/' . $key;
1130             }
1131              
1132             =head2 get_link_types
1133              
1134             my $all_link_types = $jira->get_link_types();
1135              
1136             Get the arrayref of all possible link types.
1137              
1138             =cut
1139              
1140             sub get_link_types {
1141 0     0 1   my ($self) = @_;
1142              
1143 0           my $uri = "$self->{auth_url}issueLinkType";
1144 0           my $request = GET $uri;
1145 0           my $response = $self->_perform_request($request);
1146              
1147             # dereference to arrayref, for convenience later
1148 0           my $content = $self->{_json}->decode($response->decoded_content());
1149 0           my $link_types = $content->{issueLinkTypes};
1150              
1151 0           return $link_types;
1152             }
1153              
1154             =head2 link_issues
1155              
1156             $jira->link_issues($from, $to, $type);
1157              
1158             Establish a link of the type named $type from issue key $from to issue key $to .
1159             Returns nothing on success; structure containing error messages otherwise.
1160              
1161             =cut
1162              
1163              
1164             sub link_issues {
1165 0     0 1   my ($self, $from, $to, $type) = @_;
1166              
1167 0           my $uri = "$self->{auth_url}issueLink/";
1168 0           my $link = {
1169             inwardIssue => { key => $to },
1170             outwardIssue => { key => $from },
1171             type => { name => $type },
1172             };
1173              
1174 0           my $link_json = $self->{_json}->encode($link);
1175              
1176 0           my $request = POST $uri,
1177             Content_Type => 'application/json',
1178             Content => $link_json;
1179              
1180 0           my $response = $self->_perform_request($request);
1181              
1182 0 0         if($response->code != 201) {
1183 0           return $self->{_json}->decode($response->decoded_content());
1184             }
1185 0           return;
1186             }
1187              
1188             =head2 add_issue_labels
1189              
1190             $jira->add_issue_labels($issue_key, @labels);
1191              
1192             Adds one more more labels to the specified issue.
1193              
1194             =cut
1195              
1196              
1197             sub add_issue_labels {
1198 0     0 1   my ($self, $issue_key, @labels) = @_;
1199 0           $self->update_issue($issue_key, {}, { labels => [ map {{ add => $_ }} @labels ] } );
  0            
1200             }
1201              
1202             =head2 remove_issue_labels
1203              
1204             $jira->remove_issue_labels($issue_key, @labels);
1205              
1206             Removes one more more labels from the specified issue.
1207              
1208             =cut
1209              
1210             sub remove_issue_labels {
1211 0     0 1   my ($self, $issue_key, @labels) = @_;
1212 0           $self->update_issue($issue_key, {}, { labels => [ map {{ remove => $_ }} @labels ] } );
  0            
1213             }
1214              
1215             =head2 add_issue_watchers
1216              
1217             $jira->add_issue_watchers($key, @watchers);
1218              
1219             Adds watchers to the specified issue. Returns nothing if success; otherwise returns a structure containing error message.
1220              
1221             =cut
1222              
1223             sub add_issue_watchers {
1224 0     0 1   my ($self, $key, @watchers) = @_;
1225 0           my $uri = "$self->{auth_url}issue/$key/watchers";
1226 0           foreach my $w (@watchers) {
1227             my $request = POST $uri,
1228             Content_Type => 'application/json',
1229 0           Content => $self->{_json}->encode($w);
1230 0           my $response = $self->_perform_request($request);
1231 0 0         if($response->code != 204) {
1232 0           return $self->{_json}->decode($response->decoded_content());
1233             }
1234             }
1235 0           return;
1236             }
1237              
1238             =head2 get_issue_watchers
1239              
1240             $jira->get_issue_watchers($key);
1241              
1242             Returns arryref of all watchers of the given issue.
1243              
1244             =cut
1245              
1246             sub get_issue_watchers {
1247 0     0 1   my ($self, $key) = @_;
1248 0           my $uri = "$self->{auth_url}issue/$key/watchers";
1249 0           my $request = GET $uri;
1250 0           my $response = $self->_perform_request($request);
1251 0           my $content = $self->{_json}->decode($response->decoded_content());
1252              
1253             # dereference to get just the watchers arrayref
1254 0           my $watchers = $content->{watchers};
1255 0           return $watchers;
1256             }
1257              
1258             =head2 assign_issue
1259              
1260             $jira->assign_issue($key, $assignee_name);
1261              
1262             Assigns the issue to that person. Returns the key of the issue if it succeeds.
1263              
1264             =cut
1265              
1266             sub assign_issue {
1267 0     0 1   my ($self, $key, $assignee_name) = @_;
1268              
1269 0           my $assignee = {};
1270 0           $assignee->{name} = $assignee_name;
1271              
1272 0           my $issue_json = $self->{_json}->encode($assignee);
1273 0           my $uri = "$self->{auth_url}issue/$key/assignee";
1274              
1275 0           my $request = PUT $uri,
1276             Content_Type => 'application/json',
1277             Content => $issue_json;
1278              
1279 0           my $response = $self->_perform_request($request);
1280              
1281 0           return $key;
1282             }
1283              
1284             =head2 add_issue_worklog
1285              
1286             $jira->add_issue_worklog($key, $worklog);
1287              
1288             Adds a worklog to the specified issue. Returns nothing if success; otherwise returns a structure containing error message.
1289              
1290             Sample worklog:
1291             {
1292             "comment" => "I did some work here.",
1293             "started" => "2016-05-27T02:32:26.797+0000",
1294             "timeSpentSeconds" => 12000,
1295             }
1296              
1297             =cut
1298              
1299             sub add_issue_worklog {
1300 0     0 1   my ($self, $key, $worklog) = @_;
1301 0           my $uri = "$self->{auth_url}issue/$key/worklog";
1302             my $request = POST $uri,
1303             Content_Type => 'application/json',
1304 0           Content => $self->{_json}->encode($worklog);
1305 0           my $response = $self->_perform_request($request);
1306 0 0         if($response->code != 201) {
1307 0           return $self->{_json}->decode($response->decoded_content());
1308             }
1309 0           return;
1310             }
1311              
1312             =head2 get_issue_worklogs
1313              
1314             $jira->get_issue_worklogs($key);
1315              
1316             Returns arryref of all worklogs of the given issue.
1317              
1318             =cut
1319              
1320             sub get_issue_worklogs {
1321 0     0 1   my ($self, $key) = @_;
1322 0           my $uri = "$self->{auth_url}issue/$key/worklog";
1323 0           my $request = GET $uri;
1324 0           my $response = $self->_perform_request($request);
1325 0           my $content = $self->{_json}->decode($response->decoded_content());
1326              
1327             # dereference to get just the worklogs arrayref
1328 0           my $worklogs = $content->{worklogs};
1329 0           return $worklogs;
1330             }
1331              
1332             =head1 FAQ
1333              
1334             =head2 Why is there no object for a JIRA issue?
1335              
1336             Because it seemed silly. You I write such an object and give it methods
1337             to transition itself, close itself, etc., but when you are working with JIRA
1338             from batch scripts, you're never really working with just one issue at a time.
1339             And when you have a hundred of them, it's easier to not objectify them and just
1340             use JIRA::Client::Automated as a mediator. That said, if this is important to
1341             you, I wouldn't say no to a patch offering this option.
1342              
1343             =head1 BUGS
1344              
1345             Please report bugs or feature requests to the author.
1346              
1347             =head1 AUTHOR
1348              
1349             Michael Friedman
1350              
1351             =head1 CREDITS
1352              
1353             Thanks very much to:
1354              
1355             =over 4
1356              
1357             =item Tim Bunce
1358              
1359             =back
1360              
1361             =over 4
1362              
1363             =item Dominique Dumont
1364              
1365             =back
1366              
1367             =over 4
1368              
1369             =item Zhuang (John) Li <7humblerocks@gmail.com>
1370              
1371             =back
1372              
1373             =over 4
1374              
1375             =item Ivan E. Panchenko
1376              
1377             =back
1378              
1379             =encoding utf8
1380              
1381             =over 4
1382              
1383             =item José Antonio Perez Testa
1384              
1385             =back
1386              
1387             =over 4
1388              
1389             =item Frank Schophuizen
1390              
1391             =back
1392              
1393             =over 4
1394              
1395             =item Zhenyi Zhou
1396              
1397             =back
1398              
1399             =over 4
1400              
1401             =item Roy Lyons
1402              
1403             =back
1404              
1405             =over 4
1406              
1407             =item Neil Hemingway
1408              
1409             =back
1410              
1411             =over 4
1412              
1413             =item Andreas Mager
1414              
1415             =back
1416              
1417             =over 4
1418              
1419             =item Mike Svendsen
1420              
1421             =back
1422              
1423             =head1 COPYRIGHT AND LICENSE
1424              
1425             This software is copyright (c) 2016 by Polyvore, Inc.
1426              
1427             This is free software; you can redistribute it and/or modify it under
1428             the same terms as the Perl 5 programming language system itself.
1429              
1430             =cut
1431              
1432             1;