@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
JavaScript
;
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,
};