Skip to content

Commit

Permalink
Merge pull request #413 from tiobe/34049-multiple_checks
Browse files Browse the repository at this point in the history
Allow multiple GitHub TICS Quality Gate checks in the same Pull Request (R34049)
  • Loading branch information
janssen-tiobe authored Dec 11, 2024
2 parents 62f07f5 + 6f2512d commit a28af3e
Show file tree
Hide file tree
Showing 20 changed files with 2,693 additions and 2,206 deletions.
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

0 comments on commit a28af3e

Please sign in to comment.