UNPKG

@you1anna/cypress-slack-reporter

Version:

A slack reporter for mochawesome reports generated by Cypress or other test frameworks using Mocha, for runs generated on CircleCI

572 lines (571 loc) 21.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.testables = exports.slackRunner = void 0; const webhook_1 = require("@slack/webhook"); const fs = require("fs"); const globby = require("globby"); const path = require("path"); const pino = require("pino"); const log = pino({ level: process.env.LOG_LEVEL ? process.env.LOG_LEVEL : "info", }); exports.slackRunner = async ({ ciProvider, vcsRoot, reportDir, videoDir, screenshotDir, customUrl = "", onlyFailed = false, customText = "", }) => { try { const ciEnvVars = await resolveCIProvider(ciProvider); const artefactUrl = await getArtefactUrl({ vcsRoot, ciEnvVars, ciProvider, customUrl, }); const reportHTMLUrl = await buildHTMLReportURL({ reportDir, artefactUrl, ciProvider, }); const videoAttachmentsSlack = await getVideoLinks({ artefactUrl, videosDir: videoDir, }); // const screenshotAttachmentsSlack = await getScreenshotLinks({ artefactUrl, screenshotDir, }); const prLink = await prChecker(ciEnvVars); const reportStatistics = await getTestReportStatus(reportDir); // process the test report if (onlyFailed && reportStatistics.status !== "failed") { return `onlyFailed flag set, test run status was ${reportStatistics.status}, so not sending message`; } else { const commitUrl = await getCommitUrl({ vcsRoot, ciEnvVars, }); const webhookInitialArguments = await webhookInitialArgs({ status: reportStatistics.status, ciEnvVars, commitUrl, prLink, }); const reports = await attachmentReports({ reportStatistics, reportHTMLUrl, ciEnvVars, customText, }); const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL; switch (reportStatistics.status) { case "failed": { const slackWebhookFailedUrl = process.env .SLACK_WEBHOOK_FAILED_URL; const slackWebhookUrls = slackWebhookFailedUrl ? slackWebhookFailedUrl.split(",") : SLACK_WEBHOOK_URL.split(","); return await Promise.all(slackWebhookUrls.map(async (slackWebhookUrl) => { const webhook = new webhook_1.IncomingWebhook(slackWebhookUrl, webhookInitialArguments); const artefacts = await attachmentsVideoAndScreenshots({ status: reportStatistics.status, videoAttachmentsSlack, screenshotAttachmentsSlack, }); const sendArguments = await webhookSendArgs({ argsWebhookSend: {}, messageAttachments: [reports, artefacts], }); log.info({ data: sendArguments }, "failing run"); try { const result = await webhook.send(sendArguments); log.info({ result, testStatus: reportStatistics }, "Slack message sent successfully"); return result; } catch (e) { e.code ? log.error({ code: e.code, message: e.message, data: e.original.config.data, }, "Failed to send slack message") : log.error({ e }, "Unknown error occurred whilst sending slack message"); throw new Error("An error occurred whilst sending slack message"); } })); } case "passed": { const slackWebhookPassedUrl = process.env.SLACK_WEBHOOK_PASSED_URL; const slackWebhookUrls = slackWebhookPassedUrl ? slackWebhookPassedUrl.split(",") : SLACK_WEBHOOK_URL.split(","); return await Promise.all(slackWebhookUrls.map(async (slackWebhookUrl) => { const webhook = new webhook_1.IncomingWebhook(slackWebhookUrl, webhookInitialArguments); const artefacts = await attachmentsVideoAndScreenshots({ status: reportStatistics.status, videoAttachmentsSlack, screenshotAttachmentsSlack, }); const sendArguments = await webhookSendArgs({ argsWebhookSend: {}, messageAttachments: [reports, artefacts], }); log.info({ data: sendArguments }, "passing run"); try { const result = await webhook.send(sendArguments); log.info({ result, testStatus: reportStatistics }, "Slack message sent successfully"); return result; } catch (e) { e.code ? log.error({ code: e.code, message: e.message, data: e.original.config.data, }, "Failed to send slack message") : log.error({ e }, "Unknown error occurred whilst sending slack message"); throw new Error("An error occurred whilst sending slack message"); } })); } default: { const slackWebhookErrorUrl = process.env .SLACK_WEBHOOK_ERROR_URL; const slackWebhookUrls = slackWebhookErrorUrl ? slackWebhookErrorUrl.split(",") : SLACK_WEBHOOK_URL.split(","); return await Promise.all(slackWebhookUrls.map(async (slackWebhookUrl) => { const webhook = new webhook_1.IncomingWebhook(slackWebhookUrl, webhookInitialArguments); const sendArguments = await webhookSendArgs({ argsWebhookSend: {}, messageAttachments: [reports], }); log.debug({ data: sendArguments }, "erroring run"); try { const result = await webhook.send(sendArguments); log.info({ result, testStatus: reportStatistics }, "Slack message sent successfully"); return result; } catch (e) { e.code ? log.error({ code: e.code, message: e.message, data: e.original.config.data, }, "Failed to send slack message") : log.error({ e }, "Unknown error occurred whilst sending slack message"); throw new Error("An error occurred whilst sending slack message"); } })); } } } } catch (e) { throw new Error(e); } }; const webhookInitialArgs = async ({ status, ciEnvVars, commitUrl, prLink, }) => { let statusText; switch (status) { case "passed": { statusText = "test run passed"; break; } case "failed": { statusText = "test run failed"; break; } case "error": { statusText = "test build failed"; break; } default: { statusText = "test status unknown"; break; } } let triggerText; if (!commitUrl) { triggerText = ""; } else { if (!ciEnvVars.CI_USERNAME) { triggerText = `This run was triggered by <${commitUrl}|commit>`; } else { triggerText = `This run was triggered by <${commitUrl}|${ciEnvVars.CI_USERNAME}>`; } } let prText; if (!prLink) { prText = ""; } else { prText = `${prLink}`; } let projectName; if (!ciEnvVars.CI_PROJECT_REPONAME) { projectName = "Cypress"; } else { projectName = `${ciEnvVars.CI_PROJECT_REPONAME}`; } return { text: `${projectName} ${statusText}\n${triggerText}${prText}`, }; }; const webhookSendArgs = async ({ argsWebhookSend, messageAttachments, }) => { argsWebhookSend = { attachments: messageAttachments, unfurl_links: false, unfurl_media: false, }; return argsWebhookSend; }; const attachmentReports = async ({ reportStatistics, reportHTMLUrl, ciEnvVars, customText, }) => { let branchText; if (!ciEnvVars.CI_BRANCH) { branchText = ""; } else { branchText = `Branch: ${ciEnvVars.CI_BRANCH}\n`; } let jobText; if (!ciEnvVars.JOB_NAME) { jobText = ""; } else { jobText = `Job: ${ciEnvVars.JOB_NAME}\n`; } const ENV_SUT = process.env.ENV_SUT; let envSut; if (!ENV_SUT) { envSut = ""; } else { envSut = `SUT: ${ENV_SUT}\n`; } if (!customText) { customText = ""; } else { customText = `${customText}\n`; } switch (reportStatistics.status) { case "passed": { return { color: "#36a64f", fallback: `Report available at ${reportHTMLUrl}`, text: `${branchText}${jobText}${envSut}${customText}Total Passed: ${reportStatistics.totalPasses}`, actions: [ { type: "button", text: "Test Report", url: `${reportHTMLUrl}`, style: "primary", }, { type: "button", text: "Build Logs", url: `${ciEnvVars.CI_BUILD_URL}`, style: "primary", }, ], }; } case "failed": { return { color: "#ff0000", fallback: `Report available at ${reportHTMLUrl}`, title: `Total Failed: ${reportStatistics.totalFailures}`, text: `${branchText}${jobText}${envSut}${customText}Total Tests: ${reportStatistics.totalTests}\nTotal Passed: ${reportStatistics.totalPasses} `, actions: [ { type: "button", text: "Test Report", url: `${reportHTMLUrl}`, style: "primary", }, { type: "button", text: "Build Logs", url: `${ciEnvVars.CI_BUILD_URL}`, style: "primary", }, ], }; } case "error": { return { color: "#ff0000", fallback: `Build Log available at ${ciEnvVars.CI_BUILD_URL}`, text: `${branchText}${jobText}${envSut}${customText}Total Passed: ${reportStatistics.totalPasses} `, actions: [ { type: "button", text: "Build Logs", url: `${ciEnvVars.CI_BUILD_URL}`, style: "danger", }, ], }; } default: { return {}; } } }; const attachmentsVideoAndScreenshots = async ({ status, videoAttachmentsSlack, screenshotAttachmentsSlack, }) => { switch (status) { case "passed": { return { text: `${videoAttachmentsSlack}${screenshotAttachmentsSlack}`, color: "#36a64f", }; } case "failed": { return { text: `${videoAttachmentsSlack}${screenshotAttachmentsSlack}`, color: "#ff0000", }; } default: { return {}; } } }; const getHTMLReportFilename = async (reportDir) => { const reportHTMLFullPath = await globby(path.resolve(reportDir), { expandDirectories: { files: ["*"], extensions: ["html"], }, }); if (reportHTMLFullPath.length === 0) { log.warn("Multiple html reports found & cannot determine filename, omitting html report from message"); } else if (reportHTMLFullPath.length >= 2) { log.warn("Multiple html reports found & cannot determine filename, omitting html report from message"); const reportHTMLFilename = ""; return reportHTMLFilename; } else { const reportHTMLFilename = reportHTMLFullPath .toString() .split("/") .pop(); return reportHTMLFilename; } }; const getTestReportStatus = async (reportDir) => { const reportFile = await globby(path.resolve(reportDir), { expandDirectories: { files: ["*"], extensions: ["json"], }, }); if (reportFile.length === 0) { log.warn("Cannot find test report, so sending build fail message"); return { totalSuites: 0, totalTests: 0, totalPasses: 0, totalFailures: 0, totalDuration: 0, reportFile: [], status: "error", }; } if (reportFile.length >= 2) { log.warn("Multiple json reports found, please run mochawesome-merge to provide a single report, using first report for test status"); } const rawdata = fs.readFileSync(reportFile[0]); const parsedData = JSON.parse(rawdata.toString()); const reportStats = parsedData.stats; const totalSuites = reportStats.suites; const totalTests = reportStats.tests; const totalPasses = reportStats.passes; const totalFailures = reportStats.failures; const totalDuration = reportStats.duration; if (totalTests === undefined || totalTests === 0) { reportStats.status = "error"; } else if (totalFailures > 0 || totalPasses === 0) { reportStats.status = "failed"; } else if (totalFailures === 0) { reportStats.status = "passed"; } return { totalSuites, totalTests, totalPasses, totalFailures, totalDuration, reportFile, status: reportStats.status, }; }; const prChecker = async (ciEnvVars) => { if (ciEnvVars.CI_PULL_REQUEST && ciEnvVars.CI_PULL_REQUEST.indexOf("pull") > -1) { return `<${ciEnvVars.CI_PULL_REQUEST}| - PR >`; } }; const getVideoLinks = async ({ artefactUrl, videosDir, }) => { if (!artefactUrl) { return ""; } else { log.debug({ artefactUrl, videosDir }, "getVideoLinks"); const videosURL = `${artefactUrl}`; const videos = await globby(path.resolve(process.cwd(), videosDir), { expandDirectories: { files: ["*"], extensions: ["mp4"], }, }); if (videos.length === 0) { return ""; } else { const videoLinks = await Promise.all(videos.map((videoObject) => { const trimmedVideoFilename = path.basename(videoObject); return `<${videosURL}${videoObject}|Video:- ${trimmedVideoFilename}>\n`; })); return videoLinks.join(""); } } }; const getScreenshotLinks = async ({ artefactUrl, screenshotDir, }) => { if (!artefactUrl) { return ""; } else { const screenshotURL = `${artefactUrl}`; const screenshots = await globby(path.resolve(process.cwd(), screenshotDir), { expandDirectories: { files: ["*"], extensions: ["png"], }, }); if (screenshots.length === 0) { return ""; } else { const screenshotLinks = await Promise.all(screenshots.map((screenshotObject) => { const trimmedScreenshotFilename = path.basename(screenshotObject); return `<${screenshotURL}${screenshotObject}|Screenshot:- ${trimmedScreenshotFilename}>\n`; })); return screenshotLinks.join(""); } } }; const buildHTMLReportURL = async ({ reportDir, artefactUrl, ciProvider, }) => { const reportHTMLFilename = await getHTMLReportFilename(reportDir); return artefactUrl + "/" + reportDir + "/" + reportHTMLFilename; }; const getArtefactUrl = async ({ vcsRoot, ciEnvVars, ciProvider, customUrl, }) => { if (customUrl) { return customUrl; } else if (ciProvider === "circleci") { switch (vcsRoot) { case "github": return `https://${ciEnvVars.CI_BUILD_NUM}-${ciEnvVars.CIRCLE_PROJECT_ID}-gh.circle-artifacts.com/0/`; case "bitbucket": return `https://${ciEnvVars.CI_BUILD_NUM}-${ciEnvVars.CIRCLE_PROJECT_ID}-bb.circle-artifacts.com/0/`; default: { return ""; } } } return ""; }; const getCommitUrl = async ({ vcsRoot, ciEnvVars, }) => { if (vcsRoot === "github") { return `https://github.com/${ciEnvVars.CI_PROJECT_USERNAME}/${ciEnvVars.CI_PROJECT_REPONAME}/commit/${ciEnvVars.CI_SHA1}`; } else if (vcsRoot === "bitbucket") { return `https://bitbucket.org/${ciEnvVars.CI_PROJECT_USERNAME}/${ciEnvVars.CI_PROJECT_REPONAME}/commits/${ciEnvVars.CI_SHA1}`; } else { return ""; } }; const resolveCIProvider = async (ciProvider) => { let { CI_SHA1, CI_BRANCH, CI_USERNAME, CI_BUILD_URL, CI_BUILD_NUM, CI_PULL_REQUEST, CI_PROJECT_REPONAME, CI_PROJECT_USERNAME, JOB_NAME, CIRCLE_PROJECT_ID, } = process.env; if (!ciProvider && process.env.CIRCLE_SHA1) { ciProvider = "circleci"; } if (!ciProvider && process.env.JENKINS_HOME) { ciProvider = "jenkins"; } switch (ciProvider) { case "circleci": { (CI_SHA1 = process.env.CIRCLE_SHA1), (CI_BRANCH = process.env.CIRCLE_BRANCH), (CI_USERNAME = process.env.CIRCLE_USERNAME), (CI_BUILD_URL = process.env.CIRCLE_BUILD_URL), (CI_BUILD_NUM = process.env.CIRCLE_BUILD_NUM), (CI_PULL_REQUEST = process.env.CIRCLE_PULL_REQUEST), (CI_PROJECT_REPONAME = process.env.CIRCLE_PROJECT_REPONAME), (CI_PROJECT_USERNAME = process.env.CIRCLE_PROJECT_USERNAME), (JOB_NAME = process.env.CIRCLE_JOB); CIRCLE_PROJECT_ID = process.env.CIRCLE_PROJECT_ID; } break; case "jenkins": { if (typeof process.env.GIT_URL === "undefined") { throw new Error("GIT_URL not defined!"); } const urlParts = process.env.GIT_URL.replace("https://github.com/", "").replace(".git", ""); const arr = urlParts.split("/"); (CI_SHA1 = process.env.GIT_COMMIT), (CI_BRANCH = process.env.BRANCH_NAME), (CI_USERNAME = process.env.CHANGE_AUTHOR), (CI_BUILD_URL = process.env.BUILD_URL), (CI_BUILD_NUM = process.env.BUILD_ID), (CI_PULL_REQUEST = process.env.CHANGE_ID), (CI_PROJECT_REPONAME = arr[1]), (CI_PROJECT_USERNAME = arr[0]); } break; case "bitbucket": { (CI_SHA1 = process.env.BITBUCKET_COMMIT), (CI_BUILD_NUM = process.env.BITBUCKET_BUILD_NUMBER), (CI_PROJECT_REPONAME = process.env.BITBUCKET_REPO_SLUG), (CI_PROJECT_USERNAME = process.env.BITBUCKET_WORKSPACE); break; } default: { break; } } return { CI_SHA1, CI_BRANCH, CI_USERNAME, CI_BUILD_URL, CI_BUILD_NUM, CI_PULL_REQUEST, CI_PROJECT_REPONAME, CI_PROJECT_USERNAME, JOB_NAME, CIRCLE_PROJECT_ID, }; }; exports.testables = { resolveCIProvider, getCommitUrl, getArtefactUrl, buildHTMLReportURL, getScreenshotLinks, getVideoLinks, prChecker, webhookInitialArgs, webhookSendArgs, attachmentReports, attachmentsVideoAndScreenshots, getTestReportStatus, getHTMLReportFilename, };