Skip to content

Commit

Permalink
#33012: Complete overhaul of http-client and install-tics
Browse files Browse the repository at this point in the history
Now using `@tiobe/http-client` and `@tiobe/install-tics` to retrieve data from the viewer and to get the install TICS url from the viewer. Rewrote the testing for it and fixed the proxy setup, which now passes the local tests as well as the unit tests. To do this I had to revert some libraries to get it to work.
  • Loading branch information
janssen-tiobe committed Oct 25, 2023
1 parent bff2eb4 commit 92be5bb
Show file tree
Hide file tree
Showing 17 changed files with 52,687 additions and 11,637 deletions.
2 changes: 2 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@tiobe:registry=https://artifacts.tiobe.com/repository/npm/
registry=https://registry.npmjs.org
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ The following inputs are available for this action:
| `postAnnotations` | Show the latest TICS annotations directly in the GitHub Pull Request review. | false |
| `postToConversation` | Post the summary to the conversation page of the pull request. Options are `true` (default) or `false`. | false |
| `pullRequestApproval` | Set the plugin to approve or deny a pull request, by default this is false. Options are `true` or `false`. Note that once a run that added a reviewer has been completed, this reviewer cannot be deleted from that pull request. (Always the case on versions between 2.0.0 and 2.5.0). | false |
| `retryCodes` | Status codes to retry api calls for. The default codes will be overwritten if this option is set. | false |
| `secretsFilter` | Comma-seperated list of extra secrets to mask in the console output. | false |
| `ticsAuthToken` | Authentication token to authorize the plugin when it connects to the TICS Viewer. | false |
| `tmpDir` | Location to store debug information. | false |
Expand Down
3 changes: 3 additions & 0 deletions action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ inputs:
description: Set the plugin to approve or deny a pull request, by default this is false. Options are `true` or `false`. Note that once a run that added a reviewer has been completed, this reviewer cannot be deleted from that pull request. (Always the case on versions between 2.0.0 and 2.5.0).
required: false
default: false
retryCodes:
description: Status codes to retry api calls for. The default codes will be overwritten if this option is set.
required: false
secretsFilter:
description: Comma-seperated list of extra secrets to mask in the console output.
required: false
Expand Down
62,925 changes: 51,806 additions & 11,119 deletions dist/index.js

Large diffs are not rendered by default.

766 changes: 667 additions & 99 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,18 @@
"@actions/artifact": "^1.1.2",
"@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0",
"@actions/http-client": "^2.2.0",
"@octokit/plugin-retry": "^6.0.1",
"@actions/github": "^5.1.1",
"@octokit/plugin-retry": "^5.0.5",
"@octokit/request-error": "^5.0.1",
"@tiobe/http-client": "^0.3.0",
"@tiobe/install-tics": "^0.4.0",
"canonical-path": "^1.0.0",
"compare-versions": "^6.1.0",
"proxy-agent": "^6.3.1",
"semver": "^7.5.4",
"underscore": "^1.13.6"
},
"devDependencies": {
"@types/http-proxy": "^1.17.12",
"@types/jest": "^29.5.5",
"@types/node": "^20.8.6",
"@types/underscore": "^1.11.11",
Expand Down
61 changes: 29 additions & 32 deletions src/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { getBooleanInput, getInput, isDebug } from '@actions/core';
import { context } from '@actions/github';
import { Octokit } from '@octokit/core';
import { paginateRest } from '@octokit/plugin-paginate-rest';
import { restEndpointMethods } from '@octokit/plugin-rest-endpoint-methods';
import { context, getOctokit } from '@actions/github';
import { retry } from '@octokit/plugin-retry';
import { OctokitOptions } from '@octokit/core/dist-types/types';
import { HttpClient, HttpCodes } from '@actions/http-client';
import { getTicsWebBaseUrlFromUrl } from './tics/api_helper';
import { RequestInfo, RequestInit, fetch } from 'undici';
import HttpClient from '@tiobe/http-client';
import { ProxyAgent } from 'proxy-agent';
import { EOL } from 'os';
import { getBaseUrl } from '@tiobe/install-tics';

export const githubConfig = {
baseUrl: process.env.GITHUB_API_URL ? process.env.GITHUB_API_URL : 'https://api.github.com',
Expand All @@ -21,7 +18,6 @@ export const githubConfig = {
commitSha: process.env.GITHUB_SHA ? process.env.GITHUB_SHA : '',
eventName: context.eventName,
id: `${context.runId.toString()}-${process.env.GITHUB_RUN_ATTEMPT}`,
runnerOS: process.env.RUNNER_OS ? process.env.RUNNER_OS : '',
pullRequestNumber: getPullRequestNumber(),
debugger: isDebug()
};
Expand All @@ -46,6 +42,10 @@ function getSecretsFilter(secretsFilter: string | undefined) {
return combinedFilters;
}

function getRetryCodes(retryCodes?: string): number[] {
return retryCodes?.split(',').map(r => parseInt(r)) || [419, 500, 501, 502, 503, 504];
}

export const ticsConfig = {
githubToken: getInput('githubToken', { required: true }),
projectName: getInput('projectName', { required: true }),
Expand All @@ -67,46 +67,43 @@ export const ticsConfig = {
postToConversation: getBooleanInput('postToConversation'),
pullRequestApproval: getBooleanInput('pullRequestApproval'),
recalc: getInput('recalc'),
retryCodes: getRetryCodes(getInput('retryCodes')),
ticsAuthToken: getInput('ticsAuthToken'),
tmpDir: getInput('tmpDir'),
trustStrategy: getInput('trustStrategy'),
secretsFilter: getSecretsFilter(getInput('secretsFilter')),
viewerUrl: getInput('viewerUrl')
};

export const retryConfig = {
const retryConfig = {
maxRetries: 10,
retryCodes: [HttpCodes.BadGateway, HttpCodes.ServiceUnavailable, HttpCodes.GatewayTimeout]
retryCodes: ticsConfig.retryCodes,
delay: 5
};

const ignoreSslError: boolean =
ticsConfig.hostnameVerification === '0' ||
ticsConfig.hostnameVerification === 'false' ||
ticsConfig.trustStrategy === 'self-signed' ||
ticsConfig.trustStrategy === 'all';

export const httpClient = new HttpClient('tics-github-action', undefined, {
allowRetries: true,
maxRetries: retryConfig.maxRetries,
ignoreSslError: ignoreSslError
});
export const httpClient = new HttpClient(
true,
{
authToken: ticsConfig.ticsAuthToken,
xRequestWithTics: true,
retry: {
retries: retryConfig.maxRetries,
retryDelay: retryConfig.delay * 1000,
retryOn: retryConfig.retryCodes
}
},
new ProxyAgent()
);

const octokitOptions: OctokitOptions = {
auth: ticsConfig.githubToken,
baseUrl: githubConfig.baseUrl,
request: {
agent: httpClient.getAgent(githubConfig.baseUrl),
fetch: async (url: RequestInfo, opts: RequestInit | undefined) => {
return fetch(url, {
...opts,
dispatcher: httpClient.getAgentDispatcher(githubConfig.baseUrl)
});
},
agent: new ProxyAgent(),
retries: retryConfig.maxRetries,
retryAfter: 5
retryAfter: retryConfig.delay
}
};

export const octokit = new (Octokit.plugin(paginateRest, restEndpointMethods, retry))(octokitOptions);
export const baseUrl = getTicsWebBaseUrlFromUrl(ticsConfig.ticsConfiguration);
export const octokit = getOctokit(ticsConfig.githubToken, octokitOptions, retry);
export const baseUrl = getBaseUrl(ticsConfig.ticsConfiguration);
export const viewerUrl = ticsConfig.viewerUrl ? ticsConfig.viewerUrl.replace(/\/+$/, '') : baseUrl;
4 changes: 0 additions & 4 deletions src/helper/interfaces.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,6 @@ export interface VersionResponse {
dbversion?: string;
}

export interface ArtifactsResponse {
links: Links;
}

export interface Links {
setPropPath: string;
queryArtifact: string;
Expand Down
68 changes: 15 additions & 53 deletions src/tics/analyzer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { exec } from '@actions/exec';
import { baseUrl, githubConfig, ticsConfig, viewerUrl } from '../configuration';
import { githubConfig, httpClient, ticsConfig, viewerUrl } from '../configuration';
import { logger } from '../helper/logger';
import { getInstallTicsApiUrl, httpRequest } from './api_helper';
import { Analysis, ArtifactsResponse } from '../helper/interfaces';
import { Analysis } from '../helper/interfaces';
import { getTmpDir } from '../github/artifacts';
import { InstallTics } from '@tiobe/install-tics';
import { platform } from 'os';

let errorList: string[] = [];
let warningList: string[] = [];
Expand Down Expand Up @@ -62,36 +63,21 @@ export async function runTicsAnalyzer(fileListPath: string): Promise<Analysis> {
* @returns Command to run.
*/
async function buildRunCommand(fileListPath: string): Promise<string> {
if (githubConfig.runnerOS === 'Linux') {
return `/bin/bash -c "${await getInstallTics()} ${getTicsCommand(fileListPath)}"`;
}
return `powershell "${await getInstallTics()}; if ($?) {${getTicsCommand(fileListPath)}}"`;
}

/**
* Get the command to install TICS with.
*/
async function getInstallTics(): Promise<string> {
if (!ticsConfig.installTics) return '';

const installTicsUrl = await retrieveInstallTics(githubConfig.runnerOS.toLowerCase());
const installTics = new InstallTics(true, httpClient);

if (!installTicsUrl) return '';
let installCommand = '';
if (ticsConfig.installTics) {
installCommand = await installTics.getInstallCommand(ticsConfig.ticsConfiguration);

if (githubConfig.runnerOS === 'Linux') {
let trustStrategy = '';
if (ticsConfig.trustStrategy === 'self-signed' || ticsConfig.trustStrategy === 'all') {
trustStrategy = '--insecure';
if (platform() === 'linux') {
installCommand += ' &&';
}
return `source <(curl --silent ${trustStrategy} '${installTicsUrl}') &&`;
} else {
// runnerOS is assumed to be Windows here
let trustStrategy = '';
if (ticsConfig.trustStrategy === 'self-signed' || ticsConfig.trustStrategy === 'all') {
trustStrategy = '[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true};';
}
return `Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; ${trustStrategy} iex ((New-Object System.Net.WebClient).DownloadString('${installTicsUrl}'))`;
}

if (platform() === 'linux') {
return `/bin/bash -c "${installCommand} ${getTicsCommand(fileListPath)}"`;
}
return `powershell "${installCommand}; if ($?) {${getTicsCommand(fileListPath)}}"`;
}

/**
Expand All @@ -114,30 +100,6 @@ function findInStdOutOrErr(data: string): void {
}
}

/**
* Retrieves the the TICS install url from the ticsConfiguration.
* @param os the OS the runner runs on.
* @returns the TICS install url.
*/
async function retrieveInstallTics(os: string): Promise<string | undefined> {
try {
logger.info('Trying to retrieve configuration information from TICS.');

const ticsInstallApiBaseUrl = getInstallTicsApiUrl(baseUrl, os);

const data = await httpRequest<ArtifactsResponse>(ticsInstallApiBaseUrl);

if (data?.links.installTics) {
return baseUrl + '/' + data.links.installTics;
}
} catch (error: unknown) {
let message = 'reason unknown';
if (error instanceof Error) message = error.message;
logger.exit(`An error occurred when trying to retrieve configuration information: ${message}`);
}
return;
}

/**
* Builds the TICS calculate command based on the fileListPath and the ticsConfig set.
* @param fileListPath
Expand Down
93 changes: 2 additions & 91 deletions src/tics/api_helper.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,6 @@
import { OutgoingHttpHeaders } from 'http';
import { logger } from '../helper/logger';
import { githubConfig, httpClient, retryConfig, ticsConfig, viewerUrl } from '../configuration';
import { Analysis, HttpResponse } from '../helper/interfaces';
import { HttpClientResponse } from '@actions/http-client';

/**
* Executes a GET request to the given url.
* @param url api url to perform a GET request for.
* @returns Promise of the data retrieved from the response.
*/
export async function httpRequest<T>(url: string): Promise<T | undefined> {
var headers: OutgoingHttpHeaders = {
XRequestedWith: 'tics'
};
if (ticsConfig.ticsAuthToken) {
headers.Authorization = `Basic ${ticsConfig.ticsAuthToken}`;
}

const response: HttpClientResponse = await httpClient.get(url, headers);

let errorMessage = '';
if (response.message.statusCode && retryConfig.retryCodes.includes(response.message.statusCode)) {
errorMessage += `Retried ${retryConfig.maxRetries} time(s), but got: `;
}
errorMessage += `HTTP request failed with status ${response.message.statusCode}.`;

switch (response.message.statusCode) {
case 200:
const text = await response.readBody();
try {
return <T>JSON.parse(text);
} catch (error: unknown) {
logger.exit(`${error}. Tried to parse response: ${text}`);
}
break;
case 302:
logger.exit(`${errorMessage} Please check if the given ticsConfiguration is correct (possibly http instead of https).`);
break;
case 400:
logger.exit(`${errorMessage} ${(<HttpResponse>JSON.parse(await response.readBody())).alertMessages[0].header}`);
break;
case 401:
logger.exit(
`${errorMessage} Please provide a valid TICSAUTHTOKEN in your configuration. Check ${viewerUrl}/Administration.html#page=authToken`
);
break;
case 403:
logger.exit(`${errorMessage} Forbidden call: ${url}`);
break;
case 404:
logger.exit(`${errorMessage} Please check if the given ticsConfiguration is correct.`);
break;
default:
logger.exit(`${errorMessage} ${response.message.statusMessage}`);
break;
}
return;
}
import { githubConfig, ticsConfig } from '../configuration';
import { Analysis } from '../helper/interfaces';

/**
* Creates a cli summary of all errors and bugs based on the logLevel.
Expand All @@ -69,39 +13,6 @@ export function cliSummary(analysis: Analysis): void {
}
}

/**
* Creates the TICS install data from the TICS Viewer.
* @param url url given in the ticsConfiguration.
* @param os the OS the runner runs on.
* @returns the TICS install url.
*/
export function getInstallTicsApiUrl(url: string, os: string): string {
const installTicsApi = new URL(ticsConfig.ticsConfiguration);
installTicsApi.searchParams.append('platform', os);
installTicsApi.searchParams.append('url', url);

return installTicsApi.href;
}

/**
* Returns the TIOBE web base url.
* @param url url given in the ticsConfiguration.
* @returns TIOBE web base url.
*/
export function getTicsWebBaseUrlFromUrl(url: string): string {
const cfgMarker = 'cfg?name=';
const apiMarker = '/api/';
let baseUrl = '';

if (url.includes(apiMarker + cfgMarker)) {
baseUrl = url.split(apiMarker)[0];
} else {
logger.exit('Missing configuration api in the TICS Viewer URL. Please check your workflow configuration.');
}

return baseUrl;
}

/**
* Gets query value form a url
* @param url The TICS Explorer url (e.g. <ticsUrl>/Explorer.html#axes=Project%28c-demo%29%2CBranch%28main%)
Expand Down
Loading

0 comments on commit 92be5bb

Please sign in to comment.