diff --git a/docs/commands/data.json b/docs/commands/data.json index c2d91183..5bf87418 100644 --- a/docs/commands/data.json +++ b/docs/commands/data.json @@ -153,7 +153,7 @@ { "arg": "-t, --top ", "description": "Return only top N most recent deployments", - "defaultValue": "" + "defaultValue": "50" }, { "arg": "-o, --output ", @@ -164,6 +164,11 @@ "arg": "-w, --watch", "description": "Watch the deployments for a live view", "defaultValue": false + }, + { + "arg": "-h, --hide-separators", + "description": "Display the table without separators between columns", + "defaultValue": false } ], "markdown": "## Description\n\nThis commands retrieves the list of deployments by service name, release\nenvironment, build ID, commit ID, or container image tag.\n" diff --git a/package.json b/package.json index 1b3a873a..9ce2728d 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "open": "^6.4.0", "shelljs": "^0.8.3", "simple-git": "^1.126.0", - "spektate": "^1.0.6", + "spektate": "^1.0.11", "ssh-url": "^0.1.5", "uuid": "^3.3.3", "winston": "^3.2.1" diff --git a/src/commands/deployment/get.decorator.json b/src/commands/deployment/get.decorator.json index 2a408d0a..04d56235 100644 --- a/src/commands/deployment/get.decorator.json +++ b/src/commands/deployment/get.decorator.json @@ -36,7 +36,7 @@ { "arg": "-t, --top ", "description": "Return only top N most recent deployments", - "defaultValue": "" + "defaultValue": "50" }, { "arg": "-o, --output ", @@ -47,6 +47,11 @@ "arg": "-w, --watch", "description": "Watch the deployments for a live view", "defaultValue": false + }, + { + "arg": "-h, --hide-separators", + "description": "Display the table without separators between columns", + "defaultValue": false } ] } diff --git a/src/commands/deployment/get.test.ts b/src/commands/deployment/get.test.ts index 383c836d..0fc95bec 100644 --- a/src/commands/deployment/get.test.ts +++ b/src/commands/deployment/get.test.ts @@ -5,6 +5,7 @@ import * as AzureDevOpsRepo from "spektate/lib/repository/IAzureDevOpsRepo"; import * as GitHub from "spektate/lib/repository/IGitHub"; import { ITag } from "spektate/lib/repository/Tag"; import { loadConfiguration } from "../../config"; +import * as config from "../../config"; import { getErrorMessage } from "../../lib/errorBuilder"; import { deepClone } from "../../lib/util"; import { @@ -39,6 +40,7 @@ const MOCKED_INPUT_VALUES: CommandOptions = { service: "", top: "", watch: false, + hideSeparators: false, }; const MOCKED_VALUES: ValidatedOptions = { @@ -53,6 +55,7 @@ const MOCKED_VALUES: ValidatedOptions = { service: "", top: "", watch: false, + hideSeparators: false, }; const getMockedInputValues = (): CommandOptions => { @@ -70,9 +73,13 @@ const fakeDeployments = data; const fakeClusterSyncs = require("./mocks/cluster-sync.json"); // eslint-disable-next-line @typescript-eslint/no-var-requires const fakePR = require("./mocks/pr.json"); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const fakeAuthor = require("./mocks/author.json"); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const fakeUnmergedPR = require("./mocks/unmerged-pr.json"); const mockedDeps: IDeployment[] = fakeDeployments.data.map( (dep: IDeployment) => { - return { + const newDep = { commitId: dep.commitId, deploymentId: dep.deploymentId, dockerToHldRelease: dep.dockerToHldRelease, @@ -90,6 +97,21 @@ const mockedDeps: IDeployment[] = fakeDeployments.data.map( srcToDockerBuild: dep.srcToDockerBuild, timeStamp: dep.timeStamp, }; + // Since json data has dates in string format, convert them to dates + const builds = [ + newDep.srcToDockerBuild, + newDep.hldToManifestBuild, + newDep.dockerToHldRelease, + newDep.dockerToHldReleaseStage, + ]; + builds.forEach((build) => { + if (build) { + build.startTime = new Date(build.startTime); + build.queueTime = new Date(build.queueTime); + build.finishTime = new Date(build.finishTime); + } + }); + return newDep; } ); @@ -103,6 +125,9 @@ jest .spyOn(AzureDevOpsRepo, "getManifestSyncState") .mockReturnValue(Promise.resolve(mockedClusterSyncs)); jest.spyOn(Deployment, "fetchPR").mockReturnValue(Promise.resolve(fakePR)); +jest + .spyOn(Deployment, "fetchAuthor") + .mockReturnValue(Promise.resolve(fakeAuthor)); let initObject: InitObject; @@ -130,7 +155,7 @@ describe("Test getStatus function", () => { expect(getStatus("succeeded")).toBe("\u2713"); }); it("with empty string as value", () => { - expect(getStatus("")).toBe("..."); + expect(getStatus("")).toBe(""); }); it("with other string as value", () => { expect(getStatus("test")).toBe("\u0445"); @@ -246,8 +271,8 @@ describe("Get deployments", () => { values.outputFormat = OUTPUT_FORMAT.WIDE; const deployments = await getDeployments(initObject, values); expect(deployments).not.toBeUndefined(); - expect(deployments.length).not.toBeUndefined(); - logger.info("Got " + deployments.length + " deployments"); + expect(deployments!.length).not.toBeUndefined(); + logger.info("Got " + deployments!.length + " deployments"); expect(deployments).toHaveLength(10); }); it("getDeploymentsBasedOnFilters throw error", async () => { @@ -314,36 +339,40 @@ describe("Print deployments", () => { mockedDeps, processOutputFormat("normal"), undefined, - mockedClusterSyncs + mockedClusterSyncs, + true ); expect(table).not.toBeUndefined(); const deployment = [ - "2019-08-30T21:05:19.047Z", + "Complete", "hello-bedrock", - "c626394", - 6046, + "dev", "hello-bedrock-master-6046", + 6046, + "c626394", "✓", 180, - "DEV", "706685f", "✓", 6047, + "b3a3345", "✓", - "EUROPE", + "3.41 mins", ]; expect(table).toBeDefined(); if (table) { - //Use date (index 0) as matching filter - const matchItems = table.filter((field) => field[0] === deployment[0]); + // Use image tag (index 3) as matching filter + const matchItems = table.filter( + (field: any) => field[3] === deployment[3] + ); expect(matchItems).toHaveLength(1); // one matching row (matchItems[0] as IDeployment[]).forEach((field, i) => { expect(field).toEqual(deployment[i]); }); - expect(matchItems[0]).toHaveLength(13); + expect(matchItems[0]).toHaveLength(14); table = printDeployments( mockedDeps, @@ -372,7 +401,6 @@ describe("Cluster sync", () => { expect(clusterSyncs[0].tagger).toBe("Weave Flux"); } }); - test("Verify cluster syncs - empty", async () => { // test empty manifest scenario if (initObject.manifestRepo) { @@ -383,13 +411,62 @@ describe("Cluster sync", () => { }); }); +describe("Fetch Author/PR", () => { + test("Throws exception", async () => { + jest.spyOn(Deployment, "fetchPR").mockClear(); + jest + .spyOn(Deployment, "fetchPR") + .mockRejectedValueOnce(Error("Server Error")); + jest.spyOn(Deployment, "fetchAuthor").mockClear(); + jest + .spyOn(Deployment, "fetchAuthor") + .mockRejectedValueOnce(Error("Server Error")); + MOCKED_VALUES.outputFormat = OUTPUT_FORMAT.WIDE; + MOCKED_VALUES.nTop = 10; + const table = get.displayDeployments( + MOCKED_VALUES, + mockedDeps, + mockedClusterSyncs, + initObject + ); + expect(table).toBeDefined(); + }); + test("Unmerged PR", async () => { + jest.spyOn(Deployment, "fetchPR").mockClear(); + jest.spyOn(Deployment, "fetchPR").mockReturnValue(fakeUnmergedPR); + MOCKED_VALUES.outputFormat = OUTPUT_FORMAT.WIDE; + MOCKED_VALUES.nTop = 10; + const table = await get.displayDeployments( + MOCKED_VALUES, + mockedDeps, + mockedClusterSyncs, + initObject + ); + expect(table).toBeDefined(); + }); +}); + describe("Output formats", () => { test("verify wide output", () => { - const table = printDeployments( + let table = printDeployments( mockedDeps, processOutputFormat("wide"), undefined, - mockedClusterSyncs + mockedClusterSyncs, + false + ); + expect(table).toBeDefined(); + + if (table) { + table.forEach((field) => expect(field).toHaveLength(23)); + } + + table = printDeployments( + mockedDeps, + processOutputFormat("wide"), + undefined, + mockedClusterSyncs, + true ); expect(table).toBeDefined(); @@ -397,4 +474,64 @@ describe("Output formats", () => { table.forEach((field) => expect(field).toHaveLength(19)); } }); + test("verify json output", () => { + MOCKED_VALUES.outputFormat = OUTPUT_FORMAT.JSON; + MOCKED_VALUES.nTop = 10; + const consoleSpy = jest.spyOn(console, "log"); + const table = get.displayDeployments( + MOCKED_VALUES, + mockedDeps, + mockedClusterSyncs, + initObject + ); + expect(table).toBeDefined(); + expect(consoleSpy).toHaveBeenCalled(); + expect(consoleSpy).toHaveBeenCalledWith( + JSON.stringify(mockedDeps, null, 2) + ); + }); + test("verify separators output", async () => { + MOCKED_VALUES.outputFormat = OUTPUT_FORMAT.WIDE; + MOCKED_VALUES.nTop = 1; + MOCKED_VALUES.hideSeparators = true; + let table = await get.displayDeployments( + MOCKED_VALUES, + mockedDeps, + mockedClusterSyncs, + initObject + ); + expect(table).toBeDefined(); + expect(table).toHaveLength(1); + expect(JSON.stringify(table)).not.toContain("│"); + MOCKED_VALUES.hideSeparators = false; + table = await get.displayDeployments( + MOCKED_VALUES, + mockedDeps, + mockedClusterSyncs, + initObject + ); + expect(table).toBeDefined(); + expect(table).toHaveLength(1); + expect(JSON.stringify(table)).toContain("│"); + }); + test("verify separators output", async () => { + MOCKED_VALUES.nTop = 3; + const table = await get.displayDeployments( + MOCKED_VALUES, + mockedDeps, + mockedClusterSyncs, + initObject + ); + expect(table).toBeDefined(); + expect(table).toHaveLength(3); + }); +}); + +describe("Initialization", () => { + test("verify init error", async () => { + jest.spyOn(config, "Config").mockReturnValueOnce({}); + await initialize().catch((e) => { + expect(e).toBeDefined(); + }); + }); }); diff --git a/src/commands/deployment/get.ts b/src/commands/deployment/get.ts index ed9dd9ab..d06f4f75 100644 --- a/src/commands/deployment/get.ts +++ b/src/commands/deployment/get.ts @@ -6,6 +6,7 @@ import { IDeployment, status as getDeploymentStatus, fetchPR, + fetchAuthor, getRepositoryFromURL, } from "spektate/lib/IDeployment"; import AzureDevOpsPipeline from "spektate/lib/pipeline/AzureDevOpsPipeline"; @@ -26,9 +27,15 @@ import { isIntegerString } from "../../lib/validator"; import { logger } from "../../logger"; import decorator from "./get.decorator.json"; import { IPullRequest } from "spektate/lib/repository/IPullRequest"; +import { IAuthor } from "spektate/lib/repository/Author"; +import { + DeploymentRow, + printDeploymentTable, +} from "../../lib/azure/deploymenttable"; const promises: Promise[] = []; const pullRequests: { [id: string]: IPullRequest } = {}; +const authors: { [id: string]: IAuthor } = {}; /** * Output formats to display service details */ @@ -68,6 +75,7 @@ export interface CommandOptions { service: string; deploymentId: string; top: string; + hideSeparators: boolean; } /** @@ -112,7 +120,6 @@ export const validateValues = (opts: CommandOptions): ValidatedOptions => { }); } } - return { buildId: opts.buildId, commitId: opts.commitId, @@ -125,6 +132,7 @@ export const validateValues = (opts: CommandOptions): ValidatedOptions => { service: opts.service, top: opts.top, watch: opts.watch, + hideSeparators: opts.hideSeparators, }; }; @@ -156,20 +164,17 @@ export const initialize = async (): Promise => { clusterPipeline: new AzureDevOpsPipeline( config.azure_devops.org, config.azure_devops.project, - false, config.azure_devops.access_token ), hldPipeline: new AzureDevOpsPipeline( config.azure_devops.org, config.azure_devops.project, - true, config.azure_devops.access_token ), key: config.introspection.azure.key, srcPipeline: new AzureDevOpsPipeline( config.azure_devops.org, config.azure_devops.project, - false, config.azure_devops.access_token ), accountName: config.introspection.azure.account_name, @@ -214,11 +219,46 @@ export const getClusterSyncStatuses = async ( return undefined; } } catch (err) { - throw buildError( - errorStatusCode.GIT_OPS_ERR, - "introspect-get-cmd-cluster-sync-stat-err", - err - ); + // Certainly don't want to fail the get command if cluster sync failed + // log for debugging + logger.verbose(`Could not get cluster sync status ` + err); + } +}; + +/** + * Fetches author data for deployments + * + * @param deployment deployment for which author has to be fetched + * @param initObj initialization object + */ +export const fetchAuthorInformation = async ( + deployment: IDeployment, + initObj: InitObject +): Promise => { + let commitId = + deployment.srcToDockerBuild?.sourceVersion || + deployment.hldToManifestBuild?.sourceVersion; + let repo: IAzureDevOpsRepo | IGitHub | undefined = + deployment.srcToDockerBuild?.repository || + deployment.hldToManifestBuild?.repository; + if (!repo && deployment.sourceRepo) { + repo = getRepositoryFromURL(deployment.sourceRepo); + commitId = deployment.srcToDockerBuild?.sourceVersion; + } + if (!repo && deployment.hldRepo) { + repo = getRepositoryFromURL(deployment.hldRepo); + commitId = deployment.hldToManifestBuild?.sourceVersion; + } + if (repo && commitId !== "") { + try { + const author = await fetchAuthor(repo, commitId!, initObj.accessToken); + if (author) { + authors[deployment.deploymentId] = author; + } + } catch (err) { + // verbose log, to make sure printed response from get command is scriptable + logger.verbose(`Could not get author for ${commitId}: ` + err); + } } }; @@ -241,15 +281,31 @@ export const fetchPRInformation = async ( try { const pr = await fetchPR(repo, strPr, initObj.accessToken); if (pr) { - pullRequests[strPr] = pr; + pullRequests[deployment.deploymentId] = pr; } } catch (err) { - logger.warn(`Could not get PR ${strPr} information: ` + err); + // verbose log, to make sure printed response from get command is scriptable + logger.verbose(`Could not get PR ${strPr} information: ` + err); } } } }; +/** + * Gets author information for all the deployments. + * + * @param deployments all deployments to be displayed + * @param initObj initialization object + */ +export const getAuthors = ( + deployments: IDeployment[] | undefined, + initObj: InitObject +): void => { + (deployments || []).forEach((d) => { + promises.push(fetchAuthorInformation(d, initObj)); + }); +}; + /** * Gets PR information for all the deployments. * @@ -271,11 +327,11 @@ export const getPRs = ( * @param status Status * @return a status indicator icon */ -export const getStatus = (status: string): string => { - if (status === "succeeded") { +export const getStatus = (status?: string): string => { + if (!status) { + return ""; + } else if (status === "succeeded") { return "\u2713"; - } else if (!status) { - return "..."; } return "\u0445"; }; @@ -301,166 +357,69 @@ export const printDeployments = ( deployments: IDeployment[] | undefined, outputFormat: OUTPUT_FORMAT, limit?: number, - syncStatuses?: ITag[] | undefined + syncStatuses?: ITag[] | undefined, + removeSeparators?: boolean ): Table | undefined => { + const deploymentsToDisplay: DeploymentRow[] = []; if (deployments && deployments.length > 0) { - let header = [ - "Start Time", - "Service", - "Commit", - "Image Creation", - "Image Tag", - "Result", - "Metadata Update", - "Ring", - "Hld Commit", - "Result", - ]; - let prsExist = false; - if ( - Object.keys(pullRequests).length > 0 && - outputFormat === OUTPUT_FORMAT.WIDE - ) { - header = header.concat(["Approval PR", "Merged By"]); - prsExist = true; - } - header = header.concat(["Ready to Deploy", "Result"]); - if (outputFormat === OUTPUT_FORMAT.WIDE) { - header = header.concat([ - "Duration", - "Status", - "Manifest Commit", - "End Time", - ]); - } - if (syncStatuses && syncStatuses.length > 0) { - header = header.concat(["Cluster Sync"]); - } - - const table = new Table({ - head: header, - chars: { - top: "", - "top-mid": "", - "top-left": "", - "top-right": "", - bottom: "", - "bottom-mid": "", - "bottom-left": "", - "bottom-right": "", - left: "", - "left-mid": "", - mid: "", - "mid-mid": "", - right: "", - "right-mid": "", - middle: " ", - }, - style: { "padding-left": 0, "padding-right": 0 }, - }); - const toDisplay = limit ? deployments.slice(0, limit) : deployments; toDisplay.forEach((deployment) => { const row = []; - let deploymentStatus = getDeploymentStatus(deployment); - row.push( - deployment.srcToDockerBuild - ? deployment.srcToDockerBuild.startTime.toLocaleString() - : deployment.hldToManifestBuild - ? deployment.hldToManifestBuild.startTime.toLocaleString() - : "-" - ); - row.push(deployment.service !== "" ? deployment.service : "-"); - row.push(deployment.commitId !== "" ? deployment.commitId : "-"); - row.push( - deployment.srcToDockerBuild ? deployment.srcToDockerBuild.id : "-" - ); - row.push(deployment.imageTag !== "" ? deployment.imageTag : "-"); - row.push( - deployment.srcToDockerBuild - ? getStatus(deployment.srcToDockerBuild.result) - : "" - ); - - let dockerToHldId = "-"; - let dockerToHldStatus = ""; - - if (deployment.dockerToHldRelease) { - dockerToHldId = deployment.dockerToHldRelease.id; - dockerToHldStatus = getStatus(deployment.dockerToHldRelease.status); - } else if ( - deployment.dockerToHldReleaseStage && - deployment.srcToDockerBuild - ) { - dockerToHldId = deployment.dockerToHldReleaseStage.id; - dockerToHldStatus = getStatus(deployment.srcToDockerBuild.result); - } - row.push(dockerToHldId); - - row.push( - deployment.environment !== "" - ? deployment.environment.toUpperCase() - : "-" - ); - row.push(deployment.hldCommitId || "-"); - row.push(dockerToHldStatus); - - // Print PR if available - if ( - prsExist && - deployment.pr && - deployment.pr.toString() in pullRequests - ) { - row.push(deployment.pr); - if (pullRequests[deployment.pr.toString()].mergedBy) { - row.push(pullRequests[deployment.pr.toString()].mergedBy?.name); - } else { - deploymentStatus = "Waiting"; - row.push("-"); - } - } else if (prsExist) { - row.push("-"); - row.push("-"); + const deploymentToDisplay = { + status: getDeploymentStatus(deployment), + service: deployment.service, + env: deployment.environment, + author: + deployment.deploymentId in authors + ? authors[deployment.deploymentId].name + : undefined, + srctoAcrCommitId: deployment.commitId, + srcToAcrPipelineId: deployment.srcToDockerBuild?.id, + imageTag: deployment.imageTag, + srcToAcrResult: getStatus(deployment.srcToDockerBuild?.result), + AcrToHldPipelineId: deployment.dockerToHldRelease + ? deployment.dockerToHldRelease.id + : deployment.dockerToHldReleaseStage + ? deployment.dockerToHldReleaseStage.id + : undefined, + AcrToHldResult: getStatus( + deployment.dockerToHldRelease?.status || + deployment.dockerToHldReleaseStage?.result + ), + AcrToHldCommitId: deployment.hldCommitId, + pr: + deployment.deploymentId in pullRequests + ? pullRequests[deployment.deploymentId].id.toString() + : undefined, + mergedBy: + deployment.deploymentId in pullRequests + ? pullRequests[deployment.deploymentId].mergedBy?.name + : undefined, + HldToManifestPipelineId: deployment.hldToManifestBuild?.id, + HldToManifestResult: getStatus(deployment.hldToManifestBuild?.result), + HldToManifestCommitId: deployment.manifestCommitId, + duration: duration(deployment) + " mins", + startTime: + deployment.srcToDockerBuild?.startTime?.toLocaleString() || + deployment.hldToManifestBuild?.startTime?.toLocaleString(), + syncStatus: syncStatuses + ? getClusterSyncStatusForDeployment(deployment, syncStatuses)?.name + : undefined, + }; + if (deploymentToDisplay.pr && !deploymentToDisplay.mergedBy) { + deploymentToDisplay.status = "Waiting"; } - row.push( - deployment.hldToManifestBuild ? deployment.hldToManifestBuild.id : "-" - ); - row.push( - deployment.hldToManifestBuild - ? getStatus(deployment.hldToManifestBuild.result) - : "" - ); - if (outputFormat === OUTPUT_FORMAT.WIDE) { - row.push(duration(deployment) + " mins"); - row.push(deploymentStatus); - row.push(deployment.manifestCommitId || "-"); - row.push( - deployment.hldToManifestBuild && - deployment.hldToManifestBuild.finishTime && - !isNaN(new Date(deployment.hldToManifestBuild.finishTime).getTime()) - ? deployment.hldToManifestBuild.finishTime.toLocaleString() - : deployment.srcToDockerBuild && - deployment.srcToDockerBuild.finishTime && - !isNaN(new Date(deployment.srcToDockerBuild.finishTime).getTime()) - ? deployment.srcToDockerBuild.finishTime.toLocaleString() - : "-" - ); - } - if (syncStatuses && syncStatuses.length > 0) { - const tag = getClusterSyncStatusForDeployment(deployment, syncStatuses); - if (tag) { - row.push(tag.name); - } else { - row.push("-"); - } - } - table.push(row); + deploymentsToDisplay.push(deploymentToDisplay); }); - console.log(table.toString()); - return table; + return printDeploymentTable( + outputFormat, + deploymentsToDisplay, + removeSeparators, + deploymentsToDisplay.filter((d) => d.syncStatus).length > 0 + ); } else { logger.info("No deployments found for specified filters."); return undefined; @@ -479,9 +438,14 @@ export const displayDeployments = async ( deployments: IDeployment[] | undefined, syncStatuses: ITag[] | undefined, initObj: InitObject -): Promise => { +): Promise => { + if (deployments && values.nTop) { + deployments = deployments.slice(0, values.nTop); + } + // Show authors and PRs only in wide output, to keep default narrow output fast and quick if (values.outputFormat === OUTPUT_FORMAT.WIDE) { getPRs(deployments, initObj); + getAuthors(deployments, initObj); } if (values.outputFormat === OUTPUT_FORMAT.JSON) { @@ -490,8 +454,13 @@ export const displayDeployments = async ( } await Promise.all(promises); - printDeployments(deployments, values.outputFormat, values.nTop, syncStatuses); - return deployments || []; + return printDeployments( + deployments, + values.outputFormat, + values.nTop, + syncStatuses, + values.hideSeparators + ); }; /** @@ -502,7 +471,7 @@ export const displayDeployments = async ( export const getDeployments = async ( initObj: InitObject, values: ValidatedOptions -): Promise => { +): Promise => { try { const syncStatusesPromise = getClusterSyncStatuses(initObj); const deploymentsPromise = getDeploymentsBasedOnFilters( @@ -528,7 +497,8 @@ export const getDeployments = async ( const deployments: IDeployment[] | undefined = tuple[0]; const syncStatuses: ITag[] | undefined = tuple[1]; - return await displayDeployments(values, deployments, syncStatuses, initObj); + await displayDeployments(values, deployments, syncStatuses, initObj); + return deployments; } catch (err) { throw buildError( errorStatusCode.EXE_FLOW_ERR, diff --git a/src/commands/deployment/mocks/author.json b/src/commands/deployment/mocks/author.json new file mode 100644 index 00000000..821e8aab --- /dev/null +++ b/src/commands/deployment/mocks/author.json @@ -0,0 +1,6 @@ +{ + "imageUrl": "https://www.gravatar.com/avatar/3931c180c6eb379205f3fce88d7fa4e1?r=g&d=mm", + "name": "Service Account", + "url": "https://www.gravatar.com/avatar/3931c180c6eb379205f3fce88d7fa4e1?r=g&d=mm", + "username": "me@microsoft.com" +} diff --git a/src/commands/deployment/mocks/data.json b/src/commands/deployment/mocks/data.json index 502f647c..65bdf93d 100644 --- a/src/commands/deployment/mocks/data.json +++ b/src/commands/deployment/mocks/data.json @@ -13,11 +13,7 @@ "sourceVersion": "c6263942a6b760bcd1e6984108f95ebac78d5dc2", "sourceVersionURL": "https://dev.azure.com/epicstuff/f8a98d9c-8f11-46ef-89e4-07b4a56d1ad5/_apis/build/builds/6053/sources", "startTime": "2019-08-30T21:47:37.500Z", - "status": "completed", - "repository": { - "reponame": "hello-bedrock", - "username": "samiyaakhtar" - } + "status": "completed" }, "hldToManifestBuild": { "URL": "https://dev.azure.com/epicstuff/f8a98d9c-8f11-46ef-89e4-07b4a56d1ad5/_build/results?buildId=6054", @@ -31,11 +27,7 @@ "sourceVersion": "a3dc9a7d47aca5135ca403fc3d8f5812c92d94de", "sourceVersionURL": "https://dev.azure.com/epicstuff/f8a98d9c-8f11-46ef-89e4-07b4a56d1ad5/_apis/build/builds/6054/sources", "startTime": "2019-08-30T21:49:26.207Z", - "status": "completed", - "repository": { - "reponame": "hello-bedrock-hld", - "username": "samiyaakhtar" - } + "status": "completed" }, "deploymentId": "12054c192b5d", "dockerToHldRelease": { @@ -53,7 +45,6 @@ "pr": "1123", "hldRepo": "https://github.com/samiyaakhtar/hello-bedrock-hld", "manifestRepo": "https://github.com/samiyaakhtar/hello-bedrock-manifest", - "sourceRepo": "https://github.com/samiyaakhtar/hello-bedrock", "commitId": "c626394", "hldCommitId": "a3dc9a7", "imageTag": "hello-bedrock-master-6053", @@ -447,11 +438,7 @@ "sourceVersion": "a0bca78b1abb64611dcebebd2446c6a716232269", "sourceVersionURL": "https://dev.azure.com/epicstuff/f8a98d9c-8f11-46ef-89e4-07b4a56d1ad5/_apis/build/builds/6042/sources", "startTime": "2019-08-30T20:34:01.178Z", - "status": "completed", - "repository": { - "reponame": "hello-spektate", - "username": "samiyaakhtar" - } + "status": "completed" }, "hldToManifestBuild": { "URL": "https://dev.azure.com/epicstuff/f8a98d9c-8f11-46ef-89e4-07b4a56d1ad5/_build/results?buildId=6043", @@ -465,11 +452,7 @@ "sourceVersion": "316abbc3ee0cc5c3284cc93a95fca6ce2514fe4e", "sourceVersionURL": "https://dev.azure.com/epicstuff/f8a98d9c-8f11-46ef-89e4-07b4a56d1ad5/_apis/build/builds/6043/sources", "startTime": "2019-08-30T20:35:53.160Z", - "status": "completed", - "repository": { - "reponame": "hello-spektate-hld", - "username": "samiyaakhtar" - } + "status": "completed" }, "deploymentId": "ae28bf6b2731", "dockerToHldRelease": { diff --git a/src/commands/deployment/mocks/unmerged-pr.json b/src/commands/deployment/mocks/unmerged-pr.json new file mode 100644 index 00000000..a934682b --- /dev/null +++ b/src/commands/deployment/mocks/unmerged-pr.json @@ -0,0 +1,8 @@ +{ + "description": "Updating samiya.frontend to master-20200318.1.\nPR created by: samiya2019 with buildId: 14751 and buildNumber: 20200318.1", + "id": 1372, + "sourceBranch": "DEPLOY/samiya2019-samiya.frontend-master-20200318.1", + "targetBranch": "master", + "title": "Updating samiya.frontend image tag to master-20200318.1.", + "url": "https://dev.azure.com/epicstuff/hellobedrockprivate/_git/samiya-hld/pullrequest/1371" +} diff --git a/src/lib/azure/deploymenttable.ts b/src/lib/azure/deploymenttable.ts index 40c77880..4978699b 100644 --- a/src/lib/azure/deploymenttable.ts +++ b/src/lib/azure/deploymenttable.ts @@ -3,6 +3,8 @@ import uuid from "uuid/v4"; import { logger } from "../../logger"; import { build as buildError } from "../errorBuilder"; import { errorStatusCode } from "../errorStatusCode"; +import { OUTPUT_FORMAT } from "../../commands/deployment/get"; +import Table from "cli-table"; /** * Deployment Table interface to hold necessary information about a table for deployments @@ -32,6 +34,151 @@ export interface DeploymentEntry { pr?: string; } +export interface DeploymentRow { + status?: string; + service?: string; + env?: string; + author?: string; + imageTag?: string; + srcToAcrPipelineId?: string; + srcToAcrResult?: string; + srctoAcrCommitId?: string; + AcrToHldPipelineId?: string; + AcrToHldResult?: string; + AcrToHldCommitId?: string; + HldToManifestPipelineId?: string; + HldToManifestResult?: string; + HldToManifestCommitId?: string; + pr?: string; + mergedBy?: string; + duration?: string; + startTime?: string; + syncStatus?: string; +} + +export interface TableHeader { + title?: string; + alignment?: "left" | "middle" | "right"; +} + +/** + * Prints deployment table + * @param outputFormat output format: json, wide, normal + * @param deployments list of deployments to print + * @param removeSeparators Whether to remove separators or not + */ +export const printDeploymentTable = ( + outputFormat: OUTPUT_FORMAT, + deployments: DeploymentRow[], + removeSeparators?: boolean, + clusterSyncAvailable?: boolean +): Table => { + const tableHeaders: Array = [ + outputFormat === OUTPUT_FORMAT.WIDE ? { title: "Start Time" } : {}, + { title: "Status" }, + { title: "Service" }, + { title: "Ring" }, + outputFormat === OUTPUT_FORMAT.WIDE ? { title: "Author" } : {}, + { title: "Image Tag" }, + !removeSeparators ? { title: "│" } : {}, + { title: "Image Creation", alignment: "right" }, + { title: "Commit" }, + { title: "OK" }, + !removeSeparators ? { title: "│" } : {}, + { title: "Metadata Update", alignment: "right" }, + { title: "Commit" }, + { title: "OK" }, + !removeSeparators ? { title: "│" } : {}, + outputFormat === OUTPUT_FORMAT.WIDE + ? { title: "Approval PR", alignment: "right" } + : {}, + outputFormat === OUTPUT_FORMAT.WIDE ? { title: "Merged By" } : {}, + { title: "Ready to Deploy", alignment: "right" }, + { title: "Commit" }, + { title: "OK" }, + !removeSeparators ? { title: "│" } : {}, + { title: "Duration", alignment: "right" }, + outputFormat === OUTPUT_FORMAT.WIDE && clusterSyncAvailable + ? { title: "Cluster Sync" } + : {}, + ]; + + const columnAlignment: Array<"left" | "middle" | "right"> = []; + const headers: string[] = []; + tableHeaders.forEach((header) => { + if (header.title) { + headers.push(header.title); + columnAlignment.push(header.alignment ? header.alignment : "left"); + } + }); + const table = new Table({ + head: headers, + chars: { + top: "", + "top-mid": "", + "top-left": "", + "top-right": "", + bottom: "", + "bottom-mid": "", + "bottom-left": "", + "bottom-right": "", + left: "", + "left-mid": "", + mid: "", + "mid-mid": "", + right: "", + "right-mid": "", + middle: " ", + }, + style: { "padding-left": 0, "padding-right": 0 }, + colAligns: columnAlignment, + }); + + deployments.forEach((deployment: DeploymentRow) => { + const row = []; + if (outputFormat === OUTPUT_FORMAT.WIDE) { + row.push(deployment.startTime ?? ""); + } + row.push(deployment.status ?? ""); + row.push(deployment.service ?? ""); + row.push(deployment.env ?? ""); + if (outputFormat === OUTPUT_FORMAT.WIDE) { + row.push(deployment.author ?? ""); + } + row.push(deployment.imageTag ?? ""); + + if (!removeSeparators) row.push("│"); + row.push(deployment.srcToAcrPipelineId ?? ""); + row.push(deployment.srctoAcrCommitId ?? ""); + row.push(deployment.srcToAcrResult ?? ""); + + if (!removeSeparators) row.push("│"); + row.push(deployment.AcrToHldPipelineId ?? ""); + row.push(deployment.AcrToHldCommitId ?? ""); + row.push(deployment.AcrToHldResult ?? ""); + + if (!removeSeparators) row.push("│"); + if (outputFormat === OUTPUT_FORMAT.WIDE) { + row.push(deployment.pr ?? ""); + row.push(deployment.mergedBy ?? ""); + } + row.push(deployment.HldToManifestPipelineId ?? ""); + row.push(deployment.HldToManifestCommitId ?? ""); + row.push(deployment.HldToManifestResult ?? ""); + + if (!removeSeparators) row.push("│"); + row.push(deployment.duration ?? ""); + + if (outputFormat === OUTPUT_FORMAT.WIDE && clusterSyncAvailable) { + row.push(deployment.syncStatus ?? ""); + } + + table.push(row); + }); + console.log(table.toString()); + return table; +}; + /** * Generates a RowKey GUID 12 characters long */ diff --git a/yarn.lock b/yarn.lock index b3908a6c..102934aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6729,13 +6729,14 @@ spdx-license-ids@^3.0.0: resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== -spektate@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/spektate/-/spektate-1.0.6.tgz#b855654d7ec57442b11e30a901e6ebdc0d0cd36d" - integrity sha512-P+TOQPdJjmYPwAucwIju818QaeaOrpCVfWj1RVMN6wj5iGszTk39FB9yCckHQ6o12TIo6UkZM+3I3OM1ZczN5g== +spektate@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/spektate/-/spektate-1.0.11.tgz#1997db56149f0521df3159fea7e4876317200b53" + integrity sha512-ZTSQjb+IqApmIAVFptzBCtovp5frn4W9bA28ephNuu24ddRLQzqkI1yQ8/KSVExhRza+DpcTAkR++WUiAdIX2w== dependencies: axios "^0.19.0" azure-storage "^2.10.3" + tslint-config-prettier "^1.18.0" split-string@^3.0.1, split-string@^3.0.2: version "3.1.0"