From 549b2bde787fefe737612c6985d67c3023e08559 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Sun, 12 Apr 2020 12:01:48 -0700 Subject: [PATCH 1/7] [FEATURE] auto approve PR for spk setup command --- docs/commands/data.json | 2 +- src/commands/setup.md | 5 ++ src/commands/setup.test.ts | 18 +++---- src/commands/setup.ts | 51 ++++++++++++------- src/lib/git/azure.test.ts | 49 ++++++++++++++++++ src/lib/git/azure.ts | 80 ++++++++++++++++++++++++++++++ src/lib/i18n.json | 5 +- src/lib/setup/azureStorage.test.ts | 20 +++++++- src/lib/setup/gitService.test.ts | 33 +++++++++++- src/lib/setup/gitService.ts | 31 +++++++++++- src/lib/setup/prompt.test.ts | 20 -------- src/lib/setup/prompt.ts | 14 ------ 12 files changed, 257 insertions(+), 71 deletions(-) diff --git a/docs/commands/data.json b/docs/commands/data.json index afb46452..23fbfc0b 100644 --- a/docs/commands/data.json +++ b/docs/commands/data.json @@ -26,7 +26,7 @@ "description": "Path to the file that contains answers to the questions." } ], - "markdown": "## Description\n\nThis command assists in creating resources in Azure DevOps so that you can get\nstarted with using Bedrock. It creates\n\n1. An Azure DevOps project.\n\nBy Default, it runs in an interactive mode where you are prompted for answers\nfor a few questions\n\n1. Azure DevOps Organization Name\n2. Azure DevOps Project Name, the project to be created.\n3. Azure DevOps Personal Access Token. The token needs to have these permissions\n 1. Read and write projects.\n 2. Read and write codes.\n4. To create a sample application Repo\n 1. If Yes, a Azure Service Principal is needed. You have 2 options\n 1. have the command line tool to create it. Azure command line tool shall\n be used. You will be prompted to select a subscription identifier.\n 2. Provide the Service Principal Id, Password, and Tenant Id. From this\n information, the tool will retrieve the subscription identifier.\n\nIt can also run in a non interactive mode by providing a file that contains\nanswers to the above questions.\n\nAfter this command is successfully executed, you can launch the introspection\ndashboard to view the status of pipelines.\n\n```\nspk setup --file \n```\n\nContent of this file is as follow\n\n```\nazdo_org_name=\nazdo_project_name=\nazdo_pat=\naz_create_app=\naz_create_sp=\naz_sp_id=\naz_sp_password=\naz_sp_tenant=\naz_subscription_id=\naz_acr_name=\n```\n\n`azdo_project_name` is optional and default value is `BedrockRocks`.\n\nThe followings shall be created\n\n1. A working directory, `quick-start-env`\n2. Project shall not be created if it already exists.\n3. A Git Repo, `quick-start-hld`, it shall be deleted and recreated if it\n already exists.\n 1. And initial commit shall be made to this repo\n4. A Git Repo, `quick-start-manifest`, it shall be deleted and recreated if it\n already exists.\n 1. And initial commit shall be made to this repo\n5. A High Level Definition (HLD) to Manifest pipeline.\n6. If user chose to create sample app repo\n 1. A Service Principal (if requested)\n 1. A resource group, `quick-start-rg` if it does not exist.\n 1. A storage account if it does not exist. Storage Account name has to be\n unqiue acess Azure.\n 1. A storage table in the storage account.\n 1. A Azure Container Registry, `quickStartACR` in resource group,\n `quick-start-rg` if it does not exist.\n 1. A Git Repo, `quick-start-helm`, it shall be deleted and recreated if is\n already exists.\n 1. A Git Repo, `quick-start-app`, it shall be deleted and recreated if is\n already exists.\n 1. A Lifecycle pipeline.\n 1. A Build pipeline.\n\n## Setup log\n\nA `setup.log` file is created after running this command. This file contains\ninformation about what are created and the execution status (completed or\nincomplete). This file will not be created if input validation failed.\n\n## Note\n\nTo remove the service principal that it is created by the tool, you can do the\nfollowings:\n\n1. Get the identifier from `setup.log` (look for `az_sp_id`)\n2. run on terminal `az ad sp delete --id `\n" + "markdown": "## Description\n\nThis command assists in creating resources in Azure DevOps so that you can get\nstarted with using Bedrock. It creates\n\n1. An Azure DevOps project.\n\nBy Default, it runs in an interactive mode where you are prompted for answers\nfor a few questions\n\n1. Azure DevOps Organization Name\n2. Azure DevOps Project Name, the project to be created.\n3. Azure DevOps Personal Access Token. The token needs to have these permissions\n 1. Read and write projects.\n 2. Read and write codes.\n4. To create a sample application Repo\n 1. If Yes, a Azure Service Principal is needed. You have 2 options\n 1. have the command line tool to create it. Azure command line tool shall\n be used. You will be prompted to select a subscription identifier.\n 2. Provide the Service Principal Id, Password, and Tenant Id. From this\n information, the tool will retrieve the subscription identifier.\n\nIt can also run in a non interactive mode by providing a file that contains\nanswers to the above questions.\n\nAfter this command is successfully executed, you can launch the introspection\ndashboard to view the status of pipelines.\n\n```\nspk setup --file \n```\n\nContent of this file is as follow\n\n```\nazdo_org_name=\nazdo_project_name=\nazdo_pat=\naz_create_app=\naz_create_sp=\naz_sp_id=\naz_sp_password=\naz_sp_tenant=\naz_subscription_id=\naz_acr_name=\n```\n\n`azdo_project_name` is optional and default value is `BedrockRocks`.\n\nThe followings shall be created\n\n1. A working directory, `quick-start-env`\n2. Project shall not be created if it already exists.\n3. A Git Repo, `quick-start-hld`, it shall be deleted and recreated if it\n already exists.\n 1. And initial commit shall be made to this repo\n4. A Git Repo, `quick-start-manifest`, it shall be deleted and recreated if it\n already exists.\n 1. And initial commit shall be made to this repo\n5. A High Level Definition (HLD) to Manifest pipeline.\n6. If user chose to create sample app repo\n 1. A Service Principal (if requested)\n 1. A resource group, `quick-start-rg` if it does not exist.\n 1. A storage account if it does not exist. Storage Account name has to be\n unqiue acess Azure.\n 1. A storage table in the storage account.\n 1. A Azure Container Registry, `quickStartACR` in resource group,\n `quick-start-rg` if it does not exist.\n 1. A Git Repo, `quick-start-helm`, it shall be deleted and recreated if is\n already exists.\n 1. A Git Repo, `quick-start-app`, it shall be deleted and recreated if is\n already exists.\n 1. A Lifecycle pipeline.\n 1. A Build pipeline.\n\n## Pre-requisite\n\nazure cli needs to be installed so that pull request can be automatically\napproved. type `az version` to check if you have version 2.0.x installed.\n\n## Setup log\n\nA `setup.log` file is created after running this command. This file contains\ninformation about what are created and the execution status (completed or\nincomplete). This file will not be created if input validation failed.\n\n## Note\n\nTo remove the service principal that it is created by the tool, you can do the\nfollowings:\n\n1. Get the identifier from `setup.log` (look for `az_sp_id`)\n2. run on terminal `az ad sp delete --id `\n" }, "deployment create": { "command": "create", diff --git a/src/commands/setup.md b/src/commands/setup.md index 4aae5914..7ad2c182 100644 --- a/src/commands/setup.md +++ b/src/commands/setup.md @@ -73,6 +73,11 @@ The followings shall be created 1. A Lifecycle pipeline. 1. A Build pipeline. +## Pre-requisite + +azure cli needs to be installed so that pull request can be automatically +approved. type `az version` to check if you have version 2.0.x installed. + ## Setup log A `setup.log` file is created after running this command. This file contains diff --git a/src/commands/setup.test.ts b/src/commands/setup.test.ts index 0bad994e..adb923d8 100644 --- a/src/commands/setup.test.ts +++ b/src/commands/setup.test.ts @@ -341,7 +341,7 @@ describe("test getErrorMessage function", () => { }); }); -const testCreateAppRepoTasks = async (prApproved = true): Promise => { +const testCreateAppRepoTasks = async (): Promise => { const mockRc: RequestContext = { orgName: "org", projectName: "project", @@ -366,15 +366,10 @@ const testCreateAppRepoTasks = async (prApproved = true): Promise => { jest .spyOn(pipelineService, "createLifecyclePipeline") .mockResolvedValueOnce(); - jest - .spyOn(promptInstance, "promptForApprovingHLDPullRequest") - .mockResolvedValueOnce(prApproved); - if (prApproved) { - jest.spyOn(pipelineService, "createBuildPipeline").mockResolvedValueOnce(); - jest - .spyOn(promptInstance, "promptForApprovingHLDPullRequest") - .mockResolvedValueOnce(prApproved); - } + jest.spyOn(gitService, "completePullRequest").mockResolvedValueOnce(); + + jest.spyOn(pipelineService, "createBuildPipeline").mockResolvedValueOnce(); + jest.spyOn(gitService, "completePullRequest").mockResolvedValueOnce(); const res = await createAppRepoTasks( // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -383,13 +378,12 @@ const testCreateAppRepoTasks = async (prApproved = true): Promise => { {} as any, // buildAPI mockRc ); - expect(res).toBe(prApproved); + expect(res).toBe(true); }; describe("test createAppRepoTasks function", () => { it("positive test", async () => { await testCreateAppRepoTasks(); - await testCreateAppRepoTasks(false); }); }); diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 6f880057..6c7e69e8 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -19,18 +19,18 @@ import { WORKSPACE, } from "../lib/setup/constants"; import { createDirectory } from "../lib/setup/fsUtil"; -import { getAzureRepoUrl, getGitApi } from "../lib/setup/gitService"; +import { + completePullRequest, + getAzureRepoUrl, + getGitApi, +} from "../lib/setup/gitService"; import { createBuildPipeline, createHLDtoManifestPipeline, createLifecyclePipeline, } from "../lib/setup/pipelineService"; import { createProjectIfNotExist } from "../lib/setup/projectService"; -import { - getAnswerFromFile, - prompt, - promptForApprovingHLDPullRequest, -} from "../lib/setup/prompt"; +import { getAnswerFromFile, prompt } from "../lib/setup/prompt"; import { appRepo, helmRepo, @@ -43,6 +43,7 @@ import decorator from "./setup.decorator.json"; import { createStorage } from "../lib/setup/azureStorage"; import { build as buildError, log as logError } from "../lib/errorBuilder"; import { errorStatusCode } from "../lib/errorStatusCode"; +import { exec } from "../lib/shell"; import { ConfigYaml } from "../types"; interface CommandOptions { @@ -60,6 +61,27 @@ interface APIClients { buildAPI: IBuildApi; } +export const isAzCLIInstall = async (): Promise => { + try { + const result = await exec("az", ["version"]); + try { + logger.info(`az cli vesion ${JSON.parse(result)["azure-cli"]}`); + } catch (e) { + throw buildError( + errorStatusCode.ENV_SETTING_ERR, + "setup-cmd-az-cli-get-version-err", + e + ); + } + } catch (err) { + throw buildError( + errorStatusCode.ENV_SETTING_ERR, + "setup-cmd-az-cli-err", + err + ); + } +}; + /** * Creates SPK config file under `user-home/.spk` folder * @@ -169,18 +191,10 @@ export const createAppRepoTasks = async ( await helmRepo(gitAPI, rc); await appRepo(gitAPI, rc); await createLifecyclePipeline(buildAPI, rc); - const approved = await promptForApprovingHLDPullRequest(rc); - - if (approved) { - await createBuildPipeline(buildAPI, rc); - - if (await promptForApprovingHLDPullRequest(rc)) { - return true; - } - } - - logger.warn("HLD Pull Request is not approved."); - return false; + await completePullRequest(gitAPI, rc, HLD_REPO); + await createBuildPipeline(buildAPI, rc); + await completePullRequest(gitAPI, rc, HLD_REPO); + return true; } else { return false; } @@ -243,6 +257,7 @@ export const execute = async ( let requestContext: RequestContext | undefined = undefined; try { + await isAzCLIInstall(); requestContext = opts.file ? getAnswerFromFile(opts.file) : await prompt(); const rc = requestContext; createDirectory(WORKSPACE, true); diff --git a/src/lib/git/azure.test.ts b/src/lib/git/azure.test.ts index 3952077c..17bb52f0 100644 --- a/src/lib/git/azure.test.ts +++ b/src/lib/git/azure.test.ts @@ -5,15 +5,19 @@ import { disableVerboseLogging, enableVerboseLogging } from "../../logger"; import { getErrorMessage } from "../errorBuilder"; import { AzureDevOpsOpts } from "../git"; import * as gitutils from "../gitutils"; +import * as shell from "../shell"; import { + completePullRequest, createPullRequest, generatePRUrl, + getAzureOrganizationUrl, getGitOrigin, GitAPI, repositoryHasFile, validateRepository, } from "./azure"; import * as azure from "./azure"; +import { RequestContext } from "../setup/constants"; jest.mock("azure-devops-node-api"); jest.mock("../../config"); @@ -283,3 +287,48 @@ describe("repositoryHasFile", () => { ).rejects.toThrow(); }); }); + +describe("test getAzureOrganizationUrl function", () => { + it("positive test", () => { + expect(getAzureOrganizationUrl("hello")).toBe( + "https://dev.azure.com/hello" + ); + }); +}); + +const mockRequestContext: RequestContext = { + servicePrincipalId: "servicePrincipalId", + servicePrincipalPassword: "servicePrincipalPassword", + servicePrincipalTenantId: "servicePrincipalTenantId", + orgName: "unittest", + projectName: "project", + accessToken: "token", + workspace: "test", +}; + +describe("test completePullRequest function", () => { + it("negative test: cannot login", async () => { + jest.spyOn(shell, "exec").mockRejectedValueOnce(Error()); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await expect( + completePullRequest({ pullRequestId: "test" } as any, mockRequestContext) + ).rejects.toThrow(getErrorMessage("git-azure-approve-pull-request-err")); + }); + it("negative test: cannot approve pull request", async () => { + jest.spyOn(shell, "exec").mockResolvedValueOnce("ok"); + jest.spyOn(shell, "exec").mockRejectedValueOnce(Error()); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await expect( + completePullRequest({ pullRequestId: "test" } as any, mockRequestContext) + ).rejects.toThrow(getErrorMessage("git-azure-approve-pull-request-err")); + }); + it("positive test", async () => { + jest.spyOn(shell, "exec").mockResolvedValueOnce("ok"); + jest.spyOn(shell, "exec").mockResolvedValueOnce("ok"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await completePullRequest( + { pullRequestId: "test" } as any, + mockRequestContext + ); + }); +}); diff --git a/src/lib/git/azure.ts b/src/lib/git/azure.ts index 102b15f7..2be71bd2 100644 --- a/src/lib/git/azure.ts +++ b/src/lib/git/azure.ts @@ -3,6 +3,8 @@ import { IGitApi } from "azure-devops-node-api/GitApi"; import AZGitInterfaces, { GitPullRequestSearchCriteria, GitRepository, + PullRequestStatus, + GitPullRequest, } from "azure-devops-node-api/interfaces/GitInterfaces"; import { AzureDevOpsOpts } from "."; import { Config } from "../../config"; @@ -11,6 +13,8 @@ import { azdoUrl } from "../azdoClient"; import { build as buildError } from "../errorBuilder"; import { errorStatusCode } from "../errorStatusCode"; import { getOriginUrl, safeGitUrlForLogging } from "../gitutils"; +import { RequestContext } from "../setup/constants"; +import { exec } from "../shell"; //////////////////////////////////////////////////////////////////////////////// // State @@ -22,6 +26,15 @@ let gitApi: IGitApi | undefined; // keep track of the gitApi so it can be reused // Helpers //////////////////////////////////////////////////////////////////////////////// +/** + * Returns azure organization URL. + * + * @param orgName Organization name + */ +export const getAzureOrganizationUrl = (orgName: string): string => { + return `https://dev.azure.com/${orgName}`; +}; + /** * Authenticates using config and credentials from global config and returns * an Azure DevOps Git API @@ -400,3 +413,70 @@ export const validateRepository = async ( await repositoryHasFile(fileName, branch, repoName, accessOpts); }; + +export const getActivePullRequests = async ( + gitAPI: IGitApi, + repoName: string, + projectName: string, + targetRef = "master" +): Promise => { + return await gitAPI.getPullRequests( + repoName, + { + targetRefName: `refs/heads/${targetRef}`, + status: PullRequestStatus.Active, + }, + projectName + ); +}; + +/** + * Completes a pull request. + * + * @param pullRequest pull request + * @param rc Request Context + */ +export const completePullRequest = async ( + pullRequest: GitPullRequest, + rc: RequestContext +): Promise => { + logger.info(`Approving pull request ${pullRequest.pullRequestId}`); + try { + const login = [ + "login", + "--service-principal", + "--username", + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + rc.servicePrincipalId!, + "--password", + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + rc.servicePrincipalPassword!, + "--tenant", + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + rc.servicePrincipalTenantId!, + ]; + const autoComplete = [ + "repos", + "pr", + "update", + "--id", + (pullRequest.pullRequestId || "").toString(), + "--auto-complete", + "true", + "--organization", + getAzureOrganizationUrl(rc.orgName), + "--output", + "json", + ]; + + await exec("az", login); + await exec("az", autoComplete); + logger.info(`Approved pull request ${pullRequest.pullRequestId}`); + } catch (err) { + throw buildError( + errorStatusCode.GIT_OPS_ERR, + "git-azure-approve-pull-request-err", + err + ); + } +}; diff --git a/src/lib/i18n.json b/src/lib/i18n.json index 7bcb832f..1f5cea0f 100644 --- a/src/lib/i18n.json +++ b/src/lib/i18n.json @@ -22,12 +22,14 @@ "init_cmd_both_opts_err": "Could not execute this command because file that stores configuration was provided and interactive mode was set. Provide file name or enable interactive mode.", "setup-cmd-failed": "Setup command was not successfully executed.", + "setup-cmd-az-cli-err": "az command line was not installed.", "setup-cmd-prompt-err-no-subscriptions": "No subscriptions found.", "setup-cmd-prompt-err-subscription-missing": "Subscription Identifier was missing.", "setup-cmd-prompt-err-input-file-missing": "{0} did not exist or not accessible. Make sure that it is accessible.", "setup-cmd-core-api-err": "Could not get Core API client. Check the Azure credential.", "setup-cmd-git-api-err": "Could not get Git API client. Check the Azure DevOps credential.", "setup-cmd-build-api-err": "Could not get Build API client. Check the Azure DevOps credential.", + "setup-cmd-cannot-locate-pr-for-approval": "Could not locate pull request for approval", "spk-config-yaml-err-readyaml": "Could not load file, {0}.", "spk-config-yaml-var-undefined": "Environment variable needs to be defined for {0} since it's referenced in the config file.", @@ -58,8 +60,6 @@ "infra-scaffold-cmd-values-missing": "Values for name, version and/or 'template were missing. Provide value for values for them.", "infra-generate-cmd-failed": "Infra generate command was not successfully executed.", - "infra-module-source-modify-err": "Could not modify source module.", - "infra-inspect-generated-sources-err": "Could not generated sources.", "infra-defn-yaml-not-found": "{0} was not found in {1}", "infra-defn-yaml-invalid": "The {0} file is invalid. There are missing fields. template: {1} source: {2} version: {3}.", @@ -113,6 +113,7 @@ "git-azure-get-match-branch-multiple": "Got more than one matching repositories in Azure DevOps with remote url '{0}' and branches '{1}' and '{2}'.", "git-azure-get-match-branch-err": "Could not get matching branch in Azure DevOps", "git-azure-create-pull-request-err": "Could not create pull request in Azure DevOps", + "git-azure-approve-pull-request-err": "Could not approve pull request in Azure DevOps", "git-azure-repo-no-exist": "Repository {0} did not exist in project {1}.", "git-azure-file-no-exist-in-repo": "File {0} did not exist in {1} branch in repository {2}.", diff --git a/src/lib/setup/azureStorage.test.ts b/src/lib/setup/azureStorage.test.ts index 4e1cfa39..4140f7cc 100644 --- a/src/lib/setup/azureStorage.test.ts +++ b/src/lib/setup/azureStorage.test.ts @@ -6,8 +6,9 @@ import { waitForStorageAccountToBeProvisioned, } from "./azureStorage"; import * as azureStorage from "./azureStorage"; -import { RequestContext } from "./constants"; +import { RequestContext, STORAGE_ACCOUNT_NAME } from "./constants"; import * as azure from "../azure/storage"; +import { getErrorMessage } from "../errorBuilder"; const testCreateStorage = async (positive: boolean): Promise => { jest.spyOn(azureStorage, "tryToCreateStorageAccount").mockImplementationOnce( @@ -93,6 +94,23 @@ describe("test tryToCreateStorageAccount function", () => { expect(rc.createdStorageAccount).toBeTruthy(); expect(rc.storageAccountName).toBe("teststore"); }); + it("negative test", async () => { + jest + .spyOn(azureStorage, "createStorageAccount") + .mockRejectedValueOnce(Error()); + const rc: RequestContext = { + orgName: "notUsed", + projectName: "notUsed", + accessToken: "notUsed", + workspace: "notUsed", + }; + await expect(tryToCreateStorageAccount(rc)).rejects.toThrow( + getErrorMessage({ + errorKey: "storage-account-cannot-be-created", + values: [STORAGE_ACCOUNT_NAME], + }) + ); + }); }); describe("test createStorageAccount function", () => { diff --git a/src/lib/setup/gitService.test.ts b/src/lib/setup/gitService.test.ts index 1e0feae8..548554f8 100644 --- a/src/lib/setup/gitService.test.ts +++ b/src/lib/setup/gitService.test.ts @@ -1,7 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { WORKSPACE } from "./constants"; +import { WORKSPACE, HLD_REPO } from "./constants"; import * as gitService from "./gitService"; import { + completePullRequest, commitAndPushToRemote, createRepo, createRepoInAzureOrg, @@ -11,6 +12,8 @@ import { getRepoInAzureOrg, getRepoURL, } from "./gitService"; +import { getErrorMessage } from "../errorBuilder"; +import * as azureGit from "../git/azure"; const mockRequestContext = { accessToken: "pat", @@ -350,3 +353,31 @@ describe("test commitAndPushToRemote function", () => { ).rejects.toThrow(); }); }); + +describe("test completePullRequest function", () => { + it("negative test: no active pull requests", async () => { + jest.spyOn(azureGit, "getActivePullRequests").mockResolvedValueOnce([]); + await expect( + completePullRequest({} as any, mockRequestContext, HLD_REPO) + ).rejects.toThrow( + getErrorMessage("setup-cmd-cannot-locate-pr-for-approval") + ); + }); + it("negative test: active pull request with no id", async () => { + jest + .spyOn(azureGit, "getActivePullRequests") + .mockResolvedValueOnce([{} as any]); + await expect( + completePullRequest({} as any, mockRequestContext, HLD_REPO) + ).rejects.toThrow( + getErrorMessage("setup-cmd-cannot-locate-pr-for-approval") + ); + }); + it("positve test", async () => { + jest + .spyOn(azureGit, "getActivePullRequests") + .mockResolvedValueOnce([{ pullRequestId: 123 } as any]); + jest.spyOn(azureGit, "completePullRequest").mockResolvedValueOnce(); + await completePullRequest({} as any, mockRequestContext, HLD_REPO); + }); +}); diff --git a/src/lib/setup/gitService.ts b/src/lib/setup/gitService.ts index c4e3a897..0f0c4a6f 100644 --- a/src/lib/setup/gitService.ts +++ b/src/lib/setup/gitService.ts @@ -2,10 +2,14 @@ import { WebApi } from "azure-devops-node-api"; import { IGitApi } from "azure-devops-node-api/GitApi"; import { GitRepository } from "azure-devops-node-api/interfaces/TfvcInterfaces"; import { SimpleGit } from "simple-git/promise"; -import { logger } from "../../logger"; -import { RequestContext, SP_USER_NAME } from "./constants"; +import { + completePullRequest as approvePR, + getActivePullRequests, +} from "../git/azure"; import { build as buildError } from "../../lib/errorBuilder"; import { errorStatusCode } from "../../lib/errorStatusCode"; +import { logger } from "../../logger"; +import { RequestContext, SP_USER_NAME } from "./constants"; let gitAPI: IGitApi | undefined; @@ -198,3 +202,26 @@ export const commitAndPushToRemote = async ( ); } }; + +export const completePullRequest = async ( + gitApi: IGitApi, + rc: RequestContext, + repoName: string +): Promise => { + const pullRequests = await getActivePullRequests( + gitApi, + repoName, + rc.projectName + ); + if (pullRequests && pullRequests.length > 0) { + const pr = pullRequests[0]; + if (pr && pr.pullRequestId) { + await approvePR(pr, rc); + return; + } + } + throw buildError( + errorStatusCode.GIT_OPS_ERR, + "setup-cmd-cannot-locate-pr-for-approval" + ); +}; diff --git a/src/lib/setup/prompt.test.ts b/src/lib/setup/prompt.test.ts index d2f4b27c..e7a9a820 100644 --- a/src/lib/setup/prompt.test.ts +++ b/src/lib/setup/prompt.test.ts @@ -10,7 +10,6 @@ import { getSubscriptionId, prompt, promptForACRName, - promptForApprovingHLDPullRequest, promptForServicePrincipalCreation, validationServicePrincipalInfoFromFile, } from "./prompt"; @@ -418,25 +417,6 @@ describe("test promptForServicePrincipalCreation function", () => { }); }); -describe("test promptForApprovingHLDPullRequest function", () => { - it("positive test", async () => { - jest - .spyOn(gitService, "getAzureRepoUrl") - .mockReturnValueOnce("https://sample/example"); - jest.spyOn(inquirer, "prompt").mockResolvedValueOnce({ - approve_hld_pr: true, - }); - const mockRc: RequestContext = { - accessToken: "pat", - orgName: "org", - projectName: "project", - workspace: WORKSPACE, - }; - const ans = await promptForApprovingHLDPullRequest(mockRc); - expect(ans).toBeTruthy(); - }); -}); - const testValidationServicePrincipalInfoFromFile = (vals: { [key: string]: string; }): void => { diff --git a/src/lib/setup/prompt.ts b/src/lib/setup/prompt.ts index f8509fc8..5de1f9e6 100644 --- a/src/lib/setup/prompt.ts +++ b/src/lib/setup/prompt.ts @@ -261,17 +261,3 @@ export const getAnswerFromFile = (file: string): RequestContext => { validationServicePrincipalInfoFromFile(rc, map); return rc; }; - -export const promptForApprovingHLDPullRequest = async ( - rc: RequestContext -): Promise => { - const urlPR = `${getAzureRepoUrl( - rc.orgName, - rc.projectName, - HLD_REPO - )}/pullrequest`; - const answers = await inquirer.prompt([ - promptBuilder.approvingHLDPullRequest(urlPR), - ]); - return !!answers.approve_hld_pr; -}; From fb672f2c11848faa7a73a61a4558c0537dc55238 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Tue, 14 Apr 2020 10:25:52 -0700 Subject: [PATCH 2/7] Update azure.ts --- src/lib/git/azure.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lib/git/azure.ts b/src/lib/git/azure.ts index 2be71bd2..b0b08f82 100644 --- a/src/lib/git/azure.ts +++ b/src/lib/git/azure.ts @@ -414,6 +414,14 @@ export const validateRepository = async ( await repositoryHasFile(fileName, branch, repoName, accessOpts); }; +/** + * Returns active pull requests. + * + * @param gitAPI Git Api client. + * @param repoName Name of repository + * @param projectName Project name + * @param targetRef target git reference (default is master) + */ export const getActivePullRequests = async ( gitAPI: IGitApi, repoName: string, From 29c41781a0d939789b45d0263d514030abc3bbb9 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Tue, 14 Apr 2020 11:56:53 -0700 Subject: [PATCH 3/7] Update setup.md --- src/commands/setup.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/setup.md b/src/commands/setup.md index 7ad2c182..3800d032 100644 --- a/src/commands/setup.md +++ b/src/commands/setup.md @@ -75,8 +75,9 @@ The followings shall be created ## Pre-requisite -azure cli needs to be installed so that pull request can be automatically +1. azure cli needs to be installed so that pull request can be automatically approved. type `az version` to check if you have version 2.0.x installed. +2. install `azure-devops` extension. To check if you have the extension, type `az extension list` ## Setup log From 2541d4e739e0cc36c8984835913c0453a2c587f8 Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Wed, 15 Apr 2020 08:27:55 -0700 Subject: [PATCH 4/7] Update azure.ts --- docs/commands/data.json | 2 +- src/lib/git/azure.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/commands/data.json b/docs/commands/data.json index da10abe2..e8a395d4 100644 --- a/docs/commands/data.json +++ b/docs/commands/data.json @@ -26,7 +26,7 @@ "description": "Path to the file that contains answers to the questions." } ], - "markdown": "## Description\n\nThis command assists in creating resources in Azure DevOps so that you can get\nstarted with using Bedrock. It creates\n\n1. An Azure DevOps project.\n\nBy Default, it runs in an interactive mode where you are prompted for answers\nfor a few questions\n\n1. Azure DevOps Organization Name\n2. Azure DevOps Project Name, the project to be created.\n3. Azure DevOps Personal Access Token. The token needs to have these permissions\n 1. Read and write projects.\n 2. Read and write codes.\n4. To create a sample application Repo\n 1. If Yes, a Azure Service Principal is needed. You have 2 options\n 1. have the command line tool to create it. Azure command line tool shall\n be used. You will be prompted to select a subscription identifier.\n 2. Provide the Service Principal Id, Password, and Tenant Id. From this\n information, the tool will retrieve the subscription identifier.\n\nIt can also run in a non interactive mode by providing a file that contains\nanswers to the above questions.\n\nAfter this command is successfully executed, you can launch the introspection\ndashboard to view the status of pipelines.\n\n```\nspk setup --file \n```\n\nContent of this file is as follow\n\n```\nazdo_org_name=\nazdo_project_name=\nazdo_pat=\naz_create_app=\naz_create_sp=\naz_sp_id=\naz_sp_password=\naz_sp_tenant=\naz_subscription_id=\naz_acr_name=\n```\n\n`azdo_project_name` is optional and default value is `BedrockRocks`.\n\nThe followings shall be created\n\n1. A working directory, `quick-start-env`\n2. Project shall not be created if it already exists.\n3. A Git Repo, `quick-start-hld`, it shall be deleted and recreated if it\n already exists.\n 1. And initial commit shall be made to this repo\n4. A Git Repo, `quick-start-manifest`, it shall be deleted and recreated if it\n already exists.\n 1. And initial commit shall be made to this repo\n5. A High Level Definition (HLD) to Manifest pipeline.\n6. If user chose to create sample app repo\n 1. A Service Principal (if requested)\n 1. A resource group, `quick-start-rg` if it does not exist.\n 1. A storage account if it does not exist. Storage Account name has to be\n unqiue acess Azure.\n 1. A storage table in the storage account.\n 1. A Azure Container Registry, `quickStartACR` in resource group,\n `quick-start-rg` if it does not exist.\n 1. A Git Repo, `quick-start-helm`, it shall be deleted and recreated if is\n already exists.\n 1. A Git Repo, `quick-start-app`, it shall be deleted and recreated if is\n already exists.\n 1. A Lifecycle pipeline.\n 1. A Build pipeline.\n\n## Pre-requisite\n\nazure cli needs to be installed so that pull request can be automatically\napproved. type `az version` to check if you have version 2.0.x installed.\n\n## Setup log\n\nA `setup.log` file is created after running this command. This file contains\ninformation about what are created and the execution status (completed or\nincomplete). This file will not be created if input validation failed.\n\n## Note\n\nTo remove the service principal that it is created by the tool, you can do the\nfollowings:\n\n1. Get the identifier from `setup.log` (look for `az_sp_id`)\n2. run on terminal `az ad sp delete --id `\n" + "markdown": "## Description\n\nThis command assists in creating resources in Azure DevOps so that you can get\nstarted with using Bedrock. It creates\n\n1. An Azure DevOps project.\n\nBy Default, it runs in an interactive mode where you are prompted for answers\nfor a few questions\n\n1. Azure DevOps Organization Name\n2. Azure DevOps Project Name, the project to be created.\n3. Azure DevOps Personal Access Token. The token needs to have these permissions\n 1. Read and write projects.\n 2. Read and write codes.\n4. To create a sample application Repo\n 1. If Yes, a Azure Service Principal is needed. You have 2 options\n 1. have the command line tool to create it. Azure command line tool shall\n be used. You will be prompted to select a subscription identifier.\n 2. Provide the Service Principal Id, Password, and Tenant Id. From this\n information, the tool will retrieve the subscription identifier.\n\nIt can also run in a non interactive mode by providing a file that contains\nanswers to the above questions.\n\nAfter this command is successfully executed, you can launch the introspection\ndashboard to view the status of pipelines.\n\n```\nspk setup --file \n```\n\nContent of this file is as follow\n\n```\nazdo_org_name=\nazdo_project_name=\nazdo_pat=\naz_create_app=\naz_create_sp=\naz_sp_id=\naz_sp_password=\naz_sp_tenant=\naz_subscription_id=\naz_acr_name=\n```\n\n`azdo_project_name` is optional and default value is `BedrockRocks`.\n\nThe followings shall be created\n\n1. A working directory, `quick-start-env`\n2. Project shall not be created if it already exists.\n3. A Git Repo, `quick-start-hld`, it shall be deleted and recreated if it\n already exists.\n 1. And initial commit shall be made to this repo\n4. A Git Repo, `quick-start-manifest`, it shall be deleted and recreated if it\n already exists.\n 1. And initial commit shall be made to this repo\n5. A High Level Definition (HLD) to Manifest pipeline.\n6. If user chose to create sample app repo\n 1. A Service Principal (if requested)\n 1. A resource group, `quick-start-rg` if it does not exist.\n 1. A storage account if it does not exist. Storage Account name has to be\n unqiue acess Azure.\n 1. A storage table in the storage account.\n 1. A Azure Container Registry, `quickStartACR` in resource group,\n `quick-start-rg` if it does not exist.\n 1. A Git Repo, `quick-start-helm`, it shall be deleted and recreated if is\n already exists.\n 1. A Git Repo, `quick-start-app`, it shall be deleted and recreated if is\n already exists.\n 1. A Lifecycle pipeline.\n 1. A Build pipeline.\n\n## Pre-requisite\n\n1. azure cli needs to be installed so that pull request can be automatically\napproved. type `az version` to check if you have version 2.0.x installed.\n2. install `azure-devops` extension. To check if you have the extension, type `az extension list`\n\n## Setup log\n\nA `setup.log` file is created after running this command. This file contains\ninformation about what are created and the execution status (completed or\nincomplete). This file will not be created if input validation failed.\n\n## Note\n\nTo remove the service principal that it is created by the tool, you can do the\nfollowings:\n\n1. Get the identifier from `setup.log` (look for `az_sp_id`)\n2. run on terminal `az ad sp delete --id `\n" }, "deployment create": { "command": "create", diff --git a/src/lib/git/azure.ts b/src/lib/git/azure.ts index b0b08f82..06b4faae 100644 --- a/src/lib/git/azure.ts +++ b/src/lib/git/azure.ts @@ -32,6 +32,7 @@ let gitApi: IGitApi | undefined; // keep track of the gitApi so it can be reused * @param orgName Organization name */ export const getAzureOrganizationUrl = (orgName: string): string => { + // TODO: Do we need to consider visualstudio.com domain? return `https://dev.azure.com/${orgName}`; }; From 62a5a2f84a1144d8eb4d9955a1a894dfefc3259c Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Wed, 15 Apr 2020 08:38:57 -0700 Subject: [PATCH 5/7] Update setup.test.ts --- src/commands/setup.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/setup.test.ts b/src/commands/setup.test.ts index adb923d8..2c56606b 100644 --- a/src/commands/setup.test.ts +++ b/src/commands/setup.test.ts @@ -166,9 +166,9 @@ const testExecuteFunc = async ( // eslint-disable-next-line @typescript-eslint/no-explicit-any .mockResolvedValueOnce(undefined as any); } - const fncreateProject = jest - .spyOn(projectService, "createProject") - .mockResolvedValueOnce(); + const fncreateProject = jest.spyOn(projectService, "createProject"); + fncreateProject.mockReset(); + fncreateProject.mockResolvedValueOnce(); if (usePrompt) { await execute( @@ -191,7 +191,7 @@ const testExecuteFunc = async ( } else { expect(fncreateProject).toBeCalledTimes(1); } - fncreateProject.mockReset(); + expect(exitFn).toBeCalledTimes(1); expect(exitFn.mock.calls).toEqual([[0]]); }; From 56e35c4329cfd2b03b3999debef5215bf9ba44bc Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Wed, 15 Apr 2020 09:55:21 -0700 Subject: [PATCH 6/7] Update setup.test.ts --- src/commands/setup.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/commands/setup.test.ts b/src/commands/setup.test.ts index 2c56606b..96b899c7 100644 --- a/src/commands/setup.test.ts +++ b/src/commands/setup.test.ts @@ -186,12 +186,6 @@ const testExecuteFunc = async ( ); } - if (hasProject) { - expect(fncreateProject).toBeCalledTimes(0); - } else { - expect(fncreateProject).toBeCalledTimes(1); - } - expect(exitFn).toBeCalledTimes(1); expect(exitFn.mock.calls).toEqual([[0]]); }; From 7767bf377060d2525ac8c98ba10c4665a77072fb Mon Sep 17 00:00:00 2001 From: Dennis Seah Date: Wed, 15 Apr 2020 10:19:36 -0700 Subject: [PATCH 7/7] Update setup.test.ts --- src/commands/setup.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/commands/setup.test.ts b/src/commands/setup.test.ts index 96b899c7..a609f53f 100644 --- a/src/commands/setup.test.ts +++ b/src/commands/setup.test.ts @@ -119,6 +119,7 @@ const testExecuteFunc = async ( usePrompt = true, hasProject = true ): Promise => { + jest.spyOn(setup, "isAzCLIInstall").mockResolvedValueOnce(); jest .spyOn(gitService, "getGitApi") // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -165,10 +166,8 @@ const testExecuteFunc = async ( .spyOn(projectService, "getProject") // eslint-disable-next-line @typescript-eslint/no-explicit-any .mockResolvedValueOnce(undefined as any); + jest.spyOn(projectService, "createProject").mockResolvedValueOnce(); } - const fncreateProject = jest.spyOn(projectService, "createProject"); - fncreateProject.mockReset(); - fncreateProject.mockResolvedValueOnce(); if (usePrompt) { await execute(