Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 47 additions & 14 deletions services/jira/jira-sprint.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const queryParamSchema = Joi.object({

const schema = Joi.object({
total: Joi.number(),
nextPageToken: Joi.string(),
isLast: Joi.boolean(),
issues: Joi.array()
.items(
Joi.object({
Expand Down Expand Up @@ -78,29 +80,60 @@ export default class JiraSprint extends BaseJsonService {
}
}

async handle({ sprintId }, { baseUrl }) {
// Atlassian Documentation: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-group-Search
// There are other sprint-specific APIs but those require authentication. The search API
// allows us to get the needed data without being forced to authenticate.
const json = await this._requestJson(
static isCloud(baseUrl) {
const { hostname, pathname } = new URL(baseUrl)
return (
hostname.endsWith('.atlassian.net') ||
(hostname === 'api.atlassian.com' && pathname.startsWith('/ex/jira'))
)
}

async fetchPage({ baseUrl, sprintId, isCloud, nextPageToken }) {
const searchParams = {
jql: `sprint=${sprintId} AND type IN (Bug,Improvement,Story,"Technical task")`,
fields: 'resolution',
maxResults: 500,
}
if (nextPageToken) {
searchParams.nextPageToken = nextPageToken
}
return this._requestJson(
this.authHelper.withBasicAuth({
url: `${baseUrl}/rest/api/2/search`,
// Jira Cloud removed /search in favor of /search/jql (CHANGE-2046);
// Server and Data Center keep the old endpoint.
url: `${baseUrl}/rest/api/2/${isCloud ? 'search/jql' : 'search'}`,
schema,
options: {
searchParams: {
jql: `sprint=${sprintId} AND type IN (Bug,Improvement,Story,"Technical task")`,
fields: 'resolution',
maxResults: 500,
},
},
options: { searchParams },
httpErrors: {
400: 'sprint not found',
404: 'sprint not found',
},
}),
)
}

const numTotalIssues = json.total
async handle({ sprintId }, { baseUrl }) {
// Atlassian Documentation: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-group-Search
// There are other sprint-specific APIs but those require authentication. The search API
// allows us to get the needed data without being forced to authenticate.
const isCloud = this.constructor.isCloud(baseUrl)
const json = await this.fetchPage({ baseUrl, sprintId, isCloud })

let numTotalIssues = json.total
if (isCloud) {
// The Cloud endpoint returns no total, so fetch every page and count the issues.
let page = json
while (!page.isLast && page.nextPageToken) {
page = await this.fetchPage({
baseUrl,
sprintId,
isCloud,
nextPageToken: page.nextPageToken,
})
json.issues.push(...page.issues)
}
numTotalIssues = json.issues.length
}
const numCompletedIssues = json.issues.filter(issue => {
if (issue.fields.resolution != null) {
return issue.fields.resolution.name !== 'Unresolved'
Expand Down
38 changes: 38 additions & 0 deletions services/jira/jira-sprint.tester.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,44 @@ t.create('no issues in sprint')
color: 'red',
})

t.create('jira cloud paginates with nextPageToken')
.get(`/${sprintId}.json?baseUrl=https://example.atlassian.net`)
.intercept(nock =>
nock('https://example.atlassian.net/rest/api/2')
.get('/search/jql')
.query(sprintQueryString)
.reply(200, {
nextPageToken: 'page2',
isLast: false,
issues: [
{
fields: {
resolution: {
name: 'done',
},
},
},
],
})
.get('/search/jql')
.query({ ...sprintQueryString, nextPageToken: 'page2' })
.reply(200, {
isLast: true,
issues: [
{
fields: {
resolution: null,
},
},
],
}),
)
.expectBadge({
label: 'completion',
message: '50%',
color: 'orange',
})

t.create('issue with null resolution value')
.get(`/${sprintId}.json?baseUrl=https://jira.spring.io:8080`)
.intercept(nock =>
Expand Down
Loading