Implements a custom Playwright reporter to integrate with Jira. This reporter automatically creates/updates test case and bug subtasks in Jira based on Playwright test results. It also uploads the ortoni HTML report to the parent Jira card. Key features: - Creates a regression card if none are found - Transitions test subtasks based on test results. - Creates bug subtasks for failed tests. - Uploads the Ortoni HTML report as an attachment to the Jira card. - Configurable through environment variables (Jira server, username, API key, project key, etc.). Also adds axios-retry to handle rate limiting errors from Jira API
194 lines
No EOL
7.6 KiB
TypeScript
194 lines
No EOL
7.6 KiB
TypeScript
import { PostBulkFetchIssuesRequestBody, PostBulkFetchIssuesResponse, GetIssueResponse, GetIssueTransitionsResponse, GetIssueTypesResponse, PostAddCommentResponse, PostBulkCreateIssueRequestBody, PostBulkCreateIssueResponse, PostCreateIssueRequestBody, PostCreateIssueResponse, PostTransitionIssueRequestBody, PutEditIssueRequestBody, PostBulkTransitionIssuesRequestBody, GetSprintResponse, GetJqlSearchIssueParams, GetJqlSearchIssueResponse } from "@business-logic/types/JiraApi";
|
|
import axios, { AxiosInstance } from "axios";
|
|
import axiosRetry from "axios-retry";
|
|
import FormData from 'form-data';
|
|
import path from "path";
|
|
import fs from 'fs';
|
|
|
|
const jiraUrl = process.env.JIRA_SERVER!;
|
|
const jiraUsername = process.env.JIRA_USERNAME!;
|
|
const jiraApiKey = process.env.JIRA_API_KEY!;
|
|
const boardId = process.env.JIRA_BOARD_ID!;
|
|
const encodedAuthKey = Buffer.from(`${jiraUsername}:${jiraApiKey}`).toString('base64');
|
|
|
|
export default class JiraApiUtil {
|
|
readonly baseUrl: string;
|
|
readonly issueUrl: string;
|
|
readonly bulkIssueCreateUrl: string;
|
|
readonly bulkIssueFetchUrl: string;
|
|
readonly bulkTransitionIssuesUrl: string;
|
|
readonly getCurrentSprintUrl: string;
|
|
readonly getJqlSearchIssueUrl: string;
|
|
|
|
readonly axiosClient: AxiosInstance;
|
|
|
|
constructor() {
|
|
this.baseUrl = jiraUrl;
|
|
this.issueUrl = `${this.baseUrl}/rest/api/3/issue`;
|
|
this.bulkIssueCreateUrl = `${this.issueUrl}/bulk`;
|
|
this.bulkIssueFetchUrl = `${this.issueUrl}/bulkfetch`;
|
|
this.bulkTransitionIssuesUrl = `${this.baseUrl}/rest/api/3/bulk/issues/transition`;
|
|
this.getCurrentSprintUrl = `${this.baseUrl}/rest/agile/1.0/board/${boardId}/sprint?state=active`;
|
|
this.getJqlSearchIssueUrl = `${this.baseUrl}/rest/api/3/search/jql`;
|
|
this.axiosClient = axios.create();
|
|
// interceptor to log error message from api
|
|
this.axiosClient.interceptors.response.use(
|
|
response => response,
|
|
error => {
|
|
console.error('Axios Error:', error?.response?.data || error.message);
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
// Set up retries if requests are made too quickly
|
|
axiosRetry(this.axiosClient, {
|
|
retries: 4,
|
|
retryDelay: (retryCount) => { return Math.pow(2, retryCount) * 1000; }, // Exponential backoff
|
|
retryCondition: (error) => { return error.response?.status === 429 } // If rate-limit error
|
|
});
|
|
}
|
|
|
|
getIssue(issueKey: string) {
|
|
const url = `${this.issueUrl}/${issueKey}`;
|
|
return this.axiosClient.get<GetIssueResponse>(url, {
|
|
headers: {
|
|
Authorization: `Basic ${encodedAuthKey}`,
|
|
Accept: 'application/json'
|
|
}
|
|
});
|
|
}
|
|
|
|
async getCurrentSprint() {
|
|
const res = await this.axiosClient.get<GetSprintResponse>(this.getCurrentSprintUrl, {
|
|
headers: {
|
|
Authorization: `Basic ${encodedAuthKey}`,
|
|
Accept: 'application/json'
|
|
}
|
|
});
|
|
const currentSprint = res.data.values.filter(sprint => {
|
|
return `${sprint.originBoardId}` === boardId;
|
|
});
|
|
if (currentSprint.length === 1) {
|
|
return currentSprint[0];
|
|
} else {
|
|
console.error(`JiraApiUtil >> Multiple active sprints found for board ${boardId}`);
|
|
}
|
|
}
|
|
|
|
getJqlSearchIssue(params: GetJqlSearchIssueParams) {
|
|
return this.axiosClient.get<GetJqlSearchIssueResponse>(this.getJqlSearchIssueUrl, {
|
|
params: params,
|
|
headers: {
|
|
Authorization: `Basic ${encodedAuthKey}`,
|
|
Accept: 'application/json'
|
|
}
|
|
})
|
|
}
|
|
|
|
postBulkFetchIssues(requestBody: PostBulkFetchIssuesRequestBody) {
|
|
return this.axiosClient.post<PostBulkFetchIssuesResponse>(this.bulkIssueFetchUrl, requestBody, {
|
|
headers: {
|
|
Authorization: `Basic ${encodedAuthKey}`,
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
}
|
|
|
|
getIssueTransitions(issueKey: string) {
|
|
const url = `${this.issueUrl}/${issueKey}/transitions`
|
|
return this.axiosClient.get<GetIssueTransitionsResponse>(url, {
|
|
headers: {
|
|
Authorization: `Basic ${encodedAuthKey}`,
|
|
Accept: 'application/json'
|
|
}
|
|
});
|
|
}
|
|
|
|
getIssueTypes(projectKey: string) {
|
|
const url = `${this.baseUrl}/rest/api/3/issue/createmeta/${projectKey}/issuetypes`;
|
|
return this.axiosClient.get<GetIssueTypesResponse>(url, {
|
|
headers: {
|
|
Authorization: `Basic ${encodedAuthKey}`,
|
|
Accept: 'application/json'
|
|
}
|
|
});
|
|
}
|
|
|
|
postAddComment(issueKey: string, commentBody: PostAddCommentResponse) {
|
|
const url = `${this.issueUrl}/${issueKey}/comment`;
|
|
return this.axiosClient.post(url, commentBody, {
|
|
headers: {
|
|
Authorization: `Basic ${encodedAuthKey}`,
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
}
|
|
|
|
postTransitionIssue(issueKey: string, requestBody: PostTransitionIssueRequestBody) {
|
|
const transitionUrl = `${this.issueUrl}/${issueKey}/transitions`
|
|
return this.axiosClient.post(transitionUrl, requestBody, {
|
|
headers: {
|
|
Authorization: `Basic ${encodedAuthKey}`,
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
});
|
|
}
|
|
|
|
putEditIssue(issueKey: string, requestBody: PutEditIssueRequestBody) {
|
|
const editIssueUrl = `${this.issueUrl}/${issueKey}`
|
|
return this.axiosClient.put(editIssueUrl, requestBody, {
|
|
headers: {
|
|
Authorization: `Basic ${encodedAuthKey}`,
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
}
|
|
|
|
postBulkTransitionIssues(requestBody: PostBulkTransitionIssuesRequestBody) {
|
|
return this.axiosClient.post(this.bulkTransitionIssuesUrl, requestBody, {
|
|
headers: {
|
|
Authorization: `Basic ${encodedAuthKey}`,
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
});
|
|
}
|
|
|
|
postCreateIssue(requestBody: PostCreateIssueRequestBody) {
|
|
return this.axiosClient.post<PostCreateIssueResponse>(this.issueUrl, requestBody, {
|
|
headers: {
|
|
Authorization: `Basic ${encodedAuthKey}`,
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
});
|
|
}
|
|
|
|
postBulkCreateIssue(requestBody: PostBulkCreateIssueRequestBody) {
|
|
return this.axiosClient.post<PostBulkCreateIssueResponse>(this.bulkIssueCreateUrl, requestBody, {
|
|
headers: {
|
|
Authorization: `Basic ${encodedAuthKey}`,
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
});
|
|
}
|
|
|
|
postUploadAttachment(issueKey: string, filePath: string) {
|
|
const url = `${this.baseUrl}/rest/api/3/issue/${issueKey}/attachments`
|
|
const form = new FormData();
|
|
const fileName = path.basename(filePath);
|
|
|
|
form.append('file', fs.createReadStream(filePath), fileName);
|
|
return this.axiosClient.post(url, form, {
|
|
headers: {
|
|
Authorization: `Basic ${encodedAuthKey}`,
|
|
'X-Atlassian-Token': 'no-check',
|
|
...form.getHeaders()
|
|
}
|
|
});
|
|
}
|
|
} |