cypress-xporter
Version:
CLI tool to sync Cypress test results with Jira, TestRail, and Confluence.
263 lines (232 loc) โข 6.63 kB
JavaScript
require( "dotenv" ).config();
const axios = require( "axios" );
const fs = require( "fs" );
const FormData = require( "form-data" );
const path = require( "path" );
const { uploadScreenshotAndGetUrl } = require( "../utils/uploadUtils" );
const { findScreenshotForTest } = require( "../utils/screenshotUtils" );
const { JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN, JIRA_PROJECT_KEY } = process.env;
const AUTH = {
username: JIRA_EMAIL,
password: JIRA_API_TOKEN,
};
function normalizeTitleForComparison ( text )
{
return text
?.toLowerCase()
.replace( /[^a-z0-9 ]/gi, "" )
.replace( /\s+/g, " " )
.trim();
}
// ๐ง Fallback search to compare summaries manually
async function issueAlreadyExists ( testTitle )
{
const normalizedTitle = normalizeTitleForComparison( testTitle );
try
{
console.info( `๐ Fetching all Jira bugs in project ${ JIRA_PROJECT_KEY }...` );
const res = await axios.get( `${ JIRA_BASE_URL }/rest/api/3/search`, {
params: {
jql: `project = "${ JIRA_PROJECT_KEY }" AND issuetype = Bug`,
fields: "summary,status",
maxResults: 1000,
},
auth: AUTH,
headers: { Accept: "application/json" },
} );
for ( const issue of res.data.issues || [] )
{
const issueSummary = normalizeTitleForComparison( issue.fields?.summary || "" );
const status = issue.fields?.status?.name?.toLowerCase() || "";
if (
issueSummary.includes( normalizedTitle ) &&
!["done", "closed", "won't fix", "wontfix"].includes( status )
)
{
console.log( `โ ๏ธ Skipping duplicate: found ${ issue.key }` );
return true;
}
}
return false;
} catch ( err )
{
console.warn( `โ ๏ธ Failed to fetch Jira issues: ${ err.response?.status } ${ err.response?.statusText }` );
return false;
}
}
function createADFDescription ( test, screenshotUrl )
{
const adfContent = [
{
type: "paragraph",
content: [{ type: "text", text: "โ Cypress Test Failed" }],
},
];
if ( test.file )
{
adfContent.push( {
type: "paragraph",
content: [{ type: "text", text: `๐ Spec File: ${ test.file }` }],
} );
}
if ( test.title )
{
adfContent.push( {
type: "paragraph",
content: [{ type: "text", text: `๐งช Test Name: ${ test.title }` }],
} );
}
adfContent.push(
{
type: "paragraph",
content: [{ type: "text", text: "๐ฅ Error:" }],
},
{
type: "paragraph",
content: [{ type: "text", text: test.error || "No error message provided" }],
},
{
type: "paragraph",
content: [{ type: "text", text: "๐งฌ Test Body:" }],
},
{
type: "codeBlock",
attrs: { language: "javascript" },
content: [{ type: "text", text: test.body?.slice( 0, 1000 ) || "No body available" }],
}
);
if ( screenshotUrl?.content )
{
adfContent.push( {
type: "paragraph",
content: [
{ type: "text", text: "๐ผ๏ธ View Screenshot: " },
{
type: "text",
text: "Click Here",
marks: [
{
type: "link",
attrs: {
href: screenshotUrl.content,
},
},
],
},
],
} );
}
return {
version: 1,
type: "doc",
content: adfContent,
};
}
async function attachLogsToIssue ( issueKey, logString )
{
const tempFile = `.tmp-cypress-log-${ Date.now() }.txt`;
fs.writeFileSync( tempFile, logString );
const form = new FormData();
form.append( "file", fs.createReadStream( tempFile ) );
try
{
await axios.post( `${ JIRA_BASE_URL }/rest/api/3/issue/${ issueKey }/attachments`, form, {
auth: AUTH,
headers: {
...form.getHeaders(),
"X-Atlassian-Token": "no-check",
},
} );
console.log( `๐ Attached log file to Jira issue: ${ issueKey }` );
} catch ( err )
{
console.error( `โ Failed to attach log file to ${ issueKey }` );
console.error( err.response?.data || err.message );
} finally
{
fs.unlinkSync( tempFile );
}
}
async function createJiraBug ( test )
{
const title = test.title?.trim();
const summary = `โ [Cypress] ${ title }`;
const exists = await issueAlreadyExists( title );
if ( exists )
{
console.log( `โ ๏ธ Skipping duplicate Jira bug for: ${ title }` );
return null;
}
try
{
const issueRes = await axios.post(
`${ JIRA_BASE_URL }/rest/api/3/issue`,
{
fields: {
project: { key: JIRA_PROJECT_KEY },
summary,
issuetype: { name: "Bug" },
labels: ["automated-test", "cypress"],
},
},
{
auth: AUTH,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
}
);
const issueKey = issueRes.data.key;
console.log( `โ
Created Jira issue: ${ issueKey }` );
await attachLogsToIssue( issueKey, test.error || test.body || "No log data." );
const screenshotPath = findScreenshotForTest( test );
let screenshotUrl = null;
if ( screenshotPath )
{
screenshotUrl = await uploadScreenshotAndGetUrl( issueKey, screenshotPath );
}
const updatedDescription = createADFDescription( test, screenshotUrl );
await axios.put(
`${ JIRA_BASE_URL }/rest/api/3/issue/${ issueKey }`,
{ fields: { description: updatedDescription } },
{
auth: AUTH,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
}
);
console.log( `๐ Updated Jira description for: ${ issueKey }` );
return issueKey;
} catch ( err )
{
console.error( `โ Failed to create Jira issue for test: ${ test.title }` );
console.error( err.response?.data || err.message );
return null;
}
}
exports.reportToJira = async ( failedTests = [] ) =>
{
if ( !JIRA_BASE_URL || !JIRA_API_TOKEN || !JIRA_PROJECT_KEY || !JIRA_EMAIL )
{
console.log( "โ ๏ธ Jira not fully configured in .env" );
return failedTests;
}
if ( !failedTests.length )
{
console.log( "โ
No failed tests to report to Jira." );
return failedTests;
}
console.log( `๐ Creating Jira issues for ${ failedTests.length } failed test(s)...` );
const updatedTests = [];
for ( const test of failedTests )
{
test.title = test.title?.trim();
const issueKey = await createJiraBug( test );
test.jira = issueKey || "N/A";
updatedTests.push( test );
}
return updatedTests;
};