From 38db3092ec426f678b0ceb0e41f481162e753a13 Mon Sep 17 00:00:00 2001 From: maxjeffos <44034094+maxjeffos@users.noreply.github.com> Date: Fri, 19 Jun 2020 12:46:39 -0400 Subject: [PATCH 1/3] chore: restructure tests to separate execution of checks, unit, and system tests --- .circleci/config.yml | 16 ++++++++++++++-- .gitignore | 1 - jest.config.js | 3 ++- jest.config.system.js | 15 +++++++++++++++ package.json | 6 ++++-- 5 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 jest.config.system.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 42b536dd..59268d44 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,10 +8,22 @@ jobs: steps: - checkout - run: - name: Test + name: Build command: | ./scripts/ci-build.sh - npm test + - run: + name: Checks + command: | + npm run test:checks + - run: + name: Unit Tests + command: | + npm run test:unit + - run: + name: System Tests + command: | + npm run test:system + deploy: docker: - image: circleci/node:lts diff --git a/.gitignore b/.gitignore index 21310d8d..f5ca1b9a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,5 @@ package-lock.json *.bak *.vsix .eslintcache -.circleci .ops ops/deploy/dist diff --git a/jest.config.js b/jest.config.js index a8979492..72e15093 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,6 +7,7 @@ module.exports = { }, "testPathIgnorePatterns": [ "/node_modules/", - "snykTask/src/__tests__/_test-mock-config-*" + "snykTask/src/__tests__/_test-mock-config-*", + "snykTask/src/__tests__/test-task.ts" ] } diff --git a/jest.config.system.js b/jest.config.system.js new file mode 100644 index 00000000..ff8c9896 --- /dev/null +++ b/jest.config.system.js @@ -0,0 +1,15 @@ +module.exports = { + "roots": [ + "/snykTask/src/" + ], + "transform": { + "^.+\\.tsx?$": "ts-jest" + }, + "testPathIgnorePatterns": [ + "/node_modules/", + "snykTask/src/__tests__/_test-mock-config-*" + ], + testMatch: [ + '/snykTask/src/__tests__/test-task.ts' + ], +} diff --git a/package.json b/package.json index 13ffdc0d..cdf14b8f 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,10 @@ "install_dependencies": "npm install && cd snykTask && npm install", "build": "npm run install_dependencies && npm run compile && npm run enhancer:compile && npm run deploy:compile", "pretest": "npm run build", - "test": "npm run eslint && npm run format && jest --detectOpenHandles", - "test:unit": "npm run task:test && npm run deploy:test", + "test": "npm run test:checks && npm run test:unit && npm run test:system", + "test:checks": "npm run eslint && npm run format:check", + "test:unit": "jest --detectOpenHandles", + "test:system": "jest --config jest.config.system.js --silent=false --verbose=true", "compile": "tsc -b snykTask/tsconfig.json", "eslint": "eslint --cache 'snykTask/{src,test,public/js/{!(build)}}/**/*.{js,ts}'", "format:check": "prettier --check 'snykTask/{src,test,public/js/{!(build)}}/**/*.{js,ts}'", From e1aec8a2261958e77cc4facb56d121fba94fed45 Mon Sep 17 00:00:00 2001 From: maxjeffos <44034094+maxjeffos@users.noreply.github.com> Date: Tue, 30 Jun 2020 23:00:40 -0400 Subject: [PATCH 2/3] chore: deploy test pipeline --- .circleci/config.yml | 68 ++++++++ ops/deploy/deploy.ts | 21 ++- ops/deploy/get-current-dev-ext-version.ts | 31 ++++ ops/deploy/get-next-dev-ext-version.ts | 36 ++++ ops/deploy/install-extension-to-dev-org.ts | 54 ++++++ ops/deploy/lib/azure-devops/builds.ts | 74 +++++++++ .../azure-devops/extensions.ts} | 47 +----- ops/deploy/lib/azure-devops/index.ts | 98 +++++++++++ ops/deploy/package.json | 5 + ops/deploy/run-test-pipelines.ts | 154 ++++++++++++++++++ ops/deploy/test-builds.json | 10 ++ ops/deploy/tsconfig.json | 4 - package.json | 3 +- scripts/{share-dev.sh => ci-deploy-dev.sh} | 69 ++++---- scripts/{ci-publish.sh => ci-deploy-prod.sh} | 0 scripts/ci-deploy.sh | 4 +- scripts/update-task-json-dev.js | 22 ++- 17 files changed, 598 insertions(+), 102 deletions(-) create mode 100644 ops/deploy/get-current-dev-ext-version.ts create mode 100644 ops/deploy/get-next-dev-ext-version.ts create mode 100644 ops/deploy/install-extension-to-dev-org.ts create mode 100644 ops/deploy/lib/azure-devops/builds.ts rename ops/deploy/{azure-devops.ts => lib/azure-devops/extensions.ts} (57%) create mode 100644 ops/deploy/lib/azure-devops/index.ts create mode 100644 ops/deploy/package.json create mode 100644 ops/deploy/run-test-pipelines.ts create mode 100644 ops/deploy/test-builds.json rename scripts/{share-dev.sh => ci-deploy-dev.sh} (53%) rename scripts/{ci-publish.sh => ci-deploy-prod.sh} (100%) diff --git a/.circleci/config.yml b/.circleci/config.yml index 59268d44..ef951e1b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,15 +24,77 @@ jobs: command: | npm run test:system + deploy_dev: + docker: + - image: circleci/node:lts + environment: + DEV_AZ_EXTENSION_ID: "dev-security-scan-test" + DEV_AZ_EXTENSION_NAME: "Dev - Snyk Security Scan" + DEV_AZ_TASK_FRIENDLY_NAME: "Dev - Snyk Security Scan" + DEV_AZ_TASK_NAME: "DevSnykSecurityScan" + + working_directory: ~/repo + steps: + - checkout + + - run: + name: Build Ops Sub-project + command: | + npm install + npm run deploy:format:check + npm run deploy:compile + + - run: + name: Build and Deploy to Test Environment + command: | + echo DEV_AZ_ORG: $DEV_AZ_ORG # Set in CCI Project Settings + echo DEV_AZ_PUBLISHER: $DEV_AZ_PUBLISHER # Set in CCI Project Settings + + echo DEV_AZ_EXTENSION_ID: $DEV_AZ_EXTENSION_ID + echo DEV_AZ_EXTENSION_NAME: $DEV_AZ_EXTENSION_NAME + echo DEV_AZ_TASK_FRIENDLY_NAME: $DEV_AZ_TASK_FRIENDLY_NAME + echo DEV_AZ_TASK_NAME: $DEV_AZ_TASK_NAME + + npm run deploy:compile + NEXT_DEV_VERSION=$(node ./ops/deploy/dist/get-next-dev-ext-version.js) + if [[ $? -eq 0 ]]; then + echo NEXT_DEV_VERSION: $NEXT_DEV_VERSION + else + echo "no current version. Setting NEXT_DEV_VERSION to 0.0.1" + NEXT_DEV_VERSION=0.0.1 + fi + + echo "Deploying to dev with ${NEXT_DEV_VERSION} ${AZ_ORG}" + scripts/ci-deploy.sh $NEXT_DEV_VERSION $DEV_AZ_ORG + + - run: + name: Launch Test Pipelines + command: | + node ./ops/deploy/dist/run-test-pipelines.js + deploy: docker: - image: circleci/node:lts + environment: + AZ_EXTENSION_ID: "snyk-security-scan" + AZ_EXTENSION_NAME: "Snyk Security Scan" + AZ_PUBLISHER: "Snyk" + working_directory: ~/repo steps: - checkout + - run: + name: Setup Env Vars + command: | + export AZURE_DEVOPS_EXT_PAT=$PROD_AZURE_DEVOPS_EXT_PAT + echo AZ_EXTENSION_ID: $AZ_EXTENSION_ID + echo AZ_EXTENSION_NAME: $AZ_EXTENSION_NAME + echo AZ_PUBLISHER: $AZ_PUBLISHER + - run: name: Build and Deploy command: | + export AZURE_DEVOPS_EXT_PAT=$PROD_AZURE_DEVOPS_EXT_PAT npm install npx semantic-release @@ -41,6 +103,12 @@ workflows: build_and_test: jobs: - test + - deploy_dev: + requires: + - test + filters: + branches: + ignore: master - deploy: context: nodejs-lib-release requires: diff --git a/ops/deploy/deploy.ts b/ops/deploy/deploy.ts index 34510a32..cc3b9a80 100644 --- a/ops/deploy/deploy.ts +++ b/ops/deploy/deploy.ts @@ -1,7 +1,11 @@ const fs = require("fs"); const path = require("path"); -import * as myAz from "./azure-devops"; -import * as ExtensionManagementInterfaces from "azure-devops-node-api/interfaces/ExtensionManagementInterfaces"; +import { ChildProcess, exec, ExecException } from "child_process"; + +import { + installExtension, + uninstallExtension +} from "./lib/azure-devops/extensions"; import { Command, @@ -9,7 +13,7 @@ import { InputArgs, parseInputParameters } from "./cli-args"; -import { ChildProcess, exec, ExecException } from "child_process"; +import { getWebApi } from "./lib/azure-devops"; function checkVersionsMatch(): boolean { const packageJsonFilePath = "package.json"; @@ -342,12 +346,12 @@ export async function updateOrInstallExtension( } const azUrl = getAzUrl(publishArgs.azureOrg); + const webApi = await getWebApi(azUrl, publishArgs.azureDevopsPAT); // uninstall previous - this should be an option // TODO: add an option like --install-new-version which, when set, will control whether we call uninstallPreviousVersion() / installNewVersion - await myAz.uninstallExtension( - azUrl, - publishArgs.azureDevopsPAT, + await uninstallExtension( + webApi, publishArgs.vsMarketplacePublisher, publishArgs.extensionId ); @@ -362,9 +366,8 @@ export async function updateOrInstallExtension( ); // install new version - this should be an option - await myAz.installExtension( - azUrl, - publishArgs.azureDevopsPAT, + await installExtension( + webApi, publishArgs.vsMarketplacePublisher, publishArgs.extensionId ); diff --git a/ops/deploy/get-current-dev-ext-version.ts b/ops/deploy/get-current-dev-ext-version.ts new file mode 100644 index 00000000..62976738 --- /dev/null +++ b/ops/deploy/get-current-dev-ext-version.ts @@ -0,0 +1,31 @@ +import { getExtensionInfo, getLatestVersion } from "./lib/azure-devops"; + +async function main() { + const publisherName = process.env.DEV_AZ_PUBLISHER || ""; + // warning: the Marketplace stuff calls this extensionId - the rest of the extension stuff calls it name + const extensionName = process.env.DEV_AZ_EXTENSION_ID || ""; + const azToken = process.env.DEV_AZURE_DEVOPS_EXT_PAT || ""; + + const extensionDetails = await getExtensionInfo( + azToken, + publisherName, + extensionName + ); + + if (extensionDetails) { + const latestVersion = getLatestVersion(extensionDetails); + if (latestVersion) { + console.log(latestVersion); + } else { + console.error("could not get extension version info"); + process.exit(1); + } + } else { + console.error("could not get extension info"); + process.exit(1); + } +} + +if (require.main === module) { + main(); +} diff --git a/ops/deploy/get-next-dev-ext-version.ts b/ops/deploy/get-next-dev-ext-version.ts new file mode 100644 index 00000000..3343e0a1 --- /dev/null +++ b/ops/deploy/get-next-dev-ext-version.ts @@ -0,0 +1,36 @@ +import { getExtensionInfo, getLatestVersion } from "./lib/azure-devops"; + +async function main() { + const publisherName = process.env.DEV_AZ_PUBLISHER || ""; + // warning: the Marketplace stuff calls this extensionId - the rest of the extension stuff calls it name + const extensionName = process.env.DEV_AZ_EXTENSION_ID || ""; + const azToken = process.env.DEV_AZURE_DEVOPS_EXT_PAT || ""; + + const extensionDetails = await getExtensionInfo( + azToken, + publisherName, + extensionName + ); + + if (extensionDetails) { + const latestVersion = getLatestVersion(extensionDetails); + if (latestVersion) { + const splitz = latestVersion.split("."); + const major = parseInt(splitz[0]); + const minor = parseInt(splitz[1]); + const patch = parseInt(splitz[2]) + 1; + const newVersion = `${major}.${minor}.${patch}`; + console.log(newVersion); + } else { + console.error("could not get extension version info"); + process.exit(1); + } + } else { + console.error("could not get extension info"); + process.exit(1); + } +} + +if (require.main === module) { + main(); +} diff --git a/ops/deploy/install-extension-to-dev-org.ts b/ops/deploy/install-extension-to-dev-org.ts new file mode 100644 index 00000000..e765372d --- /dev/null +++ b/ops/deploy/install-extension-to-dev-org.ts @@ -0,0 +1,54 @@ +import { WebApi } from "azure-devops-node-api"; + +import { getAzUrl, getWebApi } from "./lib/azure-devops"; +import { + installExtension, + getInstalledExtensionInfo +} from "./lib/azure-devops/extensions"; + +async function main() { + const publisherName = process.env.DEV_AZ_PUBLISHER || ""; + // warning: the Marketplace stuff calls this extensionId - the rest of the extension stuff calls it name + const extensionName = process.env.DEV_AZ_EXTENSION_ID || ""; + const azToken = process.env.DEV_AZURE_DEVOPS_EXT_PAT || ""; + const azOrg = process.env.DEV_AZ_ORG || ""; + + const azUrl = getAzUrl(azOrg); + const webApi: WebApi = await getWebApi(azUrl, azToken); + + try { + const alreadyInstalledExtensionInfo = await getInstalledExtensionInfo( + webApi, + publisherName, + extensionName + ); + const alreadyInstalledVersion = alreadyInstalledExtensionInfo.version; + console.log( + `Extension version currently installed: ${alreadyInstalledVersion}` + ); + + console.log( + "Attempting to install latest version of extension into org..." + ); + // installExtension will throw an error if it is already installed + const installRes = await installExtension( + webApi, + publisherName, + extensionName + ); + + const afterInstallExtensionInfo = await getInstalledExtensionInfo( + webApi, + publisherName, + extensionName + ); + const afterInstallExtensionVersion = afterInstallExtensionInfo.version; + console.log(`Extension version installed: ${afterInstallExtensionVersion}`); + } catch (err) { + console.log(`${err.statusCode} - ${err.message}`); + } +} + +if (require.main === module) { + main(); +} diff --git a/ops/deploy/lib/azure-devops/builds.ts b/ops/deploy/lib/azure-devops/builds.ts new file mode 100644 index 00000000..faf2238d --- /dev/null +++ b/ops/deploy/lib/azure-devops/builds.ts @@ -0,0 +1,74 @@ +import * as nodeApi from "azure-devops-node-api"; +import * as BuildInterfaces from "azure-devops-node-api/interfaces/BuildInterfaces"; + +export async function getBuildDefinitions( + webApi: nodeApi.WebApi, + projectName: string +) { + const buildApi = await webApi.getBuildApi(); + const buildDefinitions = await buildApi.getDefinitions(projectName); + return buildDefinitions; +} + +export async function getBuildDefinition( + webApi: nodeApi.WebApi, + projectName: string, + buildDefinitionId: number +) { + const buildApi = await webApi.getBuildApi(); + const buildDefinition = await buildApi.getDefinition( + projectName, + buildDefinitionId + ); + return buildDefinition; +} + +export async function getBuilds( + webApi: nodeApi.WebApi, + projectName: string, + buildDefinitionId +) { + const buildApi = await webApi.getBuildApi(); + const buildDefinition = await buildApi.getBuilds(projectName, [ + buildDefinitionId + ]); + return buildDefinition; +} + +export async function getBuild( + webApi: nodeApi.WebApi, + projectName: string, + buildId: number +) { + const buildApi = await webApi.getBuildApi(); + const buildDefinition = await buildApi.getBuild(projectName, buildId); + return buildDefinition; +} + +export async function queueBuild( + webApi: nodeApi.WebApi, + projectName: string, + build: BuildInterfaces.Build +) { + const buildApi = await webApi.getBuildApi(); + const queueBuildResult: BuildInterfaces.Build = await buildApi.queueBuild( + build, + projectName + ); + return queueBuildResult; +} + +export async function launchBuildPipeline( + webApi: nodeApi.WebApi, + azOrg: string, + projectName: string, + buildDefinitionId: number +) { + const apiUrl = `https://dev.azure.com/${azOrg}/${projectName}/_apis/build/builds?api-version=5.1`; + const res = await webApi.rest.create(apiUrl, { + definition: { + id: buildDefinitionId + } + }); + return res as any; +} diff --git a/ops/deploy/azure-devops.ts b/ops/deploy/lib/azure-devops/extensions.ts similarity index 57% rename from ops/deploy/azure-devops.ts rename to ops/deploy/lib/azure-devops/extensions.ts index 6afd6421..23e82b3d 100644 --- a/ops/deploy/azure-devops.ts +++ b/ops/deploy/lib/azure-devops/extensions.ts @@ -1,47 +1,16 @@ -import * as nodeApi from "azure-devops-node-api"; +import { WebApi } from "azure-devops-node-api"; import * as ExtensionManagementApi from "azure-devops-node-api/ExtensionManagementApi"; import * as ExtensionManagementInterfaces from "azure-devops-node-api/interfaces/ExtensionManagementInterfaces"; -import * as lim from "azure-devops-node-api/interfaces/LocationsInterfaces"; -export async function getApi( - serverUrl: string, - azureDevOpsToken: string -): Promise { - const token = azureDevOpsToken; - const authHandler = nodeApi.getPersonalAccessTokenHandler(token); - const option = undefined; - - const vsts: nodeApi.WebApi = new nodeApi.WebApi( - serverUrl, - authHandler, - option - ); - const connData: lim.ConnectionData = await vsts.connect(); - if (connData) { - if (connData.authenticatedUser) { - console.log(`Hello ${connData.authenticatedUser.providerDisplayName}`); - } - } - return vsts; -} - -export async function getWebApi( - serverUrl: string, - azureDevOpsToken: string -): Promise { - return await getApi(serverUrl, azureDevOpsToken); -} - -export async function getExtensionInfo( - serverUrl: string, - azureDevOpsToken: string, +export async function getInstalledExtensionInfo( + webApi: WebApi, publisherName: string, extensionId: string ): Promise { - const webApi: nodeApi.WebApi = await getWebApi(serverUrl, azureDevOpsToken); const extensionManagementApiObject: ExtensionManagementApi.IExtensionManagementApi = await webApi.getExtensionManagementApi(); // Although this API claims to be "ByName", it actually corresponds to the the `extensionId`. The same weirdness exists when you use the az devops CLI + // Will throw error if already installed return extensionManagementApiObject.getInstalledExtensionByName( publisherName, extensionId @@ -49,12 +18,10 @@ export async function getExtensionInfo( } export async function uninstallExtension( - serverUrl: string, - azureDevOpsToken: string, + webApi: WebApi, publisherName: string, extensionId: string ): Promise { - const webApi: nodeApi.WebApi = await getWebApi(serverUrl, azureDevOpsToken); const extensionManagementApiObject: ExtensionManagementApi.IExtensionManagementApi = await webApi.getExtensionManagementApi(); // Although this API claims to be "ByName", it actually corresponds to the the `extensionId`. The same weirdness exists when you use the az devops CLI @@ -65,12 +32,10 @@ export async function uninstallExtension( } export async function installExtension( - serverUrl: string, - azureDevOpsToken: string, + webApi: WebApi, publisherName: string, extensionId: string ): Promise { - const webApi: nodeApi.WebApi = await getWebApi(serverUrl, azureDevOpsToken); const extensionManagementApiObject: ExtensionManagementApi.IExtensionManagementApi = await webApi.getExtensionManagementApi(); // Although this API claims to be "ByName", it actually corresponds to the the `extensionId`. The same weirdness exists when you use the az devops CLI diff --git a/ops/deploy/lib/azure-devops/index.ts b/ops/deploy/lib/azure-devops/index.ts new file mode 100644 index 00000000..9962d037 --- /dev/null +++ b/ops/deploy/lib/azure-devops/index.ts @@ -0,0 +1,98 @@ +import * as nodeApi from "azure-devops-node-api"; +import * as CoreApi from "azure-devops-node-api/CoreApi"; +import { GalleryApi } from "azure-devops-node-api/GalleryApi"; +import * as CoreInterfaces from "azure-devops-node-api/interfaces/CoreInterfaces"; +import * as GalleryInterfaces from "azure-devops-node-api/interfaces/GalleryInterfaces"; +import * as lim from "azure-devops-node-api/interfaces/LocationsInterfaces"; +import { WebApi, getBasicHandler } from "azure-devops-node-api/WebApi"; + +export function getAzUrl(azureOrg: string): string { + return `https://dev.azure.com/${azureOrg}/`; +} + +export async function getApi( + serverUrl: string, + azureDevOpsToken: string +): Promise { + const token = azureDevOpsToken; + const authHandler = nodeApi.getPersonalAccessTokenHandler(token); + const option = undefined; + + const vsts: nodeApi.WebApi = new nodeApi.WebApi( + serverUrl, + authHandler, + option + ); + const connData: lim.ConnectionData = await vsts.connect(); + if (!connData?.authenticatedUser) { + console.error("failed to connect"); + } + + return vsts; +} + +export async function getWebApi( + serverUrl: string, + azureDevOpsToken: string +): Promise { + return await getApi(serverUrl, azureDevOpsToken); +} + +export async function getProject(webApi: nodeApi.WebApi, projectName: string) { + const coreApiObject: CoreApi.CoreApi = await webApi.getCoreApi(); + const project: CoreInterfaces.TeamProject = await coreApiObject.getProject( + projectName + ); + console.log(project); + return project; +} + +export async function getProjects(webApi: nodeApi.WebApi) { + const coreApiObject: CoreApi.CoreApi = await webApi.getCoreApi(); + const projects = await coreApiObject.getProjects(); + console.log(projects); + return projects; +} + +// ********************************************************** +// hack / partially from tfs-cli app/exec/extension/default.ts +export async function getGalleryApi(webApi: nodeApi.WebApi, azToken: string) { + const handler = await getCredentials(azToken); + return new GalleryApi(webApi.serverUrl, [handler]); +} + +export async function getCredentials(azToken) { + return getBasicHandler("OAuth", azToken); +} +// ********************************************************** + +export async function getExtensionInfo( + azToken: string, + publisherName: string, + extensionName: string +) { + // This is a hack based on some fun with the MS GalleryAPI + // see how we're not using the usual base API URL which includes the org - this is a generic one (but we still authenticate with our token) + const webApi = await getApi("https://marketplace.visualstudio.com/", azToken); + const galleryApi = await getGalleryApi(webApi, azToken); + + const version = undefined; + + // This API is the one used by tfx-cli when you call `tfx extesion show`. + // See https://github.com/microsoft/tfs-cli/blob/master/app/exec/extension/_lib/publish.ts#L172 + const extensionInfo = await galleryApi.getExtension( + null, + publisherName, + extensionName, + version, + GalleryInterfaces.ExtensionQueryFlags.IncludeVersions | + GalleryInterfaces.ExtensionQueryFlags.IncludeFiles | + GalleryInterfaces.ExtensionQueryFlags.IncludeCategoryAndTags | + GalleryInterfaces.ExtensionQueryFlags.IncludeSharedAccounts + ); + return extensionInfo; +} + +export function getLatestVersion(extensionDetails: any): string | undefined { + return extensionDetails?.versions[0]?.version; +} diff --git a/ops/deploy/package.json b/ops/deploy/package.json new file mode 100644 index 00000000..26ca0d2d --- /dev/null +++ b/ops/deploy/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "azure-devops-node-api": "^10.1.1" + } +} diff --git a/ops/deploy/run-test-pipelines.ts b/ops/deploy/run-test-pipelines.ts new file mode 100644 index 00000000..c208704a --- /dev/null +++ b/ops/deploy/run-test-pipelines.ts @@ -0,0 +1,154 @@ +import { WebApi } from "azure-devops-node-api"; +import * as fs from "fs"; + +import { getAzUrl, getWebApi } from "./lib/azure-devops"; + +import { getBuild, launchBuildPipeline } from "./lib/azure-devops/builds"; + +import { + BuildStatus, + BuildResult +} from "azure-devops-node-api/interfaces/BuildInterfaces"; + +interface AzureVars { + azOrg: string; + azureDevopsExtPAT: string; +} + +function getEnvVars(): AzureVars { + const azOrg = process.env.DEV_AZ_ORG || ""; + const azToken = process.env.DEV_AZURE_DEVOPS_EXT_PAT || ""; + + const vars: AzureVars = { + azOrg: azOrg, + azureDevopsExtPAT: azToken + }; + return vars; +} + +async function main() { + console.log(`pwd: ${process.cwd()}`); + + const testBuildConfigFileStr = fs.readFileSync( + "./ops/deploy/test-builds.json", + "utf8" + ); + const testBuildDefinitions = JSON.parse(testBuildConfigFileStr); + + const azVars = getEnvVars(); + const azUrl = getAzUrl(azVars.azOrg); + + const webApi: WebApi = await getWebApi(azUrl, azVars.azureDevopsExtPAT); + + const allBuilds: Promise[] = []; + + for (const nextTestBuildDefinition of testBuildDefinitions) { + const testProjectName = nextTestBuildDefinition.projectName; + const testBuildDefinitionId = nextTestBuildDefinition.buildDefinitionId; + + const buildPromise = runBuild( + webApi, + azVars.azOrg, + testProjectName, + testBuildDefinitionId + ); + + allBuilds.push(buildPromise); + } + + console.log("waiting for all builds to complete"); + try { + await Promise.all(allBuilds); + console.log("all builds complete"); + } catch (errAll) { + console.log("error awaiting all builds"); + console.log(errAll); + process.exit(1); + } +} + +/** + * + * @returns Promise such that the bool is true if the build ran succesfully without failures and false if it ran but had failures. Should reject the promise on error. + */ +async function runBuild( + webApi: WebApi, + azOrg: string, + testProjectName: string, + testBuildDefinitionId: number +): Promise { + let success = false; + + try { + const launchPipelineResult = await launchBuildPipeline( + webApi, + azOrg, + testProjectName, + testBuildDefinitionId + ); + + console.log(launchPipelineResult); + + const buildId = launchPipelineResult.result.id; + + const alwaysBeTrue = 1 === 1; + while (alwaysBeTrue) { + const checkBuildStatusRes = await getBuild( + webApi, + testProjectName, + buildId + ); + + const status = checkBuildStatusRes.status; + + console.log(`status: ${status}`); + console.log(`BuildStatus.Completed: ${BuildStatus.Completed}`); + + if (!status) { + throw new Error("status is not set"); + } + + if (status === BuildStatus.Completed) { + console.log("build is complete"); + const result = checkBuildStatusRes.result; + console.log(`build result: ${result}`); + if (result) { + if (result === BuildResult.Succeeded) { + console.log("build succeeded"); + success = true; + } else { + console.log(`build did not succeed. BuildResult code: ${result}`); + } + } + break; + } else { + console.log( + `Still waiting for build ${buildId} to complete. Status: ${status}. Time: ${new Date().getTime()}` + ); + await asyncSleep(10000); + } + } + + if (success) { + return Promise.resolve(); + } else { + console.log("resolving false - not successful"); + return Promise.reject(); + } + } catch (err) { + console.log("failed to launching / checking build"); + console.log(err); + console.log("\nrejecting - not successful"); + return Promise.reject(); + } +} + +async function asyncSleep(milliseconds: number): Promise { + return new Promise(resolve => { + setTimeout(resolve, milliseconds); + }); +} + +if (require.main === module) { + main(); +} diff --git a/ops/deploy/test-builds.json b/ops/deploy/test-builds.json new file mode 100644 index 00000000..98e94f92 --- /dev/null +++ b/ops/deploy/test-builds.json @@ -0,0 +1,10 @@ +[ + { + "projectName": "goof", + "buildDefinitionId": 7 + }, + { + "projectName": "goof_windows", + "buildDefinitionId": 10 + } +] diff --git a/ops/deploy/tsconfig.json b/ops/deploy/tsconfig.json index 8d091b7f..32def73f 100644 --- a/ops/deploy/tsconfig.json +++ b/ops/deploy/tsconfig.json @@ -21,8 +21,4 @@ "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ "inlineSources": true /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ }, - "files": [ - "./deploy.ts", - "./cli-args.ts", - ] } diff --git a/package.json b/package.json index cdf14b8f..54a7c28e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "install_dependencies": "npm install && cd snykTask && npm install", + "install_dependencies": "npm install && cd snykTask && npm install && cd .. && cd ops/deploy && npm install", "build": "npm run install_dependencies && npm run compile && npm run enhancer:compile && npm run deploy:compile", "pretest": "npm run build", "test": "npm run test:checks && npm run test:unit && npm run test:system", @@ -64,7 +64,6 @@ "@types/q": "^1.5.2", "@typescript-eslint/eslint-plugin": "^2.0.0", "@typescript-eslint/parser": "^2.0.0", - "azure-devops-node-api": "^9.0.1", "eslint": "^6.2.2", "eslint-config-prettier": "^6.1.0", "eslint-plugin-import": "^2.18.2", diff --git a/scripts/share-dev.sh b/scripts/ci-deploy-dev.sh similarity index 53% rename from scripts/share-dev.sh rename to scripts/ci-deploy-dev.sh index 55571568..4c2017bc 100755 --- a/scripts/share-dev.sh +++ b/scripts/ci-deploy-dev.sh @@ -1,13 +1,14 @@ #!/bin/bash -# This script share the extension with a specific org for development propose. +# This script deploys a dev version of the extension to the dev environment for development / testing purposes. +# It also shares / installs it into the given Azure organization. # Arguments: # $1 - Extension version # $2 - Organization that will be shared the extension # Handle arguments -AZ_EXT_NEW_VERSION="$1" -AZ_ORG="$2" +INPUT_PARAM_AZ_EXT_NEW_VERSION="$1" +INPUT_PARAM_AZ_ORG="$2" # Check if the Azure CLI is already installed. If not, install it. az -v >/dev/null 2>&1 @@ -17,6 +18,7 @@ if [[ ! $? -eq 0 ]]; then echo "Platform: ${platform}" if [[ $platform == "linux-gnu" ]]; then curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + az extension add --name azure-devops elif [[ $platform == "darwin"* ]]; then brew -v >/dev/null 2>&1 if [[ $? -eq 0 ]]; then @@ -34,51 +36,50 @@ fi # echo "See if the extension is installed..." az devops extension show \ - --publisher-name $AZ_PUBLISHER \ - --extension-name $AZ_EXTENSION_ID \ - --organization "https://dev.azure.com/${AZ_ORG}/" + --publisher-name $DEV_AZ_PUBLISHER \ + --extension-name $DEV_AZ_EXTENSION_ID \ + --organization "https://dev.azure.com/${INPUT_PARAM_AZ_ORG}/" if [[ $? -eq 0 ]]; then - echo "Extension is installed in org ${AZ_ORG}... uninstall it" + echo "Extension is installed in org ${INPUT_PARAM_AZ_ORG}... uninstall it" # Unistall the extinsion if it has been already installed in this organization. # This may not be required it works much more consistently with it. echo "Uninstall extension..." az devops extension uninstall \ - --publisher-name $AZ_PUBLISHER \ - --extension-name $AZ_EXTENSION_ID \ - --organization "https://dev.azure.com/${AZ_ORG}/" --yes + --publisher-name $DEV_AZ_PUBLISHER \ + --extension-name $DEV_AZ_EXTENSION_ID \ + --organization "https://dev.azure.com/${INPUT_PARAM_AZ_ORG}/" --yes echo "Extension uninstalled" else echo "Extension not already installed." fi - echo "About to deploy to dev environment using:" -echo "AZ_EXT_NEW_VERSION: ${AZ_EXT_NEW_VERSION}" -echo "AZ_PUBLISHER: ${AZ_PUBLISHER}" -echo "AZ_EXTENSION_ID: ${AZ_EXTENSION_ID}" -echo "AZ_TASK_NAME: ${AZ_TASK_NAME}" -echo "AZ_TASK_FRIENDLY_NAME: ${AZ_TASK_FRIENDLY_NAME}" -echo "AZ_ORG: ${AZ_ORG}" -echo "AZ_DEV_TASK_ID: ${AZ_DEV_TASK_ID}" +echo "INPUT_PARAM_AZ_EXT_NEW_VERSION: ${INPUT_PARAM_AZ_EXT_NEW_VERSION}" +echo "DEV_AZ_PUBLISHER: ${DEV_AZ_PUBLISHER}" +echo "DEV_AZ_EXTENSION_ID: ${DEV_AZ_EXTENSION_ID}" +echo "DEV_AZ_TASK_NAME: ${DEV_AZ_TASK_NAME}" +echo "DEV_AZ_TASK_FRIENDLY_NAME: ${DEV_AZ_TASK_FRIENDLY_NAME}" +echo "INPUT_PARAM_AZ_ORG: ${INPUT_PARAM_AZ_ORG}" +echo "DEV_AZ_TASK_ID: ${DEV_AZ_TASK_ID}" # Updating version in task.json file -node "${PWD}/scripts/update-task-json-dev.js" ${AZ_EXT_NEW_VERSION} +node "${PWD}/scripts/update-task-json-dev.js" ${INPUT_PARAM_AZ_EXT_NEW_VERSION} # Override version -OVERRIDE_JSON="{ \"name\": \"${AZ_EXTENSION_NAME}\", \"version\": \"${AZ_EXT_NEW_VERSION}\" }" +OVERRIDE_JSON="{ \"name\": \"${DEV_AZ_EXTENSION_NAME}\", \"version\": \"${INPUT_PARAM_AZ_EXT_NEW_VERSION}\" }" # Sharing extension echo "Publishing and sharing extension..." echo "OVERRIDE_JSON: ${OVERRIDE_JSON}" tfx extension publish --manifest-globs vss-extension-dev.json \ ---version $AZ_EXT_NEW_VERSION \ ---share-with $AZ_ORG \ ---extension-id $AZ_EXTENSION_ID \ ---publisher $AZ_PUBLISHER \ +--version $INPUT_PARAM_AZ_EXT_NEW_VERSION \ +--share-with $INPUT_PARAM_AZ_ORG \ +--extension-id $DEV_AZ_EXTENSION_ID \ +--publisher $DEV_AZ_PUBLISHER \ --override $OVERRIDE_JSON \ ---token $AZURE_DEVOPS_EXT_PAT +--token $DEV_AZURE_DEVOPS_EXT_PAT publish_exit_code=$? if [[ publish_exit_code -eq 0 ]]; then @@ -88,22 +89,8 @@ else exit ${publish_exit_code} fi -# echo "See if the extension is installed..." -az devops extension show \ - --publisher-name $AZ_PUBLISHER \ - --extension-name $AZ_EXTENSION_ID \ - --organization "https://dev.azure.com/${AZ_ORG}/" - -if [[ $? -eq 0 ]]; then - echo "Extension already installed in org ${AZ_ORG}" -else - echo "Extension not already installed." - echo "Installing extension..." - az devops extension install \ - --publisher-name $AZ_PUBLISHER \ - --extension-name $AZ_EXTENSION_ID \ - --organization "https://dev.azure.com/${AZ_ORG}/" -fi +echo "Run script to install the dev extension into the dev org in Azure DevOps..." +node ./ops/deploy/dist/install-extension-to-dev-org.js # Updating version in task.json file node "${PWD}/scripts/recovery-task-json-dev.js" diff --git a/scripts/ci-publish.sh b/scripts/ci-deploy-prod.sh similarity index 100% rename from scripts/ci-publish.sh rename to scripts/ci-deploy-prod.sh diff --git a/scripts/ci-deploy.sh b/scripts/ci-deploy.sh index a5cce588..061bb23a 100755 --- a/scripts/ci-deploy.sh +++ b/scripts/ci-deploy.sh @@ -47,8 +47,8 @@ sudo npm install -g tfx-cli@0.7.11 # Deploy project if [[ ! -z "${AZ_ORG}" ]]; then - "${PWD}/scripts/share-dev.sh" ${AZ_EXT_NEW_VERSION} ${AZ_ORG} + "${PWD}/scripts/ci-deploy-dev.sh" ${AZ_EXT_NEW_VERSION} ${AZ_ORG} else - "${PWD}/scripts/ci-publish.sh" ${AZ_EXT_NEW_VERSION} + "${PWD}/scripts/ci-deploy-prod.sh" ${AZ_EXT_NEW_VERSION} fi echo "Extesion deployed!" diff --git a/scripts/update-task-json-dev.js b/scripts/update-task-json-dev.js index 2ba6044c..2ad00d12 100644 --- a/scripts/update-task-json-dev.js +++ b/scripts/update-task-json-dev.js @@ -1,5 +1,6 @@ const fs = require("fs"); +const { exit } = require("process"); console.log("Replacing version snykTask/task.json file..."); // Get version from argument @@ -10,9 +11,24 @@ if (!version.match(/[0-9]+\.[0-9]+\.[0-9]+/)) { process.exit(); } -const taskId = process.env.AZ_DEV_TASK_ID; // don't use the production GUID for dev/test deploys -const taskName = process.env.AZ_TASK_NAME; -const taskFriendlyName = process.env.AZ_TASK_FRIENDLY_NAME; +const taskId = process.env.DEV_AZ_TASK_ID; // don't use the production GUID for dev/test deploys +const taskName = process.env.DEV_AZ_TASK_NAME; +const taskFriendlyName = process.env.DEV_AZ_TASK_FRIENDLY_NAME; + +if (!taskId) { + console.log(`taskId not set! failing`); + process.exit(1); +} + +if (!taskName) { + console.log(`taskName not set! failing`); + process.exit(1); +} + +if (!taskFriendlyName) { + console.log(`taskFriendlyName not set! failing`); + process.exit(1); +} // Break version and create the JSON to be replaced const metaVersion = version.split("."); From 0927bbd98bb58af6975d82071043fa4442e3aba8 Mon Sep 17 00:00:00 2001 From: maxjeffos <44034094+maxjeffos@users.noreply.github.com> Date: Thu, 23 Jul 2020 14:12:52 -0400 Subject: [PATCH 3/3] fix: show unique issue count rather than paths --- ui/enhancer/snyk-report.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/enhancer/snyk-report.ts b/ui/enhancer/snyk-report.ts index fc4cc22a..79b71c18 100644 --- a/ui/enhancer/snyk-report.ts +++ b/ui/enhancer/snyk-report.ts @@ -88,15 +88,15 @@ export class SnykReportTab extends Controls.BaseControl { if (json["packageManager"]) spanText = `Snyk Test for ${json["packageManager"]} (${this.formatReportName(attachmentName)})`; - if (json["vulnerabilities"] && json["vulnerabilities"].length > 0) { + if (json["uniqueCount"] && json["uniqueCount"] > 0) { $(reportItem).addClass("failed"); img.src = "../img/report-failed.png"; - spanText = `${spanText} | Found ${json["vulnerabilities"].length} issues`; + spanText = `${spanText} | Found ${json["uniqueCount"]} issues`; } else { $(reportItem).addClass("passed"); img.src = "../img/report-passed.png"; - spanText = `${spanText} | Not found issues` + spanText = `${spanText} | No issues found` } } $(img).addClass("reportImg");