From 6904d4b5ef208d9d1d177296960ac74c8a92c6cc Mon Sep 17 00:00:00 2001 From: jeeyo Date: Tue, 27 Jun 2023 14:24:27 +0700 Subject: [PATCH 1/4] use js config file --- package.json | 3 +- sample/config/dryrun.js | 5 + sample/config/dryrun.json | 5 - sample/config/github.js | 8 ++ sample/config/github.json | 8 -- sample/config/gitlab.js | 10 ++ sample/config/gitlab.json | 10 -- src/Config/@types/configArgument.ts | 19 +--- src/Config/Config.spec.ts | 148 ++++++++++++------------- src/Config/Config.ts | 161 +++++++++++----------------- yarn.lock | 5 + 11 files changed, 172 insertions(+), 210 deletions(-) create mode 100644 sample/config/dryrun.js delete mode 100644 sample/config/dryrun.json create mode 100644 sample/config/github.js delete mode 100644 sample/config/github.json create mode 100644 sample/config/gitlab.js delete mode 100644 sample/config/gitlab.json diff --git a/package.json b/package.json index 213c550..dbf643e 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "slash": "^3.0.0", "winston": "^3.3.3", "xml-js": "^1.6.11", - "yargs": "^16.2.0" + "yargs": "^16.2.0", + "zod": "^3.21.4" } } diff --git a/sample/config/dryrun.js b/sample/config/dryrun.js new file mode 100644 index 0000000..403da05 --- /dev/null +++ b/sample/config/dryrun.js @@ -0,0 +1,5 @@ +module.exports = { + dryRun: true, + buildLogFile: ['dotnetbuild;./sample/dotnetbuild/build.content;/repo/src'], + output: './tmp/out.json', +}; diff --git a/sample/config/dryrun.json b/sample/config/dryrun.json deleted file mode 100644 index afdce38..0000000 --- a/sample/config/dryrun.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dryRun": true, - "buildLogFile": "dotnetbuild;./sample/dotnetbuild/build.content;/repo/src", - "output": "./tmp/out.json" -} \ No newline at end of file diff --git a/sample/config/github.js b/sample/config/github.js new file mode 100644 index 0000000..b2a9c7b --- /dev/null +++ b/sample/config/github.js @@ -0,0 +1,8 @@ +module.exports = { + vcs: 'github', + githubRepoUrl: 'https://github.com/codeleague/codecoach.git', + githubPr: 42, + githubToken: 'mockGitHubToken', + buildLogFile: ['dotnetbuild;./sample/dotnetbuild/build.content;/repo/src'], + output: './tmp/out.json', +}; diff --git a/sample/config/github.json b/sample/config/github.json deleted file mode 100644 index 95bc233..0000000 --- a/sample/config/github.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "vcs": "github", - "githubRepoUrl": "https://github.com/codeleague/codecoach.git", - "githubPr": 42, - "githubToken": "mockGitHubToken", - "buildLogFile": "dotnetbuild;./sample/dotnetbuild/build.content;/repo/src", - "output": "./tmp/out.json" -} \ No newline at end of file diff --git a/sample/config/gitlab.js b/sample/config/gitlab.js new file mode 100644 index 0000000..e3e62a8 --- /dev/null +++ b/sample/config/gitlab.js @@ -0,0 +1,10 @@ +module.exports = { + vcs: 'gitlab', + gitlabHost: 'https://gitlab.myawesomecompany.com', + gitlabProjectId: 1234, + gitlabMrIid: 69, + gitlabToken: 'mockGitLabToken', + buildLogFile: ['dotnetbuild;./sample/dotnetbuild/build.content;/repo/src'], + output: './tmp/out.json', + removeOldComment: true, +}; diff --git a/sample/config/gitlab.json b/sample/config/gitlab.json deleted file mode 100644 index 7da16e1..0000000 --- a/sample/config/gitlab.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "vcs": "gitlab", - "gitlabHost": "https://gitlab.myawesomecompany.com", - "gitlabProjectId": 1234, - "gitlabMrIid": 69, - "gitlabToken": "mockGitLabToken", - "buildLogFile": "dotnetbuild;./sample/dotnetbuild/build.content;/repo/src", - "output": "./tmp/out.json", - "removeOldComment": true -} \ No newline at end of file diff --git a/src/Config/@types/configArgument.ts b/src/Config/@types/configArgument.ts index 6ce16fc..ff2f4f6 100644 --- a/src/Config/@types/configArgument.ts +++ b/src/Config/@types/configArgument.ts @@ -1,17 +1,4 @@ -import { BuildLogFile } from './buildLogFile'; +import { z } from 'zod'; +import { configSchema } from '../Config'; -export type ConfigArgument = { - vcs: 'gitlab' | 'github'; - githubRepoUrl: string; - githubPr: number; - githubToken: string; - gitlabHost: string; - gitlabProjectId: number; - gitlabMrIid: number; - gitlabToken: string; - buildLogFile: BuildLogFile[]; - output: string; // =logFilePath - removeOldComment: boolean; - failOnWarnings: boolean; - dryRun: boolean; -}; +export type ConfigArgument = z.infer; diff --git a/src/Config/Config.spec.ts b/src/Config/Config.spec.ts index 548559f..7eb2de6 100644 --- a/src/Config/Config.spec.ts +++ b/src/Config/Config.spec.ts @@ -1,4 +1,4 @@ -import { BuildLogFile } from './@types'; +import { BuildLogFile, ConfigArgument } from './@types'; const mockGitHubRepo = 'https://github.com/codeleague/codecoach.git'; const mockGitHubPr = 42; @@ -12,47 +12,47 @@ const mockGitLabToken = 'mockGitLabToken'; const mockLogType = 'dotnetbuild'; const mockLogFile = './sample/dotnetbuild/build.content'; const mockLogCwd = '/repo/src'; -const mockBuildLogFile = `${mockLogType};${mockLogFile};${mockLogCwd}`; -const mockOutput = './tmp/out.json'; - -const GITHUB_ENV_ARGS = [ - 'node', - 'app.ts', - '--vcs="github"', - `--githubRepoUrl=${mockGitHubRepo}`, - `--githubPr=${mockGitHubPr}`, - `--githubToken=${mockGitHubToken}`, - '--removeOldComment', - `-f=${mockBuildLogFile}`, - `-o=${mockOutput}`, -]; - -const GITHUB_FILE_ARGS = ['node', 'app.ts', '--config=sample/config/github.json']; - -const GITLAB_ENV_ARGS = [ - 'node', - 'app.ts', - '--vcs="gitlab"', - `--gitlabHost=${mockGitLabHost}`, - `--gitlabProjectId=${mockGitLabProjectId}`, - `--gitlabMrIid=${mockGitLabMrIid}`, - `--gitlabToken=${mockGitLabToken}`, - `-f=${mockBuildLogFile}`, - `-o=${mockOutput}`, - '--failOnWarnings', -]; - -const GITLAB_FILE_ARGS = ['node', 'app.ts', '--config=sample/config/gitlab.json']; - -const DRYRUN_ENV_ARGS = [ - 'node', - 'app.ts', - `-f=${mockBuildLogFile}`, - `-o=${mockOutput}`, - '--dryRun', -]; - -const DRYRUN_FILE_ARGS = ['node', 'app.ts', '--config=sample/config/dryrun.json']; +// const mockBuildLogFile = `${mockLogType};${mockLogFile};${mockLogCwd}`; +// const mockOutput = './tmp/out.json'; + +// const GITHUB_ENV_ARGS = [ +// 'node', +// 'app.ts', +// '--vcs="github"', +// `--githubRepoUrl=${mockGitHubRepo}`, +// `--githubPr=${mockGitHubPr}`, +// `--githubToken=${mockGitHubToken}`, +// '--removeOldComment', +// `-f=${mockBuildLogFile}`, +// `-o=${mockOutput}`, +// ]; + +const GITHUB_FILE_ARGS = ['node', 'app.ts', '--file=sample/config/github.js']; + +// const GITLAB_ENV_ARGS = [ +// 'node', +// 'app.ts', +// '--vcs="gitlab"', +// `--gitlabHost=${mockGitLabHost}`, +// `--gitlabProjectId=${mockGitLabProjectId}`, +// `--gitlabMrIid=${mockGitLabMrIid}`, +// `--gitlabToken=${mockGitLabToken}`, +// `-f=${mockBuildLogFile}`, +// `-o=${mockOutput}`, +// '--failOnWarnings', +// ]; + +const GITLAB_FILE_ARGS = ['node', 'app.ts', '--file=sample/config/gitlab.js']; + +// const DRYRUN_ENV_ARGS = [ +// 'node', +// 'app.ts', +// `-f=${mockBuildLogFile}`, +// `-o=${mockOutput}`, +// '--dryRun', +// ]; + +const DRYRUN_FILE_ARGS = ['node', 'app.ts', '--file=sample/config/dryrun.js']; describe('Config parsing Test', () => { beforeEach(() => { @@ -66,22 +66,22 @@ describe('Config parsing Test', () => { expect(buildLog[0].cwd).toBe(mockLogCwd); }; - it('should be able to parse GitHub config provided by environment variables', async () => { - process.argv = GITHUB_ENV_ARGS; - const config = (await import('./Config')).configs; - expect(config.vcs).toBe('github'); - expect(config.githubRepoUrl).toBe(mockGitHubRepo); - expect(config.githubPr).toBe(mockGitHubPr); - expect(config.githubToken).toBe(mockGitHubToken); - expect(config.removeOldComment).toBe(true); - expect(config.failOnWarnings).toBe(false); + // it.skip('should be able to parse GitHub config provided by environment variables', async () => { + // process.argv = GITHUB_ENV_ARGS; + // const config = (await import('./Config')).configs.file as ConfigArgument; + // expect(config.vcs).toBe('github'); + // expect(config.githubRepoUrl).toBe(mockGitHubRepo); + // expect(config.githubPr).toBe(mockGitHubPr); + // expect(config.githubToken).toBe(mockGitHubToken); + // expect(config.removeOldComment).toBe(true); + // expect(config.failOnWarnings).toBe(false); - validateBuildLog(config.buildLogFile); - }); + // validateBuildLog(config.buildLogFile); + // }); it('should be able to parse GitHub config provided by file', async () => { process.argv = GITHUB_FILE_ARGS; - const config = (await import('./Config')).configs; + const config = (await import('./Config')).configs as ConfigArgument; expect(config.vcs).toBe('github'); expect(config.githubRepoUrl).toBe(mockGitHubRepo); expect(config.githubPr).toBe(mockGitHubPr); @@ -92,23 +92,23 @@ describe('Config parsing Test', () => { validateBuildLog(config.buildLogFile); }); - it('should be able to parse GitLab config provided by environment variables', async () => { - process.argv = GITLAB_ENV_ARGS; - const config = (await import('./Config')).configs; - expect(config.vcs).toBe('gitlab'); - expect(config.gitlabHost).toBe(mockGitLabHost); - expect(config.gitlabProjectId).toBe(mockGitLabProjectId); - expect(config.gitlabMrIid).toBe(mockGitLabMrIid); - expect(config.gitlabToken).toBe(mockGitLabToken); - expect(config.removeOldComment).toBe(false); - expect(config.failOnWarnings).toBe(true); + // it.skip('should be able to parse GitLab config provided by environment variables', async () => { + // process.argv = GITLAB_ENV_ARGS; + // const config = (await import('./Config')).args.parse(process.argv); + // expect(config.vcs).toBe('gitlab'); + // expect(config.gitlabHost).toBe(mockGitLabHost); + // expect(config.gitlabProjectId).toBe(mockGitLabProjectId); + // expect(config.gitlabMrIid).toBe(mockGitLabMrIid); + // expect(config.gitlabToken).toBe(mockGitLabToken); + // expect(config.removeOldComment).toBe(false); + // expect(config.failOnWarnings).toBe(true); - validateBuildLog(config.buildLogFile); - }); + // validateBuildLog(config.buildLogFile); + // }); it('should be able to parse GitLab config provided by file', async () => { process.argv = GITLAB_FILE_ARGS; - const config = (await import('./Config')).configs; + const config = (await import('./Config')).configs as ConfigArgument; expect(config.vcs).toBe('gitlab'); expect(config.gitlabHost).toBe(mockGitLabHost); expect(config.gitlabProjectId).toBe(mockGitLabProjectId); @@ -120,17 +120,17 @@ describe('Config parsing Test', () => { validateBuildLog(config.buildLogFile); }); - it('should be able to parse dryRun config provided by environment variables', async () => { - process.argv = DRYRUN_ENV_ARGS; - const config = (await import('./Config')).configs; - expect(config.dryRun).toBe(true); + // it.skip('should be able to parse dryRun config provided by environment variables', async () => { + // process.argv = DRYRUN_ENV_ARGS; + // const config = (await import('./Config')).args.parse(process.argv); + // expect(config.dryRun).toBe(true); - validateBuildLog(config.buildLogFile); - }); + // validateBuildLog(config.buildLogFile); + // }); it('should be able to parse dryRun config provided by file', async () => { process.argv = DRYRUN_FILE_ARGS; - const config = (await import('./Config')).configs; + const config = (await import('./Config')).configs as ConfigArgument; expect(config.dryRun).toBe(true); validateBuildLog(config.buildLogFile); diff --git a/src/Config/Config.ts b/src/Config/Config.ts index 5be7890..3519b57 100644 --- a/src/Config/Config.ts +++ b/src/Config/Config.ts @@ -3,51 +3,61 @@ import { ProjectType } from './@enums'; import { BuildLogFile, ConfigArgument } from './@types'; import { DEFAULT_OUTPUT_FILE } from './constants/defaults'; import { GITHUB_ARGS, GITLAB_ARGS } from './constants/required'; -import fs from 'fs'; +import { z } from 'zod'; +import path from 'path'; const projectTypes = Object.keys(ProjectType); -const args = yargs - .config('config', (configPath) => JSON.parse(fs.readFileSync(configPath, 'utf-8'))) - .option('vcs', { - alias: 'g', - describe: 'VCS Type', - choices: ['github', 'gitlab'], - }) +export const configSchema = z + .object({ + vcs: z.enum(['github', 'gitlab']).optional().describe('VCS Type'), - .option('githubRepoUrl', { - describe: 'GitHub repo url (https or ssh)', - type: 'string', - }) - .option('githubPr', { - describe: 'GitHub PR number', - type: 'number', - }) - .option('githubToken', { - describe: 'GitHub token', - type: 'string', - }) + githubRepoUrl: z.string().optional().describe('GitHub repo url (https or ssh)'), + githubPr: z.number().optional().describe('GitHub PR number'), + githubToken: z.string().optional().describe('GitHub token'), + + gitlabHost: z + .string() + .optional() + .describe('GitLab server URL (https://gitlab.yourcompany.com)'), + gitlabProjectId: z.number().optional().describe('GitLab project ID'), + gitlabMrIid: z + .number() + .optional() + .describe('GitLab merge request IID (not to be confused with ID)'), + gitlabToken: z.string().optional().describe('GitLab token'), + + buildLogFile: z.array(z.string()).transform((files) => { + return files + .map((opt) => { + const [type, path, cwd] = opt.split(';'); + if (!projectTypes.includes(type) || !path) return null; + return { type, path, cwd: cwd ?? process.cwd() } as BuildLogFile; + }) + .filter((file) => file !== null) as BuildLogFile[]; + }).describe(`Build log content files formatted in ';[;]' +where is one of [${projectTypes.join(', ')}] + is build log file path to be processed +and is build root directory (optional (Will use current context as cwd)). +`), + + output: z.string().describe('Output parsed log file').default(DEFAULT_OUTPUT_FILE), + removeOldComment: z + .boolean() + .describe('Remove existing CodeCoach comments before putting new one') + .default(false), + failOnWarnings: z + .boolean() + .describe('Fail the job if warnings are found') + .default(false), + dryRun: z + .boolean() + .describe('Running CodeCoach without reporting to VCS') + .default(false), + }) + .refine((options) => { + if (!options.dryRun && !options.vcs) throw 'VCS type is required'; - .option('gitlabHost', { - describe: 'GitLab server URL (https://gitlab.yourcompany.com)', - type: 'string', - }) - .option('gitlabProjectId', { - describe: 'GitLab project ID', - type: 'number', - }) - .option('gitlabMrIid', { - describe: 'GitLab merge request IID (not to be confused with ID)', - type: 'number', - }) - .option('gitlabToken', { - describe: 'GitLab token', - type: 'string', - }) - .group(['vcs', 'buildLogFile', 'output', 'removeOldComment'], 'Parsing options:') - .group(GITLAB_ARGS, 'GitLab options:') - .group(GITHUB_ARGS, 'GitHub options:') - .check((options) => { // validate VCS configs if (options.vcs === 'github' && GITHUB_ARGS.some((arg) => !options[arg])) throw `GitHub requires [${GITHUB_ARGS.map((a) => `--${a}`).join(', ')}] to be set`; @@ -56,66 +66,25 @@ const args = yargs throw `GitLab requires [${GITLAB_ARGS.map((a) => `--${a}`).join(', ')}] to be set`; return true; - }) - .option('buildLogFile', { - alias: 'f', - describe: `Build log content files formatted in ';[;]' -where is one of [${projectTypes.join(', ')}] - is build log file path to be processed -and is build root directory (optional (Will use current context as cwd)). -`, - type: 'array', - string: true, - number: false, - }) - .coerce('buildLogFile', (files: string[]) => { - return files.map((opt) => { - const [type, path, cwd] = opt.split(';'); - if (!projectTypes.includes(type) || !path) return null; - return { type, path, cwd: cwd ?? process.cwd() } as BuildLogFile; - }); - }) - .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'; - return true; - }) - .option('output', { - alias: 'o', - describe: 'Output parsed log file', - type: 'string', - default: DEFAULT_OUTPUT_FILE, - }) - .option('removeOldComment', { - alias: 'r', - type: 'boolean', - describe: 'Remove existing CodeCoach comments before putting new one', - default: false, - }) - .option('failOnWarnings', { - type: 'boolean', - describe: 'Fail the job if warnings are found', - default: false, - }) - .option('dryRun', { - describe: 'Running CodeCoach without reporting to VCS', - type: 'boolean', - default: false, +export const args = yargs + .option('file', { + alias: 'f', + default: 'codecoach.config.js', }) - .check((options) => { - if (options.dryRun) return true; - if (typeof options.vcs === 'undefined') throw 'VCS type is required'; - return true; + .coerce('file', function (file) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const result = configSchema.safeParse(require(path.join(process.cwd(), file))); + if (!result.success) { + console.error(result.error); + return {}; + } + return result.data; }) .strict() .help() .wrap(120) - .parse(process.argv.slice(1)) as ConfigArgument; + .parse(process.argv.slice(1)); -export const configs = args; +export const configs = args.file as ConfigArgument; diff --git a/yarn.lock b/yarn.lock index e871886..6e35b55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5440,3 +5440,8 @@ yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +zod@^3.21.4: + version "3.21.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" + integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== From 27d1322debfbbbcdf0acb56808caeaec6ccba41a Mon Sep 17 00:00:00 2001 From: jeeyo Date: Tue, 27 Jun 2023 17:49:22 +0700 Subject: [PATCH 2/4] accept both config.js and cli --- src/Config/Config.spec.ts | 140 ++++++++++----------- src/Config/Config.ts | 166 ++++++++++++++++--------- src/Provider/GitLab/GitLabMRService.ts | 13 +- src/app.ts | 25 +++- 4 files changed, 208 insertions(+), 136 deletions(-) diff --git a/src/Config/Config.spec.ts b/src/Config/Config.spec.ts index 7eb2de6..2328c48 100644 --- a/src/Config/Config.spec.ts +++ b/src/Config/Config.spec.ts @@ -12,47 +12,47 @@ const mockGitLabToken = 'mockGitLabToken'; const mockLogType = 'dotnetbuild'; const mockLogFile = './sample/dotnetbuild/build.content'; const mockLogCwd = '/repo/src'; -// const mockBuildLogFile = `${mockLogType};${mockLogFile};${mockLogCwd}`; -// const mockOutput = './tmp/out.json'; - -// const GITHUB_ENV_ARGS = [ -// 'node', -// 'app.ts', -// '--vcs="github"', -// `--githubRepoUrl=${mockGitHubRepo}`, -// `--githubPr=${mockGitHubPr}`, -// `--githubToken=${mockGitHubToken}`, -// '--removeOldComment', -// `-f=${mockBuildLogFile}`, -// `-o=${mockOutput}`, -// ]; - -const GITHUB_FILE_ARGS = ['node', 'app.ts', '--file=sample/config/github.js']; - -// const GITLAB_ENV_ARGS = [ -// 'node', -// 'app.ts', -// '--vcs="gitlab"', -// `--gitlabHost=${mockGitLabHost}`, -// `--gitlabProjectId=${mockGitLabProjectId}`, -// `--gitlabMrIid=${mockGitLabMrIid}`, -// `--gitlabToken=${mockGitLabToken}`, -// `-f=${mockBuildLogFile}`, -// `-o=${mockOutput}`, -// '--failOnWarnings', -// ]; - -const GITLAB_FILE_ARGS = ['node', 'app.ts', '--file=sample/config/gitlab.js']; - -// const DRYRUN_ENV_ARGS = [ -// 'node', -// 'app.ts', -// `-f=${mockBuildLogFile}`, -// `-o=${mockOutput}`, -// '--dryRun', -// ]; - -const DRYRUN_FILE_ARGS = ['node', 'app.ts', '--file=sample/config/dryrun.js']; +const mockBuildLogFile = `${mockLogType};${mockLogFile};${mockLogCwd}`; +const mockOutput = './tmp/out.json'; + +const GITHUB_ENV_ARGS = [ + 'node', + 'app.ts', + '--vcs="github"', + `--githubRepoUrl=${mockGitHubRepo}`, + `--githubPr=${mockGitHubPr}`, + `--githubToken=${mockGitHubToken}`, + '--removeOldComment', + `-f=${mockBuildLogFile}`, + `-o=${mockOutput}`, +]; + +const GITHUB_FILE_ARGS = ['node', 'app.ts', '--config=sample/config/github.js']; + +const GITLAB_ENV_ARGS = [ + 'node', + 'app.ts', + '--vcs="gitlab"', + `--gitlabHost=${mockGitLabHost}`, + `--gitlabProjectId=${mockGitLabProjectId}`, + `--gitlabMrIid=${mockGitLabMrIid}`, + `--gitlabToken=${mockGitLabToken}`, + `-f=${mockBuildLogFile}`, + `-o=${mockOutput}`, + '--failOnWarnings', +]; + +const GITLAB_FILE_ARGS = ['node', 'app.ts', '--config=sample/config/gitlab.js']; + +const DRYRUN_ENV_ARGS = [ + 'node', + 'app.ts', + `-f=${mockBuildLogFile}`, + `-o=${mockOutput}`, + '--dryRun', +]; + +const DRYRUN_FILE_ARGS = ['node', 'app.ts', '--config=sample/config/dryrun.js']; describe('Config parsing Test', () => { beforeEach(() => { @@ -66,18 +66,18 @@ describe('Config parsing Test', () => { expect(buildLog[0].cwd).toBe(mockLogCwd); }; - // it.skip('should be able to parse GitHub config provided by environment variables', async () => { - // process.argv = GITHUB_ENV_ARGS; - // const config = (await import('./Config')).configs.file as ConfigArgument; - // expect(config.vcs).toBe('github'); - // expect(config.githubRepoUrl).toBe(mockGitHubRepo); - // expect(config.githubPr).toBe(mockGitHubPr); - // expect(config.githubToken).toBe(mockGitHubToken); - // expect(config.removeOldComment).toBe(true); - // expect(config.failOnWarnings).toBe(false); + it('should be able to parse GitHub config provided by environment variables', async () => { + process.argv = GITHUB_ENV_ARGS; + const config = (await import('./Config')).configs as ConfigArgument; + expect(config.vcs).toBe('github'); + expect(config.githubRepoUrl).toBe(mockGitHubRepo); + expect(config.githubPr).toBe(mockGitHubPr); + expect(config.githubToken).toBe(mockGitHubToken); + expect(config.removeOldComment).toBe(true); + expect(config.failOnWarnings).toBe(false); - // validateBuildLog(config.buildLogFile); - // }); + validateBuildLog(config.buildLogFile); + }); it('should be able to parse GitHub config provided by file', async () => { process.argv = GITHUB_FILE_ARGS; @@ -92,19 +92,19 @@ describe('Config parsing Test', () => { validateBuildLog(config.buildLogFile); }); - // it.skip('should be able to parse GitLab config provided by environment variables', async () => { - // process.argv = GITLAB_ENV_ARGS; - // const config = (await import('./Config')).args.parse(process.argv); - // expect(config.vcs).toBe('gitlab'); - // expect(config.gitlabHost).toBe(mockGitLabHost); - // expect(config.gitlabProjectId).toBe(mockGitLabProjectId); - // expect(config.gitlabMrIid).toBe(mockGitLabMrIid); - // expect(config.gitlabToken).toBe(mockGitLabToken); - // expect(config.removeOldComment).toBe(false); - // expect(config.failOnWarnings).toBe(true); + it('should be able to parse GitLab config provided by environment variables', async () => { + process.argv = GITLAB_ENV_ARGS; + const config = (await import('./Config')).configs as ConfigArgument; + expect(config.vcs).toBe('gitlab'); + expect(config.gitlabHost).toBe(mockGitLabHost); + expect(config.gitlabProjectId).toBe(mockGitLabProjectId); + expect(config.gitlabMrIid).toBe(mockGitLabMrIid); + expect(config.gitlabToken).toBe(mockGitLabToken); + expect(config.removeOldComment).toBe(false); + expect(config.failOnWarnings).toBe(true); - // validateBuildLog(config.buildLogFile); - // }); + validateBuildLog(config.buildLogFile); + }); it('should be able to parse GitLab config provided by file', async () => { process.argv = GITLAB_FILE_ARGS; @@ -120,13 +120,13 @@ describe('Config parsing Test', () => { validateBuildLog(config.buildLogFile); }); - // it.skip('should be able to parse dryRun config provided by environment variables', async () => { - // process.argv = DRYRUN_ENV_ARGS; - // const config = (await import('./Config')).args.parse(process.argv); - // expect(config.dryRun).toBe(true); + it('should be able to parse dryRun config provided by environment variables', async () => { + process.argv = DRYRUN_ENV_ARGS; + const config = (await import('./Config')).configs as ConfigArgument; + expect(config.dryRun).toBe(true); - // validateBuildLog(config.buildLogFile); - // }); + validateBuildLog(config.buildLogFile); + }); it('should be able to parse dryRun config provided by file', async () => { process.argv = DRYRUN_FILE_ARGS; diff --git a/src/Config/Config.ts b/src/Config/Config.ts index 3519b57..fc4898b 100644 --- a/src/Config/Config.ts +++ b/src/Config/Config.ts @@ -1,10 +1,9 @@ import yargs from 'yargs'; import { ProjectType } from './@enums'; -import { BuildLogFile, ConfigArgument } from './@types'; +import { BuildLogFile } from './@types'; import { DEFAULT_OUTPUT_FILE } from './constants/defaults'; import { GITHUB_ARGS, GITLAB_ARGS } from './constants/required'; import { z } from 'zod'; -import path from 'path'; const projectTypes = Object.keys(ProjectType); @@ -12,20 +11,14 @@ export const configSchema = z .object({ vcs: z.enum(['github', 'gitlab']).optional().describe('VCS Type'), - githubRepoUrl: z.string().optional().describe('GitHub repo url (https or ssh)'), - githubPr: z.number().optional().describe('GitHub PR number'), - githubToken: z.string().optional().describe('GitHub token'), + githubRepoUrl: z.string().optional(), + githubPr: z.number().optional(), + githubToken: z.string().optional(), - gitlabHost: z - .string() - .optional() - .describe('GitLab server URL (https://gitlab.yourcompany.com)'), - gitlabProjectId: z.number().optional().describe('GitLab project ID'), - gitlabMrIid: z - .number() - .optional() - .describe('GitLab merge request IID (not to be confused with ID)'), - gitlabToken: z.string().optional().describe('GitLab token'), + gitlabHost: z.string().optional(), + gitlabProjectId: z.number().optional(), + gitlabMrIid: z.number().optional(), + gitlabToken: z.string().optional(), buildLogFile: z.array(z.string()).transform((files) => { return files @@ -35,56 +28,115 @@ export const configSchema = z return { type, path, cwd: cwd ?? process.cwd() } as BuildLogFile; }) .filter((file) => file !== null) as BuildLogFile[]; - }).describe(`Build log content files formatted in ';[;]' -where is one of [${projectTypes.join(', ')}] - is build log file path to be processed -and is build root directory (optional (Will use current context as cwd)). -`), - - output: z.string().describe('Output parsed log file').default(DEFAULT_OUTPUT_FILE), - removeOldComment: z - .boolean() - .describe('Remove existing CodeCoach comments before putting new one') - .default(false), - failOnWarnings: z - .boolean() - .describe('Fail the job if warnings are found') - .default(false), - dryRun: z - .boolean() - .describe('Running CodeCoach without reporting to VCS') - .default(false), - }) - .refine((options) => { - if (!options.dryRun && !options.vcs) throw 'VCS type is required'; - - // validate VCS configs - if (options.vcs === 'github' && GITHUB_ARGS.some((arg) => !options[arg])) - throw `GitHub requires [${GITHUB_ARGS.map((a) => `--${a}`).join(', ')}] to be set`; - - if (options.vcs === 'gitlab' && GITLAB_ARGS.some((arg) => !options[arg])) - throw `GitLab requires [${GITLAB_ARGS.map((a) => `--${a}`).join(', ')}] to be set`; + }), + output: z.string().default(DEFAULT_OUTPUT_FILE), + removeOldComment: z.boolean().default(false), + failOnWarnings: z.boolean().default(false), + dryRun: z.boolean().default(false), + }) + .superRefine((options, ctx) => { + if (!options.vcs && !options.dryRun) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'VCS type is required', + }); + } + }) + .superRefine((options, ctx) => { + if (options.vcs === 'github' && GITHUB_ARGS.some((arg) => !options[arg])) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `GitHub requires [${GITHUB_ARGS.map((a) => `--${a}`).join( + ', ', + )}] to be set`, + }); + } - return true; + if (options.vcs === 'gitlab' && GITLAB_ARGS.some((arg) => !options[arg])) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `GitLab requires [${GITLAB_ARGS.map((a) => `--${a}`).join( + ', ', + )}] to be set`, + }); + } }); export const args = yargs - .option('file', { + .config('config', (file) => { + return require(file); + }) + .option('vcs', { + alias: 'g', + describe: 'VCS Type', + choices: ['github', 'gitlab'], + }) + + .option('githubRepoUrl', { + describe: 'GitHub repo url (https or ssh)', + type: 'string', + }) + .option('githubPr', { + describe: 'GitHub PR number', + type: 'number', + }) + .option('githubToken', { + describe: 'GitHub token', + type: 'string', + }) + + .option('gitlabHost', { + describe: 'GitLab server URL (https://gitlab.yourcompany.com)', + type: 'string', + }) + .option('gitlabProjectId', { + describe: 'GitLab project ID', + type: 'number', + }) + .option('gitlabMrIid', { + describe: 'GitLab merge request IID (not to be confused with ID)', + type: 'number', + }) + .option('gitlabToken', { + describe: 'GitLab token', + type: 'string', + }) + .option('buildLogFile', { alias: 'f', - default: 'codecoach.config.js', - }) - .coerce('file', function (file) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const result = configSchema.safeParse(require(path.join(process.cwd(), file))); - if (!result.success) { - console.error(result.error); - return {}; - } - return result.data; + describe: `Build log content files formatted in ';[;]' +where is one of [${projectTypes.join(', ')}] + is build log file path to be processed +and is build root directory (optional (Will use current context as cwd)). +`, + type: 'array', + string: true, + number: false, + }) + .option('output', { + alias: 'o', + describe: 'Output parsed log file', + type: 'string', + default: DEFAULT_OUTPUT_FILE, + }) + .option('removeOldComment', { + alias: 'r', + type: 'boolean', + describe: 'Remove existing CodeCoach comments before putting new one', + default: false, + }) + .option('failOnWarnings', { + type: 'boolean', + describe: 'Fail the job if warnings are found', + default: false, + }) + .option('dryRun', { + describe: 'Running CodeCoach without reporting to VCS', + type: 'boolean', + default: false, }) .strict() .help() .wrap(120) .parse(process.argv.slice(1)); -export const configs = args.file as ConfigArgument; +export const configs = configSchema.parse(args); diff --git a/src/Provider/GitLab/GitLabMRService.ts b/src/Provider/GitLab/GitLabMRService.ts index 1bedb3c..5645685 100644 --- a/src/Provider/GitLab/GitLabMRService.ts +++ b/src/Provider/GitLab/GitLabMRService.ts @@ -9,20 +9,17 @@ import { import * as Resource from '@gitbeaker/core/dist/types/resources'; import { Gitlab } from '@gitbeaker/node'; -import { configs } from '../../Config'; - export class GitLabMRService implements IGitLabMRService { private readonly projectId: number; private readonly mrIid: number; private readonly api: Resource.Gitlab; - constructor() { - this.projectId = configs.gitlabProjectId; - this.mrIid = configs.gitlabMrIid; - + constructor(token: string, host: string, projectId: number, mrIid: number) { + this.projectId = projectId; + this.mrIid = mrIid; this.api = new Gitlab({ - host: configs.gitlabHost, - token: configs.gitlabToken, + host: host, + token: token, }); } diff --git a/src/app.ts b/src/app.ts index ed6f7f3..1a450c1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -45,6 +45,11 @@ class App { private static getAdapter(): VCSAdapter | undefined { if (configs.vcs === 'github') { + if (!configs.githubToken || !configs.githubRepoUrl || !configs.githubPr) { + Log.error('GitHub requires githubToken, githubRepoUrl and githubPr to be set'); + return undefined; + } + const githubPRService = new GitHubPRService( configs.githubToken, configs.githubRepoUrl, @@ -52,7 +57,25 @@ class App { ); return new GitHubAdapter(githubPRService); } else if (configs.vcs === 'gitlab') { - return new GitLabAdapter(new GitLabMRService()); + if ( + !configs.gitlabToken || + !configs.gitlabHost || + !configs.gitlabProjectId || + !configs.gitlabMrIid + ) { + Log.error( + 'GitLab requires gitlabToken, gitlabHost, gitlabProjectId and gitlabMrIid to be set', + ); + return undefined; + } + + const gitlabMRService = new GitLabMRService( + configs.gitlabToken, + configs.gitlabHost, + configs.gitlabProjectId, + configs.gitlabMrIid, + ); + return new GitLabAdapter(gitlabMRService); } } From c18b59d7e781238acbcc546aacc0967a03d07705 Mon Sep 17 00:00:00 2001 From: jeeyo Date: Tue, 27 Jun 2023 17:56:33 +0700 Subject: [PATCH 3/4] reduce complexity --- src/app.ts | 70 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/src/app.ts b/src/app.ts index 1a450c1..572b953 100644 --- a/src/app.ts +++ b/src/app.ts @@ -43,40 +43,50 @@ class App { } } + private static getGitHubAdapter(): GitHubAdapter | undefined { + if (!configs.githubToken || !configs.githubRepoUrl || !configs.githubPr) { + Log.error('GitHub requires githubToken, githubRepoUrl and githubPr to be set'); + return undefined; + } + + const githubPRService = new GitHubPRService( + configs.githubToken, + configs.githubRepoUrl, + configs.githubPr, + ); + return new GitHubAdapter(githubPRService); + } + + private static getGitLabAdapter(): GitLabAdapter | undefined { + if ( + !configs.gitlabToken || + !configs.gitlabHost || + !configs.gitlabProjectId || + !configs.gitlabMrIid + ) { + Log.error( + 'GitLab requires gitlabToken, gitlabHost, gitlabProjectId and gitlabMrIid to be set', + ); + return undefined; + } + + const gitlabMRService = new GitLabMRService( + configs.gitlabToken, + configs.gitlabHost, + configs.gitlabProjectId, + configs.gitlabMrIid, + ); + return new GitLabAdapter(gitlabMRService); + } + private static getAdapter(): VCSAdapter | undefined { if (configs.vcs === 'github') { - if (!configs.githubToken || !configs.githubRepoUrl || !configs.githubPr) { - Log.error('GitHub requires githubToken, githubRepoUrl and githubPr to be set'); - return undefined; - } - - const githubPRService = new GitHubPRService( - configs.githubToken, - configs.githubRepoUrl, - configs.githubPr, - ); - return new GitHubAdapter(githubPRService); + return this.getGitHubAdapter(); } else if (configs.vcs === 'gitlab') { - if ( - !configs.gitlabToken || - !configs.gitlabHost || - !configs.gitlabProjectId || - !configs.gitlabMrIid - ) { - Log.error( - 'GitLab requires gitlabToken, gitlabHost, gitlabProjectId and gitlabMrIid to be set', - ); - return undefined; - } - - const gitlabMRService = new GitLabMRService( - configs.gitlabToken, - configs.gitlabHost, - configs.gitlabProjectId, - configs.gitlabMrIid, - ); - return new GitLabAdapter(gitlabMRService); + return this.getGitLabAdapter(); } + + return undefined; } private static getParser(type: ProjectType, cwd: string): Parser { From 66f1dbca8d7f13baef13c4e6c95a09139769fe2a Mon Sep 17 00:00:00 2001 From: jeeyo Date: Wed, 28 Jun 2023 17:18:24 +0700 Subject: [PATCH 4/4] print prettier cli errors --- src/Config/Config.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Config/Config.ts b/src/Config/Config.ts index fc4898b..24da0a5 100644 --- a/src/Config/Config.ts +++ b/src/Config/Config.ts @@ -64,7 +64,10 @@ export const configSchema = z export const args = yargs .config('config', (file) => { - return require(file); + console.log(`Loading config from ${file}`); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const config = require(file); + return config; }) .option('vcs', { alias: 'g', @@ -139,4 +142,14 @@ and is build root directory (optional (Will use current context as cwd)). .wrap(120) .parse(process.argv.slice(1)); -export const configs = configSchema.parse(args); +const getConfigs = () => { + const result = configSchema.safeParse(args); + if (!result.success) { + const firstIssue = result.error.issues[0]; + console.log(`${firstIssue.message} ${firstIssue.path.join('.')}`); + process.exit(1); + } + return result.data; +}; + +export const configs = getConfigs();