diff --git a/src/commands/service/create-revision.test.ts b/src/commands/service/create-revision.test.ts new file mode 100644 index 000000000..c5a570c3d --- /dev/null +++ b/src/commands/service/create-revision.test.ts @@ -0,0 +1,167 @@ +import { write } from "../../config"; +import * as azure from "../../lib/git/azure"; +import * as gitutils from "../../lib/gitutils"; +import { createTempDir } from "../../lib/ioUtil"; +import { IBedrockFile } from "../../types"; +import { + getDefaultRings, + getSourceBranch, + makePullRequest +} from "./create-revision"; + +jest + .spyOn(gitutils, "getCurrentBranch") + .mockReturnValueOnce(Promise.resolve("prod")) + .mockReturnValue(Promise.resolve("")); +const prSpy = jest + .spyOn(azure, "createPullRequest") + .mockReturnValue(Promise.resolve("done")); + +describe("Default rings", () => { + test("Get multiple default rings", () => { + const randomTmpDir = createTempDir(); + const validBedrockYaml: IBedrockFile = { + rings: { + master: { isDefault: true }, + prod: { isDefault: false }, + westus: { isDefault: true } + }, + services: { + "foo/a": { + helm: { + chart: { + chart: "elastic", + repository: "some-repo" + } + }, + k8sBackend: "backendservice", + k8sBackendPort: 1337, + pathPrefix: "servicepath", + pathPrefixMajorVersion: "v1" + } + } + }; + + write(validBedrockYaml, randomTmpDir); + const defaultRings = getDefaultRings(undefined, validBedrockYaml); + expect(defaultRings.length).toBe(2); + expect(defaultRings[0]).toBe("master"); + expect(defaultRings[1]).toBe("westus"); + }); + + test("No default rings", () => { + const randomTmpDir = createTempDir(); + const validBedrockYaml: IBedrockFile = { + rings: { + master: { isDefault: false }, + prod: { isDefault: false }, + westus: { isDefault: false } + }, + services: { + "foo/a": { + helm: { + chart: { + chart: "elastic", + repository: "some-repo" + } + }, + k8sBackend: "backendservice", + k8sBackendPort: 1337, + pathPrefix: "servicepath", + pathPrefixMajorVersion: "v1" + } + } + }; + + write(validBedrockYaml, randomTmpDir); + let hasError = false; + + try { + getDefaultRings(undefined, validBedrockYaml); + } catch (err) { + hasError = true; + } + expect(hasError).toBe(true); + }); +}); + +describe("Source branch", () => { + test("Defined source branch", async () => { + const branch = "master"; + const sourceBranch = await getSourceBranch(branch); + expect(sourceBranch).toBe("master"); + }); + test("Defined source branch", async () => { + const branch = undefined; + const sourceBranch = await getSourceBranch(branch); + expect(sourceBranch).toBe("prod"); + }); + test("No source branch", async () => { + const branch = undefined; + let hasError = false; + try { + await getSourceBranch(branch); + } catch (err) { + hasError = true; + } + expect(hasError).toBe(true); + }); +}); + +describe("Create pull request", () => { + test("invalid parameters", async () => { + for (const i of Array(4).keys()) { + let hasError = false; + try { + await makePullRequest( + ["master"], + "testTitle", + i === 0 ? undefined : "branch", + "description", + i === 1 ? undefined : "org", + i === 2 ? undefined : "url", + i === 3 ? undefined : "token" + ); + } catch (err) { + hasError = true; + } + expect(hasError).toBe(true); + } + }); + test("Valid parameters", async () => { + await makePullRequest( + ["master"], + "testTitle", + "testBranch", + "testDescription", + "testOrg", + "testUrl", + "testToken" + ); + expect(prSpy).toHaveBeenCalled(); + }); + test("Default description", async () => { + await makePullRequest( + ["master"], + "testTitle", + "testBranch", + undefined, + "testOrg", + "testUrl", + "testToken" + ); + expect(prSpy).toHaveBeenCalled(); + }); + test("Default title", async () => { + await makePullRequest( + ["master"], + undefined, + "testBranch", + "description", + "testOrg", + "testUrl", + "testToken" + ); + expect(prSpy).toHaveBeenCalled(); + }); +}); diff --git a/src/commands/service/create-revision.ts b/src/commands/service/create-revision.ts index 179cbdf1f..6b1a1b32e 100644 --- a/src/commands/service/create-revision.ts +++ b/src/commands/service/create-revision.ts @@ -9,6 +9,7 @@ import { safeGitUrlForLogging } from "../../lib/gitutils"; import { logger } from "../../logger"; +import { IBedrockFile } from "../../types"; import decorator from "./create-revision.decorator.json"; export const commandDecorator = (command: commander.Command): void => { @@ -20,7 +21,9 @@ export const commandDecorator = (command: commander.Command): void => { personalAccessToken = azure_devops && azure_devops.access_token, targetBranch } = opts; - let { description, remoteUrl, sourceBranch, title } = opts; + let { remoteUrl, sourceBranch } = opts; + const description = opts.description; + const title = opts.title; //////////////////////////////////////////////////////////////////////// // Give defaults @@ -28,43 +31,13 @@ export const commandDecorator = (command: commander.Command): void => { // default pull request against initial ring const bedrockConfig = Bedrock(); // Default to the --target-branch for creating a revision; if not specified, fallback to default rings in bedrock.yaml - const defaultRings: string[] = targetBranch - ? [targetBranch] - : Object.entries(bedrockConfig.rings || {}) - .map(([branch, config]) => ({ branch, ...config })) - .filter(ring => !!ring.isDefault) - .map(ring => ring.branch); - if (defaultRings.length === 0) { - throw Error( - `Default branches/rings must either be specified in ${join( - __dirname, - "bedrock.yaml" - )} or provided via --target-branch` - ); - } - logger.info( - `Creating pull request against branches: ${defaultRings.join(", ")}` + const defaultRings: string[] = getDefaultRings( + targetBranch, + bedrockConfig ); // default pull request source branch to the current branch - if ( - typeof sourceBranch !== "string" || - (typeof sourceBranch === "string" && sourceBranch.length === 0) - ) { - // Parse the source branch from options - // If it does not exist, parse from the git client - logger.info( - `No source-branch provided, parsing the current branch for git client` - ); - sourceBranch = await getCurrentBranch().then(branch => { - if (branch.length === 0) { - throw Error( - `Zero length branch string parsed from git client; cannot automate PR` - ); - } - return branch; - }); - } + sourceBranch = await getSourceBranch(sourceBranch); // Make sure the user isn't trying to make a PR for a branch against itself if (defaultRings.includes(sourceBranch)) { @@ -75,12 +48,6 @@ export const commandDecorator = (command: commander.Command): void => { ); } - // Give a default description - if (typeof description !== "string") { - description = `This is automated PR generated via SPK`; - logger.info(`--description not set, defaulting to: '${description}'`); - } - // Default the remote to the git origin if (typeof remoteUrl !== "string") { logger.info( @@ -91,51 +58,16 @@ export const commandDecorator = (command: commander.Command): void => { logger.info(`Parsed remote-url for origin: ${safeLoggingUrl}`); } - //////////////////////////////////////////////////////////////////////// - // Type-check data - //////////////////////////////////////////////////////////////////////// - if (typeof description !== "string") { - throw Error( - `--description must be of type 'string', ${typeof description} given.` - ); - } - if (typeof remoteUrl !== "string") { - throw Error( - `--remote-url must be of type 'string', ${typeof remoteUrl} given.` - ); - } - if (typeof sourceBranch !== "string") { - throw Error( - `--source-branch must be of type 'string', ${typeof sourceBranch} given.` - ); - } - if (typeof personalAccessToken !== "string") { - throw Error( - `--personal-access-token must be of type 'string', ${typeof personalAccessToken} given.` - ); - } - if (typeof orgName !== "string") { - throw Error( - `--org-name must be of type 'string', ${typeof orgName} given.` - ); - } + await makePullRequest( + defaultRings, + title, + sourceBranch, + description, + orgName, + remoteUrl, + personalAccessToken + ); - //////////////////////////////////////////////////////////////////////// - // Main - //////////////////////////////////////////////////////////////////////// - // Make a PR against all default rings - for (const ring of defaultRings) { - if (typeof title !== "string") { - title = `[SPK] ${sourceBranch} => ${ring}`; - logger.info(`--title not set, defaulting to: '${title}'`); - } - await createPullRequest(title, sourceBranch, ring, { - description, - orgName, - originPushUrl: remoteUrl, - personalAccessToken - }); - } await exitCmd(logger, process.exit, 0); } catch (err) { logger.error(err); @@ -143,3 +75,116 @@ export const commandDecorator = (command: commander.Command): void => { } }); }; + +/** + * Gets the default rings + * @param targetBranch Target branch/ring to create a PR against + * @param bedrockConfig The bedrock configuration file + */ +export const getDefaultRings = ( + targetBranch: string | undefined, + bedrockConfig: IBedrockFile +): string[] => { + const defaultRings: string[] = targetBranch + ? [targetBranch] + : Object.entries(bedrockConfig.rings || {}) + .map(([branch, config]) => ({ branch, ...config })) + .filter(ring => !!ring.isDefault) + .map(ring => ring.branch); + if (defaultRings.length === 0) { + throw Error( + `Default branches/rings must either be specified in ${join( + __dirname, + "bedrock.yaml" + )} or provided via --target-branch` + ); + } + logger.info( + `Creating pull request against branches: ${defaultRings.join(", ")}` + ); + return defaultRings; +}; + +/** + * Gets the source branch or parses git for the source branch + * @param sourceBranch The source branch + */ +export const getSourceBranch = async ( + sourceBranch: string | undefined +): Promise => { + if ( + typeof sourceBranch !== "string" || + (typeof sourceBranch === "string" && sourceBranch.length === 0) + ) { + // Parse the source branch from options + // If it does not exist, parse from the git client + logger.info( + `No source-branch provided, parsing the current branch for git client` + ); + sourceBranch = await getCurrentBranch(); + if (sourceBranch.length === 0) { + throw Error( + `Zero length branch string parsed from git client; cannot automate PR` + ); + } + } + return sourceBranch; +}; + +/** + * Creates a pull request from the given source branch + * @param defaultRings List of default rings + * @param title Title of pr + * @param sourceBranch Source branch for pr + * @param description Description for pr + * @param orgName Organization name + * @param remoteUrl Remote url + * @param personalAccessToken Access token + */ +export const makePullRequest = async ( + defaultRings: string[], + title: string | undefined, + sourceBranch: string | undefined, + description: string | undefined, + orgName: string | undefined, + remoteUrl: string | undefined, + personalAccessToken: string | undefined +) => { + // Give a default description + if (typeof description !== "string") { + description = `This is automated PR generated via SPK`; + logger.info(`--description not set, defaulting to: '${description}'`); + } + if (typeof remoteUrl !== "string") { + throw Error( + `--remote-url must be of type 'string', ${typeof remoteUrl} given.` + ); + } + if (typeof sourceBranch !== "string") { + throw Error( + `--source-branch must be of type 'string', ${typeof sourceBranch} given.` + ); + } + if (typeof personalAccessToken !== "string") { + throw Error( + `--personal-access-token must be of type 'string', ${typeof personalAccessToken} given.` + ); + } + if (typeof orgName !== "string") { + throw Error( + `--org-name must be of type 'string', ${typeof orgName} given.` + ); + } + for (const ring of defaultRings) { + if (typeof title !== "string") { + title = `[SPK] ${sourceBranch} => ${ring}`; + logger.info(`--title not set, defaulting to: '${title}'`); + } + await createPullRequest(title, sourceBranch, ring, { + description, + orgName, + originPushUrl: remoteUrl, + personalAccessToken + }); + } +};