diff --git a/src/commands/hld/reconcile.ts b/src/commands/hld/reconcile.ts index 13a781dea..e715f97ca 100644 --- a/src/commands/hld/reconcile.ts +++ b/src/commands/hld/reconcile.ts @@ -1,4 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/camelcase import child_process from "child_process"; import commander from "commander"; import fs from "fs"; @@ -28,7 +27,6 @@ import { errorStatusCode } from "../../lib/errorStatusCode"; * reject() */ interface ExecResult { - // eslint-disable-next-line @typescript-eslint/camelcase error?: child_process.ExecException; value?: { stdout: string; stderr: string }; } @@ -43,7 +41,6 @@ interface ExecResult { */ const exec = async (cmd: string, pipeIO = false): Promise => { return new Promise((resolve) => { - // eslint-disable-next-line @typescript-eslint/camelcase const child = child_process.exec(cmd, (error, stdout, stderr) => { return resolve({ error: error ?? undefined, diff --git a/src/commands/project/create-variable-group.test.ts b/src/commands/project/create-variable-group.test.ts index c2554776f..14b4e1e5e 100644 --- a/src/commands/project/create-variable-group.test.ts +++ b/src/commands/project/create-variable-group.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ import fs from "fs"; import yaml from "js-yaml"; import mockFs from "mock-fs"; @@ -252,7 +251,10 @@ describe("setVariableGroupInBedrockFile", () => { const bedrockFile = readBedrockFile(randomTmpDir); logger.info(`filejson: ${JSON.stringify(bedrockFile)}`); - expect(bedrockFile.variableGroups![0]).toBe(variableGroupName); + expect(bedrockFile.variableGroups).toBeDefined(); + if (bedrockFile.variableGroups) { + expect(bedrockFile.variableGroups[0]).toBe(variableGroupName); + } }); test("Should pass adding a valid variable group name when bedrock file exists when variableGroups length is > 0", async () => { @@ -273,8 +275,11 @@ describe("setVariableGroupInBedrockFile", () => { const bedrockFile = readBedrockFile(randomTmpDir); logger.info(`filejson: ${JSON.stringify(bedrockFile)}`); - expect(bedrockFile.variableGroups![0]).toBe(prevariableGroupName); - expect(bedrockFile.variableGroups![1]).toBe(variableGroupName); + expect(bedrockFile.variableGroups).toBeDefined(); + if (bedrockFile.variableGroups) { + expect(bedrockFile.variableGroups[0]).toBe(prevariableGroupName); + expect(bedrockFile.variableGroups[1]).toBe(variableGroupName); + } }); }); @@ -343,7 +348,10 @@ describe("updateLifeCyclePipeline", () => { const hldLifeCycleYaml = readYaml(hldFilePath); logger.info(`filejson: ${JSON.stringify(hldLifeCycleYaml)}`); - expect(hldLifeCycleYaml.variables!.length).toBeLessThanOrEqual(0); + expect(hldLifeCycleYaml.variables).toBeDefined(); + if (hldLifeCycleYaml.variables) { + expect(hldLifeCycleYaml.variables.length).toBeLessThanOrEqual(0); + } }); test("Should pass adding variable groups when bedrock file exists with one variableGroup", async () => { @@ -379,9 +387,13 @@ describe("updateLifeCyclePipeline", () => { const hldLifeCycleYaml = readYaml(hldFilePath); logger.info(`filejson: ${JSON.stringify(hldLifeCycleYaml)}`); - expect(hldLifeCycleYaml.variables![0]).toEqual({ - group: variableGroupName, - }); + expect(hldLifeCycleYaml.variables).toBeDefined(); + + if (hldLifeCycleYaml.variables) { + expect(hldLifeCycleYaml.variables[0]).toEqual({ + group: variableGroupName, + }); + } }); }); diff --git a/src/commands/project/create-variable-group.ts b/src/commands/project/create-variable-group.ts index 5a0fd07d5..8df593a64 100644 --- a/src/commands/project/create-variable-group.ts +++ b/src/commands/project/create-variable-group.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-use-before-define */ - import { VariableGroup } from "azure-devops-node-api/interfaces/ReleaseInterfaces"; import commander from "commander"; import path from "path"; @@ -55,118 +53,6 @@ export const validateValues = (projectName: string, orgName: string): void => { validateOrgNameThrowable(orgName); }; -/** - * Executes the command. - * - * @param variableGroupName Variable Group Name - * @param opts Option object from command - */ -export const execute = async ( - variableGroupName: string, - opts: CommandOptions, - exitFn: (status: number) => Promise -): Promise => { - if (!hasValue(variableGroupName)) { - await exitFn(1); - return; - } - - try { - const projectPath = process.cwd(); - logger.verbose(`project path: ${projectPath}`); - - checkDependencies(projectPath); - - const { azure_devops } = Config(); - - const { - registryName, - servicePrincipalId, - servicePrincipalPassword, - tenant, - hldRepoUrl = azure_devops?.hld_repository, - orgName = azure_devops?.org, - personalAccessToken = azure_devops?.access_token, - devopsProject = azure_devops?.project, - } = opts; - - const accessOpts: AzureDevOpsOpts = { - orgName, - personalAccessToken, - project: devopsProject, - }; - - logger.debug(`access options: ${JSON.stringify(accessOpts)}`); - - const errors = validateForRequiredValues(decorator, { - devopsProject, - hldRepoUrl, - orgName, - personalAccessToken, - registryName, - servicePrincipalId, - servicePrincipalPassword, - tenant, - }); - - if (errors.length !== 0) { - await exitFn(1); - return; - } - - // validateForRequiredValues assure that devopsProject - // and orgName are not empty string - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - validateValues(devopsProject!, orgName!); - - const variableGroup = await create( - variableGroupName, - registryName, - hldRepoUrl, - servicePrincipalId, - servicePrincipalPassword, - tenant, - accessOpts - ); - - // set the variable group name - // variableGroup.name is set at this point that's it should have value - // and not empty string or undefined. having || "" is just to avoid - // eslint error - setVariableGroupInBedrockFile(projectPath, variableGroup.name || ""); - - // update hld-lifecycle.yaml with variable groups in bedrock.yaml - updateLifeCyclePipeline(projectPath); - - // print newly created variable group - echo(JSON.stringify(variableGroup, null, 2)); - - logger.info( - "Successfully created a variable group in Azure DevOps project!" - ); - await exitFn(0); - } catch (err) { - logger.error(`Error occurred while creating variable group`); - logger.error(err); - await exitFn(1); - } -}; - -/** - * Adds the create command to the variable-group command object - * - * @param command Commander command object to decorate - */ -export const commandDecorator = (command: commander.Command): void => { - buildCmd(command, decorator).action( - async (variableGroupName: string, opts: CommandOptions) => { - await execute(variableGroupName, opts, async (status: number) => { - await exitCmd(logger, process.exit, status); - }); - } - ); -}; - /** * Creates a Azure DevOps variable group * @@ -319,3 +205,115 @@ export const updateLifeCyclePipeline = (rootProjectPath: string): void => { // Write out write(pipelineFile, absProjectRoot, fileName); }; + +/** + * Executes the command. + * + * @param variableGroupName Variable Group Name + * @param opts Option object from command + */ +export const execute = async ( + variableGroupName: string, + opts: CommandOptions, + exitFn: (status: number) => Promise +): Promise => { + if (!hasValue(variableGroupName)) { + await exitFn(1); + return; + } + + try { + const projectPath = process.cwd(); + logger.verbose(`project path: ${projectPath}`); + + checkDependencies(projectPath); + + const { azure_devops } = Config(); + + const { + registryName, + servicePrincipalId, + servicePrincipalPassword, + tenant, + hldRepoUrl = azure_devops?.hld_repository, + orgName = azure_devops?.org, + personalAccessToken = azure_devops?.access_token, + devopsProject = azure_devops?.project, + } = opts; + + const accessOpts: AzureDevOpsOpts = { + orgName, + personalAccessToken, + project: devopsProject, + }; + + logger.debug(`access options: ${JSON.stringify(accessOpts)}`); + + const errors = validateForRequiredValues(decorator, { + devopsProject, + hldRepoUrl, + orgName, + personalAccessToken, + registryName, + servicePrincipalId, + servicePrincipalPassword, + tenant, + }); + + if (errors.length !== 0) { + await exitFn(1); + return; + } + + // validateForRequiredValues assure that devopsProject + // and orgName are not empty string + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + validateValues(devopsProject!, orgName!); + + const variableGroup = await create( + variableGroupName, + registryName, + hldRepoUrl, + servicePrincipalId, + servicePrincipalPassword, + tenant, + accessOpts + ); + + // set the variable group name + // variableGroup.name is set at this point that's it should have value + // and not empty string or undefined. having || "" is just to avoid + // eslint error + setVariableGroupInBedrockFile(projectPath, variableGroup.name || ""); + + // update hld-lifecycle.yaml with variable groups in bedrock.yaml + updateLifeCyclePipeline(projectPath); + + // print newly created variable group + echo(JSON.stringify(variableGroup, null, 2)); + + logger.info( + "Successfully created a variable group in Azure DevOps project!" + ); + await exitFn(0); + } catch (err) { + logger.error(`Error occurred while creating variable group`); + logger.error(err); + await exitFn(1); + } +}; + +/** + * Adds the create command to the variable-group command object + * + * @param command Commander command object to decorate + */ +export const commandDecorator = (command: commander.Command): void => { + buildCmd(command, decorator).action( + async (variableGroupName: string, opts: CommandOptions) => { + await execute(variableGroupName, opts, async (status: number) => { + await exitCmd(logger, process.exit, status); + }); + } + ); +}; diff --git a/src/commands/project/pipeline.test.ts b/src/commands/project/pipeline.test.ts index 7c1ce039e..c5fd81321 100644 --- a/src/commands/project/pipeline.test.ts +++ b/src/commands/project/pipeline.test.ts @@ -14,8 +14,9 @@ import { checkDependencies, execute, fetchValidateValues, - CommandOptions, + ConfigValues, installLifecyclePipeline, + CommandOptions, } from "./pipeline"; import { deepClone } from "../../lib/util"; @@ -29,7 +30,7 @@ afterAll(() => { const gitUrl = "https://github.com/CatalystCode/spk.git"; -const mockValues: CommandOptions = { +const mockValues: ConfigValues = { buildScriptUrl: "buildScriptUrl", devopsProject: "azDoProject", orgName: "orgName", @@ -40,7 +41,7 @@ const mockValues: CommandOptions = { yamlFileBranch: "master", }; -jest.spyOn(azdo, "validateRepository").mockReturnValue(Promise.resolve()); +jest.spyOn(azdo, "validateRepository").mockResolvedValue(); const mockMissingValues: CommandOptions = { buildScriptUrl: undefined, diff --git a/src/commands/project/pipeline.ts b/src/commands/project/pipeline.ts index 086c398d3..665f8a0d3 100644 --- a/src/commands/project/pipeline.ts +++ b/src/commands/project/pipeline.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { IBuildApi } from "azure-devops-node-api/BuildApi"; import { BuildDefinition, @@ -51,6 +50,17 @@ export interface CommandOptions { yamlFileBranch: string; } +export interface ConfigValues { + devopsProject: string; + repoName: string; + orgName: string; + personalAccessToken: string; + pipelineName: string; + repoUrl: string; + buildScriptUrl: string; + yamlFileBranch: string; +} + export const checkDependencies = (projectPath: string): void => { const file: BedrockFileInfo = bedrockFileInfo(projectPath); if (file.exist === false) { @@ -79,7 +89,7 @@ export const fetchValidateValues = ( opts: CommandOptions, gitOriginUrl: string, spkConfig: ConfigYaml | undefined -): CommandOptions => { +): ConfigValues => { if (!spkConfig) { throw buildError( errorStatusCode.VALIDATION_ERR, @@ -119,7 +129,6 @@ export const fetchValidateValues = ( }); const error = validateForRequiredValues(decorator, map); - if (error.length > 0) { throw buildError( errorStatusCode.VALIDATION_ERR, @@ -127,10 +136,24 @@ export const fetchValidateValues = ( ); } - validateProjectNameThrowable(values.devopsProject!); - validateOrgNameThrowable(values.orgName!); + // validateForRequiredValues has validated the following + // values are validate, adding || "" is just to + // satisfy the no-non-null-assertion eslint rule + const configVals: ConfigValues = { + orgName: opts.orgName || "", + buildScriptUrl: opts.buildScriptUrl || BUILD_SCRIPT_URL, + devopsProject: opts.devopsProject || "", + repoName: opts.repoName || "", + personalAccessToken: opts.personalAccessToken || "", + pipelineName: opts.pipelineName || "", + repoUrl: opts.repoUrl || "", + yamlFileBranch: opts.yamlFileBranch, + }; + + validateProjectNameThrowable(configVals.devopsProject); + validateOrgNameThrowable(configVals.orgName); - return values; + return configVals; }; /** @@ -151,17 +174,17 @@ export const requiredPipelineVariables = ( }; const createPipeline = async ( - values: CommandOptions, + values: ConfigValues, devopsClient: IBuildApi, definitionBranch: string ): Promise => { const definition = definitionForAzureRepoPipeline({ branchFilters: ["master"], // hld reconcile pipeline is triggered only by merges into the master branch. maximumConcurrentBuilds: 1, - pipelineName: values.pipelineName!, - repositoryName: values.repoName!, - repositoryUrl: values.repoUrl!, - variables: requiredPipelineVariables(values.buildScriptUrl!), + pipelineName: values.pipelineName, + repositoryName: values.repoName, + repositoryUrl: values.repoUrl, + variables: requiredPipelineVariables(values.buildScriptUrl), yamlFileBranch: definitionBranch, // Pipeline is defined in master yamlFilePath: PROJECT_PIPELINE_FILENAME, // Pipeline definition lives in root directory. }); @@ -173,7 +196,7 @@ const createPipeline = async ( try { return await createPipelineForDefinition( devopsClient, - values.devopsProject!, + values.devopsProject, definition ); } catch (err) { @@ -197,11 +220,11 @@ const createPipeline = async ( * @param exitFn Exit function */ export const installLifecyclePipeline = async ( - values: CommandOptions + values: ConfigValues ): Promise => { const devopsClient = await getBuildApiClient( - values.orgName!, - values.personalAccessToken! + values.orgName, + values.personalAccessToken ); logger.info("Fetched DevOps Client"); @@ -220,7 +243,7 @@ export const installLifecyclePipeline = async ( logger.info(`Created pipeline for ${values.pipelineName}`); logger.info(`Pipeline ID: ${pipeline.id}`); - await queueBuild(devopsClient, values.devopsProject!, pipeline.id); + await queueBuild(devopsClient, values.devopsProject, pipeline.id); }; /** @@ -268,9 +291,9 @@ export const execute = async ( project: values.devopsProject, }; await validateRepository( - values.devopsProject!, + values.devopsProject, PROJECT_PIPELINE_FILENAME, - values.yamlFileBranch ? opts.yamlFileBranch : "master", + values.yamlFileBranch || "master", values.repoName, accessOpts );