-
Notifications
You must be signed in to change notification settings - Fork 0
/
jira.ts
297 lines (272 loc) · 10.1 KB
/
jira.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
import JiraApi from "jira-client";
export type TicketResponse = {
readonly total: number;
readonly issues: ReadonlyArray<string>;
};
type SprintResponse = {
readonly maxResults: number;
readonly startAt: number;
readonly isLast: boolean;
readonly values: ReadonlyArray<{
readonly id: number;
readonly state: "future" | "active" | "closed";
readonly name: string;
}>;
};
type BoardIssuesForSprintResponse = {
readonly total: number;
readonly issues: ReadonlyArray<{
readonly key: string;
}>;
};
export type JiraBoard = {
readonly id: string;
readonly type: "kanban" | "scrum";
};
/**
* Parses to issue list and total number.
*
* @param response The query response from Jira API.
* @returns An object consisting of issues and total count.
*/
const parseJiraResponse = (response: JiraApi.JsonResponse): TicketResponse => {
// TODO parse the response using io-ts.
return {
issues: response.issues.map((issue: any) => issue.key),
total: response.total,
};
};
const issuesForScrumBoard = async (
jira: JiraApi,
board: JiraBoard,
inProgressOrToDoJql: string,
) => {
// For scrum boards we have to get all (non-completed) tickets in all active sprints, then all (non-completed) tickets in all future sprints and finally all backlog tickets.
const activeSprints = (await jira.getAllSprints(
board.id,
undefined,
undefined,
"active",
)) as SprintResponse;
const futureSprints = (await jira.getAllSprints(
board.id,
undefined,
undefined,
"future",
)) as SprintResponse;
const allSprintIDs = activeSprints.values
.concat(futureSprints.values)
.map((sprint) => sprint.id.toString());
const currentAndFutureSprints = await Promise.all(
allSprintIDs.map(
(sprintID) =>
// TODO: handle pagination and get all results instead of assuming they will always be less than 1000.
jira.getBoardIssuesForSprint(
board.id,
sprintID,
undefined,
1000,
inProgressOrToDoJql,
) as Promise<BoardIssuesForSprintResponse>,
),
);
const currentAndFutureSprintTickets = currentAndFutureSprints.reduce(
(previousValue, currentValue) => ({
total: previousValue.total + currentValue.total,
issues: previousValue.issues.concat(currentValue.issues),
}),
);
// TODO: handle pagination and get all results instead of assuming they will always be less than 1000.
const backlogTickets = await jira.getIssuesForBacklog(
board.id,
undefined,
1000,
inProgressOrToDoJql,
);
return {
total: currentAndFutureSprintTickets.total + backlogTickets.total,
issues: currentAndFutureSprintTickets.issues
.concat(backlogTickets.issues)
.map((issue) => issue.key),
};
};
const issuesForKanbanBoard = async (
jira: JiraApi,
board: JiraBoard,
inProgressOrToDoJql: string,
kanbanInProgressJql: string,
kanbanToDoJql: string,
) => {
// For kanban boards we get all in progress tickets and all to do (backlog) tickets.
// HACK: The Jira API won't let us use `getIssuesForBacklog` for kanban boards, so we first get in progress tickets, then "to do" tickets, then combine.
// TODO: handle pagination and get all results instead of assuming they will always be less than 1000.
const inProgressTickets = parseJiraResponse(
await jira.getIssuesForBoard(
board.id,
undefined,
1000,
`(${inProgressOrToDoJql}) and (${kanbanInProgressJql})`,
),
);
// TODO: handle pagination and get all results instead of assuming they will always be less than 1000.
const toDoTickets = parseJiraResponse(
await jira.getIssuesForBoard(
board.id,
undefined,
1000,
`(${inProgressOrToDoJql}) and (${kanbanToDoJql})`,
),
);
return {
total: inProgressTickets.total + toDoTickets.total,
issues: inProgressTickets.issues.concat(toDoTickets.issues),
};
};
export const jiraClient = async (
jiraHost: string,
jiraPort: string | undefined,
jiraProtocol: string | undefined,
jiraUsername: string,
jiraPassword: string,
jiraBoardID: string,
durationInDays: number,
numDaysOfHistory: number,
// TODO: these JQL queries exclude stalled tickets (and perhaps others) that we should consider as in progress / to do.
// TODO: if a ticket has a fix version it will no longer appear on the kanban even if it's still in progress. Such tickets will show up here even though we shouldn't consider them truly in progress or to do.
// TODO: Include tickets in Resolved in the in progress count, since they still need to be QA'd.
inProgressOrToDoJql:
| string
| undefined = `issuetype in standardIssueTypes() and issuetype != Epic and statusCategory in ("To Do", "In Progress")`,
kanbanInProgressJql: string | undefined = `statusCategory = "In Progress"`,
kanbanToDoJql: string | undefined = `statusCategory = "To Do"`,
) => {
const jira = new JiraApi({
protocol: jiraProtocol,
host: jiraHost,
port: jiraPort,
username: jiraUsername,
password: jiraPassword,
apiVersion: "2",
strictSSL: true,
});
const board = (await jira.getBoard(jiraBoardID)) as JiraBoard;
if (board.type !== "kanban" && board.type !== "scrum") {
throw new Error(
`Unknown board type [${board.type}] for board [${jiraBoardID}].`,
);
}
/**
* Collects issues from Jira to analyse and facilitate prediction.
*
* @param searchQuery Query to retrieve data from Jira.
* @param maxResults Maximum number of results to retrieve.
* @returns The tickets retrieved from Jira.
*/
const issuesForSearchQuery = async (
searchQuery: string,
maxResults: number = 1000,
): Promise<TicketResponse> => {
const issuesResp = await jira.searchJira(searchQuery, { maxResults });
return parseJiraResponse(issuesResp);
};
return {
// TODO: It would be better to use the date QA was completed for the ticket instead of the date the
// ticket was resolved.
/**
* Gets tickets resolved in each time interval cycle.
*
* @returns An array of number of tickets resolved in each time interval.
*/
fetchResolvedTicketsPerTimeInterval: async (
jiraProjectIDs: readonly string[],
) => {
// We want to know how many tickets were completed during each time interval. If not defined,
// our time interval is just any period of two weeks.
let historyStart = -durationInDays;
let historyEnd = 0;
const ticketCounts: Promise<TicketResponse>[] = [];
while (historyStart >= -1 * numDaysOfHistory) {
const query =
`project in (${jiraProjectIDs.join(
", ",
)}) AND issuetype in standardIssueTypes() AND issuetype != Epic ` +
`AND resolved >= ${historyStart}d AND resolved <= ${historyEnd}d`;
ticketCounts.push(issuesForSearchQuery(query));
historyStart -= durationInDays;
historyEnd -= durationInDays;
}
return Promise.all(ticketCounts);
},
/**
* Gets the bug ratio for "1 bug every X stories" statement.
* @bugIssueTypes The issue types that represents bugs, e.g. Bug, Fault
* @returns Number of bugs per stories count.
*/
fetchBugRatio: async (
jiraProjectIDs: readonly string[],
bugIssueTypes: readonly string[],
) => {
// TODO: this should only count created tickets if they are higher in the backlog than the target ticket or they are already in progress or done.
// See https://github.com/agiledigital-labs/probabilistic-forecast/issues/1
const bugsQuery = `project in (${jiraProjectIDs.join(
", ",
)}) AND issuetype IN (${bugIssueTypes.join(",")}) AND created >= -${numDaysOfHistory}d`;
const bugCount = (await issuesForSearchQuery(bugsQuery, 0)).total;
// Assuming the spreadsheet doesn't count bugs as stories, so exclude bugs in this query.
const otherTicketsQuery =
`project in (${jiraProjectIDs.join(
", ",
)}) AND issuetype in standardIssueTypes() ` +
`AND issuetype != Epic AND issuetype NOT IN (${bugIssueTypes.join(",")}) AND created >= -${numDaysOfHistory}d`;
const otherTicketCount = (
await issuesForSearchQuery(otherTicketsQuery, 0)
).total;
return otherTicketCount / bugCount;
},
/**
* Gets the new story ratio for "1 new story [created] every X stories [resolved]" statement.
* @bugIssueTypes The issue types that represents bugs, e.g. Bug, Fault
* @returns Number of new stories created per resolved stories count.
*/
fetchDiscoveryRatio: async (
jiraProjectIDs: readonly string[],
bugIssueTypes: readonly string[],
) => {
// TODO: this should only count created tickets if they are higher in the backlog than the target ticket or they are already in progress or done.
// See https://github.com/agiledigital-labs/probabilistic-forecast/issues/1
const nonBugTicketsCreatedQuery =
`project in (${jiraProjectIDs.join(
", ",
)}) AND issuetype in standardIssueTypes() ` +
`AND issuetype != Epic AND issuetype NOT IN (${bugIssueTypes.join(",")}) AND created >= -${numDaysOfHistory}d`;
const nonBugTicketsCreatedCount = (
await issuesForSearchQuery(nonBugTicketsCreatedQuery, 0)
).total;
const ticketsResolvedQuery =
`project in (${jiraProjectIDs.join(
", ",
)}) AND issuetype in standardIssueTypes() ` +
`AND issuetype != Epic AND resolved >= -${numDaysOfHistory}d`;
const ticketsResolvedCount = (
await issuesForSearchQuery(ticketsResolvedQuery, 0)
).total;
return ticketsResolvedCount / nonBugTicketsCreatedCount;
},
/**
* Returns all in progress or to do tickets (issue keys) for the specified board, in order.
*
* @returns An object consisting of issues and total count.
*/
issuesForBoard: (): Promise<TicketResponse> => {
return board.type === "scrum"
? issuesForScrumBoard(jira, board, inProgressOrToDoJql)
: issuesForKanbanBoard(
jira,
board,
inProgressOrToDoJql,
kanbanInProgressJql,
kanbanToDoJql,
);
},
};
};