Skip to content

Commit 058d776

Browse files
committed
LDEV-5867 update jira api due to deprecation
1 parent e4dc54a commit 058d776

File tree

3 files changed

+236
-4
lines changed

3 files changed

+236
-4
lines changed

apps/updateserver/services/JiraChangeLogService.cfc

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,39 @@ component accessors=true {
110110

111111
private function _fetchIssues() {
112112
systemOutput("-- start fetching issues from jira --- ", true);
113-
var jira = new services.legacy.Jira( getJiraServer() );
114-
var issues = jira.listIssues( project:"LDEV", stati: [ "Deployed", "Done", "QA", "Resolved" ] ).issues;
113+
var jira = new services.JiraCloud({ domain: getJiraServer() });
114+
var issuesArray = jira.searchIssues(jql="project=LDEV AND status in (Deployed, Done, QA, Resolved)");
115+
var issuesQuery = _issuesArrayToQuery(issuesArray);
115116
systemOutput("-- finished fetching issues from jira --- ", true);
116-
return issues;
117+
return issuesQuery;
118+
}
119+
120+
private query function _issuesArrayToQuery(required array issues) {
121+
var qry = _getEmptyIssuesQuery();
122+
123+
loop array=arguments.issues item="issue" {
124+
queryAddRow( qry );
125+
querySetCell( qry, "id", issue.id );
126+
querySetCell( qry, "key", issue.key );
127+
querySetCell( qry, "summary", issue.fields.summary ?: "" );
128+
querySetCell( qry, "self", issue.self );
129+
querySetCell( qry, "type", issue.fields.issuetype.name ?: "" );
130+
querySetCell( qry, "created", len( issue.fields.created ?: "" ) ? parseDateTime( issue.fields.created ) : "" );
131+
querySetCell( qry, "updated", len( issue.fields.updated ?: "" ) ? parseDateTime( issue.fields.updated ) : "" );
132+
querySetCell( qry, "priority", issue.fields.priority.name ?: "" );
133+
querySetCell( qry, "status", issue.fields.status.name ?: "" );
134+
135+
var fixVersions = [];
136+
if (isArray(issue.fields.fixVersions)) {
137+
loop array=issue.fields.fixVersions item="fv" {
138+
arrayAppend(fixVersions, fv.name);
139+
}
140+
}
141+
querySetCell(qry, "fixVersions", fixVersions);
142+
querySetCell(qry, "labels", issue.fields.labels ?: []);
143+
}
144+
145+
return qry;
117146
}
118147

119148
private function _readExistingIssuesFromS3() {
@@ -131,7 +160,7 @@ component accessors=true {
131160
}
132161

133162
private function _getEmptyIssuesQuery() {
134-
return QueryNew( "id,key,summary,self,type,created,updated,priority,status,fixVersions" );
163+
return QueryNew( "id,key,summary,self,type,created,updated,priority,status,fixVersions,labels" );
135164
}
136165

137166
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* A modern CFML component for interacting with the Jira Cloud REST API (v3).
3+
* This component uses API token-based authentication and handles pagination correctly.
4+
*/
5+
component {
6+
7+
/**
8+
* Constructor for the JiraCloud component.
9+
*
10+
* @param config A struct containing the Jira connection details.
11+
* - domain: Your Jira domain (e.g., "your-company.atlassian.net").
12+
* - email: The email address of the user for authentication.
13+
* - apiToken: The API token generated from the user's Atlassian account.
14+
*/
15+
public function init(required struct config) {
16+
variables.config = arguments.config;
17+
18+
// Validate required config keys
19+
var requiredKeys = ["domain"];
20+
for (var key in requiredKeys) {
21+
if (!structKeyExists(variables.config, key) || isEmpty(variables.config[key])) {
22+
throw(type="Jira.ConfigurationException", message="Missing required configuration key: #key#");
23+
}
24+
}
25+
26+
// Setup auth token only if email and apiToken are provided
27+
if (structKeyExists(variables.config, "email") && !isEmpty(variables.config.email) &&
28+
structKeyExists(variables.config, "apiToken") && !isEmpty(variables.config.apiToken)) {
29+
variables.config.baseAuthToken = ToBase64(variables.config.email & ":" & variables.config.apiToken, "utf-8");
30+
} else {
31+
variables.config.baseAuthToken = "";
32+
}
33+
34+
variables.config.baseUrl = "https://" & variables.config.domain & "/rest/api/3";
35+
36+
return this;
37+
}
38+
39+
/**
40+
* Searches for issues using a JQL query.
41+
* This method handles pagination automatically and returns a complete array of issues.
42+
* Uses the new /search/jql endpoint (as of August 2025, /search is deprecated).
43+
*
44+
* @param jql The JQL query string.
45+
* @param fields An array of fields to return for each issue. Defaults to a common set.
46+
* @param maxResults The number of issues to return per page.
47+
*/
48+
public array function searchIssues(
49+
required string jql,
50+
array fields = ["summary", "status", "issuetype", "created", "updated", "priority", "fixVersions", "labels"],
51+
numeric maxResults = 1000
52+
) {
53+
var allIssues = [];
54+
var nextPageToken = "";
55+
var pageNum = 1;
56+
var maxPages = 50; // Safety limit to prevent infinite loops
57+
58+
do {
59+
var body = {
60+
"jql" = arguments.jql,
61+
"fields" = arguments.fields,
62+
"maxResults" = arguments.maxResults
63+
};
64+
65+
// Add nextPageToken for pagination if we have one
66+
if ( len( nextPageToken ) ) {
67+
body[ "nextPageToken" ] = nextPageToken;
68+
}
69+
70+
// Make the request using the new /search/jql endpoint
71+
var result = _request( method="POST", path="/search/jql", body=body );
72+
73+
// Add the fetched issues to our main array
74+
if ( isStruct( result ) && structKeyExists( result, "issues" ) && isArray( result.issues ) ) {
75+
arrayAppend( allIssues, result.issues, true );
76+
}
77+
78+
// Check if we need to continue paging using nextPageToken
79+
nextPageToken = result.nextPageToken ?: "";
80+
pageNum++;
81+
82+
} while ( len( nextPageToken ) && pageNum <= maxPages );
83+
84+
return allIssues;
85+
}
86+
87+
/**
88+
* Private helper function to execute HTTP requests to the Jira API.
89+
*/
90+
private struct function _request(
91+
required string method,
92+
required string path,
93+
struct body = {}
94+
) {
95+
var fullUrl = variables.config.baseUrl & arguments.path;
96+
97+
cfhttp( method=arguments.method, url=fullUrl, result="local.result", throwOnError=false ) {
98+
// Add Authorization header only if a token is available
99+
if ( len( variables.config.baseAuthToken ) ) {
100+
cfhttpparam( type="header", name="Authorization", value="Basic " & variables.config.baseAuthToken );
101+
}
102+
cfhttpparam( type="header", name="Accept", value="application/json" );
103+
104+
if ( structCount( arguments.body ) ) {
105+
cfhttpparam( type="header", name="Content-Type", value="application/json" );
106+
cfhttpparam( type="body", value=serializeJson( arguments.body ) );
107+
}
108+
}
109+
110+
if ( local.result.statusCode != "200 OK" ) {
111+
throw(
112+
type="HTTPException",
113+
message="#local.result.statusCode#",
114+
detail="#arguments.method# #fullUrl#" & chr( 10 ) & "Response: " & local.result.fileContent
115+
);
116+
}
117+
118+
return deserializeJson( local.result.fileContent );
119+
}
120+
121+
}

tests/testJiraChangelog.cfc

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
component extends="org.lucee.cfml.test.LuceeTestCase" labels="data-provider-integration" {
2+
3+
function beforeAll(){
4+
variables.dir = getDirectoryFromPath( getCurrentTemplatePath() );
5+
application action="update" mappings={
6+
"/services" : expandPath( dir & "../apps/updateserver/services" )
7+
};
8+
}
9+
10+
function run( testResults, testBox ) {
11+
describe( "JIRA Changelog Service Tests", function() {
12+
13+
it( title="test JiraChangeLogService can be instantiated", body=function(){
14+
var service = new services.JiraChangeLogService();
15+
expect( service ).toBeComponent();
16+
} );
17+
18+
it( title="test loadIssues reads from S3 or fetches", body=function(){
19+
var service = new services.JiraChangeLogService();
20+
service.setS3Root( expandPath( dir & "../apps/updateserver/services/legacy/build/servers/" ) );
21+
22+
// This should read existing issues.json or create empty query
23+
try {
24+
service.loadIssues( force=false );
25+
var issues = service.getIssues();
26+
expect( issues ).toBeQuery();
27+
systemOutput( "Loaded #issues.recordCount# issues from cache", true );
28+
} catch ( any e ) {
29+
systemOutput( "Error loading issues: #e.message#", true );
30+
rethrow;
31+
}
32+
} );
33+
34+
it( title="test getChangelog returns structured data", body=function(){
35+
var service = new services.JiraChangeLogService();
36+
service.setS3Root( expandPath( dir & "../apps/updateserver/services/legacy/build/servers/" ) );
37+
38+
try {
39+
service.loadIssues( force=false );
40+
41+
// Test with known version range
42+
var changelog = service.getChangelog(
43+
versionFrom = "6.0.0.0",
44+
versionTo = "6.2.0.0",
45+
detailed = false
46+
);
47+
48+
expect( changelog ).toBeStruct();
49+
systemOutput( "Changelog has #structCount( changelog )# versions", true );
50+
51+
// If we got data, verify structure
52+
if ( structCount( changelog ) > 0 ) {
53+
var firstVersion = structKeyArray( changelog )[ 1 ];
54+
expect( changelog[ firstVersion ] ).toBeStruct();
55+
systemOutput( "First version #firstVersion# has #structCount( changelog[ firstVersion ] )# issues", true );
56+
}
57+
} catch ( any e ) {
58+
systemOutput( "Error getting changelog: #e.message#", true );
59+
rethrow;
60+
}
61+
} );
62+
63+
it( title="test getChangeLogUpdated returns a date", body=function(){
64+
var service = new services.JiraChangeLogService();
65+
service.setS3Root( expandPath( dir & "../apps/updateserver/services/legacy/build/servers/" ) );
66+
67+
try {
68+
service.loadIssues( force=false );
69+
var lastUpdated = service.getChangeLogUpdated();
70+
71+
expect( lastUpdated ).toBeDate();
72+
systemOutput( "Changelog last updated: #lastUpdated#", true );
73+
} catch ( any e ) {
74+
systemOutput( "Error getting last updated: #e.message#", true );
75+
rethrow;
76+
}
77+
} );
78+
79+
} );
80+
}
81+
82+
}

0 commit comments

Comments
 (0)