Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multi project analysis #154

Merged
merged 11 commits into from
Sep 19, 2023
2 changes: 2 additions & 0 deletions .githooks/pre-commit/run
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/sh
npm run test
14,773 changes: 7,380 additions & 7,393 deletions dist/index.js

Large diffs are not rendered by default.

15 changes: 12 additions & 3 deletions src/github/annotations.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { logger } from '../helper/logger';
import { githubConfig, octokit } from '../configuration';
import { ReviewComment } from './interfaces';
import { ReviewComments } from '../helper/interfaces';
import { AnalysisResults, TicsReviewComment } from '../helper/interfaces';

/**
* Gets a list of all reviews posted on the pull request.
Expand Down Expand Up @@ -29,9 +29,18 @@ export async function getPostedReviewComments(): Promise<ReviewComment[]> {
* Deletes the review comments of previous runs.
* @param postedReviewComments Previously posted review comments.
*/
export function postAnnotations(reviewComments: ReviewComments): void {
export function postAnnotations(analysisResult: AnalysisResults): void {
logger.header('Posting annotations.');
reviewComments.postable.forEach(reviewComment => {

let postableReviewComments: TicsReviewComment[] = [];

analysisResult.projectResults.forEach(projectResult => {
if (projectResult.reviewComments) {
postableReviewComments.push(...projectResult.reviewComments.postable);
}
});

postableReviewComments.forEach(reviewComment => {
logger.warning(reviewComment.body, {
file: reviewComment.path,
startLine: reviewComment.line,
Expand Down
2 changes: 1 addition & 1 deletion src/github/commits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export async function getChangedFilesOfCommit(): Promise<ChangedFile[]> {
if (response.data.files) {
return response.data.files
.filter(item => {
// If a file is moved or renamed the status is 'renamed'.
if (item.status === 'renamed') {
// If a files has been moved without changes or if moved files are excluded, exclude them.
if (ticsConfig.excludeMovedFiles || item.changes === 0) {
Expand All @@ -29,7 +30,6 @@ export async function getChangedFilesOfCommit(): Promise<ChangedFile[]> {
return true;
})
.map(item => {
// If a file is moved or renamed the status is 'renamed'.
item.filename = normalize(item.filename);
logger.debug(item.filename);
return item;
Expand Down
23 changes: 19 additions & 4 deletions src/helper/interfaces.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,22 @@ export interface Analysis {
statusCode: number;
errorList: string[];
warningList: string[];
explorerUrl?: string;
explorerUrls: string[];
}

export interface AnalysisResults {
passed: boolean;
message: string;
missesQualityGate: boolean;
projectResults: ProjectResult[];
}

export interface ProjectResult {
project: string;
explorerUrl: string;
qualityGate?: QualityGate;
analyzedFiles: string[];
reviewComments?: TicsReviewComments;
}

export interface QualityGate {
Expand Down Expand Up @@ -60,15 +75,15 @@ export interface AnnotationApiLink {
url: string;
}

export interface ReviewComment {
export interface TicsReviewComment {
title: string;
body: string;
path?: string;
line: number;
}

export interface ReviewComments {
postable: ReviewComment[];
export interface TicsReviewComments {
postable: TicsReviewComment[];
unpostable: ExtendedAnnotation[];
}

Expand Down
59 changes: 33 additions & 26 deletions src/helper/summary.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,46 @@
import { summary } from '@actions/core';
import { SummaryTableRow } from '@actions/core/lib/summary';
import { generateExpandableAreaMarkdown, generateStatusMarkdown } from './markdown';
import { Analysis, Annotation, Condition, ExtendedAnnotation, Gate, QualityGate, ReviewComment, ReviewComments } from './interfaces';
import { AnalysisResults, Annotation, Condition, ExtendedAnnotation, Gate, TicsReviewComment, TicsReviewComments } from './interfaces';
import { ChangedFile } from '../github/interfaces';
import { githubConfig, viewerUrl } from '../configuration';
import { Status } from './enums';
import { range } from 'underscore';
import { logger } from './logger';

export function createSummaryBody(analysis: Analysis, filesAnalyzed: string[], qualityGate: QualityGate, reviewComments?: ReviewComments): string {
const failedConditions = extractFailedConditions(qualityGate.gates);

export function createSummaryBody(analysisResults: AnalysisResults): string {
logger.header('Creating summary.');
summary.addHeading('TICS Quality Gate');
summary.addHeading(`${generateStatusMarkdown(qualityGate.passed ? Status.PASSED : Status.FAILED, true)}`, 3);
summary.addHeading(`${failedConditions.length} Condition(s) failed`, 2);
failedConditions.forEach(condition => {
if (condition.details && condition.details.items.length > 0) {
summary.addRaw(`\n<details><summary>:x: ${condition.message}</summary>\n`);
summary.addBreak();
summary.addTable(createConditionTable(condition));
summary.addRaw('</details>', true);
} else {
summary.addRaw(`\n&nbsp;&nbsp;&nbsp;:x: ${condition.message}`, true);
}
});
summary.addEOL();
summary.addHeading(`${generateStatusMarkdown(analysisResults.passed ? Status.PASSED : Status.FAILED, true)}`, 3);

if (analysis.explorerUrl) summary.addLink('See the results in the TICS Viewer', analysis.explorerUrl);
analysisResults.projectResults.forEach(projectResult => {
if (projectResult.qualityGate) {
const failedConditions = extractFailedConditions(projectResult.qualityGate.gates);

if (reviewComments && reviewComments.unpostable.length > 0) {
summary.addRaw(createUnpostableAnnotationsDetails(reviewComments.unpostable));
}
summary.addHeading(projectResult.project, 2);
summary.addHeading(`${failedConditions.length} Condition(s) failed`, 3);
failedConditions.forEach(condition => {
if (condition.details && condition.details.items.length > 0) {
summary.addRaw(`\n<details><summary>:x: ${condition.message}</summary>\n`);
summary.addBreak();
summary.addTable(createConditionTable(condition));
summary.addRaw('</details>', true);
} else {
summary.addRaw(`\n&nbsp;&nbsp;&nbsp;:x: ${condition.message}`, true);
}
});
summary.addEOL();

summary.addLink('See the results in the TICS Viewer', projectResult.explorerUrl);

if (projectResult.reviewComments) {
summary.addRaw(createUnpostableAnnotationsDetails(projectResult.reviewComments.unpostable));
}

summary.addRaw(createFilesSummary(projectResult.analyzedFiles));
}
});

summary.addRaw(createFilesSummary(filesAnalyzed));
logger.info('Created summary.');

return summary.stringify();
Expand Down Expand Up @@ -76,7 +83,7 @@ export function createErrorSummary(errorList: string[], warningList: string[]):
* @returns Dropdown with all the files analyzed.
*/
export function createFilesSummary(fileList: string[]): string {
let header = 'The following files have been checked:';
let header = 'The following files have been checked for this project';
let body = '<ul>';
fileList.sort();
fileList.forEach(file => {
Expand Down Expand Up @@ -121,14 +128,14 @@ function createConditionTable(condition: Condition): SummaryTableRow[] {
* @param changedFiles List of files changed in the pull request.
* @returns List of the review comments.
*/
export function createReviewComments(annotations: ExtendedAnnotation[], changedFiles: ChangedFile[]): ReviewComments {
export function createReviewComments(annotations: ExtendedAnnotation[], changedFiles: ChangedFile[]): TicsReviewComments {
logger.info('Creating review comments from annotations.');

const sortedAnnotations = sortAnnotations(annotations);
const groupedAnnotations = groupAnnotations(sortedAnnotations, changedFiles);

let unpostable: ExtendedAnnotation[] = [];
let postable: ReviewComment[] = [];
let postable: TicsReviewComment[] = [];

groupedAnnotations.forEach(annotation => {
const displayCount = annotation.count === 1 ? '' : `(${annotation.count}x) `;
Expand Down Expand Up @@ -242,7 +249,7 @@ function findAnnotationInList(list: ExtendedAnnotation[], annotation: ExtendedAn
* @returns Summary of all the review comments that could not be posted.
*/
export function createUnpostableAnnotationsDetails(unpostableReviewComments: ExtendedAnnotation[]): string {
let label = 'Quality gate failures that cannot be annotated in <b>Files Changed</b>:';
let label = 'Quality gate failures that cannot be annotated in <b>Files Changed</b>';
let body = '';
let previousPath = '';

Expand Down
26 changes: 10 additions & 16 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { changedFilesToFile, getChangedFilesOfPullRequest } from './github/pulls
import { logger } from './helper/logger';
import { runTicsAnalyzer } from './tics/analyzer';
import { cliSummary } from './tics/api_helper';
import { getAnalyzedFiles, getAnnotations, getQualityGate, getViewerVersion } from './tics/fetcher';
import { getAnalysisResults, getViewerVersion } from './tics/fetcher';
import { postNothingAnalyzedReview, postReview } from './github/review';
import { createSummaryBody, createReviewComments } from './helper/summary';
import { createSummaryBody } from './helper/summary';
import { getPostedReviewComments, postAnnotations, deletePreviousReviewComments } from './github/annotations';
import { Events } from './helper/enums';
import { satisfies } from 'compare-versions';
import { exportVariable, summary } from '@actions/core';
import { Analysis, ReviewComments } from './helper/interfaces';
import { Analysis } from './helper/interfaces';
import { uploadArtifact } from './github/artifacts';
import { getChangedFilesOfCommit } from './github/commits';

Expand Down Expand Up @@ -67,7 +67,7 @@ async function run() {
if (!changedFilesFilePath) return logger.error('No filepath for changedfiles list.');
analysis = await runTicsAnalyzer(changedFilesFilePath);

if (!analysis.explorerUrl) {
if (analysis.explorerUrls.length === 0) {
deletePreviousComments(await getPostedComments());
if (!analysis.completed) {
await postErrorComment(analysis);
Expand All @@ -83,10 +83,9 @@ async function run() {
return;
}

const analyzedFiles = await getAnalyzedFiles(analysis.explorerUrl);
const qualityGate = await getQualityGate(analysis.explorerUrl);
const analysisResults = await getAnalysisResults(analysis.explorerUrls, changedFiles);

if (!qualityGate) return logger.exit('Quality gate could not be retrieved');
if (analysisResults.missesQualityGate) return logger.exit('Some quality gates could not be retrieved');

// If not run on a pull request no review comments have to be deleted
if (githubConfig.eventName === 'pull_request') {
Expand All @@ -96,26 +95,21 @@ async function run() {
}
}

let reviewComments: ReviewComments | undefined;
if (ticsConfig.postAnnotations) {
const annotations = await getAnnotations(qualityGate.annotationsApiV1Links);
if (annotations && annotations.length > 0) {
reviewComments = createReviewComments(annotations, changedFiles);
postAnnotations(reviewComments);
}
postAnnotations(analysisResults);
}

let reviewBody = createSummaryBody(analysis, analyzedFiles, qualityGate, reviewComments);
let reviewBody = createSummaryBody(analysisResults);

// If not run on a pull request no comments have to be deleted
// and there is no conversation to post to.
if (githubConfig.eventName === 'pull_request') {
deletePreviousComments(await getPostedComments());

await postToConversation(true, reviewBody, qualityGate.passed ? Events.APPROVE : Events.REQUEST_CHANGES);
await postToConversation(true, reviewBody, analysisResults.passed ? Events.APPROVE : Events.REQUEST_CHANGES);
}

if (!qualityGate.passed) logger.setFailed(qualityGate.message);
if (!analysisResults.passed) logger.setFailed(analysisResults.message);
}

if (ticsConfig.tmpDir || githubConfig.debugger) {
Expand Down
10 changes: 6 additions & 4 deletions src/tics/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getTmpDir } from '../github/artifacts';

let errorList: string[] = [];
let warningList: string[] = [];
let explorerUrl: string | undefined;
let explorerUrls: string[] = [];
let statusCode: number;
let completed: boolean;

Expand Down Expand Up @@ -50,7 +50,7 @@ export async function runTicsAnalyzer(fileListPath: string): Promise<Analysis> {
return {
completed: completed,
statusCode: statusCode,
explorerUrl: explorerUrl,
explorerUrls: explorerUrls,
errorList: errorList,
warningList: warningList
};
Expand Down Expand Up @@ -106,9 +106,11 @@ function findInStdOutOrErr(data: string): void {
if (warning && !warningList.find(w => w === warning?.toString())) warningList.push(warning.toString());

const findExplorerUrl = data.match(/\/Explorer.*/g);
if (!explorerUrl && findExplorerUrl) {
if (findExplorerUrl) {
const urlPath = findExplorerUrl.slice(-1).pop();
if (urlPath) explorerUrl = viewerUrl + urlPath;
if (urlPath) {
explorerUrls.push(viewerUrl + urlPath);
}
}
}

Expand Down
Loading