From 71614d87b63ad4333d4cf157f309bb902b58023e Mon Sep 17 00:00:00 2001 From: Bartosz Oudekerk Date: Mon, 15 Sep 2025 14:49:53 +0200 Subject: [PATCH 1/2] Fix after API deprecation --- lib/JIRA/REST.pm | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/lib/JIRA/REST.pm b/lib/JIRA/REST.pm index 2a0e93a..3d0e0a5 100644 --- a/lib/JIRA/REST.pm +++ b/lib/JIRA/REST.pm @@ -17,7 +17,7 @@ use HTTP::CookieJar::LWP; sub new { my ($class, %args) = &_grok_args; - my ($path, $api) = ($args{url}->path, '/rest/api/latest'); + my ($path, $api) = ($args{url}->path, '/rest/api/3'); # See if the user wants a default REST API: if ($path =~ s:(/rest/.*)$::) { $api = $1; @@ -350,18 +350,34 @@ sub set_search_iterator { sub next_issue { my ($self) = @_; - my $iter = $self->{iter} or croak $self->_error("You must call set_search_iterator before calling next_issue"); if ($iter->{offset} == $iter->{results}{total}) { - # This is the end of the search results $self->{iter} = undef; return; } elsif ($iter->{offset} == $iter->{results}{startAt} + @{$iter->{results}{issues}}) { - # Time to get the next bunch of issues - $iter->{params}{startAt} = $iter->{offset}; - $iter->{results} = $self->POST('/search', undef, $iter->{params}); + my %params = %{$iter->{params}}; + my $jql = delete $params{jql}; + + # Use the format we know works + my $search_request = { + jql => $jql, + fields => ["*all"] + }; + + my $result = $self->POST('/search/jql', undef, $search_request); + + if ($result) { + # Convert the new /search/jql response format to match the old /search format + my $converted_result = { + startAt => 0, + total => scalar(@{$result->{issues} // []}), + issues => $result->{issues} // [], + }; + + $iter->{results} = $converted_result; + } } return $iter->{results}{issues}[$iter->{offset}++ - $iter->{results}{startAt}]; @@ -379,7 +395,7 @@ sub attach_files { # FIXME: How to attach all files at once? foreach my $file (@files) { my $response = $rest->getUseragent()->post( - $rest->getHost . "/rest/api/latest/issue/$issueIdOrKey/attachments", + $rest->getHost . "/rest/api/3/issue/$issueIdOrKey/attachments", %{$rest->{_headers}}, 'X-Atlassian-Token' => 'nocheck', 'Content-Type' => 'form-data', From a56ff8f3ed7038b3605d16a173a74f3f79c12f80 Mon Sep 17 00:00:00 2001 From: Bartosz Oudekerk Date: Wed, 10 Dec 2025 12:35:36 +0100 Subject: [PATCH 2/2] Clean up ugly hack --- lib/JIRA/REST.pm | 50 ++++++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/lib/JIRA/REST.pm b/lib/JIRA/REST.pm index 3d0e0a5..1536d19 100644 --- a/lib/JIRA/REST.pm +++ b/lib/JIRA/REST.pm @@ -333,16 +333,14 @@ sub set_search_iterator { my %params = ( %$params ); # rebuild the hash to own it - $params{startAt} = 0; - $self->{iter} = { params => \%params, # params hash to be used in the next call offset => 0, # offset of the next issue to be fetched results => { # results of the last call (this one is fake) - startAt => 0, - total => -1, - issues => [], + isLast => 0, + issues => [], }, + nextPageToken => undef, # token for next page }; return; @@ -353,10 +351,13 @@ sub next_issue { my $iter = $self->{iter} or croak $self->_error("You must call set_search_iterator before calling next_issue"); - if ($iter->{offset} == $iter->{results}{total}) { - $self->{iter} = undef; - return; - } elsif ($iter->{offset} == $iter->{results}{startAt} + @{$iter->{results}{issues}}) { + if ($iter->{offset} >= @{$iter->{results}{issues}}) { + # If we've already reached the last page, we're done + if ($iter->{results}{isLast}) { + $self->{iter} = undef; + return; + } + my %params = %{$iter->{params}}; my $jql = delete $params{jql}; @@ -366,21 +367,29 @@ sub next_issue { fields => ["*all"] }; + # Add nextPageToken if we have one + if ($iter->{nextPageToken}) { + $search_request->{nextPageToken} = $iter->{nextPageToken}; + } + my $result = $self->POST('/search/jql', undef, $search_request); if ($result) { - # Convert the new /search/jql response format to match the old /search format - my $converted_result = { - startAt => 0, - total => scalar(@{$result->{issues} // []}), + $iter->{results} = { + isLast => $result->{isLast} // 0, issues => $result->{issues} // [], }; - - $iter->{results} = $converted_result; + # nextPageToken will be null/undefined on the last page + $iter->{nextPageToken} = $result->{nextPageToken}; + $iter->{offset} = 0; # Reset offset for the new page + } else { + # No more results + $self->{iter} = undef; + return; } } - return $iter->{results}{issues}[$iter->{offset}++ - $iter->{results}{startAt}]; + return $iter->{results}{issues}[$iter->{offset}++]; } sub attach_files { @@ -457,7 +466,6 @@ __END__ # Iterate on issues my $search = $jira->POST('/search', undef, { jql => 'project = "TST" and status = "open"', - startAt => 0, maxResults => 16, fields => [ qw/summary status assignee/ ], }); @@ -757,8 +765,8 @@ when the Jira API isn't enough and you have to go deeper. Sets up an iterator for the search specified by the hash reference PARAMS. It must be called before calls to B. -PARAMS must conform with the query parameters allowed for the -C Jira REST endpoint. +PARAMS must conform with the body parameters allowed for the +C Jira REST endpoint. =head2 B @@ -767,8 +775,8 @@ returns a reference to the next issue from the filter. When there are no more issues it returns undef. Using the set_search_iterator/next_issue utility methods you can iterate -through large sets of issues without worrying about the startAt/total/offset -attributes in the response from the /search REST endpoint. These methods +through large sets of issues without worrying about the isLast/nextPageToken +pagination attributes in the response from the /search/jql REST endpoint. These methods implement the "paging" algorithm needed to work with those attributes. =head2 B ISSUE FILE...