Skip to content

Commit

Permalink
Merge pull request #154 from tiobe/32831-multi_project
Browse files Browse the repository at this point in the history
Support multi project analysis
  • Loading branch information
janssen-tiobe authored Sep 19, 2023
2 parents a32ea38 + eec21c3 commit 3e65256
Show file tree
Hide file tree
Showing 20 changed files with 8,023 additions and 7,567 deletions.
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

0 comments on commit 3e65256

Please sign in to comment.