From adc07d99c30b2ad465aaafe01bdde37ad6e5a789 Mon Sep 17 00:00:00 2001 From: janssen <118828444+janssen-tiobe@users.noreply.github.com> Date: Wed, 4 Dec 2024 15:08:01 +0100 Subject: [PATCH 01/11] #34049: Added identifier to the summary body --- dist/index.js | 792 +++++++++++----------- src/action/decorate/summary.ts | 4 + src/configuration/github.ts | 21 +- src/github/comments.ts | 30 +- test/.setup/mock.ts | 14 +- test/integration/configuration.test.ts | 6 + test/integration/httpclient.test.ts | 2 + test/integration/octokit.test.ts | 2 + test/unit/action/decorate/summary.test.ts | 2 +- 9 files changed, 472 insertions(+), 401 deletions(-) diff --git a/dist/index.js b/dist/index.js index e9f7393c..bd942abd 100644 --- a/dist/index.js +++ b/dist/index.js @@ -230,6 +230,7 @@ function createSummaryBody(analysisResult) { core_1.summary.addRaw(createFilesSummary(projectResult.analyzedFiles)); } }); + core_1.summary.addRaw(`${config_1.githubConfig.getIdentifier()}`); logger_1.logger.info('Created summary.'); return core_1.summary.stringify(); } @@ -256,6 +257,7 @@ function createErrorSummaryBody(errorList, warningList) { core_1.summary.addRaw(`:warning: ${warning}${os_1.EOL}${os_1.EOL}`); } } + core_1.summary.addRaw(`${config_1.githubConfig.getIdentifier()}`); logger_1.logger.info('Created summary.'); return core_1.summary.stringify(); } @@ -269,6 +271,7 @@ function createNothingAnalyzedSummaryBody(message) { core_1.summary.addHeading('TICS Quality Gate'); core_1.summary.addHeading((0, markdown_1.generateStatusMarkdown)(enums_1.Status.PASSED, true), 3); core_1.summary.addRaw(message); + core_1.summary.addRaw(`${config_1.githubConfig.getIdentifier()}`); logger_1.logger.info('Created summary.'); return core_1.summary.stringify(); } @@ -516,6 +519,7 @@ function findAnnotationInList(list, annotation) { * @param unpostableReviewComments Review comments that could not be posted. * @returns Summary of all the review comments that could not be posted. */ +// Exported for testing function createUnpostableAnnotationsDetails(unpostableReviewComments) { const label = 'Quality gate failures that cannot be annotated in Files Changed'; let body = ''; @@ -1018,17 +1022,25 @@ class GithubConfig { event; job; action; - id; + workflow; + runNumber; + runAttempt; pullRequestNumber; debugger; + id; constructor() { this.apiUrl = github_1.context.apiUrl; this.owner = github_1.context.repo.owner; this.reponame = github_1.context.repo.repo; this.commitSha = github_1.context.sha; this.event = this.getGithubEvent(); - this.job = github_1.context.job; + this.job = github_1.context.job.replace(/\s+/g, '-').replace(/_+/g, '-'); this.action = github_1.context.action.replace('__tiobe_', ''); + this.workflow = github_1.context.workflow.replace(/\s+/g, '-').replace(/_+/g, '-'); + this.runNumber = github_1.context.runNumber; + this.runAttempt = parseInt(process.env.GITHUB_RUN_ATTEMPT ?? '0', 10); + this.pullRequestNumber = this.getPullRequestNumber(); + this.debugger = (0, core_1.isDebug)(); /** * Construct the id to use for storing tmpdirs. The action name will * be appended with a number if there are multiple runs within a job. @@ -1039,10 +1051,7 @@ class GithubConfig { * include a suffix that consists of the sequence number preceded by an underscore. * https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables */ - const runAttempt = process.env.GITHUB_RUN_ATTEMPT ?? '0'; - this.id = `${github_1.context.runId.toString()}_${runAttempt}_${this.job}_${this.action}`; - this.pullRequestNumber = this.getPullRequestNumber(); - this.debugger = (0, core_1.isDebug)(); + this.id = `${github_1.context.runId.toString()}_${this.runAttempt.toString()}_${this.job}_${this.action}`; this.removeWarningListener(); } getPullRequestNumber() { @@ -1074,6 +1083,9 @@ class GithubConfig { return github_event_1.GithubEvent.PUSH; } } + getIdentifier() { + return [this.workflow, this.job, this.runNumber, this.runAttempt].join('_'); + } removeWarningListener() { process.removeAllListeners('warning'); process.on('warning', warning => { @@ -1621,7 +1633,7 @@ async function postComment(body) { async function deletePreviousComments(comments) { logger_1.logger.header('Deleting comments of previous runs.'); for (const comment of comments) { - if (commentIncludesTicsTitle(comment.body)) { + if (shouldCommentBeDeleted(comment.body)) { try { const params = { owner: config_1.githubConfig.owner, @@ -1638,16 +1650,26 @@ async function deletePreviousComments(comments) { } logger_1.logger.info('Deleted review comments of previous runs.'); } -function commentIncludesTicsTitle(body) { - const titles = ['

TICS Quality Gate

', '## TICS Quality Gate', '## TICS Analysis']; +function shouldCommentBeDeleted(body) { if (!body) return false; + const titles = ['

TICS Quality Gate

', '## TICS Quality Gate', '## TICS Analysis']; let includesTitle = false; - titles.forEach(title => { - if (body.startsWith(title)) + for (const title of titles) { + if (body.startsWith(title)) { includesTitle = true; - }); - return includesTitle; + } + } + if (includesTitle) { + return containsIdentifier(body); + } + return false; +} +function containsIdentifier(body) { + const index = body.indexOf(`${config_1.githubConfig.workflow}_${config_1.githubConfig.job}`); + if (index === -1) + return true; + return body.substring(index, body.length - 4) !== config_1.githubConfig.getIdentifier(); } @@ -41423,6 +41445,378 @@ function cleanEscapedString(input) { } +/***/ }), + +/***/ 93822: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +exports.parseISO = parseISO; +var _index = __nccwpck_require__(17818); + +/** + * The {@link parseISO} function options. + */ + +/** + * @name parseISO + * @category Common Helpers + * @summary Parse ISO string + * + * @description + * Parse the given string in ISO 8601 format and return an instance of Date. + * + * Function accepts complete ISO 8601 formats as well as partial implementations. + * ISO 8601: http://en.wikipedia.org/wiki/ISO_8601 + * + * If the argument isn't a string, the function cannot parse the string or + * the values are invalid, it returns Invalid Date. + * + * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc). + * + * @param argument - The value to convert + * @param options - An object with options + * + * @returns The parsed date in the local time zone + * + * @example + * // Convert string '2014-02-11T11:30:30' to date: + * const result = parseISO('2014-02-11T11:30:30') + * //=> Tue Feb 11 2014 11:30:30 + * + * @example + * // Convert string '+02014101' to date, + * // if the additional number of digits in the extended year format is 1: + * const result = parseISO('+02014101', { additionalDigits: 1 }) + * //=> Fri Apr 11 2014 00:00:00 + */ +function parseISO(argument, options) { + const additionalDigits = options?.additionalDigits ?? 2; + const dateStrings = splitDateString(argument); + + let date; + if (dateStrings.date) { + const parseYearResult = parseYear(dateStrings.date, additionalDigits); + date = parseDate(parseYearResult.restDateString, parseYearResult.year); + } + + if (!date || isNaN(date.getTime())) { + return new Date(NaN); + } + + const timestamp = date.getTime(); + let time = 0; + let offset; + + if (dateStrings.time) { + time = parseTime(dateStrings.time); + if (isNaN(time)) { + return new Date(NaN); + } + } + + if (dateStrings.timezone) { + offset = parseTimezone(dateStrings.timezone); + if (isNaN(offset)) { + return new Date(NaN); + } + } else { + const dirtyDate = new Date(timestamp + time); + // JS parsed string assuming it's in UTC timezone + // but we need it to be parsed in our timezone + // so we use utc values to build date in our timezone. + // Year values from 0 to 99 map to the years 1900 to 1999 + // so set year explicitly with setFullYear. + const result = new Date(0); + result.setFullYear( + dirtyDate.getUTCFullYear(), + dirtyDate.getUTCMonth(), + dirtyDate.getUTCDate(), + ); + result.setHours( + dirtyDate.getUTCHours(), + dirtyDate.getUTCMinutes(), + dirtyDate.getUTCSeconds(), + dirtyDate.getUTCMilliseconds(), + ); + return result; + } + + return new Date(timestamp + time + offset); +} + +const patterns = { + dateTimeDelimiter: /[T ]/, + timeZoneDelimiter: /[Z ]/i, + timezone: /([Z+-].*)$/, +}; + +const dateRegex = + /^-?(?:(\d{3})|(\d{2})(?:-?(\d{2}))?|W(\d{2})(?:-?(\d{1}))?|)$/; +const timeRegex = + /^(\d{2}(?:[.,]\d*)?)(?::?(\d{2}(?:[.,]\d*)?))?(?::?(\d{2}(?:[.,]\d*)?))?$/; +const timezoneRegex = /^([+-])(\d{2})(?::?(\d{2}))?$/; + +function splitDateString(dateString) { + const dateStrings = {}; + const array = dateString.split(patterns.dateTimeDelimiter); + let timeString; + + // The regex match should only return at maximum two array elements. + // [date], [time], or [date, time]. + if (array.length > 2) { + return dateStrings; + } + + if (/:/.test(array[0])) { + timeString = array[0]; + } else { + dateStrings.date = array[0]; + timeString = array[1]; + if (patterns.timeZoneDelimiter.test(dateStrings.date)) { + dateStrings.date = dateString.split(patterns.timeZoneDelimiter)[0]; + timeString = dateString.substr( + dateStrings.date.length, + dateString.length, + ); + } + } + + if (timeString) { + const token = patterns.timezone.exec(timeString); + if (token) { + dateStrings.time = timeString.replace(token[1], ""); + dateStrings.timezone = token[1]; + } else { + dateStrings.time = timeString; + } + } + + return dateStrings; +} + +function parseYear(dateString, additionalDigits) { + const regex = new RegExp( + "^(?:(\\d{4}|[+-]\\d{" + + (4 + additionalDigits) + + "})|(\\d{2}|[+-]\\d{" + + (2 + additionalDigits) + + "})$)", + ); + + const captures = dateString.match(regex); + // Invalid ISO-formatted year + if (!captures) return { year: NaN, restDateString: "" }; + + const year = captures[1] ? parseInt(captures[1]) : null; + const century = captures[2] ? parseInt(captures[2]) : null; + + // either year or century is null, not both + return { + year: century === null ? year : century * 100, + restDateString: dateString.slice((captures[1] || captures[2]).length), + }; +} + +function parseDate(dateString, year) { + // Invalid ISO-formatted year + if (year === null) return new Date(NaN); + + const captures = dateString.match(dateRegex); + // Invalid ISO-formatted string + if (!captures) return new Date(NaN); + + const isWeekDate = !!captures[4]; + const dayOfYear = parseDateUnit(captures[1]); + const month = parseDateUnit(captures[2]) - 1; + const day = parseDateUnit(captures[3]); + const week = parseDateUnit(captures[4]); + const dayOfWeek = parseDateUnit(captures[5]) - 1; + + if (isWeekDate) { + if (!validateWeekDate(year, week, dayOfWeek)) { + return new Date(NaN); + } + return dayOfISOWeekYear(year, week, dayOfWeek); + } else { + const date = new Date(0); + if ( + !validateDate(year, month, day) || + !validateDayOfYearDate(year, dayOfYear) + ) { + return new Date(NaN); + } + date.setUTCFullYear(year, month, Math.max(dayOfYear, day)); + return date; + } +} + +function parseDateUnit(value) { + return value ? parseInt(value) : 1; +} + +function parseTime(timeString) { + const captures = timeString.match(timeRegex); + if (!captures) return NaN; // Invalid ISO-formatted time + + const hours = parseTimeUnit(captures[1]); + const minutes = parseTimeUnit(captures[2]); + const seconds = parseTimeUnit(captures[3]); + + if (!validateTime(hours, minutes, seconds)) { + return NaN; + } + + return ( + hours * _index.millisecondsInHour + + minutes * _index.millisecondsInMinute + + seconds * 1000 + ); +} + +function parseTimeUnit(value) { + return (value && parseFloat(value.replace(",", "."))) || 0; +} + +function parseTimezone(timezoneString) { + if (timezoneString === "Z") return 0; + + const captures = timezoneString.match(timezoneRegex); + if (!captures) return 0; + + const sign = captures[1] === "+" ? -1 : 1; + const hours = parseInt(captures[2]); + const minutes = (captures[3] && parseInt(captures[3])) || 0; + + if (!validateTimezone(hours, minutes)) { + return NaN; + } + + return ( + sign * + (hours * _index.millisecondsInHour + minutes * _index.millisecondsInMinute) + ); +} + +function dayOfISOWeekYear(isoWeekYear, week, day) { + const date = new Date(0); + date.setUTCFullYear(isoWeekYear, 0, 4); + const fourthOfJanuaryDay = date.getUTCDay() || 7; + const diff = (week - 1) * 7 + day + 1 - fourthOfJanuaryDay; + date.setUTCDate(date.getUTCDate() + diff); + return date; +} + +// Validation functions + +// February is null to handle the leap year (using ||) +const daysInMonths = [31, null, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + +function isLeapYearIndex(year) { + return year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0); +} + +function validateDate(year, month, date) { + return ( + month >= 0 && + month <= 11 && + date >= 1 && + date <= (daysInMonths[month] || (isLeapYearIndex(year) ? 29 : 28)) + ); +} + +function validateDayOfYearDate(year, dayOfYear) { + return dayOfYear >= 1 && dayOfYear <= (isLeapYearIndex(year) ? 366 : 365); +} + +function validateWeekDate(_year, week, day) { + return week >= 1 && week <= 53 && day >= 0 && day <= 6; +} + +function validateTime(hours, minutes, seconds) { + if (hours === 24) { + return minutes === 0 && seconds === 0; + } + + return ( + seconds >= 0 && + seconds < 60 && + minutes >= 0 && + minutes < 60 && + hours >= 0 && + hours < 25 + ); +} + +function validateTimezone(_hours, minutes) { + return minutes >= 0 && minutes <= 59; +} + + +/***/ }), + +/***/ 50135: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +exports.parseJSON = parseJSON; /** + * @name parseJSON + * @category Common Helpers + * @summary Parse a JSON date string + * + * @description + * Converts a complete ISO date string in UTC time, the typical format for transmitting + * a date in JSON, to a JavaScript `Date` instance. + * + * This is a minimal implementation for converting dates retrieved from a JSON API to + * a `Date` instance which can be used with other functions in the `date-fns` library. + * The following formats are supported: + * + * - `2000-03-15T05:20:10.123Z`: The output of `.toISOString()` and `JSON.stringify(new Date())` + * - `2000-03-15T05:20:10Z`: Without milliseconds + * - `2000-03-15T05:20:10+00:00`: With a zero offset, the default JSON encoded format in some other languages + * - `2000-03-15T05:20:10+05:45`: With a positive or negative offset, the default JSON encoded format in some other languages + * - `2000-03-15T05:20:10+0000`: With a zero offset without a colon + * - `2000-03-15T05:20:10`: Without a trailing 'Z' symbol + * - `2000-03-15T05:20:10.1234567`: Up to 7 digits in milliseconds field. Only first 3 are taken into account since JS does not allow fractional milliseconds + * - `2000-03-15 05:20:10`: With a space instead of a 'T' separator for APIs returning a SQL date without reformatting + * + * For convenience and ease of use these other input types are also supported + * via [toDate](https://date-fns.org/docs/toDate): + * + * - A `Date` instance will be cloned + * - A `number` will be treated as a timestamp + * + * Any other input type or invalid date strings will return an `Invalid Date`. + * + * @param dateStr - A fully formed ISO8601 date string to convert + * + * @returns The parsed date in the local time zone + */ +function parseJSON(dateStr) { + const parts = dateStr.match( + /(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(?:\.(\d{0,7}))?(?:Z|(.)(\d{2}):?(\d{2})?)?/, + ); + if (parts) { + // Group 8 matches the sign + return new Date( + Date.UTC( + +parts[1], + +parts[2] - 1, + +parts[3], + +parts[4] - (+parts[9] || 0) * (parts[8] == "-" ? -1 : 1), + +parts[5] - (+parts[10] || 0) * (parts[8] == "-" ? -1 : 1), + +parts[6], + +((parts[7] || "0") + "00").substring(0, 3), + ), + ); + } + return new Date(NaN); +} + + /***/ }), /***/ 5619: @@ -43949,378 +44343,6 @@ function isLeapYearIndex(year) { } -/***/ }), - -/***/ 93822: -/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { - -"use strict"; - -exports.parseISO = parseISO; -var _index = __nccwpck_require__(17818); - -/** - * The {@link parseISO} function options. - */ - -/** - * @name parseISO - * @category Common Helpers - * @summary Parse ISO string - * - * @description - * Parse the given string in ISO 8601 format and return an instance of Date. - * - * Function accepts complete ISO 8601 formats as well as partial implementations. - * ISO 8601: http://en.wikipedia.org/wiki/ISO_8601 - * - * If the argument isn't a string, the function cannot parse the string or - * the values are invalid, it returns Invalid Date. - * - * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc). - * - * @param argument - The value to convert - * @param options - An object with options - * - * @returns The parsed date in the local time zone - * - * @example - * // Convert string '2014-02-11T11:30:30' to date: - * const result = parseISO('2014-02-11T11:30:30') - * //=> Tue Feb 11 2014 11:30:30 - * - * @example - * // Convert string '+02014101' to date, - * // if the additional number of digits in the extended year format is 1: - * const result = parseISO('+02014101', { additionalDigits: 1 }) - * //=> Fri Apr 11 2014 00:00:00 - */ -function parseISO(argument, options) { - const additionalDigits = options?.additionalDigits ?? 2; - const dateStrings = splitDateString(argument); - - let date; - if (dateStrings.date) { - const parseYearResult = parseYear(dateStrings.date, additionalDigits); - date = parseDate(parseYearResult.restDateString, parseYearResult.year); - } - - if (!date || isNaN(date.getTime())) { - return new Date(NaN); - } - - const timestamp = date.getTime(); - let time = 0; - let offset; - - if (dateStrings.time) { - time = parseTime(dateStrings.time); - if (isNaN(time)) { - return new Date(NaN); - } - } - - if (dateStrings.timezone) { - offset = parseTimezone(dateStrings.timezone); - if (isNaN(offset)) { - return new Date(NaN); - } - } else { - const dirtyDate = new Date(timestamp + time); - // JS parsed string assuming it's in UTC timezone - // but we need it to be parsed in our timezone - // so we use utc values to build date in our timezone. - // Year values from 0 to 99 map to the years 1900 to 1999 - // so set year explicitly with setFullYear. - const result = new Date(0); - result.setFullYear( - dirtyDate.getUTCFullYear(), - dirtyDate.getUTCMonth(), - dirtyDate.getUTCDate(), - ); - result.setHours( - dirtyDate.getUTCHours(), - dirtyDate.getUTCMinutes(), - dirtyDate.getUTCSeconds(), - dirtyDate.getUTCMilliseconds(), - ); - return result; - } - - return new Date(timestamp + time + offset); -} - -const patterns = { - dateTimeDelimiter: /[T ]/, - timeZoneDelimiter: /[Z ]/i, - timezone: /([Z+-].*)$/, -}; - -const dateRegex = - /^-?(?:(\d{3})|(\d{2})(?:-?(\d{2}))?|W(\d{2})(?:-?(\d{1}))?|)$/; -const timeRegex = - /^(\d{2}(?:[.,]\d*)?)(?::?(\d{2}(?:[.,]\d*)?))?(?::?(\d{2}(?:[.,]\d*)?))?$/; -const timezoneRegex = /^([+-])(\d{2})(?::?(\d{2}))?$/; - -function splitDateString(dateString) { - const dateStrings = {}; - const array = dateString.split(patterns.dateTimeDelimiter); - let timeString; - - // The regex match should only return at maximum two array elements. - // [date], [time], or [date, time]. - if (array.length > 2) { - return dateStrings; - } - - if (/:/.test(array[0])) { - timeString = array[0]; - } else { - dateStrings.date = array[0]; - timeString = array[1]; - if (patterns.timeZoneDelimiter.test(dateStrings.date)) { - dateStrings.date = dateString.split(patterns.timeZoneDelimiter)[0]; - timeString = dateString.substr( - dateStrings.date.length, - dateString.length, - ); - } - } - - if (timeString) { - const token = patterns.timezone.exec(timeString); - if (token) { - dateStrings.time = timeString.replace(token[1], ""); - dateStrings.timezone = token[1]; - } else { - dateStrings.time = timeString; - } - } - - return dateStrings; -} - -function parseYear(dateString, additionalDigits) { - const regex = new RegExp( - "^(?:(\\d{4}|[+-]\\d{" + - (4 + additionalDigits) + - "})|(\\d{2}|[+-]\\d{" + - (2 + additionalDigits) + - "})$)", - ); - - const captures = dateString.match(regex); - // Invalid ISO-formatted year - if (!captures) return { year: NaN, restDateString: "" }; - - const year = captures[1] ? parseInt(captures[1]) : null; - const century = captures[2] ? parseInt(captures[2]) : null; - - // either year or century is null, not both - return { - year: century === null ? year : century * 100, - restDateString: dateString.slice((captures[1] || captures[2]).length), - }; -} - -function parseDate(dateString, year) { - // Invalid ISO-formatted year - if (year === null) return new Date(NaN); - - const captures = dateString.match(dateRegex); - // Invalid ISO-formatted string - if (!captures) return new Date(NaN); - - const isWeekDate = !!captures[4]; - const dayOfYear = parseDateUnit(captures[1]); - const month = parseDateUnit(captures[2]) - 1; - const day = parseDateUnit(captures[3]); - const week = parseDateUnit(captures[4]); - const dayOfWeek = parseDateUnit(captures[5]) - 1; - - if (isWeekDate) { - if (!validateWeekDate(year, week, dayOfWeek)) { - return new Date(NaN); - } - return dayOfISOWeekYear(year, week, dayOfWeek); - } else { - const date = new Date(0); - if ( - !validateDate(year, month, day) || - !validateDayOfYearDate(year, dayOfYear) - ) { - return new Date(NaN); - } - date.setUTCFullYear(year, month, Math.max(dayOfYear, day)); - return date; - } -} - -function parseDateUnit(value) { - return value ? parseInt(value) : 1; -} - -function parseTime(timeString) { - const captures = timeString.match(timeRegex); - if (!captures) return NaN; // Invalid ISO-formatted time - - const hours = parseTimeUnit(captures[1]); - const minutes = parseTimeUnit(captures[2]); - const seconds = parseTimeUnit(captures[3]); - - if (!validateTime(hours, minutes, seconds)) { - return NaN; - } - - return ( - hours * _index.millisecondsInHour + - minutes * _index.millisecondsInMinute + - seconds * 1000 - ); -} - -function parseTimeUnit(value) { - return (value && parseFloat(value.replace(",", "."))) || 0; -} - -function parseTimezone(timezoneString) { - if (timezoneString === "Z") return 0; - - const captures = timezoneString.match(timezoneRegex); - if (!captures) return 0; - - const sign = captures[1] === "+" ? -1 : 1; - const hours = parseInt(captures[2]); - const minutes = (captures[3] && parseInt(captures[3])) || 0; - - if (!validateTimezone(hours, minutes)) { - return NaN; - } - - return ( - sign * - (hours * _index.millisecondsInHour + minutes * _index.millisecondsInMinute) - ); -} - -function dayOfISOWeekYear(isoWeekYear, week, day) { - const date = new Date(0); - date.setUTCFullYear(isoWeekYear, 0, 4); - const fourthOfJanuaryDay = date.getUTCDay() || 7; - const diff = (week - 1) * 7 + day + 1 - fourthOfJanuaryDay; - date.setUTCDate(date.getUTCDate() + diff); - return date; -} - -// Validation functions - -// February is null to handle the leap year (using ||) -const daysInMonths = [31, null, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - -function isLeapYearIndex(year) { - return year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0); -} - -function validateDate(year, month, date) { - return ( - month >= 0 && - month <= 11 && - date >= 1 && - date <= (daysInMonths[month] || (isLeapYearIndex(year) ? 29 : 28)) - ); -} - -function validateDayOfYearDate(year, dayOfYear) { - return dayOfYear >= 1 && dayOfYear <= (isLeapYearIndex(year) ? 366 : 365); -} - -function validateWeekDate(_year, week, day) { - return week >= 1 && week <= 53 && day >= 0 && day <= 6; -} - -function validateTime(hours, minutes, seconds) { - if (hours === 24) { - return minutes === 0 && seconds === 0; - } - - return ( - seconds >= 0 && - seconds < 60 && - minutes >= 0 && - minutes < 60 && - hours >= 0 && - hours < 25 - ); -} - -function validateTimezone(_hours, minutes) { - return minutes >= 0 && minutes <= 59; -} - - -/***/ }), - -/***/ 50135: -/***/ ((__unused_webpack_module, exports) => { - -"use strict"; - -exports.parseJSON = parseJSON; /** - * @name parseJSON - * @category Common Helpers - * @summary Parse a JSON date string - * - * @description - * Converts a complete ISO date string in UTC time, the typical format for transmitting - * a date in JSON, to a JavaScript `Date` instance. - * - * This is a minimal implementation for converting dates retrieved from a JSON API to - * a `Date` instance which can be used with other functions in the `date-fns` library. - * The following formats are supported: - * - * - `2000-03-15T05:20:10.123Z`: The output of `.toISOString()` and `JSON.stringify(new Date())` - * - `2000-03-15T05:20:10Z`: Without milliseconds - * - `2000-03-15T05:20:10+00:00`: With a zero offset, the default JSON encoded format in some other languages - * - `2000-03-15T05:20:10+05:45`: With a positive or negative offset, the default JSON encoded format in some other languages - * - `2000-03-15T05:20:10+0000`: With a zero offset without a colon - * - `2000-03-15T05:20:10`: Without a trailing 'Z' symbol - * - `2000-03-15T05:20:10.1234567`: Up to 7 digits in milliseconds field. Only first 3 are taken into account since JS does not allow fractional milliseconds - * - `2000-03-15 05:20:10`: With a space instead of a 'T' separator for APIs returning a SQL date without reformatting - * - * For convenience and ease of use these other input types are also supported - * via [toDate](https://date-fns.org/docs/toDate): - * - * - A `Date` instance will be cloned - * - A `number` will be treated as a timestamp - * - * Any other input type or invalid date strings will return an `Invalid Date`. - * - * @param dateStr - A fully formed ISO8601 date string to convert - * - * @returns The parsed date in the local time zone - */ -function parseJSON(dateStr) { - const parts = dateStr.match( - /(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(?:\.(\d{0,7}))?(?:Z|(.)(\d{2}):?(\d{2})?)?/, - ); - if (parts) { - // Group 8 matches the sign - return new Date( - Date.UTC( - +parts[1], - +parts[2] - 1, - +parts[3], - +parts[4] - (+parts[9] || 0) * (parts[8] == "-" ? -1 : 1), - +parts[5] - (+parts[10] || 0) * (parts[8] == "-" ? -1 : 1), - +parts[6], - +((parts[7] || "0") + "00").substring(0, 3), - ), - ); - } - return new Date(NaN); -} - - /***/ }), /***/ 456: diff --git a/src/action/decorate/summary.ts b/src/action/decorate/summary.ts index 95ba7181..a2e27a6e 100644 --- a/src/action/decorate/summary.ts +++ b/src/action/decorate/summary.ts @@ -54,6 +54,7 @@ export function createSummaryBody(analysisResult: AnalysisResult): string { summary.addRaw(createFilesSummary(projectResult.analyzedFiles)); } }); + summary.addRaw(`${githubConfig.getIdentifier()}`); logger.info('Created summary.'); @@ -88,6 +89,7 @@ export function createErrorSummaryBody(errorList: string[], warningList: string[ summary.addRaw(`:warning: ${warning}${EOL}${EOL}`); } } + summary.addRaw(`${githubConfig.getIdentifier()}`); logger.info('Created summary.'); return summary.stringify(); @@ -105,6 +107,7 @@ export function createNothingAnalyzedSummaryBody(message: string): string { summary.addHeading(generateStatusMarkdown(Status.PASSED, true), 3); summary.addRaw(message); + summary.addRaw(`${githubConfig.getIdentifier()}`); logger.info('Created summary.'); return summary.stringify(); @@ -374,6 +377,7 @@ function findAnnotationInList(list: ExtendedAnnotation[], annotation: ExtendedAn * @param unpostableReviewComments Review comments that could not be posted. * @returns Summary of all the review comments that could not be posted. */ +// Exported for testing export function createUnpostableAnnotationsDetails(unpostableReviewComments: ExtendedAnnotation[]): string { const label = 'Quality gate failures that cannot be annotated in Files Changed'; let body = ''; diff --git a/src/configuration/github.ts b/src/configuration/github.ts index 2c39449c..1805d632 100644 --- a/src/configuration/github.ts +++ b/src/configuration/github.ts @@ -11,9 +11,12 @@ export class GithubConfig { readonly event: GithubEvent; readonly job: string; readonly action: string; - readonly id: string; + readonly workflow: string; + readonly runNumber: number; + readonly runAttempt: number; readonly pullRequestNumber: number | undefined; readonly debugger: boolean; + readonly id: string; constructor() { this.apiUrl = context.apiUrl; @@ -21,8 +24,13 @@ export class GithubConfig { this.reponame = context.repo.repo; this.commitSha = context.sha; this.event = this.getGithubEvent(); - this.job = context.job; + this.job = context.job.replace(/\s+/g, '-').replace(/_+/g, '-'); this.action = context.action.replace('__tiobe_', ''); + this.workflow = context.workflow.replace(/\s+/g, '-').replace(/_+/g, '-'); + this.runNumber = context.runNumber; + this.runAttempt = parseInt(process.env.GITHUB_RUN_ATTEMPT ?? '0', 10); + this.pullRequestNumber = this.getPullRequestNumber(); + this.debugger = isDebug(); /** * Construct the id to use for storing tmpdirs. The action name will @@ -34,10 +42,7 @@ export class GithubConfig { * include a suffix that consists of the sequence number preceded by an underscore. * https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables */ - const runAttempt = process.env.GITHUB_RUN_ATTEMPT ?? '0'; - this.id = `${context.runId.toString()}_${runAttempt}_${this.job}_${this.action}`; - this.pullRequestNumber = this.getPullRequestNumber(); - this.debugger = isDebug(); + this.id = `${context.runId.toString()}_${this.runAttempt.toString()}_${this.job}_${this.action}`; this.removeWarningListener(); } @@ -71,6 +76,10 @@ export class GithubConfig { } } + getIdentifier(): string { + return [this.workflow, this.job, this.runNumber, this.runAttempt].join('_'); + } + removeWarningListener(): void { process.removeAllListeners('warning'); process.on('warning', warning => { diff --git a/src/github/comments.ts b/src/github/comments.ts index 41320a27..a716cbc7 100644 --- a/src/github/comments.ts +++ b/src/github/comments.ts @@ -59,7 +59,7 @@ export async function postComment(body: string): Promise { export async function deletePreviousComments(comments: Comment[]): Promise { logger.header('Deleting comments of previous runs.'); for (const comment of comments) { - if (commentIncludesTicsTitle(comment.body)) { + if (shouldCommentBeDeleted(comment.body)) { try { const params = { owner: githubConfig.owner, @@ -76,16 +76,30 @@ export async function deletePreviousComments(comments: Comment[]): Promise logger.info('Deleted review comments of previous runs.'); } -function commentIncludesTicsTitle(body?: string): boolean { - const titles = ['

TICS Quality Gate

', '## TICS Quality Gate', '## TICS Analysis']; - +function shouldCommentBeDeleted(body?: string): boolean { if (!body) return false; + const titles = ['

TICS Quality Gate

', '## TICS Quality Gate', '## TICS Analysis']; + let includesTitle = false; - titles.forEach(title => { - if (body.startsWith(title)) includesTitle = true; - }); + for (const title of titles) { + if (body.startsWith(title)) { + includesTitle = true; + } + } + + if (includesTitle) { + return containsIdentifier(body); + } + + return false; +} + +function containsIdentifier(body: string): boolean { + const index = body.indexOf(`${githubConfig.workflow}_${githubConfig.job}`); + + if (index === -1) return true; - return includesTitle; + return body.substring(index, body.length - 4) !== githubConfig.getIdentifier(); } diff --git a/test/.setup/mock.ts b/test/.setup/mock.ts index 482ea806..60b9b501 100644 --- a/test/.setup/mock.ts +++ b/test/.setup/mock.ts @@ -13,6 +13,10 @@ export const githubConfigMock: { id: string; pullRequestNumber: number | undefined; debugger: boolean; + workflow: string; + runNumber: number; + runAttempt: number; + getIdentifier(): string; } = { apiUrl: 'github.com/api/v1/', owner: 'tester', @@ -23,7 +27,13 @@ export const githubConfigMock: { action: 'tics-github-action', id: '123_TICS_1_tics-github-action', pullRequestNumber: 1, - debugger: false + debugger: false, + workflow: 'tics-client', + runNumber: 1, + runAttempt: 2, + getIdentifier(): string { + return [this.workflow, this.job, this.runNumber, this.runAttempt].join('_'); + } }; export const ticsConfigMock = { @@ -121,6 +131,7 @@ export const contextMock: { job: string; runId: number; runNumber: number; + workflow: string; payload: { pull_request: | { @@ -140,6 +151,7 @@ export const contextMock: { job: 'TICS', runId: 123, runNumber: 1, + workflow: 'tics_client', payload: { pull_request: { number: 1 diff --git a/test/integration/configuration.test.ts b/test/integration/configuration.test.ts index 07b21e51..7a62fc87 100644 --- a/test/integration/configuration.test.ts +++ b/test/integration/configuration.test.ts @@ -28,6 +28,8 @@ describe('pullRequestNumber', () => { eventName: 'pull_request', runId: 1, runNumber: 1, + job: 'TICS', + workflow: 'tics_client', repo: { owner: 'owner', repo: 'repo' @@ -53,6 +55,8 @@ describe('pullRequestNumber', () => { eventName: 'pull_request', runId: 1, runNumber: 1, + job: 'TICS', + workflow: 'tics_client', repo: { owner: 'owner', repo: 'repo' @@ -80,6 +84,8 @@ describe('pullRequestNumber', () => { eventName: 'pull_request', runId: 1, runNumber: 1, + job: 'TICS', + workflow: 'tics_client', repo: { owner: 'owner', repo: 'repo' diff --git a/test/integration/httpclient.test.ts b/test/integration/httpclient.test.ts index f329337a..96d7b733 100644 --- a/test/integration/httpclient.test.ts +++ b/test/integration/httpclient.test.ts @@ -10,6 +10,8 @@ process.env['http_proxy'] = proxyUrl; // set required inputs process.env.GITHUB_REPOSITORY = 'owner/repo'; process.env.GITHUB_ACTION = '_tics-github-action'; +process.env.GITHUB_JOB = 'tics_client'; +process.env.GITHUB_WORKFLOW = 'tics client'; process.env.INPUT_GITHUBTOKEN = 'token'; process.env.INPUT_MODE = 'client'; process.env.INPUT_PROJECTNAME = 'tics-github-action'; diff --git a/test/integration/octokit.test.ts b/test/integration/octokit.test.ts index 0f850a67..0b9b8a8f 100644 --- a/test/integration/octokit.test.ts +++ b/test/integration/octokit.test.ts @@ -13,6 +13,8 @@ process.env['http_proxy'] = proxyUrl; process.env.GITHUB_REPOSITORY = 'owner/repo'; process.env.GITHUB_API_URL = 'https://api.github.com'; process.env.GITHUB_ACTION = '_tics-github-action'; +process.env.GITHUB_JOB = 'tics_client'; +process.env.GITHUB_WORKFLOW = 'tics client'; process.env.INPUT_MODE = 'client'; process.env.INPUT_PROJECTNAME = 'tics-github-action'; process.env.INPUT_VIEWERURL = 'http://localhost/tiobeweb/TICS/api/cfg?name=default'; diff --git a/test/unit/action/decorate/summary.test.ts b/test/unit/action/decorate/summary.test.ts index d5440bc8..a449ab31 100644 --- a/test/unit/action/decorate/summary.test.ts +++ b/test/unit/action/decorate/summary.test.ts @@ -145,7 +145,7 @@ describe('createErrorSummary', () => { describe('createNothingAnalyzedSummaryBody', () => { test('Should return summary with the message given', async () => { const body = createNothingAnalyzedSummaryBody('message'); - expect(body).toEqual('

TICS Quality Gate

\n

:heavy_check_mark: Passed

\nmessage'); + expect(body).toEqual('

TICS Quality Gate

\n

:heavy_check_mark: Passed

\nmessagetics-client_TICS_1_2'); }); }); From 3360c0b35c75cccd9f35e9a07b0e4f5d80096c11 Mon Sep 17 00:00:00 2001 From: janssen <118828444+janssen-tiobe@users.noreply.github.com> Date: Wed, 4 Dec 2024 16:07:45 +0100 Subject: [PATCH 02/11] #34049: Created tests, fixed check when to delete a comment --- dist/index.js | 31 ++++++++++++--- src/github/comments.ts | 35 ++++++++++++++--- test/unit/github/comments.test.ts | 63 ++++++++++++++++++++++++++----- 3 files changed, 109 insertions(+), 20 deletions(-) diff --git a/dist/index.js b/dist/index.js index bd942abd..ae1706f1 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1661,15 +1661,36 @@ function shouldCommentBeDeleted(body) { } } if (includesTitle) { - return containsIdentifier(body); + return isWorkflowAndJobInAnotherRun(body); } return false; } -function containsIdentifier(body) { - const index = body.indexOf(`${config_1.githubConfig.workflow}_${config_1.githubConfig.job}`); - if (index === -1) +function isWorkflowAndJobInAnotherRun(body) { + const regex = /([^\s]+)<\/i>/g; + let identifier = ''; + // Get the last match of the tag. + let match = null; + while ((match = regex.exec(body))) { + if (match[1] !== '') { + identifier = match[1]; + } + } + // If no identifier is found, the comment is + // of the old format and should be replaced. + if (identifier === '') return true; - return body.substring(index, body.length - 4) !== config_1.githubConfig.getIdentifier(); + const split = identifier.split('_'); + // If the identifier does not match the correct format, do not replace. + if (split.length !== 4) { + logger_1.logger.debug(`Identifier is not of the correct format: ${identifier}`); + return false; + } + // If the workflow or job are different, do not replace. + if (split[0] !== config_1.githubConfig.workflow || split[1] !== config_1.githubConfig.job) { + return false; + } + // Only replace if the run number or run attempt are different. + return parseInt(split[2], 10) !== config_1.githubConfig.runNumber || parseInt(split[3], 10) !== config_1.githubConfig.runAttempt; } diff --git a/src/github/comments.ts b/src/github/comments.ts index a716cbc7..62e0dd0a 100644 --- a/src/github/comments.ts +++ b/src/github/comments.ts @@ -90,16 +90,41 @@ function shouldCommentBeDeleted(body?: string): boolean { } if (includesTitle) { - return containsIdentifier(body); + return isWorkflowAndJobInAnotherRun(body); } return false; } -function containsIdentifier(body: string): boolean { - const index = body.indexOf(`${githubConfig.workflow}_${githubConfig.job}`); +function isWorkflowAndJobInAnotherRun(body: string): boolean { + const regex = /([^\s]+)<\/i>/g; - if (index === -1) return true; + let identifier = ''; + // Get the last match of the tag. + let match: RegExpExecArray | null = null; + while ((match = regex.exec(body))) { + if (match[1] !== '') { + identifier = match[1]; + } + } + + // If no identifier is found, the comment is + // of the old format and should be replaced. + if (identifier === '') return true; + + const split = identifier.split('_'); + + // If the identifier does not match the correct format, do not replace. + if (split.length !== 4) { + logger.debug(`Identifier is not of the correct format: ${identifier}`); + return false; + } + + // If the workflow or job are different, do not replace. + if (split[0] !== githubConfig.workflow || split[1] !== githubConfig.job) { + return false; + } - return body.substring(index, body.length - 4) !== githubConfig.getIdentifier(); + // Only replace if the run number or run attempt are different. + return parseInt(split[2], 10) !== githubConfig.runNumber || parseInt(split[3], 10) !== githubConfig.runAttempt; } diff --git a/test/unit/github/comments.test.ts b/test/unit/github/comments.test.ts index 5c614d67..dc95b274 100644 --- a/test/unit/github/comments.test.ts +++ b/test/unit/github/comments.test.ts @@ -130,7 +130,7 @@ describe('deletePreviousComments', () => { }); test('Should call deleteComment with values', async () => { - deletePreviousComments([commentWithBody]); + await deletePreviousComments([commentWithBody]); const calledWith = { owner: githubConfig.owner, repo: githubConfig.reponame, @@ -139,11 +139,41 @@ describe('deletePreviousComments', () => { expect(deleteCommentSpy).toHaveBeenCalledWith(calledWith); }); - test('Should call deleteComment with values', async () => { + test('Should not call deleteComment if body is not TICS', async () => { await deletePreviousComments([commentWithoutBody]); expect(deleteCommentSpy).toHaveBeenCalledTimes(0); }); + test('Should call deleteComment if identifier workflow and job match', async () => { + await deletePreviousComments([commentWithIdentifierSameJob]); + const calledWith = { + owner: githubConfig.owner, + repo: githubConfig.reponame, + comment_id: 0 + }; + expect(deleteCommentSpy).toHaveBeenCalledWith(calledWith); + }); + + test('Should not call deleteComment if identifier workflow and job match, but they are in the same run', async () => { + await deletePreviousComments([commentWithIdentifierSameJobAndRun]); + expect(deleteCommentSpy).toHaveBeenCalledTimes(0); + }); + + test('Should not call deleteComment if identifier workflow and job do not match', async () => { + await deletePreviousComments([commentWithIdentifierOtherJob]); + expect(deleteCommentSpy).toHaveBeenCalledTimes(0); + }); + + test('Should not call deleteComment if identifier is of the wrong format', async () => { + const debugSpy = jest.spyOn(logger, 'debug'); + + await deletePreviousComments([commentWithIdentifierWrongFormat]); + + expect(deleteCommentSpy).toHaveBeenCalledTimes(0); + expect(debugSpy).toHaveBeenCalledTimes(1); + expect(debugSpy).toHaveBeenCalledWith('Identifier is not of the correct format: tics-client_OTHER_1'); + }); + test('Should post a notice when deleteComment throws', async () => { deleteCommentSpy.mockRejectedValue(Error()); const noticeSpy = jest.spyOn(logger, 'notice'); @@ -167,13 +197,26 @@ const commentWithBody: Comment = { }; const commentWithoutBody: Comment = { - url: '', - html_url: '', - issue_url: '', - id: 0, - node_id: '', - user: null, - created_at: '', - updated_at: '', + ...commentWithBody, body: undefined }; + +const commentWithIdentifierSameJob: Comment = { + ...commentWithBody, + body: '

TICS Quality Gate

This distractsMessage Heretics-client_TICS_1_1' +}; + +const commentWithIdentifierSameJobAndRun: Comment = { + ...commentWithBody, + body: '

TICS Quality Gate

Message Heretics-client_TICS_1_2' +}; + +const commentWithIdentifierOtherJob: Comment = { + ...commentWithBody, + body: '

TICS Quality Gate

Message Heretics-client_OTHER_1_1' +}; + +const commentWithIdentifierWrongFormat: Comment = { + ...commentWithBody, + body: '

TICS Quality Gate

Message Heretics-client_OTHER_1' +}; From 21bd5f3267529ac222b8c2fd1b0216723b7a3435 Mon Sep 17 00:00:00 2001 From: janssen <118828444+janssen-tiobe@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:40:39 +0100 Subject: [PATCH 03/11] #34049: Using a comment instead of shown text for identifier Also fixed comments of review --- dist/index.js | 14 +++++++------- src/action/decorate/summary.ts | 6 +++--- src/configuration/github.ts | 6 +++--- src/github/comments.ts | 2 +- test/.setup/mock.ts | 4 ++-- test/unit/action/decorate/summary.test.ts | 2 +- test/unit/github/comments.test.ts | 8 ++++---- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/dist/index.js b/dist/index.js index ae1706f1..719f7439 100644 --- a/dist/index.js +++ b/dist/index.js @@ -230,7 +230,7 @@ function createSummaryBody(analysisResult) { core_1.summary.addRaw(createFilesSummary(projectResult.analyzedFiles)); } }); - core_1.summary.addRaw(`${config_1.githubConfig.getIdentifier()}`); + core_1.summary.addRaw(``); logger_1.logger.info('Created summary.'); return core_1.summary.stringify(); } @@ -257,7 +257,7 @@ function createErrorSummaryBody(errorList, warningList) { core_1.summary.addRaw(`:warning: ${warning}${os_1.EOL}${os_1.EOL}`); } } - core_1.summary.addRaw(`${config_1.githubConfig.getIdentifier()}`); + core_1.summary.addRaw(``); logger_1.logger.info('Created summary.'); return core_1.summary.stringify(); } @@ -271,7 +271,7 @@ function createNothingAnalyzedSummaryBody(message) { core_1.summary.addHeading('TICS Quality Gate'); core_1.summary.addHeading((0, markdown_1.generateStatusMarkdown)(enums_1.Status.PASSED, true), 3); core_1.summary.addRaw(message); - core_1.summary.addRaw(`${config_1.githubConfig.getIdentifier()}`); + core_1.summary.addRaw(``); logger_1.logger.info('Created summary.'); return core_1.summary.stringify(); } @@ -1034,9 +1034,9 @@ class GithubConfig { this.reponame = github_1.context.repo.repo; this.commitSha = github_1.context.sha; this.event = this.getGithubEvent(); - this.job = github_1.context.job.replace(/\s+/g, '-').replace(/_+/g, '-'); + this.job = github_1.context.job.replace(/[\s|_]+/g, '-'); this.action = github_1.context.action.replace('__tiobe_', ''); - this.workflow = github_1.context.workflow.replace(/\s+/g, '-').replace(/_+/g, '-'); + this.workflow = github_1.context.workflow.replace(/[\s|_]+/g, '-'); this.runNumber = github_1.context.runNumber; this.runAttempt = parseInt(process.env.GITHUB_RUN_ATTEMPT ?? '0', 10); this.pullRequestNumber = this.getPullRequestNumber(); @@ -1083,7 +1083,7 @@ class GithubConfig { return github_event_1.GithubEvent.PUSH; } } - getIdentifier() { + getCommentIdentifier() { return [this.workflow, this.job, this.runNumber, this.runAttempt].join('_'); } removeWarningListener() { @@ -1666,7 +1666,7 @@ function shouldCommentBeDeleted(body) { return false; } function isWorkflowAndJobInAnotherRun(body) { - const regex = /([^\s]+)<\/i>/g; + const regex = //g; let identifier = ''; // Get the last match of the tag. let match = null; diff --git a/src/action/decorate/summary.ts b/src/action/decorate/summary.ts index a2e27a6e..e3de689e 100644 --- a/src/action/decorate/summary.ts +++ b/src/action/decorate/summary.ts @@ -54,7 +54,7 @@ export function createSummaryBody(analysisResult: AnalysisResult): string { summary.addRaw(createFilesSummary(projectResult.analyzedFiles)); } }); - summary.addRaw(`${githubConfig.getIdentifier()}`); + summary.addRaw(``); logger.info('Created summary.'); @@ -89,7 +89,7 @@ export function createErrorSummaryBody(errorList: string[], warningList: string[ summary.addRaw(`:warning: ${warning}${EOL}${EOL}`); } } - summary.addRaw(`${githubConfig.getIdentifier()}`); + summary.addRaw(``); logger.info('Created summary.'); return summary.stringify(); @@ -107,7 +107,7 @@ export function createNothingAnalyzedSummaryBody(message: string): string { summary.addHeading(generateStatusMarkdown(Status.PASSED, true), 3); summary.addRaw(message); - summary.addRaw(`${githubConfig.getIdentifier()}`); + summary.addRaw(``); logger.info('Created summary.'); return summary.stringify(); diff --git a/src/configuration/github.ts b/src/configuration/github.ts index 1805d632..f538a0e6 100644 --- a/src/configuration/github.ts +++ b/src/configuration/github.ts @@ -24,9 +24,9 @@ export class GithubConfig { this.reponame = context.repo.repo; this.commitSha = context.sha; this.event = this.getGithubEvent(); - this.job = context.job.replace(/\s+/g, '-').replace(/_+/g, '-'); + this.job = context.job.replace(/[\s|_]+/g, '-'); this.action = context.action.replace('__tiobe_', ''); - this.workflow = context.workflow.replace(/\s+/g, '-').replace(/_+/g, '-'); + this.workflow = context.workflow.replace(/[\s|_]+/g, '-'); this.runNumber = context.runNumber; this.runAttempt = parseInt(process.env.GITHUB_RUN_ATTEMPT ?? '0', 10); this.pullRequestNumber = this.getPullRequestNumber(); @@ -76,7 +76,7 @@ export class GithubConfig { } } - getIdentifier(): string { + getCommentIdentifier(): string { return [this.workflow, this.job, this.runNumber, this.runAttempt].join('_'); } diff --git a/src/github/comments.ts b/src/github/comments.ts index 62e0dd0a..c05a8c81 100644 --- a/src/github/comments.ts +++ b/src/github/comments.ts @@ -97,7 +97,7 @@ function shouldCommentBeDeleted(body?: string): boolean { } function isWorkflowAndJobInAnotherRun(body: string): boolean { - const regex = /([^\s]+)<\/i>/g; + const regex = //g; let identifier = ''; // Get the last match of the tag. diff --git a/test/.setup/mock.ts b/test/.setup/mock.ts index 60b9b501..d1d7c830 100644 --- a/test/.setup/mock.ts +++ b/test/.setup/mock.ts @@ -16,7 +16,7 @@ export const githubConfigMock: { workflow: string; runNumber: number; runAttempt: number; - getIdentifier(): string; + getCommentIdentifier(): string; } = { apiUrl: 'github.com/api/v1/', owner: 'tester', @@ -31,7 +31,7 @@ export const githubConfigMock: { workflow: 'tics-client', runNumber: 1, runAttempt: 2, - getIdentifier(): string { + getCommentIdentifier(): string { return [this.workflow, this.job, this.runNumber, this.runAttempt].join('_'); } }; diff --git a/test/unit/action/decorate/summary.test.ts b/test/unit/action/decorate/summary.test.ts index a449ab31..7af2dcb9 100644 --- a/test/unit/action/decorate/summary.test.ts +++ b/test/unit/action/decorate/summary.test.ts @@ -145,7 +145,7 @@ describe('createErrorSummary', () => { describe('createNothingAnalyzedSummaryBody', () => { test('Should return summary with the message given', async () => { const body = createNothingAnalyzedSummaryBody('message'); - expect(body).toEqual('

TICS Quality Gate

\n

:heavy_check_mark: Passed

\nmessagetics-client_TICS_1_2'); + expect(body).toEqual('

TICS Quality Gate

\n

:heavy_check_mark: Passed

\nmessage'); }); }); diff --git a/test/unit/github/comments.test.ts b/test/unit/github/comments.test.ts index dc95b274..5f5f42c3 100644 --- a/test/unit/github/comments.test.ts +++ b/test/unit/github/comments.test.ts @@ -203,20 +203,20 @@ const commentWithoutBody: Comment = { const commentWithIdentifierSameJob: Comment = { ...commentWithBody, - body: '

TICS Quality Gate

This distractsMessage Heretics-client_TICS_1_1' + body: '

TICS Quality Gate

Message Here' }; const commentWithIdentifierSameJobAndRun: Comment = { ...commentWithBody, - body: '

TICS Quality Gate

Message Heretics-client_TICS_1_2' + body: '

TICS Quality Gate

Message Here' }; const commentWithIdentifierOtherJob: Comment = { ...commentWithBody, - body: '

TICS Quality Gate

Message Heretics-client_OTHER_1_1' + body: '

TICS Quality Gate

Message Here' }; const commentWithIdentifierWrongFormat: Comment = { ...commentWithBody, - body: '

TICS Quality Gate

Message Heretics-client_OTHER_1' + body: '

TICS Quality Gate

Message Here' }; From eb7ae26c84dd33202354daa0c37db85be3b56f9c Mon Sep 17 00:00:00 2001 From: janssen <118828444+janssen-tiobe@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:56:10 +0100 Subject: [PATCH 04/11] #34049: Added some more tests --- test/unit/configuration/github.test.ts | 23 ++++++++ test/unit/github/comments.test.ts | 12 +++- test/unit/helper/logger.test.ts | 76 +++++++++++++++++++------- 3 files changed, 89 insertions(+), 22 deletions(-) diff --git a/test/unit/configuration/github.test.ts b/test/unit/configuration/github.test.ts index b95e7895..6d4c7d8a 100644 --- a/test/unit/configuration/github.test.ts +++ b/test/unit/configuration/github.test.ts @@ -1,6 +1,7 @@ import * as core from '@actions/core'; import { GithubConfig } from '../../../src/configuration/github'; import { contextMock } from '../../.setup/mock'; +import { GithubEvent } from '../../../src/configuration/github-event'; describe('GitHub Configuration', () => { let githubConfig: GithubConfig; @@ -92,4 +93,26 @@ describe('GitHub Configuration', () => { githubConfig = new GithubConfig(); githubConfig.removeWarningListener(); }); + + test('getCommentIdentifier', () => { + githubConfig = new GithubConfig(); + expect(githubConfig.getCommentIdentifier()).toEqual('tics-client_TICS_1_1'); + }); + + test('getGithubEvent', () => { + contextMock.eventName = 'undefined'; + expect(new GithubConfig().event).toEqual(GithubEvent.PUSH); + contextMock.eventName = GithubEvent.PUSH.name; + expect(new GithubConfig().event).toEqual(GithubEvent.PUSH); + contextMock.eventName = GithubEvent.PULL_REQUEST.name; + expect(new GithubConfig().event).toEqual(GithubEvent.PULL_REQUEST); + contextMock.eventName = GithubEvent.PULL_REQUEST_TARGET.name; + expect(new GithubConfig().event).toEqual(GithubEvent.PULL_REQUEST_TARGET); + contextMock.eventName = GithubEvent.WORKFLOW_CALL.name; + expect(new GithubConfig().event).toEqual(GithubEvent.WORKFLOW_CALL); + contextMock.eventName = GithubEvent.WORKFLOW_DISPATCH.name; + expect(new GithubConfig().event).toEqual(GithubEvent.WORKFLOW_DISPATCH); + contextMock.eventName = GithubEvent.WORKFLOW_RUN.name; + expect(new GithubConfig().event).toEqual(GithubEvent.WORKFLOW_RUN); + }); }); diff --git a/test/unit/github/comments.test.ts b/test/unit/github/comments.test.ts index 5f5f42c3..a0e25378 100644 --- a/test/unit/github/comments.test.ts +++ b/test/unit/github/comments.test.ts @@ -139,11 +139,16 @@ describe('deletePreviousComments', () => { expect(deleteCommentSpy).toHaveBeenCalledWith(calledWith); }); - test('Should not call deleteComment if body is not TICS', async () => { + test('Should not call deleteComment if body is undefined', async () => { await deletePreviousComments([commentWithoutBody]); expect(deleteCommentSpy).toHaveBeenCalledTimes(0); }); + test('Should not call deleteComment if body is not TICS', async () => { + await deletePreviousComments([commentWithoutTics]); + expect(deleteCommentSpy).toHaveBeenCalledTimes(0); + }); + test('Should call deleteComment if identifier workflow and job match', async () => { await deletePreviousComments([commentWithIdentifierSameJob]); const calledWith = { @@ -201,6 +206,11 @@ const commentWithoutBody: Comment = { body: undefined }; +const commentWithoutTics: Comment = { + ...commentWithBody, + body: 'Other action content' +}; + const commentWithIdentifierSameJob: Comment = { ...commentWithBody, body: '

TICS Quality Gate

Message Here' diff --git a/test/unit/helper/logger.test.ts b/test/unit/helper/logger.test.ts index 0b4124ac..e007b077 100644 --- a/test/unit/helper/logger.test.ts +++ b/test/unit/helper/logger.test.ts @@ -1,27 +1,46 @@ import * as core from '@actions/core'; import { logger } from '../../../src/helper/logger'; +let infoSpy: jest.SpyInstance; +let debugSpy: jest.SpyInstance; +let noticeSpy: jest.SpyInstance; +let errorSpy: jest.SpyInstance; +let warningSpy: jest.SpyInstance; +let setFailedSpy: jest.SpyInstance; + +beforeEach(() => { + infoSpy = jest.spyOn(core, 'info'); + debugSpy = jest.spyOn(core, 'debug'); + noticeSpy = jest.spyOn(core, 'notice'); + errorSpy = jest.spyOn(core, 'error'); + warningSpy = jest.spyOn(core, 'warning'); + setFailedSpy = jest.spyOn(core, 'setFailed'); +}); + +afterEach(() => { + jest.resetAllMocks(); +}); + describe('info', () => { test('Should call core.info on info', () => { - const info = jest.spyOn(core, 'info'); const addNewline = jest.spyOn(logger, 'addNewline'); logger.info('string'); - expect(info).toHaveBeenCalledTimes(1); - expect(info).toHaveBeenCalledWith('string'); + expect(infoSpy).toHaveBeenCalledTimes(1); + expect(infoSpy).toHaveBeenCalledWith('string'); expect(addNewline).toHaveBeenCalledTimes(0); expect(logger.called).toEqual('info'); }); test('Should call core.info on header', () => { - const info = jest.spyOn(core, 'info'); const addNewline = jest.spyOn(logger, 'addNewline'); + logger.info('error'); logger.header('string'); - expect(info).toHaveBeenCalledTimes(2); // once for header, once for newline - expect(info).toHaveBeenCalledWith(expect.stringContaining('string')); + expect(infoSpy).toHaveBeenCalledTimes(2); // once for header, once for newline + expect(infoSpy).toHaveBeenCalledWith(expect.stringContaining('string')); expect(addNewline).toHaveBeenCalledTimes(1); expect(addNewline).toHaveBeenCalledWith('header'); expect(logger.called).toEqual('header'); @@ -30,14 +49,13 @@ describe('info', () => { describe('debug', () => { test('Should call core.debug on debug', () => { - const debug = jest.spyOn(core, 'debug'); const addNewline = jest.spyOn(logger, 'addNewline'); logger.setSecretsFilter(['token']); logger.debug('string token secret'); - expect(debug).toHaveBeenCalledTimes(1); - expect(debug).toHaveBeenCalledWith('string token ***'); + expect(debugSpy).toHaveBeenCalledTimes(1); + expect(debugSpy).toHaveBeenCalledWith('string token ***'); expect(addNewline).toHaveBeenCalledTimes(0); expect(logger.called).toEqual('debug'); }); @@ -45,13 +63,12 @@ describe('debug', () => { describe('notice', () => { test('Should call core.notice on notice', () => { - const debug = jest.spyOn(core, 'notice'); const addNewline = jest.spyOn(logger, 'addNewline'); logger.notice('string'); - expect(debug).toHaveBeenCalledTimes(1); - expect(debug).toHaveBeenCalledWith('string', undefined); + expect(noticeSpy).toHaveBeenCalledTimes(1); + expect(noticeSpy).toHaveBeenCalledWith('string', undefined); expect(addNewline).toHaveBeenCalledTimes(0); expect(logger.called).toEqual('notice'); }); @@ -59,13 +76,12 @@ describe('notice', () => { describe('warning', () => { test('Should call core.warning on warning', () => { - const debug = jest.spyOn(core, 'warning'); const addNewline = jest.spyOn(logger, 'addNewline'); logger.warning('string'); - expect(debug).toHaveBeenCalledTimes(1); - expect(debug).toHaveBeenCalledWith('\u001b[33mstring', undefined); + expect(warningSpy).toHaveBeenCalledTimes(1); + expect(warningSpy).toHaveBeenCalledWith('\u001b[33mstring', undefined); expect(addNewline).toHaveBeenCalledTimes(0); expect(logger.called).toEqual('warning'); }); @@ -73,13 +89,12 @@ describe('warning', () => { describe('error', () => { test('Should call core.error on error', () => { - const error = jest.spyOn(core, 'error'); const addNewline = jest.spyOn(logger, 'addNewline'); logger.error('error'); - expect(error).toHaveBeenCalledTimes(1); - expect(error).toHaveBeenCalledWith(expect.stringContaining('error'), undefined); + expect(errorSpy).toHaveBeenCalledTimes(1); + expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('error'), undefined); expect(addNewline).toHaveBeenCalledTimes(1); expect(addNewline).toHaveBeenCalledWith('error'); expect(logger.called).toEqual('error'); @@ -88,15 +103,34 @@ describe('error', () => { describe('setFailed', () => { test('Should call core.setFailed on setFailed', () => { - const setFailed = jest.spyOn(core, 'setFailed'); const addNewline = jest.spyOn(logger, 'addNewline'); logger.setFailed('error'); - expect(setFailed).toHaveBeenCalledTimes(1); - expect(setFailed).toHaveBeenCalledWith(expect.stringContaining('error')); + expect(setFailedSpy).toHaveBeenCalledTimes(1); + expect(setFailedSpy).toHaveBeenCalledWith(expect.stringContaining('error')); expect(addNewline).toHaveBeenCalledTimes(1); expect(addNewline).toHaveBeenCalledWith('error'); expect(logger.called).toEqual('error'); }); }); + +describe('maskOutput', () => { + test('Should filter out JAVA options', () => { + const message = 'Picked up JAVA_OPTIONS testing once'; + + logger.info(message); + logger.notice(message); + logger.debug(message); + logger.warning(message); + logger.error(message); + logger.setFailed(message); + + expect(infoSpy).toHaveBeenCalledTimes(0); + expect(noticeSpy).toHaveBeenCalledTimes(0); + expect(debugSpy).toHaveBeenCalledTimes(0); + expect(warningSpy).toHaveBeenCalledTimes(0); + expect(errorSpy).toHaveBeenCalledTimes(0); + expect(setFailedSpy).toHaveBeenCalledTimes(0); + }); +}); From c3e9c627a1f41c52ecaac7df60a0b2b556793392 Mon Sep 17 00:00:00 2001 From: janssen <118828444+janssen-tiobe@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:49:04 +0100 Subject: [PATCH 05/11] #34049: Add step name to the summary --- dist/index.js | 107 ++++++++++++++++++---- src/action/decorate/action.ts | 4 +- src/action/decorate/markdown.ts | 16 ++++ src/action/decorate/summary.ts | 36 +++++--- src/analysis/client/process-analysis.ts | 6 +- src/analysis/qserver.ts | 4 +- src/configuration/github.ts | 4 +- src/github/runs.ts | 32 +++++++ test/unit/action/decorate/action.test.ts | 4 +- test/unit/action/decorate/summary.test.ts | 46 ++++++---- test/unit/analysis/qserver.test.ts | 2 +- 11 files changed, 196 insertions(+), 65 deletions(-) create mode 100644 src/github/runs.ts diff --git a/dist/index.js b/dist/index.js index f4bb7249..db9ae634 100644 --- a/dist/index.js +++ b/dist/index.js @@ -47,10 +47,10 @@ const annotations_1 = __nccwpck_require__(31058); async function decorateAction(analysisResult, analysis) { let summaryBody; if (analysisResult) { - summaryBody = (0, summary_1.createSummaryBody)(analysisResult); + summaryBody = await (0, summary_1.createSummaryBody)(analysisResult); } else { - summaryBody = (0, summary_1.createErrorSummaryBody)(analysis.errorList, analysis.warningList); + summaryBody = await (0, summary_1.createErrorSummaryBody)(analysis.errorList, analysis.warningList); } if (config_1.githubConfig.event.isPullRequest) { await (0, pull_request_1.decoratePullRequest)(analysisResult?.passed ?? false, summaryBody); @@ -71,6 +71,8 @@ async function decorateAction(analysisResult, analysis) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.generateStatusMarkdown = generateStatusMarkdown; exports.generateExpandableAreaMarkdown = generateExpandableAreaMarkdown; +exports.generateItalic = generateItalic; +exports.generateComment = generateComment; const os_1 = __nccwpck_require__(22037); const enums_1 = __nccwpck_require__(31655); /** @@ -101,6 +103,20 @@ function generateStatusMarkdown(status, hasSuffix = false) { function generateExpandableAreaMarkdown(header, body) { return `
${header}${os_1.EOL}${body}
${os_1.EOL}${os_1.EOL}`; } +/** + * Generates italic text for markdown. + * @param text The text to make italic. + */ +function generateItalic(text) { + return `${text}`; +} +/** + * Generates a hidden comment for markdown. + * @param comment The text of the comment. + */ +function generateComment(comment) { + return ``; +} /***/ }), @@ -201,10 +217,10 @@ const logger_1 = __nccwpck_require__(26440); const url_1 = __nccwpck_require__(76259); const markdown_1 = __nccwpck_require__(60517); const config_1 = __nccwpck_require__(86444); -function createSummaryBody(analysisResult) { +const runs_1 = __nccwpck_require__(50046); +async function createSummaryBody(analysisResult) { logger_1.logger.header('Creating summary.'); - core_1.summary.addHeading('TICS Quality Gate'); - core_1.summary.addHeading((0, markdown_1.generateStatusMarkdown)(getStatus(analysisResult.passed, analysisResult.passedWithWarning), true), 3); + setSummaryHeader(getStatus(analysisResult.passed, analysisResult.passedWithWarning)); analysisResult.projectResults.forEach(projectResult => { if (projectResult.qualityGate) { const failedOrWarnConditions = extractFailedOrWarningConditions(projectResult.qualityGate.gates); @@ -230,7 +246,7 @@ function createSummaryBody(analysisResult) { core_1.summary.addRaw(createFilesSummary(projectResult.analyzedFiles)); } }); - core_1.summary.addRaw(``); + await setSummaryFooter(); logger_1.logger.info('Created summary.'); return core_1.summary.stringify(); } @@ -240,10 +256,9 @@ function createSummaryBody(analysisResult) { * @param warningList list containing all the warnings found in the TICS run. * @returns string containing the error summary. */ -function createErrorSummaryBody(errorList, warningList) { +async function createErrorSummaryBody(errorList, warningList) { logger_1.logger.header('Creating summary.'); - core_1.summary.addHeading('TICS Quality Gate'); - core_1.summary.addHeading((0, markdown_1.generateStatusMarkdown)(enums_1.Status.FAILED, true), 3); + setSummaryHeader(enums_1.Status.FAILED); if (errorList.length > 0) { core_1.summary.addHeading('The following errors have occurred during analysis:', 2); for (const error of errorList) { @@ -257,7 +272,7 @@ function createErrorSummaryBody(errorList, warningList) { core_1.summary.addRaw(`:warning: ${warning}${os_1.EOL}${os_1.EOL}`); } } - core_1.summary.addRaw(``); + await setSummaryFooter(); logger_1.logger.info('Created summary.'); return core_1.summary.stringify(); } @@ -266,15 +281,23 @@ function createErrorSummaryBody(errorList, warningList) { * @param message Message to display in the body of the comment. * @returns string containing the error summary. */ -function createNothingAnalyzedSummaryBody(message) { +async function createNothingAnalyzedSummaryBody(message) { logger_1.logger.header('Creating summary.'); - core_1.summary.addHeading('TICS Quality Gate'); - core_1.summary.addHeading((0, markdown_1.generateStatusMarkdown)(enums_1.Status.PASSED, true), 3); + setSummaryHeader(enums_1.Status.PASSED); core_1.summary.addRaw(message); - core_1.summary.addRaw(``); + await setSummaryFooter(); logger_1.logger.info('Created summary.'); return core_1.summary.stringify(); } +function setSummaryHeader(status) { + core_1.summary.addHeading('TICS Quality Gate'); + core_1.summary.addHeading((0, markdown_1.generateStatusMarkdown)(status, true), 3); +} +async function setSummaryFooter() { + core_1.summary.addEOL(); + core_1.summary.addRaw((0, markdown_1.generateItalic)(`Step: ${await (0, runs_1.getCurrentStepName)()}`), true); + core_1.summary.addRaw((0, markdown_1.generateComment)(config_1.githubConfig.getCommentIdentifier())); +} function getConditionHeading(failedOrWarnConditions) { const countFailedConditions = failedOrWarnConditions.filter(c => !c.passed).length; const countWarnConditions = failedOrWarnConditions.filter(c => c.passed && c.passedWithWarning).length; @@ -672,14 +695,14 @@ async function processIncompleteAnalysis(analysis) { let summaryBody; if (!analysis.completed) { failedMessage = 'Failed to complete TICS analysis.'; - summaryBody = (0, summary_1.createErrorSummaryBody)(analysis.errorList, analysis.warningList); + summaryBody = await (0, summary_1.createErrorSummaryBody)(analysis.errorList, analysis.warningList); } else if (analysis.warningList.find(w => w.includes('[WARNING 5057]'))) { - summaryBody = (0, summary_1.createNothingAnalyzedSummaryBody)('No changed files applicable for TICS analysis quality gating.'); + summaryBody = await (0, summary_1.createNothingAnalyzedSummaryBody)('No changed files applicable for TICS analysis quality gating.'); } else { failedMessage = 'Explorer URL not returned from TICS analysis.'; - summaryBody = (0, summary_1.createErrorSummaryBody)(analysis.errorList, analysis.warningList); + summaryBody = await (0, summary_1.createErrorSummaryBody)(analysis.errorList, analysis.warningList); } if (config_1.githubConfig.event.isPullRequest) { await (0, pull_request_1.postToConversation)(false, summaryBody); @@ -806,13 +829,13 @@ async function qServerAnalysis() { }; if (!verdict.passed) { verdict.message = 'Failed to complete TICSQServer analysis.'; - const summaryBody = (0, summary_1.createErrorSummaryBody)(analysis.errorList, analysis.warningList); + const summaryBody = await (0, summary_1.createErrorSummaryBody)(analysis.errorList, analysis.warningList); if (config_1.githubConfig.event.isPullRequest) { await (0, pull_request_1.postToConversation)(false, summaryBody); } } else if (analysis.warningList.find(w => w.includes('[WARNING 5057]'))) { - const summaryBody = (0, summary_1.createNothingAnalyzedSummaryBody)('No changed files applicable for TICS analysis quality gating.'); + const summaryBody = await (0, summary_1.createNothingAnalyzedSummaryBody)('No changed files applicable for TICS analysis quality gating.'); if (config_1.githubConfig.event.isPullRequest) { await (0, pull_request_1.postToConversation)(false, summaryBody); } @@ -1023,6 +1046,7 @@ class GithubConfig { job; action; workflow; + runId; runNumber; runAttempt; pullRequestNumber; @@ -1037,6 +1061,7 @@ class GithubConfig { this.job = github_1.context.job.replace(/[\s|_]+/g, '-'); this.action = github_1.context.action.replace('__tiobe_', ''); this.workflow = github_1.context.workflow.replace(/[\s|_]+/g, '-'); + this.runId = github_1.context.runId; this.runNumber = github_1.context.runNumber; this.runAttempt = parseInt(process.env.GITHUB_RUN_ATTEMPT ?? '0', 10); this.pullRequestNumber = this.getPullRequestNumber(); @@ -1051,7 +1076,7 @@ class GithubConfig { * include a suffix that consists of the sequence number preceded by an underscore. * https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables */ - this.id = `${github_1.context.runId.toString()}_${this.runAttempt.toString()}_${this.job}_${this.action}`; + this.id = `${this.runId.toString()}_${this.runAttempt.toString()}_${this.job}_${this.action}`; this.removeWarningListener(); } getPullRequestNumber() { @@ -1919,6 +1944,48 @@ async function postReview(body, event) { } +/***/ }), + +/***/ 50046: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getCurrentStepName = getCurrentStepName; +const logger_1 = __nccwpck_require__(26440); +const response_1 = __nccwpck_require__(81934); +const config_1 = __nccwpck_require__(86444); +const octokit_1 = __nccwpck_require__(10775); +/** + * Create review on the pull request from the analysis given. + * @param body Body containing the summary of the review + * @param event Either approve or request changes in the review. + */ +async function getCurrentStepName() { + const params = { + owner: config_1.githubConfig.owner, + repo: config_1.githubConfig.reponame, + run_id: config_1.githubConfig.runId, + attempt_number: config_1.githubConfig.runAttempt + }; + let stepname = config_1.githubConfig.action; + try { + logger_1.logger.debug('Retrieving step name for current step...'); + const response = await octokit_1.octokit.rest.actions.listJobsForWorkflowRunAttempt(params); + const jobs = response.data.jobs.filter(j => j.status === 'in_progress'); + if (jobs.length === 1) { + stepname = jobs[0].name; + } + } + catch (error) { + const message = (0, response_1.handleOctokitError)(error); + logger_1.logger.notice(`Posting the review failed: ${message}`); + } + return stepname; +} + + /***/ }), /***/ 31655: diff --git a/src/action/decorate/action.ts b/src/action/decorate/action.ts index d39be771..8a09f8eb 100644 --- a/src/action/decorate/action.ts +++ b/src/action/decorate/action.ts @@ -12,9 +12,9 @@ import { Analysis, AnalysisResult } from '../../helper/interfaces'; export async function decorateAction(analysisResult: AnalysisResult | undefined, analysis: Analysis): Promise { let summaryBody; if (analysisResult) { - summaryBody = createSummaryBody(analysisResult); + summaryBody = await createSummaryBody(analysisResult); } else { - summaryBody = createErrorSummaryBody(analysis.errorList, analysis.warningList); + summaryBody = await createErrorSummaryBody(analysis.errorList, analysis.warningList); } if (githubConfig.event.isPullRequest) { diff --git a/src/action/decorate/markdown.ts b/src/action/decorate/markdown.ts index ac08af29..195da8a6 100644 --- a/src/action/decorate/markdown.ts +++ b/src/action/decorate/markdown.ts @@ -30,3 +30,19 @@ export function generateStatusMarkdown(status: Status, hasSuffix = false): strin export function generateExpandableAreaMarkdown(header: string, body: string): string { return `
${header}${EOL}${body}
${EOL}${EOL}`; } + +/** + * Generates italic text for markdown. + * @param text The text to make italic. + */ +export function generateItalic(text: string): string { + return `${text}`; +} + +/** + * Generates a hidden comment for markdown. + * @param comment The text of the comment. + */ +export function generateComment(comment: string): string { + return ``; +} diff --git a/src/action/decorate/summary.ts b/src/action/decorate/summary.ts index e3de689e..09461f2b 100644 --- a/src/action/decorate/summary.ts +++ b/src/action/decorate/summary.ts @@ -3,7 +3,6 @@ import { format } from 'date-fns'; import { range } from 'underscore'; import { summary } from '@actions/core'; import { SummaryTableRow } from '@actions/core/lib/summary'; - import { ChangedFile } from '../../github/interfaces'; import { Status } from '../../helper/enums'; import { logger } from '../../helper/logger'; @@ -17,13 +16,13 @@ import { TicsReviewComment, TicsReviewComments } from '../../helper/interfaces'; -import { generateExpandableAreaMarkdown, generateStatusMarkdown } from './markdown'; +import { generateComment, generateExpandableAreaMarkdown, generateItalic, generateStatusMarkdown } from './markdown'; import { githubConfig, ticsConfig } from '../../configuration/config'; +import { getCurrentStepName } from '../../github/runs'; -export function createSummaryBody(analysisResult: AnalysisResult): string { +export async function createSummaryBody(analysisResult: AnalysisResult): Promise { logger.header('Creating summary.'); - summary.addHeading('TICS Quality Gate'); - summary.addHeading(generateStatusMarkdown(getStatus(analysisResult.passed, analysisResult.passedWithWarning), true), 3); + setSummaryHeader(getStatus(analysisResult.passed, analysisResult.passedWithWarning)); analysisResult.projectResults.forEach(projectResult => { if (projectResult.qualityGate) { @@ -54,7 +53,7 @@ export function createSummaryBody(analysisResult: AnalysisResult): string { summary.addRaw(createFilesSummary(projectResult.analyzedFiles)); } }); - summary.addRaw(``); + await setSummaryFooter(); logger.info('Created summary.'); @@ -67,11 +66,10 @@ export function createSummaryBody(analysisResult: AnalysisResult): string { * @param warningList list containing all the warnings found in the TICS run. * @returns string containing the error summary. */ -export function createErrorSummaryBody(errorList: string[], warningList: string[]): string { +export async function createErrorSummaryBody(errorList: string[], warningList: string[]): Promise { logger.header('Creating summary.'); - summary.addHeading('TICS Quality Gate'); - summary.addHeading(generateStatusMarkdown(Status.FAILED, true), 3); + setSummaryHeader(Status.FAILED); if (errorList.length > 0) { summary.addHeading('The following errors have occurred during analysis:', 2); @@ -89,7 +87,7 @@ export function createErrorSummaryBody(errorList: string[], warningList: string[ summary.addRaw(`:warning: ${warning}${EOL}${EOL}`); } } - summary.addRaw(``); + await setSummaryFooter(); logger.info('Created summary.'); return summary.stringify(); @@ -100,19 +98,29 @@ export function createErrorSummaryBody(errorList: string[], warningList: string[ * @param message Message to display in the body of the comment. * @returns string containing the error summary. */ -export function createNothingAnalyzedSummaryBody(message: string): string { +export async function createNothingAnalyzedSummaryBody(message: string): Promise { logger.header('Creating summary.'); - summary.addHeading('TICS Quality Gate'); - summary.addHeading(generateStatusMarkdown(Status.PASSED, true), 3); + setSummaryHeader(Status.PASSED); summary.addRaw(message); - summary.addRaw(``); + await setSummaryFooter(); logger.info('Created summary.'); return summary.stringify(); } +function setSummaryHeader(status: Status) { + summary.addHeading('TICS Quality Gate'); + summary.addHeading(generateStatusMarkdown(status, true), 3); +} + +async function setSummaryFooter() { + summary.addEOL(); + summary.addRaw(generateItalic(`Step: ${await getCurrentStepName()}`), true); + summary.addRaw(generateComment(githubConfig.getCommentIdentifier())); +} + function getConditionHeading(failedOrWarnConditions: Condition[]): string { const countFailedConditions = failedOrWarnConditions.filter(c => !c.passed).length; const countWarnConditions = failedOrWarnConditions.filter(c => c.passed && c.passedWithWarning).length; diff --git a/src/analysis/client/process-analysis.ts b/src/analysis/client/process-analysis.ts index 4e858572..ca340a25 100644 --- a/src/analysis/client/process-analysis.ts +++ b/src/analysis/client/process-analysis.ts @@ -19,12 +19,12 @@ export async function processIncompleteAnalysis(analysis: Analysis): Promise w.includes('[WARNING 5057]'))) { - summaryBody = createNothingAnalyzedSummaryBody('No changed files applicable for TICS analysis quality gating.'); + summaryBody = await createNothingAnalyzedSummaryBody('No changed files applicable for TICS analysis quality gating.'); } else { failedMessage = 'Explorer URL not returned from TICS analysis.'; - summaryBody = createErrorSummaryBody(analysis.errorList, analysis.warningList); + summaryBody = await createErrorSummaryBody(analysis.errorList, analysis.warningList); } if (githubConfig.event.isPullRequest) { diff --git a/src/analysis/qserver.ts b/src/analysis/qserver.ts index b1818745..c7d5e3f7 100644 --- a/src/analysis/qserver.ts +++ b/src/analysis/qserver.ts @@ -28,12 +28,12 @@ export async function qServerAnalysis(): Promise { if (!verdict.passed) { verdict.message = 'Failed to complete TICSQServer analysis.'; - const summaryBody = createErrorSummaryBody(analysis.errorList, analysis.warningList); + const summaryBody = await createErrorSummaryBody(analysis.errorList, analysis.warningList); if (githubConfig.event.isPullRequest) { await postToConversation(false, summaryBody); } } else if (analysis.warningList.find(w => w.includes('[WARNING 5057]'))) { - const summaryBody = createNothingAnalyzedSummaryBody('No changed files applicable for TICS analysis quality gating.'); + const summaryBody = await createNothingAnalyzedSummaryBody('No changed files applicable for TICS analysis quality gating.'); if (githubConfig.event.isPullRequest) { await postToConversation(false, summaryBody); } diff --git a/src/configuration/github.ts b/src/configuration/github.ts index f538a0e6..873abc4a 100644 --- a/src/configuration/github.ts +++ b/src/configuration/github.ts @@ -12,6 +12,7 @@ export class GithubConfig { readonly job: string; readonly action: string; readonly workflow: string; + readonly runId: number; readonly runNumber: number; readonly runAttempt: number; readonly pullRequestNumber: number | undefined; @@ -27,6 +28,7 @@ export class GithubConfig { this.job = context.job.replace(/[\s|_]+/g, '-'); this.action = context.action.replace('__tiobe_', ''); this.workflow = context.workflow.replace(/[\s|_]+/g, '-'); + this.runId = context.runId; this.runNumber = context.runNumber; this.runAttempt = parseInt(process.env.GITHUB_RUN_ATTEMPT ?? '0', 10); this.pullRequestNumber = this.getPullRequestNumber(); @@ -42,7 +44,7 @@ export class GithubConfig { * include a suffix that consists of the sequence number preceded by an underscore. * https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables */ - this.id = `${context.runId.toString()}_${this.runAttempt.toString()}_${this.job}_${this.action}`; + this.id = `${this.runId.toString()}_${this.runAttempt.toString()}_${this.job}_${this.action}`; this.removeWarningListener(); } diff --git a/src/github/runs.ts b/src/github/runs.ts new file mode 100644 index 00000000..c91a6287 --- /dev/null +++ b/src/github/runs.ts @@ -0,0 +1,32 @@ +import { logger } from '../helper/logger'; +import { handleOctokitError } from '../helper/response'; +import { githubConfig } from '../configuration/config'; +import { octokit } from './octokit'; + +/** + * Create review on the pull request from the analysis given. + * @param body Body containing the summary of the review + * @param event Either approve or request changes in the review. + */ +export async function getCurrentStepName(): Promise { + const params = { + owner: githubConfig.owner, + repo: githubConfig.reponame, + run_id: githubConfig.runId, + attempt_number: githubConfig.runAttempt + }; + + let stepname = githubConfig.action; + try { + logger.debug('Retrieving step name for current step...'); + const response = await octokit.rest.actions.listJobsForWorkflowRunAttempt(params); + const jobs = response.data.jobs.filter(j => j.status === 'in_progress'); + if (jobs.length === 1) { + stepname = jobs[0].name; + } + } catch (error: unknown) { + const message = handleOctokitError(error); + logger.notice(`Posting the review failed: ${message}`); + } + return stepname; +} diff --git a/test/unit/action/decorate/action.test.ts b/test/unit/action/decorate/action.test.ts index e1e5deed..61d8d671 100644 --- a/test/unit/action/decorate/action.test.ts +++ b/test/unit/action/decorate/action.test.ts @@ -14,8 +14,8 @@ describe('decorateAction', () => { let spyPostAnnotations: jest.SpyInstance; beforeEach(() => { - spyCreateSummaryBody = jest.spyOn(summary, 'createSummaryBody').mockReturnValue('body'); - spyCreateErrorSummaryBody = jest.spyOn(summary, 'createErrorSummaryBody').mockReturnValue('body'); + spyCreateSummaryBody = jest.spyOn(summary, 'createSummaryBody').mockResolvedValue('body'); + spyCreateErrorSummaryBody = jest.spyOn(summary, 'createErrorSummaryBody').mockResolvedValue('body'); spyDecoratePullRequest = jest.spyOn(pullRequest, 'decoratePullRequest').mockImplementation(); spyPostAnnotations = jest.spyOn(annotations, 'postAnnotations').mockImplementation(); }); diff --git a/test/unit/action/decorate/summary.test.ts b/test/unit/action/decorate/summary.test.ts index 7af2dcb9..2223a8c8 100644 --- a/test/unit/action/decorate/summary.test.ts +++ b/test/unit/action/decorate/summary.test.ts @@ -26,8 +26,8 @@ describe('createSummaryBody', () => { ticsConfigMock.displayUrl = 'http://viewer.url/'; }); - test('Should contain blocking after if there are soaked violations', () => { - const string = createSummaryBody(analysisResultsSoaked); + test('Should contain blocking after if there are soaked violations', async () => { + const string = await createSummaryBody(analysisResultsSoaked); expect(string).toContain('

:x: Failed

'); expect(string).toContain('

1 Condition(s) failed

'); @@ -40,8 +40,8 @@ describe('createSummaryBody', () => { summary.clear(); }); - test('Should not contain blocking after if there are no soaked violations', () => { - const string = createSummaryBody(analysisResultsNotSoaked); + test('Should not contain blocking after if there are no soaked violations', async () => { + const string = await createSummaryBody(analysisResultsNotSoaked); expect(string).toContain('

:x: Failed

'); expect(string).toContain('

1 Condition(s) failed

'); @@ -54,8 +54,8 @@ describe('createSummaryBody', () => { summary.clear(); }); - test('Should contain blocking after if there are partly violations', () => { - const string = createSummaryBody(analysisResultsPartlySoakedPassed); + test('Should contain blocking after if there are partly violations', async () => { + const string = await createSummaryBody(analysisResultsPartlySoakedPassed); expect(string).toContain('

:warning: Passed with warnings

'); expect(string).toContain('

1 Condition(s) passed with warning

'); @@ -67,8 +67,8 @@ describe('createSummaryBody', () => { summary.clear(); }); - test('Should contain blocking after for one of the two conditions', () => { - const string = createSummaryBody(analysisResultsPartlySoakedFailed); + test('Should contain blocking after for one of the two conditions', async () => { + const string = await createSummaryBody(analysisResultsPartlySoakedFailed); expect(string).toContain('

:x: Failed

'); expect(string).toContain('

1 Condition(s) failed, 1 Condition(s) passed with warning

'); @@ -82,8 +82,8 @@ describe('createSummaryBody', () => { summary.clear(); }); - test('Should pass with no conditions that passed with warnings', () => { - const string = createSummaryBody(analysisResultsNoSoakedPassed); + test('Should pass with no conditions that passed with warnings', async () => { + const string = await createSummaryBody(analysisResultsNoSoakedPassed); expect(string).toContain('

:heavy_check_mark: Passed

'); expect(string).toContain('

All conditions passed

'); @@ -93,10 +93,10 @@ describe('createSummaryBody', () => { }); describe('createErrorSummary', () => { - test('Should return summary of two errors', () => { + test('Should return summary of two errors', async () => { githubConfigMock.debugger = false; - const body = createErrorSummaryBody(['Error', 'Error'], []); + const body = await createErrorSummaryBody(['Error', 'Error'], []); summary.clear(); expect(body).toContainTimes('

The following errors have occurred during analysis:

', 1); @@ -105,10 +105,10 @@ describe('createErrorSummary', () => { expect(body).toContainTimes(':warning: Warning', 0); }); - test('Should return summary of zero warnings on logLevel default', () => { + test('Should return summary of zero warnings on logLevel default', async () => { githubConfigMock.debugger = false; - const body = createErrorSummaryBody([], ['Warning', 'Warning']); + const body = await createErrorSummaryBody([], ['Warning', 'Warning']); summary.clear(); expect(body).toContainTimes('

The following errors have occurred during analysis:

', 0); @@ -117,35 +117,41 @@ describe('createErrorSummary', () => { expect(body).toContainTimes(':warning: Warning', 0); }); - test('Should return summary of two warnings on logLevel debug', () => { + test('Should return summary of two warnings on logLevel debug', async () => { githubConfigMock.debugger = true; - const body = createErrorSummaryBody([], ['Warning', 'Warning']); + const body = await createErrorSummaryBody([], ['Warning', 'Warning']); summary.clear(); expect(body).toContainTimes('

The following errors have occurred during analysis:

', 0); expect(body).toContainTimes('

The following warnings have occurred during analysis:

', 1); expect(body).toContainTimes(':x: Error', 0); expect(body).toContainTimes(':warning: Warning', 2); + expect(body).toContainTimes('\ntics-github-action', 1); + expect(body).toContain('\n'); }); - test('Should return summary of one error and two warnings', () => { + test('Should return summary of one error and two warnings', async () => { githubConfigMock.debugger = true; - const body = createErrorSummaryBody(['Error'], ['Warning', 'Warning']); + const body = await createErrorSummaryBody(['Error'], ['Warning', 'Warning']); summary.clear(); expect(body).toContainTimes('

The following errors have occurred during analysis:

', 1); expect(body).toContainTimes('

The following warnings have occurred during analysis:

', 1); expect(body).toContainTimes(':x: Error', 1); expect(body).toContainTimes(':warning: Warning', 2); + expect(body).toContainTimes('\ntics-github-action', 1); + expect(body).toContainTimes('\n', 1); }); }); describe('createNothingAnalyzedSummaryBody', () => { test('Should return summary with the message given', async () => { - const body = createNothingAnalyzedSummaryBody('message'); - expect(body).toEqual('

TICS Quality Gate

\n

:heavy_check_mark: Passed

\nmessage'); + const body = await createNothingAnalyzedSummaryBody('message'); + expect(body).toEqual( + '

TICS Quality Gate

\n

:heavy_check_mark: Passed

\nmessage\ntics-github-action\n' + ); }); }); diff --git a/test/unit/analysis/qserver.test.ts b/test/unit/analysis/qserver.test.ts index d33015c6..f011c223 100644 --- a/test/unit/analysis/qserver.test.ts +++ b/test/unit/analysis/qserver.test.ts @@ -33,7 +33,7 @@ describe('SetFailed checks (QServer)', () => { spyGetLastQServerRunDate = jest.spyOn(viewer, 'getLastQServerRunDate'); jest.spyOn(action, 'decorateAction'); - jest.spyOn(summary, 'createNothingAnalyzedSummaryBody').mockReturnValue('body'); + jest.spyOn(summary, 'createNothingAnalyzedSummaryBody').mockResolvedValue('body'); }); afterEach(() => { From 05cfbf4c7cad8663ce6fd275c600d172a39a4977 Mon Sep 17 00:00:00 2001 From: janssen <118828444+janssen-tiobe@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:56:04 +0100 Subject: [PATCH 06/11] #34049: Fix failing tests + add runs test --- test/.setup/mock.ts | 3 ++ test/unit/action/decorate/summary.test.ts | 6 +-- test/unit/github/runs.test.ts | 61 +++++++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 test/unit/github/runs.test.ts diff --git a/test/.setup/mock.ts b/test/.setup/mock.ts index d1d7c830..911ae437 100644 --- a/test/.setup/mock.ts +++ b/test/.setup/mock.ts @@ -112,6 +112,9 @@ jest.mock('../../src/github/octokit', () => { }, repos: { getCommit: jest.fn() + }, + actions: { + listJobsForWorkflowRunAttempt: jest.fn() } }, graphql: jest.fn() diff --git a/test/unit/action/decorate/summary.test.ts b/test/unit/action/decorate/summary.test.ts index 2223a8c8..7971c06c 100644 --- a/test/unit/action/decorate/summary.test.ts +++ b/test/unit/action/decorate/summary.test.ts @@ -127,7 +127,7 @@ describe('createErrorSummary', () => { expect(body).toContainTimes('

The following warnings have occurred during analysis:

', 1); expect(body).toContainTimes(':x: Error', 0); expect(body).toContainTimes(':warning: Warning', 2); - expect(body).toContainTimes('\ntics-github-action', 1); + expect(body).toContainTimes('\nStep: tics-github-action', 1); expect(body).toContain('\n'); }); @@ -141,7 +141,7 @@ describe('createErrorSummary', () => { expect(body).toContainTimes('

The following warnings have occurred during analysis:

', 1); expect(body).toContainTimes(':x: Error', 1); expect(body).toContainTimes(':warning: Warning', 2); - expect(body).toContainTimes('\ntics-github-action', 1); + expect(body).toContainTimes('\nStep: tics-github-action', 1); expect(body).toContainTimes('\n', 1); }); }); @@ -150,7 +150,7 @@ describe('createNothingAnalyzedSummaryBody', () => { test('Should return summary with the message given', async () => { const body = await createNothingAnalyzedSummaryBody('message'); expect(body).toEqual( - '

TICS Quality Gate

\n

:heavy_check_mark: Passed

\nmessage\ntics-github-action\n' + '

TICS Quality Gate

\n

:heavy_check_mark: Passed

\nmessage\nStep: tics-github-action\n' ); }); }); diff --git a/test/unit/github/runs.test.ts b/test/unit/github/runs.test.ts new file mode 100644 index 00000000..58215ca5 --- /dev/null +++ b/test/unit/github/runs.test.ts @@ -0,0 +1,61 @@ +import { getCurrentStepName } from '../../../src/github/runs'; +import { octokit } from '../../../src/github/octokit'; +import { githubConfigMock } from '../../.setup/mock'; + +describe('postReview', () => { + let listJobsSpy: jest.SpyInstance; + + beforeEach(() => { + listJobsSpy = jest.spyOn(octokit.rest.actions, 'listJobsForWorkflowRunAttempt'); + }); + + test('Should return name when only one step is in progress', async () => { + listJobsSpy.mockResolvedValue({ + data: { + jobs: [ + { + name: 'Step 1', + status: 'completed' + }, + { + name: 'Step 2', + status: 'in_progress' + } + ] + } + }); + + const name = await getCurrentStepName(); + + expect(name).toStrictEqual('Step 2'); + }); + + test('Should not return name when only one step is in progress', async () => { + listJobsSpy.mockResolvedValue({ + data: { + jobs: [ + { + name: 'Step 1', + status: 'in_progress' + }, + { + name: 'Step 2', + status: 'in_progress' + } + ] + } + }); + + const name = await getCurrentStepName(); + + expect(name).toStrictEqual(githubConfigMock.action); + }); + + test('Should post a notice on createReview', async () => { + listJobsSpy.mockRejectedValue(new Error()); + + const name = await getCurrentStepName(); + + expect(name).toStrictEqual(githubConfigMock.action); + }); +}); From 6e08353b6722105473ed1bae75bcdbf3e3eb3782 Mon Sep 17 00:00:00 2001 From: janssen <118828444+janssen-tiobe@users.noreply.github.com> Date: Fri, 6 Dec 2024 16:36:49 +0100 Subject: [PATCH 07/11] #34049: Corrected data usage of listJobsForWorkflowRunAttempt api call --- dist/index.js | 7 +++-- src/github/runs.ts | 8 ++++-- test/unit/github/runs.test.ts | 54 +++++++++++++++++++++++++++++------ 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/dist/index.js b/dist/index.js index a3cb78a1..b0662bcc 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1979,9 +1979,12 @@ async function getCurrentStepName() { try { logger_1.logger.debug('Retrieving step name for current step...'); const response = await octokit_1.octokit.rest.actions.listJobsForWorkflowRunAttempt(params); - const jobs = response.data.jobs.filter(j => j.status === 'in_progress'); + const jobs = response.data.jobs.filter(j => j.name.replace(/[\s|_]+/g, '-') === config_1.githubConfig.job); if (jobs.length === 1) { - stepname = jobs[0].name; + const steps = jobs[0].steps?.filter(s => s.status === 'in_progress'); + if (steps?.length === 1) { + stepname = steps[0].name; + } } } catch (error) { diff --git a/src/github/runs.ts b/src/github/runs.ts index c91a6287..045d93a7 100644 --- a/src/github/runs.ts +++ b/src/github/runs.ts @@ -20,9 +20,13 @@ export async function getCurrentStepName(): Promise { try { logger.debug('Retrieving step name for current step...'); const response = await octokit.rest.actions.listJobsForWorkflowRunAttempt(params); - const jobs = response.data.jobs.filter(j => j.status === 'in_progress'); + const jobs = response.data.jobs.filter(j => j.name.replace(/[\s|_]+/g, '-') === githubConfig.job); + if (jobs.length === 1) { - stepname = jobs[0].name; + const steps = jobs[0].steps?.filter(s => s.status === 'in_progress'); + if (steps?.length === 1) { + stepname = steps[0].name; + } } } catch (error: unknown) { const message = handleOctokitError(error); diff --git a/test/unit/github/runs.test.ts b/test/unit/github/runs.test.ts index 58215ca5..bf63d636 100644 --- a/test/unit/github/runs.test.ts +++ b/test/unit/github/runs.test.ts @@ -14,12 +14,30 @@ describe('postReview', () => { data: { jobs: [ { - name: 'Step 1', - status: 'completed' + name: 'TICS', + steps: [ + { + name: 'Step 1', + status: 'completed' + }, + { + name: 'Step 2', + status: 'in_progress' + } + ] }, { - name: 'Step 2', - status: 'in_progress' + name: 'TICS2', + steps: [ + { + name: 'Step 1', + status: 'in_progress' + }, + { + name: 'Step 2', + status: 'in_progress' + } + ] } ] } @@ -30,17 +48,35 @@ describe('postReview', () => { expect(name).toStrictEqual('Step 2'); }); - test('Should not return name when only one step is in progress', async () => { + test('Should not return name when multiple steps are in progress', async () => { listJobsSpy.mockResolvedValue({ data: { jobs: [ { - name: 'Step 1', - status: 'in_progress' + name: 'TICS', + steps: [ + { + name: 'Step 1', + status: 'in_progress' + }, + { + name: 'Step 2', + status: 'in_progress' + } + ] }, { - name: 'Step 2', - status: 'in_progress' + name: 'TICS2', + steps: [ + { + name: 'Step 1', + status: 'completed' + }, + { + name: 'Step 2', + status: 'completed' + } + ] } ] } From 441b76b71bacff81dd3aef2ff21fb743bd674e0b Mon Sep 17 00:00:00 2001 From: janssen <118828444+janssen-tiobe@users.noreply.github.com> Date: Fri, 6 Dec 2024 16:50:04 +0100 Subject: [PATCH 08/11] #34049: Added more debug info --- dist/index.js | 1 + src/github/runs.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/dist/index.js b/dist/index.js index b0662bcc..66c55a95 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1979,6 +1979,7 @@ async function getCurrentStepName() { try { logger_1.logger.debug('Retrieving step name for current step...'); const response = await octokit_1.octokit.rest.actions.listJobsForWorkflowRunAttempt(params); + logger_1.logger.debug(JSON.stringify(response.data)); const jobs = response.data.jobs.filter(j => j.name.replace(/[\s|_]+/g, '-') === config_1.githubConfig.job); if (jobs.length === 1) { const steps = jobs[0].steps?.filter(s => s.status === 'in_progress'); diff --git a/src/github/runs.ts b/src/github/runs.ts index 045d93a7..11b4a14e 100644 --- a/src/github/runs.ts +++ b/src/github/runs.ts @@ -20,6 +20,7 @@ export async function getCurrentStepName(): Promise { try { logger.debug('Retrieving step name for current step...'); const response = await octokit.rest.actions.listJobsForWorkflowRunAttempt(params); + logger.debug(JSON.stringify(response.data)); const jobs = response.data.jobs.filter(j => j.name.replace(/[\s|_]+/g, '-') === githubConfig.job); if (jobs.length === 1) { From 85550a6cf08989454a48e03569274230a400e962 Mon Sep 17 00:00:00 2001 From: janssen <118828444+janssen-tiobe@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:07:44 +0100 Subject: [PATCH 09/11] #34049: Added more information in the summary, step retrieval changed --- dist/index.js | 8 +++++--- src/action/decorate/summary.ts | 2 +- src/configuration/github.ts | 2 ++ src/github/runs.ts | 4 ++-- test/.setup/mock.ts | 2 ++ test/unit/action/decorate/summary.test.ts | 6 +++--- test/unit/github/runs.test.ts | 12 ++++++++---- 7 files changed, 23 insertions(+), 13 deletions(-) diff --git a/dist/index.js b/dist/index.js index 66c55a95..525a3a5b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -296,7 +296,7 @@ function setSummaryHeader(status) { } async function setSummaryFooter() { core_1.summary.addEOL(); - core_1.summary.addRaw((0, markdown_1.generateItalic)(`Step: ${await (0, runs_1.getCurrentStepName)()}`), true); + core_1.summary.addRaw((0, markdown_1.generateItalic)(`Workflow: ${config_1.githubConfig.workflow}, Job: ${config_1.githubConfig.job}, Step: ${await (0, runs_1.getCurrentStepName)()}`), true); core_1.summary.addRaw((0, markdown_1.generateComment)(config_1.githubConfig.getCommentIdentifier())); } function getConditionHeading(failedOrWarnConditions) { @@ -1057,6 +1057,7 @@ class GithubConfig { runAttempt; pullRequestNumber; debugger; + runnerName; id; constructor() { this.apiUrl = github_1.context.apiUrl; @@ -1072,6 +1073,7 @@ class GithubConfig { this.runAttempt = parseInt(process.env.GITHUB_RUN_ATTEMPT ?? '0', 10); this.pullRequestNumber = this.getPullRequestNumber(); this.debugger = (0, core_1.isDebug)(); + this.runnerName = process.env.RUNNER_NAME ?? ''; /** * Construct the id to use for storing tmpdirs. The action name will * be appended with a number if there are multiple runs within a job. @@ -1980,7 +1982,7 @@ async function getCurrentStepName() { logger_1.logger.debug('Retrieving step name for current step...'); const response = await octokit_1.octokit.rest.actions.listJobsForWorkflowRunAttempt(params); logger_1.logger.debug(JSON.stringify(response.data)); - const jobs = response.data.jobs.filter(j => j.name.replace(/[\s|_]+/g, '-') === config_1.githubConfig.job); + const jobs = response.data.jobs.filter(j => j.status === 'in_progress' && j.runner_name === config_1.githubConfig.runnerName); if (jobs.length === 1) { const steps = jobs[0].steps?.filter(s => s.status === 'in_progress'); if (steps?.length === 1) { @@ -1990,7 +1992,7 @@ async function getCurrentStepName() { } catch (error) { const message = (0, response_1.handleOctokitError)(error); - logger_1.logger.notice(`Posting the review failed: ${message}`); + logger_1.logger.notice(`Retrieving the step name failed: ${message}`); } return stepname; } diff --git a/src/action/decorate/summary.ts b/src/action/decorate/summary.ts index a8b52d5d..7083f22e 100644 --- a/src/action/decorate/summary.ts +++ b/src/action/decorate/summary.ts @@ -119,7 +119,7 @@ function setSummaryHeader(status: Status) { async function setSummaryFooter() { summary.addEOL(); - summary.addRaw(generateItalic(`Step: ${await getCurrentStepName()}`), true); + summary.addRaw(generateItalic(`Workflow: ${githubConfig.workflow}, Job: ${githubConfig.job}, Step: ${await getCurrentStepName()}`), true); summary.addRaw(generateComment(githubConfig.getCommentIdentifier())); } diff --git a/src/configuration/github.ts b/src/configuration/github.ts index 873abc4a..f303e858 100644 --- a/src/configuration/github.ts +++ b/src/configuration/github.ts @@ -17,6 +17,7 @@ export class GithubConfig { readonly runAttempt: number; readonly pullRequestNumber: number | undefined; readonly debugger: boolean; + readonly runnerName: string; readonly id: string; constructor() { @@ -33,6 +34,7 @@ export class GithubConfig { this.runAttempt = parseInt(process.env.GITHUB_RUN_ATTEMPT ?? '0', 10); this.pullRequestNumber = this.getPullRequestNumber(); this.debugger = isDebug(); + this.runnerName = process.env.RUNNER_NAME ?? ''; /** * Construct the id to use for storing tmpdirs. The action name will diff --git a/src/github/runs.ts b/src/github/runs.ts index 11b4a14e..0a850ef6 100644 --- a/src/github/runs.ts +++ b/src/github/runs.ts @@ -21,7 +21,7 @@ export async function getCurrentStepName(): Promise { logger.debug('Retrieving step name for current step...'); const response = await octokit.rest.actions.listJobsForWorkflowRunAttempt(params); logger.debug(JSON.stringify(response.data)); - const jobs = response.data.jobs.filter(j => j.name.replace(/[\s|_]+/g, '-') === githubConfig.job); + const jobs = response.data.jobs.filter(j => j.status === 'in_progress' && j.runner_name === githubConfig.runnerName); if (jobs.length === 1) { const steps = jobs[0].steps?.filter(s => s.status === 'in_progress'); @@ -31,7 +31,7 @@ export async function getCurrentStepName(): Promise { } } catch (error: unknown) { const message = handleOctokitError(error); - logger.notice(`Posting the review failed: ${message}`); + logger.notice(`Retrieving the step name failed: ${message}`); } return stepname; } diff --git a/test/.setup/mock.ts b/test/.setup/mock.ts index 911ae437..b6e99d0f 100644 --- a/test/.setup/mock.ts +++ b/test/.setup/mock.ts @@ -16,6 +16,7 @@ export const githubConfigMock: { workflow: string; runNumber: number; runAttempt: number; + runnerName: string; getCommentIdentifier(): string; } = { apiUrl: 'github.com/api/v1/', @@ -31,6 +32,7 @@ export const githubConfigMock: { workflow: 'tics-client', runNumber: 1, runAttempt: 2, + runnerName: 'Github Actions 1', getCommentIdentifier(): string { return [this.workflow, this.job, this.runNumber, this.runAttempt].join('_'); } diff --git a/test/unit/action/decorate/summary.test.ts b/test/unit/action/decorate/summary.test.ts index a06f1a73..5a7e0bb3 100644 --- a/test/unit/action/decorate/summary.test.ts +++ b/test/unit/action/decorate/summary.test.ts @@ -129,7 +129,7 @@ describe('createErrorSummary', () => { expect(body).toContainTimes('

The following warnings have occurred during analysis:

', 1); expect(body).toContainTimes(':x: Error', 0); expect(body).toContainTimes(':warning: Warning', 2); - expect(body).toContainTimes('\nStep: tics-github-action', 1); + expect(body).toContainTimes('\nWorkflow: tics-client, Job: TICS, Step: tics-github-action', 1); expect(body).toContain('\n'); }); @@ -143,7 +143,7 @@ describe('createErrorSummary', () => { expect(body).toContainTimes('

The following warnings have occurred during analysis:

', 1); expect(body).toContainTimes(':x: Error', 1); expect(body).toContainTimes(':warning: Warning', 2); - expect(body).toContainTimes('\nStep: tics-github-action', 1); + expect(body).toContainTimes('\nWorkflow: tics-client, Job: TICS, Step: tics-github-action', 1); expect(body).toContainTimes('\n', 1); }); }); @@ -152,7 +152,7 @@ describe('createNothingAnalyzedSummaryBody', () => { test('Should return summary with the message given', async () => { const body = await createNothingAnalyzedSummaryBody('message'); expect(body).toEqual( - '

TICS Quality Gate

\n

:heavy_check_mark: Passed

\nmessage\nStep: tics-github-action\n' + '

TICS Quality Gate

\n

:heavy_check_mark: Passed

\nmessage\nWorkflow: tics-client, Job: TICS, Step: tics-github-action\n' ); }); }); diff --git a/test/unit/github/runs.test.ts b/test/unit/github/runs.test.ts index bf63d636..e8e72631 100644 --- a/test/unit/github/runs.test.ts +++ b/test/unit/github/runs.test.ts @@ -14,7 +14,8 @@ describe('postReview', () => { data: { jobs: [ { - name: 'TICS', + runner_name: 'Github Actions 1', + status: 'in_progress', steps: [ { name: 'Step 1', @@ -27,7 +28,8 @@ describe('postReview', () => { ] }, { - name: 'TICS2', + name: 'Github Actions 2', + status: 'completed', steps: [ { name: 'Step 1', @@ -53,7 +55,8 @@ describe('postReview', () => { data: { jobs: [ { - name: 'TICS', + runner_name: 'Github Actions 1', + status: 'in_progress', steps: [ { name: 'Step 1', @@ -66,7 +69,8 @@ describe('postReview', () => { ] }, { - name: 'TICS2', + name: 'Github Actions 1', + status: 'completed', steps: [ { name: 'Step 1', From d7c1bfb63cf9f7fa28fcbcad37a8198a71c1db17 Mon Sep 17 00:00:00 2001 From: janssen <118828444+janssen-tiobe@users.noreply.github.com> Date: Mon, 9 Dec 2024 13:08:02 +0100 Subject: [PATCH 10/11] #34049: Formatting the step name differently --- dist/index.js | 16 +++++++++------- src/action/decorate/summary.ts | 4 ++-- src/github/runs.ts | 12 +++++++----- test/unit/action/decorate/summary.test.ts | 6 +++--- test/unit/github/runs.test.ts | 22 +++++++++++++--------- 5 files changed, 34 insertions(+), 26 deletions(-) diff --git a/dist/index.js b/dist/index.js index 525a3a5b..75c1c290 100644 --- a/dist/index.js +++ b/dist/index.js @@ -296,7 +296,7 @@ function setSummaryHeader(status) { } async function setSummaryFooter() { core_1.summary.addEOL(); - core_1.summary.addRaw((0, markdown_1.generateItalic)(`Workflow: ${config_1.githubConfig.workflow}, Job: ${config_1.githubConfig.job}, Step: ${await (0, runs_1.getCurrentStepName)()}`), true); + core_1.summary.addRaw((0, markdown_1.generateItalic)(await (0, runs_1.getCurrentStepPath)()), true); core_1.summary.addRaw((0, markdown_1.generateComment)(config_1.githubConfig.getCommentIdentifier())); } function getConditionHeading(failedOrWarnConditions) { @@ -1960,7 +1960,7 @@ async function postReview(body, event) { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.getCurrentStepName = getCurrentStepName; +exports.getCurrentStepPath = getCurrentStepPath; const logger_1 = __nccwpck_require__(26440); const response_1 = __nccwpck_require__(81934); const config_1 = __nccwpck_require__(86444); @@ -1970,23 +1970,25 @@ const octokit_1 = __nccwpck_require__(10775); * @param body Body containing the summary of the review * @param event Either approve or request changes in the review. */ -async function getCurrentStepName() { +async function getCurrentStepPath() { const params = { owner: config_1.githubConfig.owner, repo: config_1.githubConfig.reponame, run_id: config_1.githubConfig.runId, attempt_number: config_1.githubConfig.runAttempt }; - let stepname = config_1.githubConfig.action; + const stepname = [config_1.githubConfig.workflow, config_1.githubConfig.job, config_1.githubConfig.action]; try { logger_1.logger.debug('Retrieving step name for current step...'); const response = await octokit_1.octokit.rest.actions.listJobsForWorkflowRunAttempt(params); logger_1.logger.debug(JSON.stringify(response.data)); const jobs = response.data.jobs.filter(j => j.status === 'in_progress' && j.runner_name === config_1.githubConfig.runnerName); if (jobs.length === 1) { - const steps = jobs[0].steps?.filter(s => s.status === 'in_progress'); + const job = jobs[0]; + stepname[1] = job.name; + const steps = job.steps?.filter(s => s.status === 'in_progress'); if (steps?.length === 1) { - stepname = steps[0].name; + stepname[2] = steps[0].name; } } } @@ -1994,7 +1996,7 @@ async function getCurrentStepName() { const message = (0, response_1.handleOctokitError)(error); logger_1.logger.notice(`Retrieving the step name failed: ${message}`); } - return stepname; + return stepname.join(' / '); } diff --git a/src/action/decorate/summary.ts b/src/action/decorate/summary.ts index 7083f22e..f360f315 100644 --- a/src/action/decorate/summary.ts +++ b/src/action/decorate/summary.ts @@ -18,7 +18,7 @@ import { } from '../../helper/interfaces'; import { generateComment, generateExpandableAreaMarkdown, generateItalic, generateStatusMarkdown } from './markdown'; import { githubConfig, ticsConfig } from '../../configuration/config'; -import { getCurrentStepName } from '../../github/runs'; +import { getCurrentStepPath } from '../../github/runs'; const capitalize = (s: string): string => s && String(s[0]).toUpperCase() + String(s).slice(1); @@ -119,7 +119,7 @@ function setSummaryHeader(status: Status) { async function setSummaryFooter() { summary.addEOL(); - summary.addRaw(generateItalic(`Workflow: ${githubConfig.workflow}, Job: ${githubConfig.job}, Step: ${await getCurrentStepName()}`), true); + summary.addRaw(generateItalic(await getCurrentStepPath()), true); summary.addRaw(generateComment(githubConfig.getCommentIdentifier())); } diff --git a/src/github/runs.ts b/src/github/runs.ts index 0a850ef6..619b97b4 100644 --- a/src/github/runs.ts +++ b/src/github/runs.ts @@ -8,7 +8,7 @@ import { octokit } from './octokit'; * @param body Body containing the summary of the review * @param event Either approve or request changes in the review. */ -export async function getCurrentStepName(): Promise { +export async function getCurrentStepPath(): Promise { const params = { owner: githubConfig.owner, repo: githubConfig.reponame, @@ -16,7 +16,7 @@ export async function getCurrentStepName(): Promise { attempt_number: githubConfig.runAttempt }; - let stepname = githubConfig.action; + const stepname = [githubConfig.workflow, githubConfig.job, githubConfig.action]; try { logger.debug('Retrieving step name for current step...'); const response = await octokit.rest.actions.listJobsForWorkflowRunAttempt(params); @@ -24,14 +24,16 @@ export async function getCurrentStepName(): Promise { const jobs = response.data.jobs.filter(j => j.status === 'in_progress' && j.runner_name === githubConfig.runnerName); if (jobs.length === 1) { - const steps = jobs[0].steps?.filter(s => s.status === 'in_progress'); + const job = jobs[0]; + stepname[1] = job.name; + const steps = job.steps?.filter(s => s.status === 'in_progress'); if (steps?.length === 1) { - stepname = steps[0].name; + stepname[2] = steps[0].name; } } } catch (error: unknown) { const message = handleOctokitError(error); logger.notice(`Retrieving the step name failed: ${message}`); } - return stepname; + return stepname.join(' / '); } diff --git a/test/unit/action/decorate/summary.test.ts b/test/unit/action/decorate/summary.test.ts index 5a7e0bb3..2f9bb9de 100644 --- a/test/unit/action/decorate/summary.test.ts +++ b/test/unit/action/decorate/summary.test.ts @@ -129,7 +129,7 @@ describe('createErrorSummary', () => { expect(body).toContainTimes('

The following warnings have occurred during analysis:

', 1); expect(body).toContainTimes(':x: Error', 0); expect(body).toContainTimes(':warning: Warning', 2); - expect(body).toContainTimes('\nWorkflow: tics-client, Job: TICS, Step: tics-github-action', 1); + expect(body).toContainTimes('\ntics-client / TICS / tics-github-action', 1); expect(body).toContain('\n'); }); @@ -143,7 +143,7 @@ describe('createErrorSummary', () => { expect(body).toContainTimes('

The following warnings have occurred during analysis:

', 1); expect(body).toContainTimes(':x: Error', 1); expect(body).toContainTimes(':warning: Warning', 2); - expect(body).toContainTimes('\nWorkflow: tics-client, Job: TICS, Step: tics-github-action', 1); + expect(body).toContainTimes('\ntics-client / TICS / tics-github-action', 1); expect(body).toContainTimes('\n', 1); }); }); @@ -152,7 +152,7 @@ describe('createNothingAnalyzedSummaryBody', () => { test('Should return summary with the message given', async () => { const body = await createNothingAnalyzedSummaryBody('message'); expect(body).toEqual( - '

TICS Quality Gate

\n

:heavy_check_mark: Passed

\nmessage\nWorkflow: tics-client, Job: TICS, Step: tics-github-action\n' + '

TICS Quality Gate

\n

:heavy_check_mark: Passed

\nmessage\ntics-client / TICS / tics-github-action\n' ); }); }); diff --git a/test/unit/github/runs.test.ts b/test/unit/github/runs.test.ts index e8e72631..8bbe7cdc 100644 --- a/test/unit/github/runs.test.ts +++ b/test/unit/github/runs.test.ts @@ -1,4 +1,4 @@ -import { getCurrentStepName } from '../../../src/github/runs'; +import { getCurrentStepPath } from '../../../src/github/runs'; import { octokit } from '../../../src/github/octokit'; import { githubConfigMock } from '../../.setup/mock'; @@ -14,6 +14,7 @@ describe('postReview', () => { data: { jobs: [ { + name: 'TICS Client', runner_name: 'Github Actions 1', status: 'in_progress', steps: [ @@ -28,7 +29,8 @@ describe('postReview', () => { ] }, { - name: 'Github Actions 2', + name: 'TICS Client', + runner_name: 'Github Actions 2', status: 'completed', steps: [ { @@ -45,9 +47,9 @@ describe('postReview', () => { } }); - const name = await getCurrentStepName(); + const name = await getCurrentStepPath(); - expect(name).toStrictEqual('Step 2'); + expect(name).toStrictEqual('tics-client / TICS Client / Step 2'); }); test('Should not return name when multiple steps are in progress', async () => { @@ -55,6 +57,7 @@ describe('postReview', () => { data: { jobs: [ { + name: 'TICS', runner_name: 'Github Actions 1', status: 'in_progress', steps: [ @@ -69,7 +72,8 @@ describe('postReview', () => { ] }, { - name: 'Github Actions 1', + name: 'TICS', + runner_name: 'Github Actions 1', status: 'completed', steps: [ { @@ -86,16 +90,16 @@ describe('postReview', () => { } }); - const name = await getCurrentStepName(); + const name = await getCurrentStepPath(); - expect(name).toStrictEqual(githubConfigMock.action); + expect(name).toStrictEqual('tics-client / TICS / tics-github-action'); }); test('Should post a notice on createReview', async () => { listJobsSpy.mockRejectedValue(new Error()); - const name = await getCurrentStepName(); + const name = await getCurrentStepPath(); - expect(name).toStrictEqual(githubConfigMock.action); + expect(name).toStrictEqual('tics-client / TICS / tics-github-action'); }); }); From 6f2512dccd9375c0e94fe9c51c39e01fbb9f8593 Mon Sep 17 00:00:00 2001 From: janssen <118828444+janssen-tiobe@users.noreply.github.com> Date: Mon, 9 Dec 2024 13:36:34 +0100 Subject: [PATCH 11/11] #34049: Added title to italic tag --- dist/index.js | 7 ++++--- src/action/decorate/markdown.ts | 4 ++-- src/action/decorate/summary.ts | 3 ++- test/unit/action/decorate/summary.test.ts | 6 +++--- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/dist/index.js b/dist/index.js index 75c1c290..fb2f2b16 100644 --- a/dist/index.js +++ b/dist/index.js @@ -107,8 +107,8 @@ function generateExpandableAreaMarkdown(header, body) { * Generates italic text for markdown. * @param text The text to make italic. */ -function generateItalic(text) { - return `${text}`; +function generateItalic(text, title) { + return `${text}`; } /** * Generates a hidden comment for markdown. @@ -296,7 +296,8 @@ function setSummaryHeader(status) { } async function setSummaryFooter() { core_1.summary.addEOL(); - core_1.summary.addRaw((0, markdown_1.generateItalic)(await (0, runs_1.getCurrentStepPath)()), true); + core_1.summary.addRaw('

'); + core_1.summary.addRaw((0, markdown_1.generateItalic)(await (0, runs_1.getCurrentStepPath)(), 'Workflow / Job / Step'), true); core_1.summary.addRaw((0, markdown_1.generateComment)(config_1.githubConfig.getCommentIdentifier())); } function getConditionHeading(failedOrWarnConditions) { diff --git a/src/action/decorate/markdown.ts b/src/action/decorate/markdown.ts index 195da8a6..68c8a9e8 100644 --- a/src/action/decorate/markdown.ts +++ b/src/action/decorate/markdown.ts @@ -35,8 +35,8 @@ export function generateExpandableAreaMarkdown(header: string, body: string): st * Generates italic text for markdown. * @param text The text to make italic. */ -export function generateItalic(text: string): string { - return `${text}`; +export function generateItalic(text: string, title?: string): string { + return `${text}`; } /** diff --git a/src/action/decorate/summary.ts b/src/action/decorate/summary.ts index f360f315..b99d69b4 100644 --- a/src/action/decorate/summary.ts +++ b/src/action/decorate/summary.ts @@ -119,7 +119,8 @@ function setSummaryHeader(status: Status) { async function setSummaryFooter() { summary.addEOL(); - summary.addRaw(generateItalic(await getCurrentStepPath()), true); + summary.addRaw('

'); + summary.addRaw(generateItalic(await getCurrentStepPath(), 'Workflow / Job / Step'), true); summary.addRaw(generateComment(githubConfig.getCommentIdentifier())); } diff --git a/test/unit/action/decorate/summary.test.ts b/test/unit/action/decorate/summary.test.ts index 2f9bb9de..db0ae2f7 100644 --- a/test/unit/action/decorate/summary.test.ts +++ b/test/unit/action/decorate/summary.test.ts @@ -129,7 +129,7 @@ describe('createErrorSummary', () => { expect(body).toContainTimes('

The following warnings have occurred during analysis:

', 1); expect(body).toContainTimes(':x: Error', 0); expect(body).toContainTimes(':warning: Warning', 2); - expect(body).toContainTimes('\ntics-client / TICS / tics-github-action', 1); + expect(body).toContainTimes('\n

tics-client / TICS / tics-github-action', 1); expect(body).toContain('\n'); }); @@ -143,7 +143,7 @@ describe('createErrorSummary', () => { expect(body).toContainTimes('

The following warnings have occurred during analysis:

', 1); expect(body).toContainTimes(':x: Error', 1); expect(body).toContainTimes(':warning: Warning', 2); - expect(body).toContainTimes('\ntics-client / TICS / tics-github-action', 1); + expect(body).toContainTimes('\n

tics-client / TICS / tics-github-action', 1); expect(body).toContainTimes('\n', 1); }); }); @@ -152,7 +152,7 @@ describe('createNothingAnalyzedSummaryBody', () => { test('Should return summary with the message given', async () => { const body = await createNothingAnalyzedSummaryBody('message'); expect(body).toEqual( - '

TICS Quality Gate

\n

:heavy_check_mark: Passed

\nmessage\ntics-client / TICS / tics-github-action\n' + '

TICS Quality Gate

\n

:heavy_check_mark: Passed

\nmessage\n

tics-client / TICS / tics-github-action\n' ); }); });