diff --git a/package.json b/package.json index 88111af..f93e46c 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "typescript": "^4.1.2" }, "dependencies": { + "@codecoach/api-client": "^1.0.45", "@octokit/core": "^3.2.4", "@octokit/rest": "^18.12.0", "js-yaml": "^4.1.0", diff --git a/sample/config/data-config.yaml b/sample/config/data-config.yaml new file mode 100644 index 0000000..ecb9d16 --- /dev/null +++ b/sample/config/data-config.yaml @@ -0,0 +1,11 @@ +repo: + url: https://github.com/codeleague/codecoach + runId: 40 + headCommit: 123qwe123qwe123qwe123qwe123qwe123qwe123q + branch: main +buildLogFiles: + - type: tslint + path: /path/to/log.json + cwd: /repo/src +output: ./output.json +apiServer: https://localhost:3000 diff --git a/src/Api.ts b/src/Api.ts new file mode 100644 index 0000000..59093ad --- /dev/null +++ b/src/Api.ts @@ -0,0 +1,16 @@ +import axios from 'axios'; + +import { RunApi } from '@codecoach/api-client'; + +export class Api { + private readonly axiosInstance; + constructor(private readonly apiServer: string) { + this.axiosInstance = axios.create({ + baseURL: this.apiServer, + }); + } + + get runClient(): RunApi { + return new RunApi(undefined, '', this.axiosInstance); + } +} diff --git a/src/Config/@enums/command.ts b/src/Config/@enums/command.ts new file mode 100644 index 0000000..af5450e --- /dev/null +++ b/src/Config/@enums/command.ts @@ -0,0 +1,4 @@ +export enum COMMAND { + DEFAULT = 'default', + COLLECT = 'collect', +} diff --git a/src/Config/@enums/index.ts b/src/Config/@enums/index.ts index 4f98e6c..899e093 100644 --- a/src/Config/@enums/index.ts +++ b/src/Config/@enums/index.ts @@ -1 +1,2 @@ export { ProjectType } from './projectType'; +export { COMMAND } from './command'; diff --git a/src/Config/@types/appConfig.ts b/src/Config/@types/appConfig.ts index 2f8878b..d9dfc55 100644 --- a/src/Config/@types/appConfig.ts +++ b/src/Config/@types/appConfig.ts @@ -1,6 +1,9 @@ +import { COMMAND } from '../@enums'; import { BuildLogFile } from './buildLogFile'; export type AppConfig = { + command: COMMAND; logFilePath: string; buildLogFiles: BuildLogFile[]; + apiServer: string; }; diff --git a/src/Config/@types/configArgument.ts b/src/Config/@types/configArgument.ts index ab7ffb3..d715a9c 100644 --- a/src/Config/@types/configArgument.ts +++ b/src/Config/@types/configArgument.ts @@ -1,11 +1,24 @@ import { BuildLogFile } from './buildLogFile'; -export type ConfigArgument = { +type BaseConfigArgument = { url: string; - pr: number; buildLogFile: BuildLogFile[]; output: string; - token: string; - removeOldComment: boolean; config: string; }; + +export type PrConfigArgument = { + pr: number; + removeOldComment: boolean; + token: string; + apiServer: never; +} & BaseConfigArgument; + +export type DataConfigArgument = { + headCommit: string; + runId: number; + branch: string; + apiServer: string; +} & BaseConfigArgument; + +export type ConfigArgument = PrConfigArgument | DataConfigArgument; diff --git a/src/Config/@types/configYAML.ts b/src/Config/@types/configYAML.ts index ca5d438..79b263d 100644 --- a/src/Config/@types/configYAML.ts +++ b/src/Config/@types/configYAML.ts @@ -1,14 +1,31 @@ import { BuildLogFile } from './buildLogFile'; -export type ConfigYAML = { +type BaseConfigYAML = { repo: { url: string; - pr: number; - token: string; userAgent: string; timeZone: string; - removeOldComment: boolean; }; buildLogFiles: BuildLogFile[]; output: string; }; + +export type PrConfigYAML = { + repo: { + token: string; + pr: number; + removeOldComment: boolean; + }; + apiServer: never; +} & BaseConfigYAML; + +export type DataConfigYAML = { + repo: { + headCommit: string; + runId: number; + branch: string; + }; + apiServer: string; +} & BaseConfigYAML; + +export type ConfigYAML = PrConfigYAML | DataConfigYAML; diff --git a/src/Config/@types/index.ts b/src/Config/@types/index.ts index 1307efc..22c4350 100644 --- a/src/Config/@types/index.ts +++ b/src/Config/@types/index.ts @@ -1,5 +1,6 @@ export { AppConfig } from './appConfig'; export { ConfigObject } from './configObject'; -export { ProviderConfig } from './providerConfig'; -export { ConfigArgument } from './configArgument'; +export * from './providerConfig'; +export * from './configArgument'; +export * from './configYAML'; export { BuildLogFile } from './buildLogFile'; diff --git a/src/Config/@types/providerConfig.ts b/src/Config/@types/providerConfig.ts index bb3f034..45320f3 100644 --- a/src/Config/@types/providerConfig.ts +++ b/src/Config/@types/providerConfig.ts @@ -1,6 +1,17 @@ -export type ProviderConfig = { - token: string; +type BaseProviderConfig = { repoUrl: string; +}; + +export type PrProviderConfig = { + token: string; prId: number; removeOldComment: boolean; -}; +} & BaseProviderConfig; + +export type DataProviderConfig = { + runId: number; + headCommit: string; + branch: string; +} & BaseProviderConfig; + +export type ProviderConfig = PrProviderConfig | DataProviderConfig; diff --git a/src/Config/Config.spec.ts b/src/Config/Config.spec.ts index 548902d..8e9abce 100644 --- a/src/Config/Config.spec.ts +++ b/src/Config/Config.spec.ts @@ -1,6 +1,7 @@ +import { DataProviderConfig, PrProviderConfig } from '.'; import { Config } from './Config'; -const MOCK_ARGS = [ +const PR_MOCK_ARGS = [ '/usr/local/Cellar/node/15.13.0/bin/node', '/Users/codecoach/src/app.ts', '--url=https://github.com/codeleague/codecoach.git', @@ -11,13 +12,25 @@ const MOCK_ARGS = [ '-o=./tmp/out.json', ]; -const MOCK_ARGS_W_CONFIG_YAML = [ +const PR_MOCK_ARGS_W_COMMAND = [ + '/usr/local/Cellar/node/15.13.0/bin/node', + '/Users/codecoach/src/app.ts', + 'comment', + '--url=https://github.com/codeleague/codecoach.git', + '--removeOldComment', + '--token=placeyourtokenhere', + '--pr=15', + '-f=dotnetbuild;./sample/dotnetbuild/build.content;/repo/src', + '-o=./tmp/out.json', +]; + +const PR_MOCK_ARGS_W_CONFIG_YAML = [ '/usr/local/Cellar/node/15.13.0/bin/node', '/Users/codecoach/src/app.ts', '--config=sample/config/config.yaml', ]; -export const EXPECTED_MOCK_ARGS = [ +export const PR_EXPECTED_MOCK_ARGS = [ '/usr/local/Cellar/node/15.13.0/bin/node', '/Users/codecoach/src/app.ts', 'https://github.com/codeleague/codecoach.git', @@ -28,7 +41,39 @@ export const EXPECTED_MOCK_ARGS = [ './tmp/out.json', ]; -describe('Config Test', () => { +const DATA_MOCK_ARGS = [ + '/usr/local/Cellar/node/15.13.0/bin/node', + '/Users/codecoach/src/app.ts', + 'collect', + '--url=https://github.com/codeleague/codecoach.git', + '-r=3', + '-b=main', + '-c=headCommitsha', + '-f=dotnetbuild;./sample/dotnetbuild/build.content;/repo/src', + '-o=./tmp/out.json', + '--api=https://localhost:3000', +]; + +const DATA_MOCK_ARGS_W_CONFIG_YAML = [ + '/usr/local/Cellar/node/15.13.0/bin/node', + '/Users/codecoach/src/app.ts', + 'collect', + '--config=sample/config/data-config.yaml', +]; + +export const DATA_EXPECTED_MOCK_ARGS = [ + '/usr/local/Cellar/node/15.13.0/bin/node', + '/Users/codecoach/src/app.ts', + 'https://github.com/codeleague/codecoach.git', + 3, + 'main', + 'headCommitsha', + 'dotnetbuild;./sample/dotnetbuild/build.content;/repo/src', + './tmp/out.json', + 'https://localhost:3000', +]; + +describe('PR config Test', () => { let config: typeof Config; beforeEach(() => { @@ -36,15 +81,47 @@ describe('Config Test', () => { }); it('Should able to parse this args and run without throwing error', async () => { - process.argv = MOCK_ARGS; + process.argv = PR_MOCK_ARGS; + config = (await import('./Config')).Config; + let fullfillConfig = (await config).provider as PrProviderConfig; + expect(fullfillConfig.repoUrl).toBe(PR_EXPECTED_MOCK_ARGS[2]); + expect(fullfillConfig.removeOldComment).toBe(PR_EXPECTED_MOCK_ARGS[3]); + + process.argv = PR_MOCK_ARGS_W_COMMAND; + config = (await import('./Config')).Config; + fullfillConfig = (await config).provider as PrProviderConfig; + expect(fullfillConfig.repoUrl).toBe(PR_EXPECTED_MOCK_ARGS[2]); + expect(fullfillConfig.removeOldComment).toBe(PR_EXPECTED_MOCK_ARGS[3]); + }); + + it('Should able to use a config file without passing other args', async () => { + process.argv = PR_MOCK_ARGS_W_CONFIG_YAML; config = (await import('./Config')).Config; const fullfillConfig = await config; - expect(fullfillConfig.provider.repoUrl).toBe(EXPECTED_MOCK_ARGS[2]); - expect(fullfillConfig.provider.removeOldComment).toBe(EXPECTED_MOCK_ARGS[3]); + expect(fullfillConfig.app.buildLogFiles[0].type).toBe('tslint'); + }); +}); + +describe('Data config Test', () => { + let config: typeof Config; + + beforeEach(() => { + jest.resetModules(); + }); + + it('Should able to parse this args and run without throwing error', async () => { + process.argv = DATA_MOCK_ARGS; + config = (await import('./Config')).Config; + const providerConfig = (await config).provider as DataProviderConfig; + expect(providerConfig.repoUrl).toBe(DATA_EXPECTED_MOCK_ARGS[2]); + expect(providerConfig.runId).toBe(DATA_EXPECTED_MOCK_ARGS[3]); + expect(providerConfig.branch).toBe(DATA_EXPECTED_MOCK_ARGS[4]); + expect(providerConfig.headCommit).toBe(DATA_EXPECTED_MOCK_ARGS[5]); + expect((await config).app.apiServer).toBe(DATA_EXPECTED_MOCK_ARGS[8]); }); it('Should able to use a config file without passing other args', async () => { - process.argv = MOCK_ARGS_W_CONFIG_YAML; + process.argv = DATA_MOCK_ARGS_W_CONFIG_YAML; config = (await import('./Config')).Config; const fullfillConfig = await config; expect(fullfillConfig.app.buildLogFiles[0].type).toBe('tslint'); diff --git a/src/Config/Config.ts b/src/Config/Config.ts index d66d1b8..f9c0df3 100644 --- a/src/Config/Config.ts +++ b/src/Config/Config.ts @@ -1,11 +1,21 @@ -import yargs from 'yargs'; -import { ProjectType } from './@enums'; +import yargs, { Arguments } from 'yargs'; +import { ProjectType, COMMAND } from './@enums'; import { BuildLogFile, ConfigArgument, ConfigObject } from './@types'; import { buildAppConfig, buildProviderConfig } from './configBuilder'; import { DEFAULT_OUTPUT_FILE } from './constants/defaults'; -import { REQUIRED_ARGS } from './constants/required'; +import { DATA_REQUIRED_ARGS, PR_REQUIRED_ARGS } from './constants/required'; const projectTypes = Object.keys(ProjectType); +let command: COMMAND = COMMAND.DEFAULT; + +const validateRequiredArgs = (options: Arguments, requiredArgs: string[]) => { + const validRequiredArgs = requiredArgs.every( + (el) => options[el] != undefined || options[el] != null, + ); + if (!validRequiredArgs) + throw new Error(`please fill all required fields ${requiredArgs.join(', ')}`); + return true; +}; const args = yargs .option('config', { @@ -16,13 +26,11 @@ const args = yargs describe: 'GitHub repo url (https or ssh)', type: 'string', }) - .option('pr', { - describe: 'PR number', - type: 'number', - }) - .option('token', { - describe: 'GitHub token', + .option('output', { + alias: 'o', + describe: 'Output parsed log file', type: 'string', + default: DEFAULT_OUTPUT_FILE, }) .option('buildLogFile', { alias: 'f', @@ -42,45 +50,87 @@ and is build root directory (optional (Will use current context as cwd)). return { type, path, cwd: cwd ?? process.cwd() } as BuildLogFile; }); }) - .option('output', { - alias: 'o', - describe: 'Output parsed log file', - type: 'string', - default: DEFAULT_OUTPUT_FILE, - }) - .option('removeOldComment', { - type: 'boolean', - describe: 'Remove existing CodeCoach comments before putting new one', - default: false, - }) - .check((options) => { - // check required arguments - const useConfigArgs = options.config === undefined; - const validRequiredArgs = REQUIRED_ARGS.every( - (el) => options[el] != undefined || options[el] != null, - ); - if (useConfigArgs && !validRequiredArgs) - throw `please fill all required fields ${REQUIRED_ARGS.join(', ')}`; - return true; - }) + .command(['$0', 'comment'], 'Report Lint results on a pull request', (yarg) => + yarg + .option('pr', { + describe: 'PR number', + type: 'number', + }) + .option('removeOldComment', { + type: 'boolean', + describe: 'Remove existing CodeCoach comments before putting new one', + default: false, + }) + .option('token', { + describe: 'GitHub token', + type: 'string', + }) + .check((options) => { + // check arguments parsing + const useConfigArgs = options.config === undefined; + if (!useConfigArgs) return true; + + validateRequiredArgs(options, PR_REQUIRED_ARGS); + + if (!options.pr || Array.isArray(options.pr)) + throw new Error('--pr config should be a single number'); + return true; + }), + ) + .command( + 'collect', + 'Collect and store Lint results', + (yarg) => + yarg + .option('headCommit', { + alias: 'c', + describe: 'The latest commit sha', + type: 'string', + }) + .option('runId', { + alias: 'r', + describe: 'The latest run id', + type: 'number', + }) + .option('branch', { + alias: 'b', + describe: 'The branch that this command is run on', + type: 'string', + }) + .option('apiServer', { + alias: 'api', + describe: 'The url of api server, e.g., http://localhost:3000', + type: 'string', + }) + .check((options) => { + // check arguments parsing + const useConfigArgs = options.config === undefined; + if (!useConfigArgs) return true; + + validateRequiredArgs(options, DATA_REQUIRED_ARGS); + + if (!options.runId || Array.isArray(options.runId)) + throw new Error('--runId config should be a single number'); + return true; + }), + () => { + command = COMMAND.COLLECT; + }, + ) .check((options) => { // check arguments parsing const useConfigArgs = options.config === undefined; if (!useConfigArgs) return true; - - if (!options.pr || Array.isArray(options.pr)) - throw '--pr config should be a single number'; if (!options.buildLogFile || options.buildLogFile.some((file) => file === null)) - throw 'all of `--buildLogFile` options should have correct format'; + throw new Error('all of `--buildLogFile` options should have correct format'); return true; }) .help() - .wrap(120) - .parse(process.argv.slice(1)) as ConfigArgument; + .wrap(120).argv as ConfigArgument; export const Config: Promise = (async () => { return Object.freeze({ - app: await buildAppConfig(args), - provider: await buildProviderConfig(args), + app: await buildAppConfig(args, command), + provider: await buildProviderConfig(args, command), }); })(); diff --git a/src/Config/YML.spec.ts b/src/Config/YML.spec.ts index 7a55a06..1a9922e 100644 --- a/src/Config/YML.spec.ts +++ b/src/Config/YML.spec.ts @@ -1,10 +1,27 @@ +import { DataConfigYAML, PrConfigYAML } from '.'; +import { COMMAND } from './@enums'; import { YML } from './YML'; -describe('YML Test', () => { +describe('PR YML Test', () => { it('Should able to validate yaml file correctly', async () => { - const config = await YML.parse('sample/config/config.yaml'); + const config = (await YML.parse( + 'sample/config/config.yaml', + COMMAND.DEFAULT, + )) as PrConfigYAML; expect(config.output).toBe('./output.json'); expect(config.repo.pr).toBe(40); expect(config.repo.removeOldComment).toBe(false); }); }); + +describe('Data YML Test', () => { + it('Should able to validate yaml file correctly', async () => { + const config = (await YML.parse( + 'sample/config/data-config.yaml', + COMMAND.COLLECT, + )) as DataConfigYAML; + expect(config.output).toBe('./output.json'); + expect(config.repo.runId).toBe(40); + expect(config.repo.headCommit).toBe('123qwe123qwe123qwe123qwe123qwe123qwe123q'); + }); +}); diff --git a/src/Config/YML.ts b/src/Config/YML.ts index 1345d8e..c56cd3a 100644 --- a/src/Config/YML.ts +++ b/src/Config/YML.ts @@ -1,24 +1,70 @@ import { File } from '../File'; import yaml from 'js-yaml'; -import { REQUIRED_YAML_ARGS, REQUIRED_YAML_PROVIDER_ARGS } from './constants/required'; -import { ConfigYAML } from './@types/configYAML'; +import { + REQUIRED_YAML_ARGS, + PR_REQUIRED_YAML_PROVIDER_ARGS, + DATA_REQUIRED_YAML_PROVIDER_ARGS, + DATA_REQUIRED_YAML_ARGS, +} from './constants/required'; +import { ConfigYAML, DataConfigYAML, PrConfigYAML } from './@types/configYAML'; +import { COMMAND } from './@enums'; export class YML { - private static transform(config: ConfigYAML): ConfigYAML { - if (!config.repo.pr || !Number.isInteger(config.repo.pr)) - throw 'provider.pr is required or invalid number type'; - - // required types - const validRequiredArgs = REQUIRED_YAML_ARGS.every( + private static transform(config: ConfigYAML, command: COMMAND): ConfigYAML { + // require types + const requiredArgs = + command === COMMAND.COLLECT ? DATA_REQUIRED_YAML_ARGS : REQUIRED_YAML_ARGS; + const validRequiredArg = requiredArgs.every( (el) => config[el] != undefined || config[el] != null, ); - if (!validRequiredArgs) - throw `please fill all required fields ${REQUIRED_YAML_ARGS.join(', ')}`; - const validRequiredProviderArgs = REQUIRED_YAML_PROVIDER_ARGS.every( + if (!validRequiredArg) + throw new Error(`please fill all required fields ${requiredArgs.join(', ')}`); + + switch (command) { + case COMMAND.COLLECT: + return this.transformDataConfig(config as DataConfigYAML); + case COMMAND.DEFAULT: + return this.transformPrConfig(config as PrConfigYAML); + default: + throw new Error(`Command ${command} is invalid`); + } + } + + private static transformDataConfig(config: DataConfigYAML): DataConfigYAML { + if (!config.repo.runId || !Number.isInteger(config.repo.runId)) + throw new Error('provider.runId is required or invalid number type'); + + const validRequiredProviderArgs = DATA_REQUIRED_YAML_PROVIDER_ARGS.every( + (el) => config.repo[el] !== undefined || config.repo[el] != null, + ); + + if (!validRequiredProviderArgs) + throw new Error( + `please fill all required fields ${DATA_REQUIRED_YAML_PROVIDER_ARGS.join(', ')}`, + ); + + return { + ...config, + repo: { + ...config.repo, + runId: Number(config.repo.runId), + headCommit: config.repo.headCommit, + branch: config.repo.branch, + }, + }; + } + + private static transformPrConfig(config: PrConfigYAML): PrConfigYAML { + if (!config.repo.pr || !Number.isInteger(config.repo.pr)) + throw new Error('provider.pr is required or invalid number type'); + + const validRequiredProviderArgs = PR_REQUIRED_YAML_PROVIDER_ARGS.every( (el) => config.repo[el] != undefined || config.repo[el] != null, ); if (!validRequiredProviderArgs) - throw `please fill all required fields ${REQUIRED_YAML_PROVIDER_ARGS.join(', ')}`; + throw new Error( + `please fill all required fields ${PR_REQUIRED_YAML_PROVIDER_ARGS.join(', ')}`, + ); return { ...config, @@ -30,10 +76,10 @@ export class YML { }; } - static async parse(path: string): Promise { + static async parse(path: string, command: COMMAND): Promise { const file = await File.readFileHelper(path); const ymlFile = yaml.loadAll(file); - const transformed = this.transform(ymlFile[0]); + const transformed = this.transform(ymlFile[0], command); return transformed; } } diff --git a/src/Config/configBuilder.ts b/src/Config/configBuilder.ts index f686039..d6ceb03 100644 --- a/src/Config/configBuilder.ts +++ b/src/Config/configBuilder.ts @@ -1,28 +1,69 @@ -import { AppConfig, ConfigArgument, ProviderConfig } from './@types'; +import { COMMAND } from './@enums'; +import { + AppConfig, + ConfigArgument, + ProviderConfig, + DataConfigArgument, + PrConfigArgument, + DataConfigYAML, + PrConfigYAML, + PrProviderConfig, + DataProviderConfig, +} from './@types'; import { YML } from './YML'; -const buildYMLConfig = async (args: ConfigArgument) => { +const buildYMLConfig = async (args: ConfigArgument, command: COMMAND) => { if (!args.config) return; - return YML.parse(args.config); + return YML.parse(args.config, command); }; export const buildProviderConfig = async ( arg: ConfigArgument, + command: COMMAND, ): Promise => { - const configFile = await buildYMLConfig(arg); - - return { - token: configFile?.repo.token || arg.token, - repoUrl: configFile?.repo.url || arg.url, - prId: configFile?.repo.pr || arg.pr, - removeOldComment: configFile?.repo.removeOldComment || arg.removeOldComment, - }; + const configFile = await buildYMLConfig(arg, command); + switch (command) { + case COMMAND.COLLECT: + return buildDataProviderConfig( + arg as DataConfigArgument, + configFile as DataConfigYAML, + ); + case COMMAND.DEFAULT: + return buildPrProviderConfig(arg as PrConfigArgument, configFile as PrConfigYAML); + default: + throw new Error(`Command ${command} is invalid`); + } }; -export const buildAppConfig = async (arg: ConfigArgument): Promise => { - const configFile = await buildYMLConfig(arg); +const buildPrProviderConfig = ( + arg: PrConfigArgument, + configFile: PrConfigYAML, +): PrProviderConfig => ({ + token: configFile?.repo.token || arg.token, + repoUrl: configFile?.repo.url || arg.url, + prId: configFile?.repo.pr || arg.pr, + removeOldComment: configFile?.repo.removeOldComment || arg.removeOldComment, +}); + +const buildDataProviderConfig = ( + arg: DataConfigArgument, + configFile: DataConfigYAML, +): DataProviderConfig => ({ + repoUrl: configFile?.repo.url || arg.url, + runId: configFile?.repo.runId || arg.runId, + headCommit: configFile?.repo.headCommit || arg.headCommit, + branch: configFile?.repo.branch || arg.branch, +}); + +export const buildAppConfig = async ( + arg: ConfigArgument, + command: COMMAND, +): Promise => { + const configFile = await buildYMLConfig(arg, command); return { + command: command, logFilePath: configFile?.output || arg.output, buildLogFiles: configFile?.buildLogFiles || arg.buildLogFile, + apiServer: configFile?.apiServer || arg.apiServer, }; }; diff --git a/src/Config/constants/required.ts b/src/Config/constants/required.ts index 691a375..b9c4343 100644 --- a/src/Config/constants/required.ts +++ b/src/Config/constants/required.ts @@ -1,15 +1,42 @@ -import { ConfigArgument } from '..'; -import { ConfigYAML } from '../@types/configYAML'; +import { + PrConfigYAML, + DataConfigYAML, + ConfigYAML, + PrConfigArgument, + DataConfigArgument, +} from '../@types'; -type RequiredArgs = (keyof ConfigArgument)[]; -export const REQUIRED_ARGS: RequiredArgs = ['url', 'pr', 'buildLogFile', 'token']; +type PrRequiredArgs = (keyof PrConfigArgument)[]; +export const PR_REQUIRED_ARGS: PrRequiredArgs = ['pr', 'buildLogFile', 'token', 'url']; + +type DataRequiredArgs = (keyof DataConfigArgument)[]; +export const DATA_REQUIRED_ARGS: DataRequiredArgs = [ + 'url', + 'headCommit', + 'runId', + 'buildLogFile', + 'branch', + 'apiServer', +]; type RequiredYamlArgs = (keyof ConfigYAML)[]; export const REQUIRED_YAML_ARGS: RequiredYamlArgs = ['repo', 'buildLogFiles']; -type RequiredYamlProviderArgs = (keyof ConfigYAML['repo'])[]; -export const REQUIRED_YAML_PROVIDER_ARGS: RequiredYamlProviderArgs = [ +type PrRequiredYamlProviderArgs = (keyof PrConfigYAML['repo'])[]; +export const PR_REQUIRED_YAML_PROVIDER_ARGS: PrRequiredYamlProviderArgs = [ 'url', 'pr', 'token', ]; + +type DataRequiredYamlProviderArgs = (keyof DataConfigYAML['repo'])[]; +export const DATA_REQUIRED_YAML_PROVIDER_ARGS: DataRequiredYamlProviderArgs = [ + 'branch', + 'headCommit', + 'runId', +]; + +export const DATA_REQUIRED_YAML_ARGS: (keyof DataConfigYAML)[] = [ + ...REQUIRED_YAML_ARGS, + 'apiServer', +]; diff --git a/src/Parser/@types/log.type.ts b/src/Parser/@types/log.type.ts index fcf30c9..f37831b 100644 --- a/src/Parser/@types/log.type.ts +++ b/src/Parser/@types/log.type.ts @@ -1,4 +1,5 @@ import { LogSeverity } from '..'; +import { ProjectType } from '../../Config'; export type LogType = { log: string; @@ -8,4 +9,23 @@ export type LogType = { line?: number; lineOffset?: number; valid: boolean; + linter?: ProjectType; +}; + +export type Issue = { message: string; filePath: string; column?: number } & Omit< + LogType, + 'msg' | 'valid' | 'lineOffset' | 'log' | 'source' +>; + +export type Run = { + id: number; + timestamp: Date; + headCommit: { + sha: string; + }; + repository: { + url: string; + }; + branch: string; + issues: Issue[]; }; diff --git a/src/app.ts b/src/app.ts index 72ae00e..607cf5c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,16 @@ #!/usr/bin/env node -import { BuildLogFile, Config, ConfigObject, ProjectType } from './Config'; +import { IIssue } from '@codecoach/api-client'; +import { Api } from './Api'; +import { + BuildLogFile, + Config, + ConfigObject, + DataProviderConfig, + ProjectType, + PrProviderConfig, +} from './Config'; +import { COMMAND } from '../src/Config/@enums'; import { File } from './File'; import { Log } from './Logger'; import { @@ -15,20 +25,13 @@ import { } from './Parser'; import { DartLintParser } from './Parser/DartLintParser'; import { GitHub, GitHubPRService, VCS } from './Provider'; - class App { private vcs: VCS; private config: ConfigObject; async start(): Promise { this.config = await Config; - - const githubPRService = new GitHubPRService( - this.config.provider.token, - this.config.provider.repoUrl, - this.config.provider.prId, - ); - this.vcs = new GitHub(githubPRService, this.config.provider.removeOldComment); + const { repoUrl } = this.config.provider; const logs = await this.parseBuildData(this.config.app.buildLogFiles); Log.info('Build data parsing completed'); @@ -38,10 +41,60 @@ class App { .then(() => Log.info('Write output completed')) .catch((error) => Log.error('Write output failed', { error })); - await this.vcs.report(logs); - Log.info('Report to VCS completed'); + switch (this.config.app.command) { + case COMMAND.DEFAULT: + const { token, removeOldComment, prId } = this.config + .provider as PrProviderConfig; + const githubPRService = new GitHubPRService(token, repoUrl, prId); + this.vcs = new GitHub(githubPRService, removeOldComment); + + await this.vcs.report(logs); + Log.info('Report to VCS completed'); + break; + + case COMMAND.COLLECT: + const { headCommit, runId, branch } = this.config.provider as DataProviderConfig; + const issues = this.mapLogTypeToIssue(logs); + + await new Api(this.config.app.apiServer).runClient.createRun({ + githubRunId: runId, + timestamp: new Date().toISOString(), + issues: issues['true'], + invalidIssues: issues['false'], + branch, + headCommit: { + sha: headCommit, + }, + repository: { + url: repoUrl.replace(/.git$/i, ''), + }, + }); + Log.info('Data successfully sent'); + break; + + default: + Log.error(`Command: ${this.config.app.command} is invalid`); + break; + } } + private mapLogTypeToIssue = (list: LogType[]) => + list.reduce((previous: Record, currentItem: LogType) => { + const { msg, severity, source, valid, line, lineOffset, linter } = currentItem; + const group = valid.toString(); + const issue: IIssue = { + filepath: source, + message: msg, + severity: severity, + line: line as number, + column: lineOffset, + linter: linter ?? '', + }; + if (!previous[group]) previous[group] = []; + previous[group].push(issue); + return previous; + }, {} as Record); + private static getParser(type: ProjectType, cwd: string): Parser { switch (type) { case ProjectType.dotnetbuild: @@ -66,7 +119,7 @@ class App { Log.debug('Parsing', { type, path, cwd }); const content = await File.readFileHelper(path); const parser = App.getParser(type, cwd); - return parser.parse(content); + return parser.parse(content).map((log) => ({ ...log, linter: type })); }); return (await Promise.all(logsTasks)).flatMap((x) => x); diff --git a/yarn.lock b/yarn.lock index e324d1a..8bc01d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -271,6 +271,13 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@codecoach/api-client@^1.0.45": + version "1.0.45" + resolved "https://registry.yarnpkg.com/@codecoach/api-client/-/api-client-1.0.45.tgz#69688f085852662e81771a2b104f65cae147152f" + integrity sha512-u57Lgyv38bCs71WiQtF+CqOJxcHC/u0OhteCgdXDwQ5/bH9peKNqzVKlEVKuFcZ3RlTU8AU/EE2cB/yllA0P6w== + dependencies: + axios "^0.21.4" + "@dabh/diagnostics@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31" @@ -1107,6 +1114,13 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.1.tgz#e1e82e4f3e999e2cfd61b161280d16a111f86428" integrity sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA== +axios@^0.21.4: + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== + dependencies: + follow-redirects "^1.14.0" + babel-jest@^26.3.0: version "26.3.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.3.0.tgz#10d0ca4b529ca3e7d1417855ef7d7bd6fc0c3463" @@ -2180,6 +2194,11 @@ fn.name@1.x.x: resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== +follow-redirects@^1.14.0: + version "1.14.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.5.tgz#f09a5848981d3c772b5392309778523f8d85c381" + integrity sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA== + for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"