diff --git a/packages/azpipelines/BuildTasks/InstallDataPackageTask/InstallDataPackage.ts b/packages/azpipelines/BuildTasks/InstallDataPackageTask/InstallDataPackage.ts index 5841943b8..e5da181d0 100644 --- a/packages/azpipelines/BuildTasks/InstallDataPackageTask/InstallDataPackage.ts +++ b/packages/azpipelines/BuildTasks/InstallDataPackageTask/InstallDataPackage.ts @@ -12,6 +12,7 @@ import { import ArtifactHelper from "../Common/ArtifactHelper"; const fs = require("fs"); import SFPStatsSender from "@dxatscale/sfpowerscripts.core/lib/utils/SFPStatsSender" +import { PackageInstallationStatus } from "@dxatscale/sfpowerscripts.core/lib/package/PackageInstallationResult"; async function run() { try { @@ -97,44 +98,50 @@ async function run() { true ) - await installDataPackageImpl.exec(); + let result = await installDataPackageImpl.exec(); let elapsedTime=Date.now()-startTime; + if (result.result === PackageInstallationStatus.Succeeded) { + //No environment info available, create and push + if (packageMetadataFromStorage.deployments == null) { + packageMetadataFromStorage.deployments = new Array(); + packageMetadataFromStorage.deployments.push({ + target_org: target_org, + sub_directory: subdirectory, + installation_time:elapsedTime, + timestamp:Date.now() + }); + } else { + //Update existing environment map + packageMetadataFromStorage.deployments.push({ + target_org: target_org, + sub_directory: subdirectory, + installation_time:elapsedTime, + timestamp:Date.now() + }); + } - //No environment info available, create and push - if (packageMetadataFromStorage.deployments == null) { - packageMetadataFromStorage.deployments = new Array(); - packageMetadataFromStorage.deployments.push({ - target_org: target_org, - sub_directory: subdirectory, - installation_time:elapsedTime, - timestamp:Date.now() - }); - } else { - //Update existing environment map - packageMetadataFromStorage.deployments.push({ - target_org: target_org, - sub_directory: subdirectory, - installation_time:elapsedTime, - timestamp:Date.now() - }); - } + await updatePackageDeploymentDetails( + packageMetadataFromStorage, + extensionManagementApi, + extensionName + ); - await updatePackageDeploymentDetails( - packageMetadataFromStorage, - extensionManagementApi, - extensionName - ); + SFPStatsSender.logElapsedTime("package.installation.elapsed_time",elapsedTime,{package:sfdx_package,type:"data", target_org:target_org}); + SFPStatsSender.logCount("package.installation",{package:sfdx_package,type:"data",target_org:target_org}); - SFPStatsSender.logElapsedTime("package.installation.elapsed_time",elapsedTime,{package:sfdx_package,type:"data", target_org:target_org}); - SFPStatsSender.logCount("package.installation",{package:sfdx_package,type:"data",target_org:target_org}); + tl.setResult(tl.TaskResult.Succeeded, "Package installed successfully"); + } else if (result.result === PackageInstallationStatus.Failed) { + tl.setResult(tl.TaskResult.Failed, result.message); + + SFPStatsSender.logCount("package.installation.failure",{package:tl.getInput("package",false),type:"data"}); + } - tl.setResult(tl.TaskResult.Succeeded, "Package installed successfully"); } catch (err) { diff --git a/packages/azpipelines/BuildTasks/InstallUnlockedPackageTask/InstallUnlockedPackage.ts b/packages/azpipelines/BuildTasks/InstallUnlockedPackageTask/InstallUnlockedPackage.ts index 5db91f11f..65affae57 100644 --- a/packages/azpipelines/BuildTasks/InstallUnlockedPackageTask/InstallUnlockedPackage.ts +++ b/packages/azpipelines/BuildTasks/InstallUnlockedPackageTask/InstallUnlockedPackage.ts @@ -112,20 +112,28 @@ async function run() { sourceDirectory ); + let result: PackageInstallationResult = await installUnlockedPackageImpl.exec(); let elapsedTime=Date.now()-startTime; - - SFPStatsSender.logElapsedTime("package.installation.elapsed_time",elapsedTime,{package:sfdx_package,type:"unlocked", target_org:target_org}) - SFPStatsSender.logCount("package.installation",{package:sfdx_package,type:"unlocked",target_org:target_org}) - - let result: PackageInstallationResult = await installUnlockedPackageImpl.exec(); - if (result.result == PackageInstallationStatus.Skipped) { + if (result.result === PackageInstallationStatus.Skipped) { tl.setResult( tl.TaskResult.Skipped, "Skipping Package Installation as already installed" ); - } else { + } else if (result.result === PackageInstallationStatus.Failed) { + SFPStatsSender.logCount("package.installation.failure",{package:tl.getInput("package",false),type:"unlocked"}) + + tl.error(result.message); + tl.setResult( + tl.TaskResult.Failed, + result.message + ); + } else if (result.result === PackageInstallationStatus.Succeeded) { + + SFPStatsSender.logElapsedTime("package.installation.elapsed_time",elapsedTime,{package:sfdx_package,type:"unlocked", target_org:target_org}) + SFPStatsSender.logCount("package.installation",{package:sfdx_package,type:"unlocked",target_org:target_org}) + if (package_installedfrom != "Custom") { //No environment info available, create and push if (isNullOrUndefined(packageMetadataFromStorage.deployments)) { @@ -154,11 +162,12 @@ async function run() { } tl.setResult(tl.TaskResult.Succeeded, "Package Installed Successfully"); + } else { + throw new Error(`Unhandled package installation result ${result.result}`); } } catch (err) { SFPStatsSender.logCount("package.installation.failure",{package:tl.getInput("package",false),type:"unlocked"}) tl.setResult(tl.TaskResult.Failed, err.message); - } } diff --git a/packages/core/src/deploy/DeployImpl.ts b/packages/core/src/deploy/DeployImpl.ts new file mode 100644 index 000000000..afe6cfcff --- /dev/null +++ b/packages/core/src/deploy/DeployImpl.ts @@ -0,0 +1,401 @@ +import ArtifactFilePathFetcher from "../artifacts/ArtifactFilePathFetcher"; +import simplegit, { SimpleGit } from "simple-git/promise"; +import PackageMetadata from "../PackageMetadata"; +import ManifestHelpers from "../manifest/ManifestHelpers"; +import InstallSourcePackageImpl from "../sfdxwrappers/InstallSourcePackageImpl"; +import InstallDataPackageImpl from "../sfdxwrappers/InstallDataPackageImpl"; +import InstallUnlockedPackageImpl from "../sfdxwrappers/InstallUnlockedPackageImpl"; +import TriggerApexTestImpl from "../sfdxwrappers/TriggerApexTestImpl"; +import SFPStatsSender from "../utils/SFPStatsSender"; +import fs = require("fs"); +import path = require("path"); +import { + PackageInstallationResult, + PackageInstallationStatus, +} from "../package/PackageInstallationResult"; +import SFPLogger from "../utils/SFPLogger"; +import { EOL } from "os"; + + +export default class DeployImpl { + + + constructor( + private targetusername: string, + private artifactDir: string, + private wait_time: string, + private logsGroupSymbol: string[], + private tags: any, + private isValidateMode: boolean, + private coverageThreshold?: number + ){} + + public async exec(): Promise<{deployed: string[], skipped: string[], failed: string[]}> { + SFPLogger.isSupressLogs = true; + let deployed: string[] = []; + let skipped: string[] = []; + let failed: string[] = []; + + try { + let queue: any[] = this.getPackagesToDeploy(); + + SFPStatsSender.logGauge( + "deploy.scheduled", + queue.length, + this.tags + ); + + console.log(`Packages to be deployed:`, queue.map( (pkg) => pkg.package)); + + await this.validateArtifacts(); + + for (let i = 0 ; i < queue.length ; i++) { + let artifacts = ArtifactFilePathFetcher.fetchArtifactFilePaths( + this.artifactDir, + queue[i].package + ); + + let packageMetadata: PackageMetadata = JSON.parse( + fs.readFileSync(artifacts[0].packageMetadataFilePath, 'utf8') + ); + + let packageType: string = packageMetadata.package_type; + + if (this.logsGroupSymbol?.[0]) + console.log(this.logsGroupSymbol[0], "Installing", queue[i].package); + + let isApexFoundMessage: string = + packageMetadata.package_type === "unlocked" ? "" : `Contains Apex Classes/Triggers: ${packageMetadata.isApexFound}${EOL}` + + console.log( + `-------------------------Installing Package------------------------------------${EOL}` + + `Name: ${queue[i].package}${EOL}` + + `Type: ${packageMetadata.package_type}${EOL}` + + `Version Number: ${packageMetadata.package_version_number}${EOL}` + + `Metadata Count: ${packageMetadata.metadataCount}${EOL}` + + isApexFoundMessage + + `-------------------------------------------------------------------------------${EOL}` + ); + + + let packageInstallationResult = await this.installPackage( + packageType, + this.isValidateMode, + queue[i].package, + this.targetusername, + artifacts[0].sourceDirectoryPath, + packageMetadata, + this.isSkipTesting(queue[i]), + queue[i].aliasfy, + this.wait_time + ); + + if (packageInstallationResult.result === PackageInstallationStatus.Succeeded) + deployed.push(queue[i].package); + else if (packageInstallationResult.result === PackageInstallationStatus.Skipped) + skipped.push(queue[i].package); + else if (packageInstallationResult.result === PackageInstallationStatus.Failed) { + failed = queue.slice(i).map( (pkg) => pkg.package); + throw new Error(packageInstallationResult.message); + } + else + throw new Error(`Unhandled PackageInstallationResult ${packageInstallationResult.result}`); + + + if ( + this.isValidateMode && + (packageType === "unlocked" || packageType === "source") && + packageMetadata.isApexFound + ) { + if (!this.isSkipTesting(queue[i])) { + let testResult = await this.triggerApexTests( + queue[i].package, + this.targetusername, + queue[i].skipCoverageValidation, + this.coverageThreshold + ); + + if (!testResult.result) { + if (i !== queue.length - 1) + failed = queue.slice(i+1).map((pkg) => pkg.package); + + throw new Error(testResult.message); + } else + console.log(testResult.message); + } else + console.log(`Skipping testing of ${queue[i].package}\n`); + } + } + + if (this.logsGroupSymbol?.[1]) + console.log(this.logsGroupSymbol[1]); + + return { + deployed: deployed, + skipped: skipped, + failed: failed + }; + } catch (err) { + console.log(err); + + return { + deployed: deployed, + skipped: skipped, + failed: failed + }; + } + } + + /** + * Decider for which package installation type to run + */ + private async installPackage( + packageType: string, + isValidateMode: boolean, + sfdx_package: string, + targetUsername: string, + sourceDirectoryPath: string, + packageMetadata: PackageMetadata, + skipTesting: boolean, + aliasfy: boolean, + wait_time: string + ): Promise { + let packageInstallationResult: PackageInstallationResult; + + if (!isValidateMode) { + if (packageType === "unlocked") { + packageInstallationResult = await this.installUnlockedPackage( + targetUsername, + packageMetadata, + wait_time + ); + } else if (packageType === "source") { + let options = { + optimizeDeployment: true, + skipTesting: skipTesting, + }; + + packageInstallationResult = await this.installSourcePackage( + sfdx_package, + targetUsername, + sourceDirectoryPath, + packageMetadata, + options, + null, + wait_time + ); + } else if (packageType === "data") { + packageInstallationResult = await this.installDataPackage( + sfdx_package, + targetUsername, + sourceDirectoryPath, + packageMetadata + ); + } else { + throw new Error(`Unhandled package type ${packageType}`); + } + } else { + if (packageType === "source" || packageType === "unlocked") { + let options = { + optimizeDeployment: false, + skipTesting: true, + }; + + let subdirectory: string = aliasfy ? targetUsername : null; + + packageInstallationResult= await this.installSourcePackage( + sfdx_package, + targetUsername, + sourceDirectoryPath, + packageMetadata, + options, + subdirectory, + wait_time + ); + } else if ( packageType === "data") { + packageInstallationResult = await this.installDataPackage( + sfdx_package, + targetUsername, + sourceDirectoryPath, + packageMetadata + ); + } else { + throw new Error(`Unhandled package type ${packageType}`); + } + } + return packageInstallationResult; + } + + private installUnlockedPackage( + targetUsername: string, + packageMetadata: PackageMetadata, + wait_time: string + ): Promise { + let options = { + installationkey: null, + apexcompile: "package", + securitytype: "AdminsOnly", + upgradetype: "Mixed" + }; + + let installUnlockedPackageImpl: InstallUnlockedPackageImpl = new InstallUnlockedPackageImpl( + packageMetadata.package_version_id, + targetUsername, + options, + wait_time, + "10", + true, + packageMetadata + ); + + return installUnlockedPackageImpl.exec(); + } + + private installSourcePackage( + sfdx_package: string, + targetUsername: string, + sourceDirectoryPath: string, + packageMetadata: PackageMetadata, + options: any, + subdirectory: string, + wait_time: string + ): Promise { + + let installSourcePackageImpl: InstallSourcePackageImpl = new InstallSourcePackageImpl( + sfdx_package, + targetUsername, + sourceDirectoryPath, + subdirectory, + options, + wait_time, + true, + packageMetadata, + false + ); + + return installSourcePackageImpl.exec(); + } + + private installDataPackage( + sfdx_package: string, + targetUsername: string, + sourceDirectoryPath: string, + packageMetadata: PackageMetadata + ): Promise { + let installDataPackageImpl: InstallDataPackageImpl = new InstallDataPackageImpl( + sfdx_package, + targetUsername, + sourceDirectoryPath, + null, + packageMetadata, + true, + false + ); + return installDataPackageImpl.exec(); + } + + private async triggerApexTests( + sfdx_package: string, + targetUsername: string, + skipCoverageValidation: boolean, + coverageThreshold: number + ): Promise<{ + id: string, + result: boolean, + message: string + }> { + let test_options = { + wait_time: "60", + testlevel: "RunAllTestsInPackage", + package: sfdx_package, + synchronous: false, + validateIndividualClassCoverage: false, + validatePackageCoverage: !skipCoverageValidation, + coverageThreshold: coverageThreshold || 75, + outputdir: ".testresults" + }; + + let triggerApexTestImpl: TriggerApexTestImpl = new TriggerApexTestImpl( + targetUsername, + test_options, + null + ); + + return await triggerApexTestImpl.exec(); + } + + /** + * Checks if package should be installed to target username + * @param packageDescriptor + */ + private isSkipDeployment(packageDescriptor: any, targetUsername: string): boolean { + let skipDeployOnOrgs = packageDescriptor.skipDeployOnOrgs; + if (skipDeployOnOrgs) { + if (typeof(skipDeployOnOrgs) !== "string") + throw new Error(`Expected comma-separated string for "skipDeployOnOrgs". Received ${JSON.stringify(packageDescriptor,null,4)}`); + else + return ( + skipDeployOnOrgs + .split(",") + .map((org) => org.trim()) + .includes(targetUsername) + ); + } else + return false; + } + + private isSkipTesting(packageDescriptor: any): boolean { + return packageDescriptor.skipTesting ? true : false; + } + + /** + * Verify that artifacts are on the same source version as HEAD + */ + private async validateArtifacts(): Promise { + let git: SimpleGit = simplegit(); + + let head: string = await git.revparse([`HEAD`]); + + let artifacts = ArtifactFilePathFetcher.fetchArtifactFilePaths(this.artifactDir); + + for (let artifact of artifacts) { + let packageMetadata: PackageMetadata = JSON.parse( + fs.readFileSync(artifact.packageMetadataFilePath, "utf8") + ); + + if ( + packageMetadata.sourceVersion != null && + packageMetadata.sourceVersion != head + ) { + throw new Error(`${packageMetadata.package_name} is on a different source version.` + + `Artifacts must be on the same source version in order to determine the order of deployment.` + ); + } + } + } + + /** + * Returns the packages in the project config that have an artifact + */ + private getPackagesToDeploy(): any[] { + let packagesToDeploy: any[]; + + let packages = ManifestHelpers.getSFDXPackageManifest(null)["packageDirectories"]; + let artifacts = ArtifactFilePathFetcher.findArtifacts(this.artifactDir); + + + packagesToDeploy = packages.filter( (pkg) => { + let pattern = RegExp(`^${pkg.package}_sfpowerscripts_artifact.*`); + return artifacts.find((artifact) => pattern.test(path.basename(artifact))); + }); + + + // Filter out packages that are to be skipped on the target org + packagesToDeploy = packagesToDeploy.filter( (pkg) => !this.isSkipDeployment(pkg, this.targetusername)); + + if (packagesToDeploy == null || packagesToDeploy.length === 0) + throw new Error(`No artifacts from project config to be deployed`); + else + return packagesToDeploy + } +} diff --git a/packages/core/src/manifest/ManifestHelpers.ts b/packages/core/src/manifest/ManifestHelpers.ts index b3bf26e0a..af9ca02e8 100644 --- a/packages/core/src/manifest/ManifestHelpers.ts +++ b/packages/core/src/manifest/ManifestHelpers.ts @@ -1,5 +1,4 @@ import { isNullOrUndefined } from "util"; -import SFPLogger from "../utils/SFPLogger"; let fs = require("fs-extra"); let path = require("path"); const Table = require("cli-table"); @@ -210,7 +209,7 @@ export default class ManifestHelpers { let type = mdapiPackageManifest["Package"]["types"]; pushTypeMembersIntoTable(type); } - SFPLogger.log("The following metadata will be deployed:"); - SFPLogger.log(table.toString()); + console.log("The following metadata will be deployed:"); + console.log(table.toString()); } } diff --git a/packages/core/src/sfdxwrappers/CreateUnlockedPackageImpl.ts b/packages/core/src/sfdxwrappers/CreateUnlockedPackageImpl.ts index 69e640cac..b6441a80d 100644 --- a/packages/core/src/sfdxwrappers/CreateUnlockedPackageImpl.ts +++ b/packages/core/src/sfdxwrappers/CreateUnlockedPackageImpl.ts @@ -243,10 +243,14 @@ export default class CreateUnlockedPackageImpl { //Cleanup sfpowerscripts constructs if (this.isOrgDependentPackage) delete packageDescriptorInWorkingDirectory["dependencies"]; + delete packageDescriptorInWorkingDirectory["type"]; delete packageDescriptorInWorkingDirectory["preDeploymentSteps"]; delete packageDescriptorInWorkingDirectory["postDeploymentSteps"]; - delete packageDescriptorInWorkingDirectory["permissionSetsToAssign"] + delete packageDescriptorInWorkingDirectory["permissionSetsToAssign"]; + delete packageDescriptorInWorkingDirectory["skipDeployOnOrgs"]; + delete packageDescriptorInWorkingDirectory["skipTesting"]; + delete packageDescriptorInWorkingDirectory["skipCoverageValidation"]; fs.writeJsonSync( path.join(workingDirectory, "sfdx-project.json"), diff --git a/packages/core/src/sfdxwrappers/DeploySourceToOrgImpl.ts b/packages/core/src/sfdxwrappers/DeploySourceToOrgImpl.ts index 4b3276cbe..71d81ac03 100644 --- a/packages/core/src/sfdxwrappers/DeploySourceToOrgImpl.ts +++ b/packages/core/src/sfdxwrappers/DeploySourceToOrgImpl.ts @@ -70,7 +70,7 @@ export default class DeploySourceToOrgImpl { let deploy_id = ""; try { let command = this.buildExecCommand(); - SFPLogger.log(command); + console.log(command); let result = child_process.execSync(command, { cwd: this.project_directory, encoding: "utf8", @@ -85,11 +85,11 @@ export default class DeploySourceToOrgImpl { } if (this.deployment_options["checkonly"]) - SFPLogger.log( + console.log( `Validation only deployment is in progress.... Unleashing the power of your code!` ); else - SFPLogger.log( + console.log( `Deployment is in progress.... Unleashing the power of your code!` ); @@ -107,25 +107,25 @@ export default class DeploySourceToOrgImpl { ); } catch (err) { if (this.deployment_options["checkonly"]) - SFPLogger.log(`Validation Failed`); - else SFPLogger.log(`Deployment Failed`); + console.log(`Validation Failed`); + else console.log(`Deployment Failed`); break; } let resultAsJSON = JSON.parse(result); if (resultAsJSON["status"] == 1) { - SFPLogger.log("Validation/Deployment Failed"); + console.log("Validation/Deployment Failed"); commandExecStatus = false; break; } else if ( resultAsJSON["result"]["status"] == "InProgress" || resultAsJSON["result"]["status"] == "Pending" ) { - SFPLogger.log( + console.log( `Processing ${resultAsJSON.result.numberComponentsDeployed} out of ${resultAsJSON.result.numberComponentsTotal}` ); } else if (resultAsJSON["result"]["status"] == "Succeeded") { - SFPLogger.log("Validation/Deployment Succeeded"); + console.log("Validation/Deployment Succeeded"); commandExecStatus = true; break; } @@ -206,7 +206,7 @@ export default class DeploySourceToOrgImpl { private convertApexTestSuiteToListOfApexClasses( apextestsuite: string ): Promise { - SFPLogger.log( + console.log( `Converting an apex test suite ${apextestsuite} to its consituent apex test classes` ); diff --git a/packages/core/src/sfdxwrappers/InstallDataPackageImpl.ts b/packages/core/src/sfdxwrappers/InstallDataPackageImpl.ts index a43b84b61..30438a2ee 100644 --- a/packages/core/src/sfdxwrappers/InstallDataPackageImpl.ts +++ b/packages/core/src/sfdxwrappers/InstallDataPackageImpl.ts @@ -4,7 +4,6 @@ import child_process = require("child_process"); import { onExit } from "../utils/OnExit"; import fs = require("fs"); import ArtifactInstallationStatusChecker from "../artifacts/ArtifactInstallationStatusChecker"; -import SFPLogger from "../utils/SFPLogger"; import { PackageInstallationResult, PackageInstallationStatus } from "../package/PackageInstallationResult"; import ManifestHelpers from "../manifest/ManifestHelpers"; @@ -22,12 +21,12 @@ export default class InstallDataPackageImpl { ) {} public async exec(): Promise { - + let packageDirectory: string; try { let packageDescriptor = ManifestHelpers.getSFDXPackageDescriptor(this.sourceDirectory, this.sfdx_package); - + if (this.subDirectory) { packageDirectory = path.join( packageDescriptor["path"], @@ -50,11 +49,11 @@ export default class InstallDataPackageImpl { isPackageInstalled = await ArtifactInstallationStatusChecker.checkWhetherPackageIsIntalledInOrg(this.targetusername,this.packageMetadata,this.subDirectory, this.isPackageCheckHandledByCaller); if(isPackageInstalled) { - SFPLogger.log("Skipping Package Installation") + console.log("Skipping Package Installation") return { result: PackageInstallationStatus.Skipped } } } - + if ( @@ -90,11 +89,11 @@ export default class InstallDataPackageImpl { await ArtifactInstallationStatusChecker.updatePackageInstalledInOrg(this.targetusername,this.packageMetadata,this.subDirectory,this.isPackageCheckHandledByCaller); - + return {result: PackageInstallationStatus.Succeeded}; } catch (err) { - throw err; + return {result: PackageInstallationStatus.Failed, message: err.message}; } finally { let csvIssuesReportFilepath: string = path.join(this.sourceDirectory, packageDirectory, `CSVIssuesReport.csv`) if (fs.existsSync(csvIssuesReportFilepath)) { diff --git a/packages/core/src/sfdxwrappers/InstallSourcePackageImpl.ts b/packages/core/src/sfdxwrappers/InstallSourcePackageImpl.ts index a3f8eb15b..6886d23d7 100644 --- a/packages/core/src/sfdxwrappers/InstallSourcePackageImpl.ts +++ b/packages/core/src/sfdxwrappers/InstallSourcePackageImpl.ts @@ -45,7 +45,7 @@ export default class InstallSourcePackageImpl { this.isPackageCheckHandledByCaller ); if (isPackageInstalled) { - SFPLogger.log("Skipping Package Installation"); + console.log("Skipping Package Installation"); return { result: PackageInstallationStatus.Skipped }; } } @@ -261,7 +261,7 @@ export default class InstallSourcePackageImpl { private isAllTestsToBeTriggered(packageMetadata: PackageMetadata) { if (packageMetadata.package_type == "delta") { - console.log( + SFPLogger.log( ` ----------------------------------WARNING! NON OPTIMAL DEPLOYMENT---------------------------------------------${EOL}` + `This package has apex classes/triggers, In order to deploy optimally, each class need to have a minimum ${EOL}` + `75% test coverage, However being a dynamically generated delta package, we will deploying via triggering all local tests${EOL}` + @@ -274,7 +274,7 @@ export default class InstallSourcePackageImpl { this.packageMetadata.isApexFound == true && this.packageMetadata.apexTestClassses == null ) { - console.log( + SFPLogger.log( ` ----------------------------------WARNING! NON OPTIMAL DEPLOYMENT--------------------------------------------${EOL}` + `This package has apex classes/triggers, In order to deploy optimally, each class need to have a minimum ${EOL}` + `75% test coverage,We are unable to find any test classes in the given package, hence will be deploying ${EOL}` + @@ -399,7 +399,7 @@ export default class InstallSourcePackageImpl { try { result = await OrgDetails.getOrgDetails(target_org); } catch (err) { - console.log( + SFPLogger.log( ` -------------------------WARNING! SKIPPING TESTS AS ORG TYPE CANNOT BE DETERMINED! ------------------------------------${EOL}` + `Tests are mandatory for deployments to production and cannot be skipped. This deployment might fail as org${EOL}` + `type cannot be determined` + @@ -411,14 +411,14 @@ export default class InstallSourcePackageImpl { } if (result && result["IsSandbox"]) { - console.log( + SFPLogger.log( ` --------------------------------------WARNING! SKIPPING TESTS-------------------------------------------------${EOL}` + `Skipping tests for deployment to sandbox. Be cautious that deployments to prod will require tests and >75% code coverage ${EOL}` + `-------------------------------------------------------------------------------------------------------------` ); mdapi_options["testlevel"] = "NoTestRun"; } else { - console.log( + SFPLogger.log( ` -------------------------WARNING! TESTS ARE MANDATORY FOR PROD DEPLOYMENTS------------------------------------${EOL}` + `Tests are mandatory for deployments to production and cannot be skipped. Running all local tests! ${EOL}` + `-------------------------------------------------------------------------------------------------------------` diff --git a/packages/core/src/sfdxwrappers/InstallUnlockedPackageImpl.ts b/packages/core/src/sfdxwrappers/InstallUnlockedPackageImpl.ts index 79ffd8cb8..f77e8d85e 100644 --- a/packages/core/src/sfdxwrappers/InstallUnlockedPackageImpl.ts +++ b/packages/core/src/sfdxwrappers/InstallUnlockedPackageImpl.ts @@ -3,7 +3,6 @@ import { isNullOrUndefined } from "util"; import { onExit } from "../utils/OnExit"; import PackageMetadata from "../PackageMetadata"; import ManifestHelpers from "../manifest/ManifestHelpers"; -import SFPLogger from "../utils/SFPLogger"; import { PackageInstallationResult, PackageInstallationStatus } from "../package/PackageInstallationResult"; import AssignPermissionSetsImpl from "./AssignPermissionSetsImpl"; @@ -20,42 +19,46 @@ export default class InstallUnlockedPackageImpl { ) {} public async exec(): Promise { + try { + let isPackageInstalled = false; + if (this.skip_if_package_installed) { + isPackageInstalled = this.checkWhetherPackageIsIntalledInOrg(); + } + if (!isPackageInstalled) { + //Print Metadata carried in the package + ManifestHelpers.printMetadataToDeploy(this.packageMetadata?.payload); - let isPackageInstalled = false; - if (this.skip_if_package_installed) { - isPackageInstalled = this.checkWhetherPackageIsIntalledInOrg(); - } - - if (!isPackageInstalled) { - - //Print Metadata carried in the package - ManifestHelpers.printMetadataToDeploy(this.packageMetadata?.payload); - - let command = this.buildPackageInstallCommand(); - let child = child_process.exec(command); + let command = this.buildPackageInstallCommand(); + let child = child_process.exec(command); - child.stderr.on("data", (data) => { - SFPLogger.log(data.toString()); - }); + child.stderr.on("data", (data) => { + console.log(data.toString()); + }); - child.stdout.on("data", (data) => { - SFPLogger.log(data.toString()); - }); + child.stdout.on("data", (data) => { + console.log(data.toString()); + }); - await onExit(child); + await onExit(child); - //apply post deployment steps - if(this.sourceDirectory) - this.applyPermsets(); + //apply post deployment steps + if(this.sourceDirectory) + this.applyPermsets(); - return { result: PackageInstallationStatus.Succeeded} - } else { - SFPLogger.log("Skipping Package Installation") - return { result: PackageInstallationStatus.Skipped } + return { result: PackageInstallationStatus.Succeeded} + } else { + console.log("Skipping Package Installation") + return { result: PackageInstallationStatus.Skipped } + } + } catch (err) { + return { + result: PackageInstallationStatus.Failed, + message: err.message + } } } @@ -94,13 +97,13 @@ export default class InstallUnlockedPackageImpl { if (!isNullOrUndefined(this.options["installationkey"])) command += ` --installationkey=${this.options["installationkey"]}`; - SFPLogger.log(`Generated Command ${command}`); + console.log(`Generated Command ${command}`); return command; } private checkWhetherPackageIsIntalledInOrg(): boolean { try { - SFPLogger.log(`Checking Whether Package with ID ${this.package_version_id} is installed in ${this.targetusername}`) + console.log(`Checking Whether Package with ID ${this.package_version_id} is installed in ${this.targetusername}`) let command = `sfdx sfpowerkit:package:version:info -u ${this.targetusername} --json`; let result = JSON.parse(child_process.execSync(command).toString()); if (result.status == 0) { @@ -110,7 +113,7 @@ export default class InstallUnlockedPackageImpl { return true; }); if (packageFound) { - SFPLogger.log( + console.log( "Package To be installed was found in the target org", packageFound ); @@ -118,7 +121,7 @@ export default class InstallUnlockedPackageImpl { } } } catch (error) { - SFPLogger.log( + console.log( "Unable to check whether this package is installed in the target org" ); return false; diff --git a/packages/core/src/sfdxwrappers/TriggerApexTestImpl.ts b/packages/core/src/sfdxwrappers/TriggerApexTestImpl.ts index e882ca9c0..a3baa9851 100644 --- a/packages/core/src/sfdxwrappers/TriggerApexTestImpl.ts +++ b/packages/core/src/sfdxwrappers/TriggerApexTestImpl.ts @@ -5,7 +5,6 @@ import path = require("path"); import MDAPIPackageGenerator from "../generators/MDAPIPackageGenerator"; import ApexTypeFetcher, { ApexSortedByType } from "../parser/ApexTypeFetcher"; import ManifestHelpers from "../manifest/ManifestHelpers"; -import SFPLogger from "../utils/SFPLogger"; import SFPStatsSender from "../utils/SFPStatsSender"; const Table = require("cli-table"); @@ -202,7 +201,7 @@ export default class TriggerApexTestImpl { if (this.apexSortedByType["parseError"].length > 0) { for (let parseError of this.apexSortedByType["parseError"]) { - SFPLogger.log(`Failed to parse ${parseError.name}`); + console.log(`Failed to parse ${parseError.name}`); } } @@ -219,7 +218,7 @@ export default class TriggerApexTestImpl { command += ` -s ${this.test_options["apextestsuite"]}`; } - SFPLogger.log(`Generated Command: ${command}`); + console.log(`Generated Command: ${command}`); return command; } @@ -244,7 +243,7 @@ export default class TriggerApexTestImpl { ) .toString(); - SFPLogger.log("test_id", test_id); + console.log("test_id", test_id); return test_id; } @@ -332,7 +331,7 @@ export default class TriggerApexTestImpl { this.test_options.coverageThreshold = 75; } - SFPLogger.log( + console.log( `Validating individual classes for code coverage greater than ${this.test_options.coverageThreshold} percent` ); @@ -550,7 +549,7 @@ export default class TriggerApexTestImpl { // Filter out undetermined classes that failed to parse for (let parseError of this.apexSortedByType["parseError"]) { if (parseError["name"] === packageClass) { - SFPLogger.log( + console.log( `Skipping coverage validation for ${packageClass}, unable to determine identity of class` ); return false; @@ -583,7 +582,7 @@ export default class TriggerApexTestImpl { } private printTestSummary(testResult: any, packageCoverage: number){ - SFPLogger.log("\n\n\n=== Test Summary"); + console.log("\n\n\n=== Test Summary"); let table = new Table({ head: ["Name", "Value"] }); @@ -605,11 +604,11 @@ export default class TriggerApexTestImpl { table.push(keyValuePair); }) - SFPLogger.log(table.toString()); + console.log(table.toString()); } private printTestResults(testResult: any) { - SFPLogger.log("=== Test Results"); + console.log("=== Test Results"); let table = new Table({ head: ["Test Name", "Outcome", "Message", "Runtime (ms)"] @@ -624,13 +623,13 @@ export default class TriggerApexTestImpl { ]); }); - SFPLogger.log(table.toString()); + console.log(table.toString()); } private printClassesWithInvalidCoverage( classesWithInvalidCoverage: { name: string; coveredPercent: number }[] ) { - SFPLogger.log( + console.log( `The following classes do not satisfy the ${this.test_options["coverageThreshold"]}% code coverage requirement:` ); @@ -651,6 +650,6 @@ export default class TriggerApexTestImpl { ]); }); - SFPLogger.log(table.toString()); + console.log(table.toString()); } } diff --git a/packages/sfpowerscripts-cli/messages/deploy.json b/packages/sfpowerscripts-cli/messages/deploy.json new file mode 100644 index 000000000..460d2e43b --- /dev/null +++ b/packages/sfpowerscripts-cli/messages/deploy.json @@ -0,0 +1,9 @@ +{ + "commandDescription": "Deploy packages according to MPD in project configuration file", + "targetOrgFlagDescription": "Alias/User Name of the target environment", + "artifactDirectoryFlagDescription": "The directory containing artifacts to be deployed", + "waitTimeFlagDescription": "Wait time for command to finish in minutes", + "logsGroupSymbolFlagDescription": "Symbol used by CICD platform to group/collapse logs in the console. Provide an opening group, and an optional closing group symbol.", + "tagFlagDescription":"Tag the deploy with a label, useful for identification in metrics", + "validateModeFlagDescription": "Enable for validation deployments" +} diff --git a/packages/sfpowerscripts-cli/src/commands/sfpowerscripts/Build.ts b/packages/sfpowerscripts-cli/src/commands/sfpowerscripts/Build.ts index 080dd4ae6..9c8d5ddf4 100644 --- a/packages/sfpowerscripts-cli/src/commands/sfpowerscripts/Build.ts +++ b/packages/sfpowerscripts-cli/src/commands/sfpowerscripts/Build.ts @@ -26,6 +26,7 @@ export default class Build extends SfpowerscriptsCommand { protected static requiresUsername = false; protected static requiresDevhubUsername = false; + protected static requiresProject = true; protected static flagsConfig = { devhubalias: flags.string({ diff --git a/packages/sfpowerscripts-cli/src/commands/sfpowerscripts/Deploy.ts b/packages/sfpowerscripts-cli/src/commands/sfpowerscripts/Deploy.ts new file mode 100644 index 000000000..603c4a312 --- /dev/null +++ b/packages/sfpowerscripts-cli/src/commands/sfpowerscripts/Deploy.ts @@ -0,0 +1,148 @@ +import { flags } from "@salesforce/command"; +import SfpowerscriptsCommand from "../../SfpowerscriptsCommand"; +import { Messages } from "@salesforce/core"; +import SFPStatsSender from "@dxatscale/sfpowerscripts.core/lib/utils/SFPStatsSender"; +import DeployImpl from "@dxatscale/sfpowerscripts.core/lib/deploy/DeployImpl"; + +// Initialize Messages with the current plugin directory +Messages.importMessagesDirectory(__dirname); + +// Load the specific messages for this file. Messages from @salesforce/command, @salesforce/core, +// or any library that is using the messages framework can also be loaded this way. +const messages = Messages.loadMessages("@dxatscale/sfpowerscripts", "deploy"); + +export default class Deploy extends SfpowerscriptsCommand { + public static description = messages.getMessage("commandDescription"); + + public static examples = [ + `$ sfdx sfpowerscripts:Deploy -u ` + ]; + + protected static requiresUsername = false; + protected static requiresDevhubUsername = false; + protected static requiresProject = true; + + protected static flagsConfig = { + targetorg: flags.string({ + char: "u", + description: messages.getMessage("targetOrgFlagDescription"), + default: "scratchorg", + required: true + }), + artifactdir: flags.directory({ + description: messages.getMessage("artifactDirectoryFlagDescription"), + default: "artifacts", + }), + waittime: flags.string({ + description: messages.getMessage("waitTimeFlagDescription"), + default: "120", + }), + logsgroupsymbol: flags.array({ + char: "g", + description: messages.getMessage("logsGroupSymbolFlagDescription") + }), + tag: flags.string({ + char: 't', + description: messages.getMessage('tagFlagDescription') + }), + validatemode: flags.boolean({ + description: messages.getMessage("validateModeFlagDescription"), + hidden: true, + default: false, + }) + }; + + public async execute() { + let executionStartTime = Date.now(); + let deploymentResult: {deployed: string[], skipped: string[], failed: string[]}; + + let tags = { + targetOrg: this.flags.targetorg + }; + + if (this.flags.tag != null) { + tags["tag"] = this.flags.tag; + } + + try { + let deployImpl: DeployImpl = new DeployImpl( + this.flags.targetorg, + this.flags.artifactdir, + this.flags.waittime, + this.flags.logsgroupsymbol, + tags, + this.flags.validatemode + ); + + deploymentResult = await deployImpl.exec(); + + if (deploymentResult.failed.length > 0) { + process.exitCode = 1; + } + } catch (error) { + console.log(error); + process.exitCode = 1; + } finally { + let totalElapsedTime: number = Date.now() - executionStartTime; + + if (this.flags.logsgroupsymbol?.[0]) + console.log(this.flags.logsgroupsymbol[0], "Deployment Summary"); + + console.log( + `----------------------------------------------------------------------------------------------------` + ); + console.log( + `${deploymentResult.deployed.length} packages deployed in ${this.getFormattedTime( + totalElapsedTime + )} with {${deploymentResult.failed.length}} errors and {${deploymentResult.skipped.length}} skipped` + ); + + + if (deploymentResult.skipped.length > 0) { + console.log(`\nPackages Skipped`, deploymentResult.skipped); + } + + if (deploymentResult.failed.length > 0) { + console.log(`\nPackages Failed to Deploy`, deploymentResult.failed); + } + console.log( + `----------------------------------------------------------------------------------------------------` + ); + + SFPStatsSender.logGauge( + "deploy.duration", + totalElapsedTime, + tags + ); + + SFPStatsSender.logGauge( + "deploy.succeeded", + deploymentResult.deployed.length, + tags + ); + + if (deploymentResult.skipped.length > 0) { + SFPStatsSender.logGauge( + "deploy.skipped", + deploymentResult.skipped.length, + tags + ); + } + + if (deploymentResult.failed.length > 0) { + SFPStatsSender.logGauge( + "deploy.failed", + deploymentResult.failed.length, + tags + ); + } + } + } + + private getFormattedTime(milliseconds: number): string { + let date = new Date(0); + date.setSeconds(milliseconds / 1000); // specify value for SECONDS here + let timeString = date.toISOString().substr(11, 8); + return timeString; + } +} diff --git a/packages/sfpowerscripts-cli/src/commands/sfpowerscripts/InstallDataPackage.ts b/packages/sfpowerscripts-cli/src/commands/sfpowerscripts/InstallDataPackage.ts index 43f65e719..4b661142a 100644 --- a/packages/sfpowerscripts-cli/src/commands/sfpowerscripts/InstallDataPackage.ts +++ b/packages/sfpowerscripts-cli/src/commands/sfpowerscripts/InstallDataPackage.ts @@ -3,6 +3,7 @@ import InstallDataPackageImpl from '@dxatscale/sfpowerscripts.core/lib/sfdxwrapp import { Messages } from '@salesforce/core'; import SFPStatsSender from '@dxatscale/sfpowerscripts.core/lib/utils/SFPStatsSender'; import InstallPackageCommand from '../../InstallPackageCommand'; +import { PackageInstallationStatus } from '@dxatscale/sfpowerscripts.core/lib/package/PackageInstallationResult'; const fs = require("fs"); // Initialize Messages with the current plugin directory @@ -63,12 +64,19 @@ export default class InstallDataPackage extends InstallPackageCommand { skipIfAlreadyInstalled ) - await installDataPackageImpl.exec(); + let result = await installDataPackageImpl.exec(); let elapsedTime=Date.now()-startTime; - SFPStatsSender.logElapsedTime("package.installation.elapsed_time",elapsedTime,{package:sfdx_package,type:"unlocked", target_org:targetOrg}) - SFPStatsSender.logCount("package.installation",{package:sfdx_package,type:"unlocked",target_org:targetOrg}) + if (result.result === PackageInstallationStatus.Failed) { + SFPStatsSender.logCount("package.installation.failure",{package:sfdx_package,type:"data"}); + throw new Error(result.message); + } else if (result.result === PackageInstallationStatus.Succeeded) { + SFPStatsSender.logElapsedTime("package.installation.elapsed_time",elapsedTime,{package:sfdx_package,type:"unlocked", target_org:targetOrg}); + SFPStatsSender.logCount("package.installation",{package:sfdx_package,type:"unlocked",target_org:targetOrg}); + } + + } catch(err) { console.log(err); diff --git a/packages/sfpowerscripts-cli/src/commands/sfpowerscripts/InstallUnlockedPackage.ts b/packages/sfpowerscripts-cli/src/commands/sfpowerscripts/InstallUnlockedPackage.ts index 99654fcb1..d7e3d7fa3 100644 --- a/packages/sfpowerscripts-cli/src/commands/sfpowerscripts/InstallUnlockedPackage.ts +++ b/packages/sfpowerscripts-cli/src/commands/sfpowerscripts/InstallUnlockedPackage.ts @@ -3,6 +3,7 @@ import { flags } from '@salesforce/command'; import { Messages } from '@salesforce/core'; import SFPStatsSender from '@dxatscale/sfpowerscripts.core/lib/utils/SFPStatsSender'; import InstallPackageCommand from '../../InstallPackageCommand'; +import { PackageInstallationStatus } from '@dxatscale/sfpowerscripts.core/lib/package/PackageInstallationResult'; const fs = require("fs"); // Initialize Messages with the current plugin directory @@ -100,15 +101,18 @@ export default class InstallUnlockedPackage extends InstallPackageCommand { sourceDirectory ); - await installUnlockedPackageImpl.exec(); + let result = await installUnlockedPackageImpl.exec(); let elapsedTime=Date.now()-startTime; - SFPStatsSender.logElapsedTime("package.installation.elapsed_time",elapsedTime,{package:sfdx_package,type:"unlocked", target_org:targetOrg}) - SFPStatsSender.logCount("package.installation",{package:sfdx_package,type:"unlocked",target_org:targetOrg}) - - + if (result.result === PackageInstallationStatus.Failed) { + SFPStatsSender.logCount("package.installation.failure",{package:sfdx_package,type:"unlocked"}) + throw new Error(result.message); + } else if (result.result === PackageInstallationStatus.Succeeded) { + SFPStatsSender.logElapsedTime("package.installation.elapsed_time",elapsedTime,{package:sfdx_package,type:"unlocked", target_org:targetOrg}); + SFPStatsSender.logCount("package.installation",{package:sfdx_package,type:"unlocked",target_org:targetOrg}); + } } catch(err) { console.log(err); process.exitCode=1;