Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4,357 changes: 2,238 additions & 2,119 deletions dist/index.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/action/decorate/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { Analysis, AnalysisResult } from '../../helper/interfaces';
export async function decorateAction(analysisResult: AnalysisResult | undefined, analysis: Analysis): Promise<void> {
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) {
Expand Down
16 changes: 16 additions & 0 deletions src/action/decorate/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,19 @@ export function generateStatusMarkdown(status: Status, hasSuffix = false): strin
export function generateExpandableAreaMarkdown(header: string, body: string): string {
return `<details><summary>${header}</summary>${EOL}${body}</details>${EOL}${EOL}`;
}

/**
* Generates italic text for markdown.
* @param text The text to make italic.
*/
export function generateItalic(text: string, title?: string): string {
return `<i ${title ? `title="${title}"` : ''}>${text}</i>`;
}

/**
* Generates a hidden comment for markdown.
* @param comment The text of the comment.
*/
export function generateComment(comment: string): string {
return `<!--${comment}-->`;
}
35 changes: 24 additions & 11 deletions src/action/decorate/summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -17,15 +16,15 @@ 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 { getCurrentStepPath } from '../../github/runs';

const capitalize = (s: string): string => s && String(s[0]).toUpperCase() + String(s).slice(1);

export function createSummaryBody(analysisResult: AnalysisResult): string {
export async function createSummaryBody(analysisResult: AnalysisResult): Promise<string> {
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) {
Expand Down Expand Up @@ -56,6 +55,7 @@ export function createSummaryBody(analysisResult: AnalysisResult): string {
summary.addRaw(createFilesSummary(projectResult.analyzedFiles));
}
});
await setSummaryFooter();

logger.info('Created summary.');

Expand All @@ -68,11 +68,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<string> {
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);
Expand All @@ -90,6 +89,7 @@ export function createErrorSummaryBody(errorList: string[], warningList: string[
summary.addRaw(`:warning: ${warning}${EOL}${EOL}`);
}
}
await setSummaryFooter();

logger.info('Created summary.');
return summary.stringify();
Expand All @@ -100,18 +100,30 @@ 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<string> {
logger.header('Creating summary.');

summary.addHeading('TICS Quality Gate');
summary.addHeading(generateStatusMarkdown(Status.PASSED, true), 3);
setSummaryHeader(Status.PASSED);

summary.addRaw(message);
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('<h2></h2>');
summary.addRaw(generateItalic(await getCurrentStepPath(), 'Workflow / Job / Step'), 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;
Expand Down Expand Up @@ -378,6 +390,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 <b>Files Changed</b>';
let body = '';
Expand Down
6 changes: 3 additions & 3 deletions src/analysis/client/process-analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ export async function processIncompleteAnalysis(analysis: Analysis): Promise<str
let summaryBody: string;
if (!analysis.completed) {
failedMessage = 'Failed to complete TICS analysis.';
summaryBody = createErrorSummaryBody(analysis.errorList, analysis.warningList);
summaryBody = await createErrorSummaryBody(analysis.errorList, analysis.warningList);
} else if (analysis.warningList.find(w => 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) {
Expand Down
4 changes: 2 additions & 2 deletions src/analysis/qserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ export async function qServerAnalysis(): Promise<Verdict> {
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);
}
Expand Down
25 changes: 19 additions & 6 deletions src/configuration/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,30 @@ export class GithubConfig {
readonly event: GithubEvent;
readonly job: string;
readonly action: string;
readonly id: string;
readonly workflow: string;
readonly runId: number;
readonly runNumber: number;
readonly runAttempt: number;
readonly pullRequestNumber: number | undefined;
readonly debugger: boolean;
readonly runnerName: string;
readonly id: string;

constructor() {
this.apiUrl = context.apiUrl;
this.owner = context.repo.owner;
this.reponame = context.repo.repo;
this.commitSha = context.sha;
this.event = this.getGithubEvent();
this.job = context.job;
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();
this.debugger = isDebug();
this.runnerName = process.env.RUNNER_NAME ?? '';

/**
* Construct the id to use for storing tmpdirs. The action name will
Expand All @@ -34,10 +46,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 = `${this.runId.toString()}_${this.runAttempt.toString()}_${this.job}_${this.action}`;

this.removeWarningListener();
}
Expand Down Expand Up @@ -71,6 +80,10 @@ export class GithubConfig {
}
}

getCommentIdentifier(): string {
return [this.workflow, this.job, this.runNumber, this.runAttempt].join('_');
}

removeWarningListener(): void {
process.removeAllListeners('warning');
process.on('warning', warning => {
Expand Down
55 changes: 47 additions & 8 deletions src/github/comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export async function postComment(body: string): Promise<void> {
export async function deletePreviousComments(comments: Comment[]): Promise<void> {
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,
Expand All @@ -76,16 +76,55 @@ export async function deletePreviousComments(comments: Comment[]): Promise<void>
logger.info('Deleted review comments of previous runs.');
}

function commentIncludesTicsTitle(body?: string): boolean {
const titles = ['<h1>TICS Quality Gate</h1>', '## TICS Quality Gate', '## TICS Analysis'];

function shouldCommentBeDeleted(body?: string): boolean {
if (!body) return false;

const titles = ['<h1>TICS Quality Gate</h1>', '## 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 isWorkflowAndJobInAnotherRun(body);
}

return false;
}

function isWorkflowAndJobInAnotherRun(body: string): boolean {
const regex = /<!--([^\s]+)-->/g;

let identifier = '';
// Get the last match of the <i> 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 includesTitle;
// Only replace if the run number or run attempt are different.
return parseInt(split[2], 10) !== githubConfig.runNumber || parseInt(split[3], 10) !== githubConfig.runAttempt;
}
39 changes: 39 additions & 0 deletions src/github/runs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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 getCurrentStepPath(): Promise<string> {
const params = {
owner: githubConfig.owner,
repo: githubConfig.reponame,
run_id: githubConfig.runId,
attempt_number: githubConfig.runAttempt
};

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);
logger.debug(JSON.stringify(response.data));
const jobs = response.data.jobs.filter(j => j.status === 'in_progress' && j.runner_name === githubConfig.runnerName);

if (jobs.length === 1) {
const job = jobs[0];
stepname[1] = job.name;
const steps = job.steps?.filter(s => s.status === 'in_progress');
if (steps?.length === 1) {
stepname[2] = steps[0].name;
}
}
} catch (error: unknown) {
const message = handleOctokitError(error);
logger.notice(`Retrieving the step name failed: ${message}`);
}
return stepname.join(' / ');
}
19 changes: 18 additions & 1 deletion test/.setup/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ export const githubConfigMock: {
id: string;
pullRequestNumber: number | undefined;
debugger: boolean;
workflow: string;
runNumber: number;
runAttempt: number;
runnerName: string;
getCommentIdentifier(): string;
} = {
apiUrl: 'github.com/api/v1/',
owner: 'tester',
Expand All @@ -23,7 +28,14 @@ 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,
runnerName: 'Github Actions 1',
getCommentIdentifier(): string {
return [this.workflow, this.job, this.runNumber, this.runAttempt].join('_');
}
};

export const ticsConfigMock = {
Expand Down Expand Up @@ -102,6 +114,9 @@ jest.mock('../../src/github/octokit', () => {
},
repos: {
getCommit: jest.fn()
},
actions: {
listJobsForWorkflowRunAttempt: jest.fn()
}
},
graphql: jest.fn()
Expand All @@ -121,6 +136,7 @@ export const contextMock: {
job: string;
runId: number;
runNumber: number;
workflow: string;
payload: {
pull_request:
| {
Expand All @@ -140,6 +156,7 @@ export const contextMock: {
job: 'TICS',
runId: 123,
runNumber: 1,
workflow: 'tics_client',
payload: {
pull_request: {
number: 1
Expand Down
Loading
Loading