diff --git a/.github/workflows/sfp-build-docker.yml b/.github/workflows/sfp-build-docker.yml index f2eb7bf09..83fea5599 100644 --- a/.github/workflows/sfp-build-docker.yml +++ b/.github/workflows/sfp-build-docker.yml @@ -110,20 +110,9 @@ jobs: repo: ${{ inputs.repo }} image: ${{ inputs.image }} existing-tag: ${{ env.PKG_VERSION }}-${{ inputs.suffix-tag }} - new-tag: ${{ github.ref_name }} + new-tag: development registry: ghcr.io username: ${{ secrets.username }} token: ${{ secrets.token }} - #Tag the image as develop - - name: 'Tag Docker' - uses: ./.github/actions/tagDocker - with: - repo: ${{ inputs.repo }} - image: ${{ inputs.image }} - existing-tag: ${{ env.PKG_VERSION }}-${{ inputs.suffix-tag }} - new-tag: ${{ env.RELEASE_NAME }} - registry: ghcr.io - username: ${{ secrets.username }} - token: ${{ secrets.token }} \ No newline at end of file diff --git a/decision records/validate/005-validate-to-skip-testing.md b/decision records/validate/005-validate-to-skip-testing.md new file mode 100644 index 000000000..5dd3688fe --- /dev/null +++ b/decision records/validate/005-validate-to-skip-testing.md @@ -0,0 +1,36 @@ +# Trigger tests only for impacted packages during validate + +* Status: Approved +* Deciders: @azlam-abdulsalam, @nbatterham, @Rocko1204 +* Issue: #5 + +## Context and Problem Statement + +In the context of validating a change in CI/review orgs, sfp currently triggers all Apex tests for changed packages. However, these changes might not always be directly introduced by a developer from a branch. They could also result from syncing an upstream change or the org being a few commits behind. + +See the original issue by @petter-eikeland + +i.e. given that validate orgs were created at midnight, and 2 PRs have been merged to master/main changing package A and package B, respectively, both of which containing Apex classes and tests. When creating a 3rd PR, only changing package C (also with Apex classes and tests), validate would pick up changes in all 3 packages (A, B and C). In this scenario, validate deploys package A, and then executes the corresponding Apex tests. Then the same for package B and then finally package C. Ideally, validate would only deploy the (already validated) changes in package A and B (to update to validate org to what's on master/main), and only execute the Apex tests for package C. +E.g. if 'git diff HEAD origin/master --quiet ' + package["path"] + '/*' --> then run Apex tests. If not, skip Apex tests for the package. + +This additional execution of Apex tests for already validated changes has an especially significant impact on our current cadence given Salesforce's infrastructure issues during release windows. + +This model introduces considerable delay when PRs are synchronized in sfops style `Review` environments, as the +environment has a life cycle till the PR is addressed + +## Decision + +After reviewing multiple orgs, we've determined there's no added value in triggering Apex tests for packages that are synchronized rather than directly impacted. The changes in such packages are minimal since the PR doesn't introduce any new modifications to the out-of-sync packages. Therefore, it's more efficient to only trigger tests for the directly impacted packages. + +Initially, we considered adding a new flag and specific mode for this feature. However, to simplify adoption and because we foresee no significant impacts, we've decided against introducing a new mode. Instead, we will add flags to capture the SHA ref of the head of the incoming branch and the SHA ref of the base branch. It's crucial to ensure these refs are accurate and do not reflect any temporary detached commit IDs created by CI/CD systems such as GitHub. + +This also means the following changes to behavior of how validate works + +- Differentiate between packages that need to be synchronized vs validated +- Allow validate to commit the changed packages in to the target org, reverting earlier change to disableArtifactUpdate,so + users can control the behavior required, Ideally disableArtifactUpdate will be set to false, so retries to the same + review org within a range of commits will save further time +- Only commit the packages to an org, if the test pass for validate packages, however commit packages to org immediately for packages that are to be synchronized, when the deployment is succesful +- Package Diff comparison logic need to be updated to ensure, that if the commit id in the org is incorrect, assume the package is never built + + diff --git a/packages/sfp-cli/messages/validate.json b/packages/sfp-cli/messages/validate.json index f74d9828e..e51cc6e4c 100644 --- a/packages/sfp-cli/messages/validate.json +++ b/packages/sfp-cli/messages/validate.json @@ -7,7 +7,8 @@ "logsGroupSymbolFlagDescription": "Symbol used by CICD platform to group/collapse logs in the console. Provide an opening group, and an optional closing group symbol.", "deleteScratchOrgFlagDescription": "Delete scratch-org validation environment, after the command has finished running", "keysFlagDescription": "Keys to be used while installing any managed package dependencies. Required format is a string of key-value pairs separated by spaces e.g. packageA:pw123 packageB:pw123 packageC:pw123", - "baseBranchFlagDescription": "The pull request base branch", + "baseRefFlagDescription": "The sha/ref to the base commit against which this ref will be merged into, In CI/CD platforms please pass in the full sha as opposed to branch name", + "refFlagDescription": "The sha/ref that need to be validated, this should not be the merge ref in some ci/cd systems, rather the head ref of the branch that is proposed to be merged", "tagFlagDescription": "Tag the build with a label, useful to identify in metrics", "disableDiffCheckFlagDescription": "Disables diff check while validating, this will validate all the packages in the repository", "disableArtifactUpdateFlagDescription": "Do not update information about deployed artifacts to the org", diff --git a/packages/sfp-cli/messages/validateAgainstOrg.json b/packages/sfp-cli/messages/validateAgainstOrg.json index e3d765fde..4601e3a7b 100644 --- a/packages/sfp-cli/messages/validateAgainstOrg.json +++ b/packages/sfp-cli/messages/validateAgainstOrg.json @@ -5,7 +5,8 @@ "logsGroupSymbolFlagDescription": "Symbol used by CICD platform to group/collapse logs in the console. Provide an opening group, and an optional closing group symbol.", "diffCheckFlagDescription": "Only build the packages which have changed by analyzing previous tags", "disableArtifactUpdateFlagDescription": "Do not update information about deployed artifacts to the org", - "baseBranchFlagDescription": "The pull request base branch", + "baseRefFlagDescription": "The sha/ref to the base commit against which this ref will be merged into, In CI/CD platforms please pass in the full sha as opposed to branch name", + "refFlagDescription": "The sha/ref that need to be validated, this should not be the merge ref in some ci/cd systems, rather the head ref of the branch that is proposed to be merged", "fastfeedbackFlagDescription": "Enable validation in fast feedback mode, In fast feedback mode, validation will only do selective deployment of changed components and selective tests", "releaseConfigFileFlagDescription":"(Required if the release modes are ff-relese-config or thorough-release-config), Path to the config file which determines which impacted domains need to be validated", "devhubAliasFlagDescription": "Provide the alias of the devhub previously authenticated, used for installing or updating dependency in individual/thorough modes", diff --git a/packages/sfp-cli/package.json b/packages/sfp-cli/package.json index d40002bc9..627aad98e 100644 --- a/packages/sfp-cli/package.json +++ b/packages/sfp-cli/package.json @@ -1,7 +1,7 @@ { "name": "@flxbl-io/sfp", "description": "sfp is a CLI tool to help you manage your Salesforce projects in an artifact centric model", - "version": "37.1.4", + "version": "38.2.10", "license": "MIT", "author": "flxblio", "release": "March 24", diff --git a/packages/sfp-cli/src/commands/impact/package.ts b/packages/sfp-cli/src/commands/impact/package.ts index be2ff8a86..6d8d3f504 100644 --- a/packages/sfp-cli/src/commands/impact/package.ts +++ b/packages/sfp-cli/src/commands/impact/package.ts @@ -4,11 +4,11 @@ import { Stage } from '../../impl/Stage'; import SFPLogger, { COLOR_KEY_MESSAGE, ConsoleLogger } from '@flxbl-io/sfp-logger'; import { Flags } from '@oclif/core'; import { loglevel } from '../../flags/sfdxflags'; -import { ZERO_BORDER_TABLE } from '../../ui/TableConstants'; import ImpactedPackageResolver, { ImpactedPackageProps } from '../../impl/impact/ImpactedPackagesResolver'; -const Table = require('cli-table'); import path from 'path'; import * as fs from 'fs-extra'; +import ImpactedPackagesDisplayer from '../../core/display/ImpactedPackagesDisplayer'; +import { LoggerLevel } from '@flxbl-io/sfp-logger'; Messages.importMessagesDirectory(__dirname); @@ -41,13 +41,10 @@ export default class Package extends SfpCommand { const impactedPackageResolver = new ImpactedPackageResolver(this.props, new ConsoleLogger()); let packagesToBeBuiltWithReasons = await impactedPackageResolver.getImpactedPackages(); - let packageDiffTable = this.createDiffPackageScheduledDisplayedAsATable(packagesToBeBuiltWithReasons); const packagesToBeBuilt = Array.from(packagesToBeBuiltWithReasons.keys()); - - //Log Packages to be built - SFPLogger.log(COLOR_KEY_MESSAGE('Packages impacted...')); - SFPLogger.log(packageDiffTable.toString()); - + SFPLogger.log(COLOR_KEY_MESSAGE('Packages impacted...'),LoggerLevel.INFO,new ConsoleLogger()); + ImpactedPackagesDisplayer.displayImpactedPackages(packagesToBeBuiltWithReasons, new ConsoleLogger()); + const outputPath = path.join(process.cwd(), 'impacted-package.json'); if (packagesToBeBuilt && packagesToBeBuilt.length > 0) @@ -59,22 +56,7 @@ export default class Package extends SfpCommand { return packagesToBeBuilt; } - private createDiffPackageScheduledDisplayedAsATable(packagesToBeBuilt: Map) { - let tableHead = ['Package', 'Reason', 'Last Known Tag']; - let table = new Table({ - head: tableHead, - chars: ZERO_BORDER_TABLE, - }); - for (const pkg of packagesToBeBuilt.keys()) { - let item = [ - pkg, - packagesToBeBuilt.get(pkg).reason, - packagesToBeBuilt.get(pkg).tag ? packagesToBeBuilt.get(pkg).tag : '', - ]; - table.push(item); - } - return table; - } + } diff --git a/packages/sfp-cli/src/commands/validate/org.ts b/packages/sfp-cli/src/commands/validate/org.ts index ca027eff6..b9dd90b9a 100644 --- a/packages/sfp-cli/src/commands/validate/org.ts +++ b/packages/sfp-cli/src/commands/validate/org.ts @@ -40,15 +40,18 @@ export default class ValidateAgainstOrg extends SfpCommand { default: false, }), disableartifactupdate: Flags.boolean({ - deprecated: { - message: "--disableartifactupdate flag is deprecated, Artifacts used for validation are never recorded in the org " - }, description: messages.getMessage('disableArtifactUpdateFlagDescription'), default: false, }), logsgroupsymbol, - basebranch: Flags.string({ - description: messages.getMessage('baseBranchFlagDescription'), + ref: Flags.string({ + aliases: ['branch'], + dependsOn: ['baseRef'], + description: messages.getMessage('refFlagDescription'), + }), + baseRef: Flags.string({ + aliases: ['basebranch'], + description: messages.getMessage('baseRefFlagDescription'), }), orginfo: Flags.boolean({ description: messages.getMessage('orgInfoFlagDescription'), @@ -98,6 +101,13 @@ export default class ValidateAgainstOrg extends SfpCommand { )}` ) ); + if(this.flags.ref) { + SFPLogger.log(COLOR_HEADER(`Ref: ${this.flags.ref}`)); + } + if(this.flags.baseRef) { + SFPLogger.log(COLOR_HEADER(`Base Ref: ${this.flags.baseRef}`)); + } + if (this.flags.mode != ValidationMode.FAST_FEEDBACK) { SFPLogger.log(COLOR_HEADER(`Coverage Percentage: ${this.flags.coveragepercent}`)); } @@ -119,8 +129,9 @@ export default class ValidateAgainstOrg extends SfpCommand { logsGroupSymbol: this.flags.logsgroupsymbol, targetOrg: this.flags.targetorg, diffcheck: this.flags.diffcheck, - baseBranch: this.flags.basebranch, - disableArtifactCommit: true, + branch: this.flags.ref, + baseBranch: this.flags.baseRef, + disableArtifactCommit: this.flags.disableartifactupdate, disableSourcePackageOverride: this.flags.disablesourcepkgoverride, disableParallelTestExecution: this.flags.disableparalleltesting, orgInfo: this.flags.orginfo, diff --git a/packages/sfp-cli/src/commands/validate/pool.ts b/packages/sfp-cli/src/commands/validate/pool.ts index d10677634..e45e6c3a3 100644 --- a/packages/sfp-cli/src/commands/validate/pool.ts +++ b/packages/sfp-cli/src/commands/validate/pool.ts @@ -68,8 +68,14 @@ export default class ValidateAgainstPool extends SfpCommand { required: false, description: messages.getMessage('keysFlagDescription'), }), - basebranch: Flags.string({ - description: messages.getMessage('baseBranchFlagDescription'), + ref: Flags.string({ + aliases: ['branch'], + dependsOn: ['baseRef'], + description: messages.getMessage('refFlagDescription'), + }), + baseRef: Flags.string({ + aliases: ['basebranch'], + description: messages.getMessage('baseRefFlagDescription'), }), tag: Flags.string({ description: messages.getMessage('tagFlagDescription'), @@ -126,6 +132,14 @@ export default class ValidateAgainstPool extends SfpCommand { if (this.flags.mode != ValidationMode.FAST_FEEDBACK) { SFPLogger.log(COLOR_HEADER(`Coverage Percentage: ${this.flags.coveragepercent}`)); } + + if(this.flags.ref) { + SFPLogger.log(COLOR_HEADER(`Ref: ${this.flags.ref}`)); + } + if(this.flags.baseRef) { + SFPLogger.log(COLOR_HEADER(`Base Ref: ${this.flags.baseRef}`)); + } + @@ -148,7 +162,8 @@ export default class ValidateAgainstPool extends SfpCommand { shapeFile: this.flags.shapefile, isDeleteScratchOrg: this.flags.deletescratchorg, keys: this.flags.keys, - baseBranch: this.flags.basebranch, + branch: this.flags.ref, + baseBranch: this.flags.baseRef, diffcheck: !this.flags.disablediffcheck, disableArtifactCommit: true, orgInfo: this.flags.orginfo, diff --git a/packages/sfp-cli/src/core/display/ImpactedPackagesDisplayer.ts b/packages/sfp-cli/src/core/display/ImpactedPackagesDisplayer.ts new file mode 100644 index 000000000..30bb91fee --- /dev/null +++ b/packages/sfp-cli/src/core/display/ImpactedPackagesDisplayer.ts @@ -0,0 +1,25 @@ +import SFPLogger, { Logger, LoggerLevel } from '@flxbl-io/sfp-logger'; +import { ZERO_BORDER_TABLE } from './TableConstants'; +const Table = require('cli-table'); + +export default class ImpactedPackagesDisplayer { + + public static displayImpactedPackages(packagesToBeBuilt: Map,logger:Logger) { + let tableHead = ['Package', 'Reason', 'Last Known Commit Id/Tag']; + let table = new Table({ + head: tableHead, + chars: ZERO_BORDER_TABLE, + }); + for (const pkg of packagesToBeBuilt.keys()) { + let item = [ + pkg, + packagesToBeBuilt.get(pkg).reason, + packagesToBeBuilt.get(pkg).tag ? packagesToBeBuilt.get(pkg).tag : '', + ]; + table.push(item); + } + //Log Packages to be built + SFPLogger.log(table.toString(),LoggerLevel.INFO,logger); + } + +} diff --git a/packages/sfp-cli/src/core/git/Git.ts b/packages/sfp-cli/src/core/git/Git.ts index be6a1e1fe..95c58b29a 100644 --- a/packages/sfp-cli/src/core/git/Git.ts +++ b/packages/sfp-cli/src/core/git/Git.ts @@ -29,6 +29,10 @@ export default class Git { return this._git.revparse(['HEAD']); } + async getBaseBranchCommit(baseBranch:string): Promise { + return this._git.revparse([baseBranch]); + } + async show(options: string[]): Promise { return this._git.show(options); } diff --git a/packages/sfp-cli/src/core/org/SFPOrg.ts b/packages/sfp-cli/src/core/org/SFPOrg.ts index e774e7ad5..e3a8b4bb4 100644 --- a/packages/sfp-cli/src/core/org/SFPOrg.ts +++ b/packages/sfp-cli/src/core/org/SFPOrg.ts @@ -6,15 +6,16 @@ import QueryHelper from '../queryHelper/QueryHelper'; import { convertUsernameToAlias } from '../utils/AliasList'; import ObjectCRUDHelper from '../utils/ObjectCRUDHelper'; import InstalledPackagesQueryExecutor from './packageQuery/InstalledPackagesQueryExecutor'; +import dedent from 'dedent'; export default class SFPOrg extends Org { /** * Get list of all artifacts in an org */ - public async getInstalledArtifacts(orderBy: string = `CreatedDate`,logger?:Logger) { - let records=[] + public async getInstalledArtifacts(orderBy: string = `CreatedDate`, logger?: Logger) { + let records = []; try { - records = await QueryHelper.query( + records = await QueryHelper.query( `SELECT Id, Name, CommitId__c, Version__c, Tag__c FROM SfpowerscriptsArtifact2__c ORDER BY ${orderBy} ASC`, this.getConnection(), false @@ -74,55 +75,57 @@ export default class SFPOrg extends Org { * @param {SfpPackage} sfpPackage */ public async updateArtifactInOrg(logger: Logger, sfpPackage: SfpPackage): Promise { - let artifactId = await this.getArtifactRecordId(sfpPackage); + try { + let artifactId = await this.getArtifactRecordId(sfpPackage); - SFPLogger.log( - COLOR_KEY_MESSAGE( - `Existing artifact record id for ${sfpPackage.packageName} in Org for ${ - sfpPackage.package_version_number - }: ${artifactId ? artifactId : 'N/A'}` - ), - LoggerLevel.INFO, - logger - ); + SFPLogger.log( + COLOR_KEY_MESSAGE( + `Existing artifact record id for ${sfpPackage.packageName} in Org for ${ + sfpPackage.package_version_number + }: ${artifactId ? artifactId : 'N/A'}` + ), + LoggerLevel.INFO, + logger + ); - let packageName = sfpPackage.package_name; + let packageName = sfpPackage.package_name; - if (artifactId == null) { - artifactId = await ObjectCRUDHelper.createRecord( - this.getConnection(), - 'SfpowerscriptsArtifact2__c', - { + if (artifactId == null) { + artifactId = await ObjectCRUDHelper.createRecord(this.getConnection(), 'SfpowerscriptsArtifact2__c', { Name: packageName, Tag__c: sfpPackage.tag, Version__c: sfpPackage.package_version_number, CommitId__c: sfpPackage.sourceVersion, - } - ); - } else { - artifactId = await ObjectCRUDHelper.updateRecord( - this.getConnection(), - 'SfpowerscriptsArtifact2__c', - { + }); + } else { + artifactId = await ObjectCRUDHelper.updateRecord(this.getConnection(), 'SfpowerscriptsArtifact2__c', { Id: artifactId, Name: packageName, Tag__c: sfpPackage.tag, Version__c: sfpPackage.package_version_number, CommitId__c: sfpPackage.sourceVersion, - } + }); + } + + SFPLogger.log( + COLOR_KEY_MESSAGE( + `Updated Org with new Artifact ${packageName} ${sfpPackage.sourceVersion} ${ + sfpPackage.package_version_number + } ${artifactId ? artifactId : ''}` + ), + LoggerLevel.INFO, + logger + ); + return artifactId; + } catch (error) { + SFPLogger.log(dedent( + `Unable to fetch any sfp artifacts in the org,skipping updates in the org + - 1. sfp artifact package is not installed in the org + - 2. The required prerequisite object is not deployed to this org`), + LoggerLevel.WARN, + logger ); } - - SFPLogger.log( - COLOR_KEY_MESSAGE( - `Updated Org with new Artifact ${packageName} ${sfpPackage.package_version_number} ${ - artifactId ? artifactId : '' - }` - ), - LoggerLevel.INFO, - logger - ); - return artifactId; } private async getArtifactRecordId(sfpPackage: SfpPackage): Promise { @@ -192,16 +195,16 @@ export default class SFPOrg extends Org { /** * Return all artifacts including sfp as well as external unlocked/managed */ - public async getAllInstalledArtifacts():Promise { + public async getAllInstalledArtifacts(): Promise { let artifacts = await this.getInstalledArtifacts(`Name`); - let installedArtifacts: InstalledArtifact[]=[]; + let installedArtifacts: InstalledArtifact[] = []; let installed2GPPackages = await this.getAllInstalled2GPPackages(); artifacts.forEach((artifact) => { let installedArtifact: InstalledArtifact = { name: artifact.Name, version: artifact.Version__c, - commitId:artifact.CommitId__c, + commitId: artifact.CommitId__c, isInstalledBysfp: true, }; let packageFound = installed2GPPackages.find((elem) => elem.name == artifact.Name); @@ -243,7 +246,6 @@ const packageQuery = 'WHERE IsDeprecated != true ' + 'ORDER BY NamespacePrefix, Name'; - export interface InstalledArtifact { name: string; version: string; diff --git a/packages/sfp-cli/src/core/package/diff/PackageDiffImpl.ts b/packages/sfp-cli/src/core/package/diff/PackageDiffImpl.ts index 63c262eef..3764d16c3 100644 --- a/packages/sfp-cli/src/core/package/diff/PackageDiffImpl.ts +++ b/packages/sfp-cli/src/core/package/diff/PackageDiffImpl.ts @@ -2,12 +2,13 @@ const fs = require('fs'); const path = require('path'); import Git from '../../git/Git'; import IgnoreFiles from '../../ignore/IgnoreFiles'; -import SFPLogger, { COLOR_ERROR, COLOR_KEY_MESSAGE, Logger, LoggerLevel } from '@flxbl-io/sfp-logger'; +import SFPLogger, { COLOR_ERROR, COLOR_KEY_MESSAGE, COLOR_WARNING, Logger, LoggerLevel } from '@flxbl-io/sfp-logger'; import ProjectConfig from '../../project/ProjectConfig'; import GitTags from '../../git/GitTags'; import lodash = require('lodash'); import { EOL } from 'os'; import { PackageType } from '../SfpPackage'; +import dedent from 'dedent'; export class PackageDiffOptions { skipPackageDescriptorChange?: boolean = false; @@ -15,9 +16,10 @@ export class PackageDiffOptions { useLatestGitTags?:boolean=true; packagesMappedToLastKnownCommitId?: { [p: string]: string }; pathToReplacementForceIgnore?: string; - useBranchCompare?: boolean = false; + useBranchCompare?: boolean = false; // Compare among branches branch?: string; baseBranch?: string; + fallBackToNoTag?: boolean = false; } export default class PackageDiffImpl { @@ -65,11 +67,21 @@ export default class PackageDiffImpl { modified_files = await git.diff([`${tag}`, `HEAD`, `--no-renames`, `--name-only`]); } } catch (error) { + if(this.diffOptions?.fallBackToNoTag) + { + SFPLogger.log(COLOR_WARNING(dedent(`Unable to compute diff, + The head of the branch is not reachable from the commit id ${tag} for ${this.sfdx_package} + Attempting to build the package without diffing against the previous version`)),LoggerLevel.INFO,this.logger); + return { isToBeBuilt: true, reason: `Previous version is from an earlier branch` }; + } + else + { SFPLogger.log(COLOR_ERROR(`Unable to compute diff, The head of the branch is not reachable from the commit id ${tag}`)); - SFPLogger.log(COLOR_ERROR(`Check your current branch (in case of build) or the scratch org in case of validate command`)); + SFPLogger.log(COLOR_ERROR(`Check your current branch (in case of build) or the review org in case of validate command`)); SFPLogger.log(COLOR_ERROR(`Actual error received:`)); SFPLogger.log(COLOR_ERROR(error)); - throw new Error(`Failed to compute git diff for package ${this.sfdx_package} against commit id ${tag}`) + throw new Error(`Failed to compute git diff for package ${this.sfdx_package} against commit id ${tag}`) + } } let packageType: string = ProjectConfig.getPackageType(projectConfig, this.sfdx_package); diff --git a/packages/sfp-cli/src/core/package/packageCreators/CreateDiffPackageImpl.ts b/packages/sfp-cli/src/core/package/packageCreators/CreateDiffPackageImpl.ts index dc98b3d21..2e4ff6af5 100644 --- a/packages/sfp-cli/src/core/package/packageCreators/CreateDiffPackageImpl.ts +++ b/packages/sfp-cli/src/core/package/packageCreators/CreateDiffPackageImpl.ts @@ -43,12 +43,8 @@ export default class CreateDiffPackageImp extends CreateSourcePackageImpl { //Fetch Baseline commit from DevHub or the provided org for validation let commitsOfPackagesInstalled = {}; - if (this.packageCreationParams.baselineOrg) { - let baselineOrg = await SFPOrg.create({ aliasOrUsername: this.packageCreationParams.baselineOrg }); - commitsOfPackagesInstalled = await this.getCommitsOfPackagesInstalledInOrg(baselineOrg); - } else { - commitsOfPackagesInstalled = await this.getCommitsOfPackagesInstalledInOrg(devhubOrg); - } + commitsOfPackagesInstalled = await this.getCommitsOfPackagesInstalledInOrg(devhubOrg); + if (this.packageCreationParams.revisionFrom) { this.sfpPackage.commitSHAFrom = this.packageCreationParams.revisionFrom; diff --git a/packages/sfp-cli/src/impl/deploy/DeployImpl.ts b/packages/sfp-cli/src/impl/deploy/DeployImpl.ts index 384e0ca80..55f17ab00 100644 --- a/packages/sfp-cli/src/impl/deploy/DeployImpl.ts +++ b/packages/sfp-cli/src/impl/deploy/DeployImpl.ts @@ -5,7 +5,7 @@ import ProjectConfig from '../../core/project/ProjectConfig'; import semver = require('semver'); import PromoteUnlockedPackageImpl from '../../core/package/promote/PromoteUnlockedPackageImpl'; import { DeploymentType } from '../../core/deployers/DeploymentExecutor'; -import { COLOR_KEY_MESSAGE,COLOR_KEY_VALUE,COLOR_HEADER } from '@flxbl-io/sfp-logger'; +import { COLOR_KEY_MESSAGE, COLOR_KEY_VALUE, COLOR_HEADER } from '@flxbl-io/sfp-logger'; import { PackageInstallationResult, PackageInstallationStatus, @@ -23,10 +23,9 @@ import GroupConsoleLogs from '../../ui/GroupConsoleLogs'; import { ZERO_BORDER_TABLE } from '../../ui/TableConstants'; import convertBuildNumDotDelimToHyphen from '../../core/utils/VersionNumberConverter'; import ReleaseConfigLoader from '../release/ReleaseConfigLoader'; -import fs from 'fs-extra'; import { Align, getMarkdownTable } from 'markdown-table-ts'; import FileOutputHandler from '../../outputs/FileOutputHandler'; - +import { ValidateProps } from '../validate/ValidateImpl'; const Table = require('cli-table'); const retry = require('async-retry'); @@ -55,9 +54,10 @@ export interface DeployProps { devhubUserName?: string; disableArtifactCommit?: boolean; selectiveComponentDeployment?: boolean; - maxRetryCount?:number; - releaseConfigPath?:string; - filterByProvidedArtifacts?:string[]; + maxRetryCount?: number; + releaseConfigPath?: string; + filterByProvidedArtifacts?: string[]; + impactedPackagesAsPerBranch?: Map; } export default class DeployImpl { @@ -66,10 +66,8 @@ export default class DeployImpl { private targetOrg: SFPOrg; constructor(private props: DeployProps) { - //Set defaults - if(!this.props.maxRetryCount) - this.props.maxRetryCount = 1; + if (!this.props.maxRetryCount) this.props.maxRetryCount = 1; } public set postDeployHook(hook: PostDeployHook) { @@ -96,14 +94,20 @@ export default class DeployImpl { //Convert artifacts to SfpPackages let sfpPackages = await this.generateSfpPackageFromArtifacts(artifacts); - - //Filter artifacts based on release config - if(this.props.releaseConfigPath) - sfpPackages = this.filterSfPPackagesBasedOnReleaseConfig(sfpPackages,this.props.releaseConfigPath,this.props.logger); - else if(this.props.filterByProvidedArtifacts) - sfpPackages = this.filterSfPPackagesBasedOnArtifacts(sfpPackages,this.props.filterByProvidedArtifacts,this.props.logger); - + if (this.props.releaseConfigPath) + sfpPackages = this.filterSfPPackagesBasedOnReleaseConfig( + sfpPackages, + this.props.releaseConfigPath, + this.props.logger + ); + else if (this.props.filterByProvidedArtifacts) + sfpPackages = this.filterSfPPackagesBasedOnArtifacts( + sfpPackages, + this.props.filterByProvidedArtifacts, + this.props.logger + ); + //Grab the latest projectConfig from Packages let sfpPackageInquirer: SfpPackageInquirer = new SfpPackageInquirer(sfpPackages, this.props.logger); let sfdxProjectConfig = sfpPackageInquirer.getLatestProjectConfig(); @@ -116,11 +120,7 @@ export default class DeployImpl { packagesToPackageInfo = await this.getPackagesToPackageInfo(sfpPackages); - SFPLogger.log( - 'Artifacts' + JSON.stringify(packagesToPackageInfo), - LoggerLevel.TRACE, - this.props.logger - ); + SFPLogger.log('Artifacts' + JSON.stringify(packagesToPackageInfo), LoggerLevel.TRACE, this.props.logger); queue = this.getPackagesToDeploy(sfdxProjectConfig, packagesToPackageInfo); @@ -147,7 +147,12 @@ export default class DeployImpl { LoggerLevel.TRACE, this.props.logger ); - this.printArtifactVersionsWhenSkipped(queue, packagesToPackageInfo, isBaselinOrgModeActivated,this.props); + this.printArtifactVersionsWhenSkipped( + queue, + packagesToPackageInfo, + isBaselinOrgModeActivated, + this.props + ); queue = filteredDeploymentQueue; } else { this.printArtifactVersions(queue, packagesToPackageInfo); @@ -167,7 +172,7 @@ export default class DeployImpl { let groupSection; if (this.props.currentStage == Stage.VALIDATE) { groupSection = new GroupConsoleLogs( - `Validating: ${i + 1}/${queue.length} ${queue[i].packageName}`, + `Validating/Synchronizing: ${i + 1}/${queue.length} ${queue[i].packageName}`, this.props.logger ).begin(); } else @@ -203,7 +208,7 @@ export default class DeployImpl { await this.promotePackagesBeforeInstallation(packageInfo.sourceDirectory, sfpPackage); } catch (error) { //skip packages already promoted - SFPLogger.log(`Artifact already promoted .. skipping`,LoggerLevel.WARN); + SFPLogger.log(`Artifact already promoted .. skipping`, LoggerLevel.WARN); } this.displayRetryHeader(isToBeRetried, attemptCount); @@ -221,7 +226,12 @@ export default class DeployImpl { ); //Handle specific error condition which need a retry, overriding the set value - isToBeRetried = handleRetryOnSpecificConditions(isToBeRetried, installPackageResult, attemptCount,this.props.maxRetryCount); + isToBeRetried = handleRetryOnSpecificConditions( + isToBeRetried, + installPackageResult, + attemptCount, + this.props.maxRetryCount + ); if (isToBeRetried) { throw new Error(installPackageResult.message); @@ -236,11 +246,16 @@ export default class DeployImpl { message: error, }; - - FileOutputHandler.getInstance().writeOutput(`deployment-error.md`,`### 💣 Deployment Failed 💣`); - FileOutputHandler.getInstance().appendOutput(`deployment-error.md`,`Artifact Installation failed for **${queue[i].packageName}**`); - FileOutputHandler.getInstance().appendOutput(`deployment-error.md`,`Reasons:`); - FileOutputHandler.getInstance().appendOutput(`deployment-error.md`,`${error}`); + FileOutputHandler.getInstance().writeOutput( + `deployment-error.md`, + `### 💣 Deployment Failed 💣` + ); + FileOutputHandler.getInstance().appendOutput( + `deployment-error.md`, + `Artifact Installation failed for **${queue[i].packageName}**` + ); + FileOutputHandler.getInstance().appendOutput(`deployment-error.md`, `Reasons:`); + FileOutputHandler.getInstance().appendOutput(`deployment-error.md`, `${error}`); return failedPackageInstallationResult; } @@ -250,16 +265,13 @@ export default class DeployImpl { isToBeRetried: boolean, installPackageResult: PackageInstallationResult, retryCount: number, - maxRetryCount:number + maxRetryCount: number ): boolean { //override current value when encountering such issue if (installPackageResult.result === PackageInstallationStatus.Failed) { - if (installPackageResult.message?.includes('ongoing background job')) - return true; - else if (isToBeRetried && retryCount <= maxRetryCount ) - return true; - else - return false; + if (installPackageResult.message?.includes('ongoing background job')) return true; + else if (isToBeRetried && retryCount <= maxRetryCount) return true; + else return false; } else return false; } }, @@ -275,7 +287,7 @@ export default class DeployImpl { } // Only deploy post hook when package installation is successful - if(packageInstallationResult.result === PackageInstallationStatus.Succeeded) { + if (packageInstallationResult.result === PackageInstallationStatus.Succeeded) { let postHookStatus = await this._postDeployHook?.postDeployPackage( sfpPackage, packageInstallationResult, @@ -309,7 +321,7 @@ export default class DeployImpl { failed: failed, queue: queue, packagesToPackageInfo: packagesToPackageInfo, - error: null + error: null, }; } catch (err) { SFPLogger.log(err, LoggerLevel.ERROR, this.props.logger); @@ -324,46 +336,59 @@ export default class DeployImpl { }; } } - private filterSfPPackagesBasedOnReleaseConfig(sfpPackages: SfpPackage[], releaseConfigPath: string,logger:Logger): SfpPackage[] { - if(!releaseConfigPath) - return sfpPackages; - else - { - SFPLogger.log(COLOR_KEY_MESSAGE(`Filtering packages to be deployed based on release config ${COLOR_KEY_VALUE(releaseConfigPath)}`),LoggerLevel.INFO,logger); - let releaseConfigLoader:ReleaseConfigLoader = new ReleaseConfigLoader(logger,releaseConfigPath); - let packages = releaseConfigLoader.getPackagesAsPerReleaseConfig(); - //Filter artifacts based on packages - let filteredSfPPackages:SfpPackage[] = []; + private filterSfPPackagesBasedOnReleaseConfig( + sfpPackages: SfpPackage[], + releaseConfigPath: string, + logger: Logger + ): SfpPackage[] { + if (!releaseConfigPath) return sfpPackages; + else { + SFPLogger.log( + COLOR_KEY_MESSAGE( + `Filtering packages to be deployed based on release config ${COLOR_KEY_VALUE(releaseConfigPath)}` + ), + LoggerLevel.INFO, + logger + ); + let releaseConfigLoader: ReleaseConfigLoader = new ReleaseConfigLoader(logger, releaseConfigPath); + let packages = releaseConfigLoader.getPackagesAsPerReleaseConfig(); + //Filter artifacts based on packages + let filteredSfPPackages: SfpPackage[] = []; for (const sfpPackage of sfpPackages) { if (packages.includes(sfpPackage.packageName)) { filteredSfPPackages.push(sfpPackage); } - } - return filteredSfPPackages; - } - + } + return filteredSfPPackages; + } } - private filterSfPPackagesBasedOnArtifacts(sfpPackages: SfpPackage[], artifacts:string[],logger:Logger): SfpPackage[] { - if(!artifacts || artifacts.length==0) - return sfpPackages; - else - { - SFPLogger.log(COLOR_KEY_MESSAGE(`Filtering packages to be deployed based on provided artifacts ${COLOR_KEY_VALUE(artifacts)}`),LoggerLevel.INFO,logger); - //Filter artifacts based on packages - let filteredSfPPackages:SfpPackage[] = []; - - for (const sfpPackage of sfpPackages) { - if (artifacts.includes(sfpPackage.packageName)) { - filteredSfPPackages.push(sfpPackage); - } - } - return filteredSfPPackages; - } - - } + private filterSfPPackagesBasedOnArtifacts( + sfpPackages: SfpPackage[], + artifacts: string[], + logger: Logger + ): SfpPackage[] { + if (!artifacts || artifacts.length == 0) return sfpPackages; + else { + SFPLogger.log( + COLOR_KEY_MESSAGE( + `Filtering packages to be deployed based on provided artifacts ${COLOR_KEY_VALUE(artifacts)}` + ), + LoggerLevel.INFO, + logger + ); + //Filter artifacts based on packages + let filteredSfPPackages: SfpPackage[] = []; + for (const sfpPackage of sfpPackages) { + if (artifacts.includes(sfpPackage.packageName)) { + filteredSfPPackages.push(sfpPackage); + } + } + return filteredSfPPackages; + } + } private async generateSfpPackageFromArtifacts(artifacts: Artifact[]): Promise { let sfpPackages: SfpPackage[] = []; @@ -380,8 +405,7 @@ export default class DeployImpl { console.log( COLOR_KEY_MESSAGE(`Attempting to promote artifact ${sfpPackage.packageName} before installation`) ); - if(!this.props.isDryRun) - { + if (!this.props.isDryRun) { let promoteUnlockedPackageImpl: PromoteUnlockedPackageImpl = new PromoteUnlockedPackageImpl( sourceDirectory, sfpPackage.package_version_id, @@ -395,9 +419,9 @@ export default class DeployImpl { private displayRetryHeader(isRetryOnFailure: boolean, count: number) { if (isRetryOnFailure && count > 1) { - SFPLogger.printHeaderLine('',COLOR_HEADER,LoggerLevel.INFO,this.props.logger); + SFPLogger.printHeaderLine('', COLOR_HEADER, LoggerLevel.INFO, this.props.logger); SFPLogger.log(`Retrying On Failure Attempt: ${count}`, LoggerLevel.INFO, this.props.logger); - SFPLogger.printHeaderLine('',COLOR_HEADER,LoggerLevel.INFO,this.props.logger); + SFPLogger.printHeaderLine('', COLOR_HEADER, LoggerLevel.INFO, this.props.logger); } } @@ -410,7 +434,16 @@ export default class DeployImpl { } else alwaysDeployMessage = undefined; //Display header - SFPLogger.printHeaderLine(`Installing artifact:${pkg}`,COLOR_HEADER,LoggerLevel.INFO,this.props.logger); + let message = 'Installing'; + if (this.props.currentStage == Stage.VALIDATE) { + if (this.props.impactedPackagesAsPerBranch) { + let isPackageImpacted = this.props.impactedPackagesAsPerBranch?.get(pkg); + message = isPackageImpacted ? 'Validating' : 'Synchronizing'; + } else { + message = 'Validating'; + } + } + SFPLogger.printHeaderLine(`${message} artifact:${pkg}`, COLOR_HEADER, LoggerLevel.INFO, this.props.logger); SFPLogger.log(COLOR_HEADER(`Name: ${COLOR_KEY_MESSAGE(pkg)}`), LoggerLevel.INFO, this.props.logger); SFPLogger.log(`Type: ${COLOR_KEY_MESSAGE(sfpPackage.packageType)}`, LoggerLevel.INFO, this.props.logger); SFPLogger.log( @@ -420,12 +453,8 @@ export default class DeployImpl { ); this.displayTestInfoHeader(sfpPackage); if (pkgDescriptor.aliasfy) - SFPLogger.log( - `Aliasified Package: ${COLOR_KEY_MESSAGE(`True`)}`, - LoggerLevel.INFO, - this.props.logger - ); - if(sfpPackage.isApexFound) + SFPLogger.log(`Aliasified Package: ${COLOR_KEY_MESSAGE(`True`)}`, LoggerLevel.INFO, this.props.logger); + if (sfpPackage.isApexFound) SFPLogger.log( `Contains Apex Classes/Triggers: ${COLOR_KEY_MESSAGE(sfpPackage.isApexFound)}`, LoggerLevel.INFO, @@ -433,31 +462,22 @@ export default class DeployImpl { ); if (sfpPackage.packageType == PackageType.Source || sfpPackage.packageType == PackageType.Unlocked) { if (!pkgDescriptor.aliasfy) { - SFPLogger.log( - `Metadata to be deployed: ${COLOR_KEY_MESSAGE(sfpPackage.metadataCount)}`, - LoggerLevel.INFO, - this.props.logger - ); + SFPLogger.log( + `Metadata to be deployed: ${COLOR_KEY_MESSAGE(sfpPackage.metadataCount)}`, + LoggerLevel.INFO, + this.props.logger + ); } } if (pkgDescriptor.skipTesting) { - SFPLogger.log( - `Skip Testing: ${COLOR_KEY_MESSAGE('true')}`, - LoggerLevel.INFO, - this.props.logger - ); - } - else{ - SFPLogger.log( - `Skip Testing: ${COLOR_KEY_MESSAGE('false')}`, - LoggerLevel.INFO, - this.props.logger - ); + SFPLogger.log(`Skip Testing: ${COLOR_KEY_MESSAGE('true')}`, LoggerLevel.INFO, this.props.logger); + } else { + SFPLogger.log(`Skip Testing: ${COLOR_KEY_MESSAGE('false')}`, LoggerLevel.INFO, this.props.logger); } if (alwaysDeployMessage) SFPLogger.log(alwaysDeployMessage, LoggerLevel.INFO, this.props.logger); - SFPLogger.printHeaderLine('',COLOR_HEADER,LoggerLevel.INFO,this.props.logger); + SFPLogger.printHeaderLine('', COLOR_HEADER, LoggerLevel.INFO, this.props.logger); } private displayTestInfoHeader(sfpPackage: SfpPackage) { @@ -470,12 +490,7 @@ export default class DeployImpl { LoggerLevel.INFO, this.props.logger ); - else - SFPLogger.log( - `Trigger All Tests: ${COLOR_KEY_MESSAGE(`true`)}`, - LoggerLevel.INFO, - this.props.logger - ); + else SFPLogger.log(`Trigger All Tests: ${COLOR_KEY_MESSAGE(`true`)}`, LoggerLevel.INFO, this.props.logger); } } @@ -483,7 +498,7 @@ export default class DeployImpl { queue: SfpPackage[], packagesToPackageInfo: { [p: string]: PackageInfo }, isBaselinOrgModeActivated: boolean, - props:DeployProps + props: DeployProps ) { let groupSection = new GroupConsoleLogs(`Full Installation Breakdown`, this.props.logger).begin(); let maxTable = new Table({ @@ -502,7 +517,6 @@ export default class DeployImpl { SFPLogger.log(maxTable.toString(), LoggerLevel.INFO, this.props.logger); - //Insane Hack //TODO: Export the value to the caller printDeploymentBreakDownInMarkdown(); @@ -532,28 +546,26 @@ export default class DeployImpl { SFPLogger.log(minTable.toString(), LoggerLevel.INFO, this.props.logger); groupSection.end(); - - function printDeploymentBreakDownInMarkdown() { let tableData = { table: { - head: [ + head: [ 'Artifact', 'Incoming Version', - isBaselinOrgModeActivated ? 'Version in baseline org' : 'Version in org', + isBaselinOrgModeActivated ? 'Version in baseline org' : 'Version in org', 'To be installed?', - 'Promotion Status' + 'Promotion Status', ], - body: [] + body: [], }, - alignment: [Align.Left, Align.Left, Align.Left,Align.Right], + alignment: [Align.Left, Align.Left, Align.Left, Align.Right], }; for (const pkg of queue) { - tableData.table.body.push(getRowForMarkdownTable(pkg,props)); + tableData.table.body.push(getRowForMarkdownTable(pkg, props)); } const table = getMarkdownTable(tableData); - const outputHandler:FileOutputHandler = FileOutputHandler.getInstance(); - outputHandler.appendOutput('deployment-breakdown.md',table) ; + const outputHandler: FileOutputHandler = FileOutputHandler.getInstance(); + outputHandler.appendOutput('deployment-breakdown.md', table); } function processColoursForAllPackages(pkg) { @@ -565,25 +577,21 @@ export default class DeployImpl { let isPackageInstalled = pkgInfo.isPackageInstalled ? 'No' : 'Yes'; if (pkgInfo.isPackageInstalled) { - packageName = COLOR_SUCCESS(packageName); - versionNumber = COLOR_SUCCESS(versionNumber); - versionInstalledInOrg = COLOR_SUCCESS(versionInstalledInOrg); - isPackageInstalled = COLOR_SUCCESS(isPackageInstalled); - } - else - { + packageName = COLOR_SUCCESS(packageName); + versionNumber = COLOR_SUCCESS(versionNumber); + versionInstalledInOrg = COLOR_SUCCESS(versionInstalledInOrg); + isPackageInstalled = COLOR_SUCCESS(isPackageInstalled); + } else { packageName = COLOR_ERROR(packageName); versionNumber = COLOR_ERROR(versionNumber); versionInstalledInOrg = COLOR_ERROR(versionInstalledInOrg); isPackageInstalled = COLOR_ERROR(isPackageInstalled); - } return [packageName, versionNumber, versionInstalledInOrg, isPackageInstalled]; } - - function getRowForMarkdownTable(pkg:SfpPackage, props:DeployProps) { + function getRowForMarkdownTable(pkg: SfpPackage, props: DeployProps) { const pkgInfo = packagesToPackageInfo[pkg.packageName]; let packageName = pkg.packageName; @@ -592,42 +600,43 @@ export default class DeployImpl { let isPackageToBeInstalled = pkgInfo.isPackageInstalled ? 'No' : 'Yes'; let promotionStatus = 'N/A'; - if(isPackageToBeInstalled=="Yes") - { + if (isPackageToBeInstalled == 'Yes') { isPackageToBeInstalled = `![Yes](https://img.shields.io/badge/Yes-green.svg)`; packageName = `**${packageName}**`; - if(pkg.packageType==PackageType.Unlocked) - { - if (props.promotePackagesBeforeDeploymentToOrg == props.targetUsername && versionInstalledInOrg == "N/A") { + if (pkg.packageType == PackageType.Unlocked) { + if ( + props.promotePackagesBeforeDeploymentToOrg == props.targetUsername && + versionInstalledInOrg == 'N/A' + ) { promotionStatus = '![Pending](https://img.shields.io/badge/Pending-yellow.svg)'; - } - else if(props.promotePackagesBeforeDeploymentToOrg == props.targetUsername ) { - let versionInstalledInOrgConvertedToSemver = convertBuildNumDotDelimToHyphen(versionInstalledInOrg); + } else if (props.promotePackagesBeforeDeploymentToOrg == props.targetUsername) { + let versionInstalledInOrgConvertedToSemver = convertBuildNumDotDelimToHyphen( + versionInstalledInOrg + ); let versionNumberConvertedToSemver = convertBuildNumDotDelimToHyphen(versionNumber); - if (semver.diff(versionInstalledInOrgConvertedToSemver, versionNumberConvertedToSemver) == 'prerelease') { - promotionStatus = '![Already Promoted](https://img.shields.io/badge/Already%20Promoted-red.svg)'; - } - else { + if ( + semver.diff(versionInstalledInOrgConvertedToSemver, versionNumberConvertedToSemver) == + 'prerelease' + ) { + promotionStatus = + '![Already Promoted](https://img.shields.io/badge/Already%20Promoted-red.svg)'; + } else { promotionStatus = '![Pending](https://img.shields.io/badge/Pending-yellow.svg)'; } - } - else - { + } else { promotionStatus = 'N/A'; } } - versionNumber = `**${versionNumber}**`; - versionInstalledInOrg = `**${versionInstalledInOrg}**`; - } - else - { + versionNumber = `**${versionNumber}**`; + versionInstalledInOrg = `**${versionInstalledInOrg}**`; + } else { versionNumber = `**${versionNumber}**`; versionInstalledInOrg = `**${versionInstalledInOrg}**`; } - return [packageName, versionNumber, versionInstalledInOrg, isPackageToBeInstalled,promotionStatus]; - } + return [packageName, versionNumber, versionInstalledInOrg, isPackageToBeInstalled, promotionStatus]; + } } private printArtifactVersions(queue: SfpPackage[], packagesToPackageInfo: { [p: string]: PackageInfo }) { @@ -643,34 +652,41 @@ export default class DeployImpl { SFPLogger.log(table.toString(), LoggerLevel.INFO, this.props.logger); groupSection.end(); - printDeploymentBreakDownInMarkdown(); - + printDeploymentBreakDownInMarkdown(this.props); - function printDeploymentBreakDownInMarkdown() { + function printDeploymentBreakDownInMarkdown(props:DeployProps) { let tableData = { table: { - head: [ - 'Package', - 'Version to be installed' - ], - body: [] + head: ['Package', props.currentStage==Stage.VALIDATE?`Version`:`Commit Id`, 'Reason?'], + body: [], }, - alignment: [Align.Left, Align.Left, Align.Left,Align.Right], + alignment: [Align.Left, Align.Left, Align.Left, Align.Left], }; for (const pkg of queue) { - tableData.table.body.push(getRowForMarkdownTable(pkg)); + tableData.table.body.push(getRowForMarkdownTable(pkg,props)); } - const outputHandler:FileOutputHandler = FileOutputHandler.getInstance(); - outputHandler.writeOutput('deployment-breakdown.md',`Please find the artifacts that will be installed below`); - outputHandler.appendOutput('deployment-breakdown.md',`\n\n${getMarkdownTable(tableData)}`) ; + const outputHandler: FileOutputHandler = FileOutputHandler.getInstance(); + outputHandler.writeOutput( + 'deployment-breakdown.md', + `Please find the artifacts that will be installed below` + ); + outputHandler.appendOutput('deployment-breakdown.md', `\n\n${getMarkdownTable(tableData)}`); } - function getRowForMarkdownTable(pkg:SfpPackage) { + function getRowForMarkdownTable(pkg: SfpPackage,props:DeployProps) { let packageName = pkg.packageName; - let versionNumber = pkg.versionNumber; - return [packageName, versionNumber]; - } + if (props.currentStage == Stage.VALIDATE) { + if ( + props.impactedPackagesAsPerBranch && + props.impactedPackagesAsPerBranch.get(pkg.packageName) + ) + return [packageName, pkg.sourceVersion, '![validation](https://img.shields.io/badge/validation-yellow.svg)']; + else return [packageName, pkg.sourceVersion,`![sync](https://img.shields.io/badge/sync-green.svg)`]; + } else { + return [packageName, pkg.versionNumber, 'Deploy']; + } + } } private async filterByPackagesInstalledInTheOrg( @@ -690,10 +706,7 @@ export default class DeployImpl { clonedQueue[i].packageName, packageManifest ); - let packageInstalledInTheOrg = await targetOrg.isArtifactInstalledInOrg( - this.props.logger, - sfpPackage - ); + let packageInstalledInTheOrg = await targetOrg.isArtifactInstalledInOrg(this.props.logger, sfpPackage); if (packageInstalledInTheOrg.versionNumber) packageInfo.versionInstalledInOrg = packageInstalledInTheOrg.versionNumber; if (packageInstalledInTheOrg.isInstalled) { @@ -754,12 +767,12 @@ export default class DeployImpl { //Compute Deployment Type let deploymentType = this.props.deploymentMode === DeploymentMode.SOURCEPACKAGES_PUSH - ? DeploymentType.SOURCE_PUSH : DeploymentType.MDAPI_DEPLOY; + ? DeploymentType.SOURCE_PUSH + : DeploymentType.MDAPI_DEPLOY; //Add Installation Options let installationOptions = new SfpPackageInstallationOptions(); - installationOptions.installationkey = null, - installationOptions.apexcompile = 'package'; + (installationOptions.installationkey = null), (installationOptions.apexcompile = 'package'); installationOptions.waitTime = waitTime; installationOptions.apiVersion = apiVersion; installationOptions.publishWaitTime = 60; @@ -811,7 +824,7 @@ export default class DeployImpl { */ private isSkipDeployment(packageDescriptor: any, targetUsername: string): boolean { let skipDeployOnOrgs: string[] = packageDescriptor.skipDeployOnOrgs; - if(packageDescriptor.skipInstallOnOrgs) { + if (packageDescriptor.skipInstallOnOrgs) { skipDeployOnOrgs = packageDescriptor.skipInstallOnOrgs; } if (skipDeployOnOrgs) { diff --git a/packages/sfp-cli/src/impl/parallelBuilder/BuildImpl.ts b/packages/sfp-cli/src/impl/parallelBuilder/BuildImpl.ts index 4bdcec10e..de5b2fb6d 100644 --- a/packages/sfp-cli/src/impl/parallelBuilder/BuildImpl.ts +++ b/packages/sfp-cli/src/impl/parallelBuilder/BuildImpl.ts @@ -61,6 +61,8 @@ export interface BuildProps { baseBranch?: string; diffOptions?: PackageDiffOptions; includeOnlyPackages?: string[]; + impactedPackagesAsPerBranch?: Map; + ref?: string; } export default class BuildImpl { private limiter: Bottleneck; @@ -80,12 +82,14 @@ export default class BuildImpl { private repository_url: string; private commit_id: string; + private base_branch_commit_id: string; private logger = new ConsoleLogger(); private recursiveAll = (a) => Promise.all(a).then((r) => r.length == a.length ? r : this.recursiveAll(a), ); + public constructor(private props: BuildProps) { this.limiter = new Bottleneck({ @@ -110,7 +114,9 @@ export default class BuildImpl { let git = await Git.initiateRepo(new ConsoleLogger()); this.repository_url = await git.getRemoteOriginUrl(this.props.repourl); - this.commit_id = await git.getHeadCommit(); + this.commit_id = this.props.impactedPackagesAsPerBranch? this.props.ref:await git.getHeadCommit(); + if(this.props.baseBranch) + this.base_branch_commit_id = await git.getBaseBranchCommit(this.props.baseBranch); this.packagesToBeBuilt = this.getPackagesToBeBuilt( this.props.projectDirectory, @@ -263,7 +269,7 @@ export default class BuildImpl { private createDiffPackageScheduledDisplayedAsATable( packagesToBeBuilt: Map, ) { - let tableHead = ["Package", "Reason to be built", "Last Known Tag"]; + let tableHead = ["Package", "Reason to be built", "Last Known Tag/Commit Id"]; if ( this.isMultiConfigFilesEnabled && this.props.currentStage == Stage.BUILD @@ -423,11 +429,11 @@ export default class BuildImpl { COLOR_KEY_MESSAGE( `Build will include the below packages release configs (domain(s))(domain)`, ), - LoggerLevel.TRACE, + LoggerLevel.INFO, ); SFPLogger.log( COLOR_KEY_VALUE(`${includeOnlyPackages.toString()}`), - LoggerLevel.TRACE, + LoggerLevel.INFO, ); } } @@ -754,6 +760,9 @@ export default class BuildImpl { + let isPackageImpacted = this.props.impactedPackagesAsPerBranch + ? this.props.impactedPackagesAsPerBranch.get(sfdx_package) + : true; return SfpPackageBuilder.buildPackageFromProjectDirectory( new FileLogger(`.sfpowerscripts/logs/${sfdx_package}`), @@ -762,7 +771,7 @@ export default class BuildImpl { { overridePackageTypeWith: this.props.overridePackageTypes ? this.props.overridePackageTypes[sfdx_package] : undefined, branch: this.props.branch, - sourceVersion: this.commit_id, + sourceVersion: isPackageImpacted?this.commit_id:this.base_branch_commit_id, repositoryUrl: this.repository_url, configFilePath: configFilePath, pathToReplacementForceIgnore: this.getPathToForceIgnoreForCurrentStage( diff --git a/packages/sfp-cli/src/impl/validate/ValidateImpl.ts b/packages/sfp-cli/src/impl/validate/ValidateImpl.ts index 6abbd1016..79bdbc3fb 100644 --- a/packages/sfp-cli/src/impl/validate/ValidateImpl.ts +++ b/packages/sfp-cli/src/impl/validate/ValidateImpl.ts @@ -1,496 +1,429 @@ -import BuildImpl, { BuildProps } from "../parallelBuilder/BuildImpl"; -import DeployImpl, { - DeploymentMode, - DeployProps, - DeploymentResult, -} from "../deploy/DeployImpl"; -import ArtifactGenerator from "../../core/artifacts/generators/ArtifactGenerator"; -import { Stage } from "../Stage"; -import SFPLogger, { - COLOR_KEY_VALUE, - COLOR_TRACE, - ConsoleLogger, - Logger, - LoggerLevel, -} from "@flxbl-io/sfp-logger"; +import BuildImpl, { BuildProps } from '../parallelBuilder/BuildImpl'; +import DeployImpl, { DeploymentMode, DeployProps, DeploymentResult } from '../deploy/DeployImpl'; +import ArtifactGenerator from '../../core/artifacts/generators/ArtifactGenerator'; +import { Stage } from '../Stage'; +import SFPLogger, { COLOR_KEY_VALUE, ConsoleLogger, Logger, LoggerLevel } from '@flxbl-io/sfp-logger'; import { - PackageInstallationResult, - PackageInstallationStatus, -} from "../../core/package/packageInstallers/PackageInstallationResult"; -import { PackageDiffOptions } from "../../core/package/diff/PackageDiffImpl"; -import PoolFetchImpl from "../../core/scratchorg/pool/PoolFetchImpl"; -import { Org } from "@salesforce/core"; -import InstalledArtifactsDisplayer from "../../core/display/InstalledArtifactsDisplayer"; -import ValidateError from "../../errors/ValidateError"; -import ScratchOrg from "../../core/scratchorg/ScratchOrg"; -import { COLOR_KEY_MESSAGE } from "@flxbl-io/sfp-logger"; -import { COLOR_WARNING } from "@flxbl-io/sfp-logger"; -import { COLOR_ERROR } from "@flxbl-io/sfp-logger"; -import { COLOR_HEADER } from "@flxbl-io/sfp-logger"; -import { COLOR_SUCCESS } from "@flxbl-io/sfp-logger"; -import { COLOR_TIME } from "@flxbl-io/sfp-logger"; -import SFPStatsSender from "../../core/stats/SFPStatsSender"; -import ScratchOrgInfoFetcher from "../../core/scratchorg/pool/services/fetchers/ScratchOrgInfoFetcher"; -import ScratchOrgInfoAssigner from "../../core/scratchorg/pool/services/updaters/ScratchOrgInfoAssigner"; -import ValidateResult from "./ValidateResult"; -import PoolOrgDeleteImpl from "../../core/scratchorg/pool/PoolOrgDeleteImpl"; -import SFPOrg from "../../core/org/SFPOrg"; -import SfpPackage, { - PackageType, -} from "../../core/package/SfpPackage"; - -import getFormattedTime from "../../core/utils/GetFormattedTime"; -import { PostDeployHook } from "../deploy/PostDeployHook"; -import * as rimraf from "rimraf"; -import ProjectConfig from "../../core/project/ProjectConfig"; -import InstallUnlockedPackageCollection from "../../core/package/packageInstallers/InstallUnlockedPackageCollection"; -import ExternalPackage2DependencyResolver from "../../core/package/dependencies/ExternalPackage2DependencyResolver"; -import ExternalDependencyDisplayer from "../../core/display/ExternalDependencyDisplayer"; -import { PreDeployHook } from "../deploy/PreDeployHook"; -import GroupConsoleLogs from "../../ui/GroupConsoleLogs"; -import { mapInstalledArtifactstoPkgAndCommits } from "../../utils/FetchArtifactsFromOrg"; -import { ApexTestValidator } from "./ApexTestValidator"; -import OrgInfoDisplayer from "../../ui/OrgInfoDisplayer"; -import FileOutputHandler from "../../outputs/FileOutputHandler"; -import { ReleaseConfigAggregator } from "../release/ReleaseConfigAggregator"; - + PackageInstallationResult, + PackageInstallationStatus, +} from '../../core/package/packageInstallers/PackageInstallationResult'; +import { PackageDiffOptions } from '../../core/package/diff/PackageDiffImpl'; +import PoolFetchImpl from '../../core/scratchorg/pool/PoolFetchImpl'; +import { Org } from '@salesforce/core'; +import InstalledArtifactsDisplayer from '../../core/display/InstalledArtifactsDisplayer'; +import ValidateError from '../../errors/ValidateError'; +import ScratchOrg from '../../core/scratchorg/ScratchOrg'; +import { COLOR_KEY_MESSAGE } from '@flxbl-io/sfp-logger'; +import { COLOR_WARNING } from '@flxbl-io/sfp-logger'; +import { COLOR_ERROR } from '@flxbl-io/sfp-logger'; +import { COLOR_HEADER } from '@flxbl-io/sfp-logger'; +import { COLOR_SUCCESS } from '@flxbl-io/sfp-logger'; +import { COLOR_TIME } from '@flxbl-io/sfp-logger'; +import SFPStatsSender from '../../core/stats/SFPStatsSender'; +import ScratchOrgInfoFetcher from '../../core/scratchorg/pool/services/fetchers/ScratchOrgInfoFetcher'; +import ScratchOrgInfoAssigner from '../../core/scratchorg/pool/services/updaters/ScratchOrgInfoAssigner'; +import ValidateResult from './ValidateResult'; +import PoolOrgDeleteImpl from '../../core/scratchorg/pool/PoolOrgDeleteImpl'; +import SFPOrg from '../../core/org/SFPOrg'; +import SfpPackage, { PackageType } from '../../core/package/SfpPackage'; + +import getFormattedTime from '../../core/utils/GetFormattedTime'; +import { PostDeployHook } from '../deploy/PostDeployHook'; +import * as rimraf from 'rimraf'; +import ProjectConfig from '../../core/project/ProjectConfig'; +import InstallUnlockedPackageCollection from '../../core/package/packageInstallers/InstallUnlockedPackageCollection'; +import ExternalPackage2DependencyResolver from '../../core/package/dependencies/ExternalPackage2DependencyResolver'; +import ExternalDependencyDisplayer from '../../core/display/ExternalDependencyDisplayer'; +import { PreDeployHook } from '../deploy/PreDeployHook'; +import GroupConsoleLogs from '../../ui/GroupConsoleLogs'; +import { mapInstalledArtifactstoPkgAndCommits } from '../../utils/FetchArtifactsFromOrg'; +import { ApexTestValidator } from './ApexTestValidator'; +import OrgInfoDisplayer from '../../ui/OrgInfoDisplayer'; +import FileOutputHandler from '../../outputs/FileOutputHandler'; +import { ReleaseConfigAggregator } from '../release/ReleaseConfigAggregator'; +import ImpactedPackageResolver from '../impact/ImpactedPackagesResolver'; +import ImpactedPackagesDisplayer from '../../core/display/ImpactedPackagesDisplayer'; export enum ValidateAgainst { - PROVIDED_ORG = "PROVIDED_ORG", - PRECREATED_POOL = "PRECREATED_POOL", + PROVIDED_ORG = 'PROVIDED_ORG', + PRECREATED_POOL = 'PRECREATED_POOL', } export enum ValidationMode { - INDIVIDUAL = "individual", - FAST_FEEDBACK = "fastfeedback", - THOROUGH = "thorough", - FASTFEEDBACK_LIMITED_BY_RELEASE_CONFIG = "ff-release-config", - THOROUGH_LIMITED_BY_RELEASE_CONFIG = "thorough-release-config", + INDIVIDUAL = 'individual', + FAST_FEEDBACK = 'fastfeedback', + THOROUGH = 'thorough', + FASTFEEDBACK_LIMITED_BY_RELEASE_CONFIG = 'ff-release-config', + THOROUGH_LIMITED_BY_RELEASE_CONFIG = 'thorough-release-config', } export interface ValidateProps { - installExternalDependencies?: boolean; - validateAgainst: ValidateAgainst; - validationMode: ValidationMode; - releaseConfigPaths?: string[]; - coverageThreshold: number; - logsGroupSymbol: string[]; - targetOrg?: string; - hubOrg?: Org; - pools?: string[]; - shapeFile?: string; - isDeleteScratchOrg?: boolean; - keys?: string; - baseBranch?: string; - diffcheck?: boolean; - disableArtifactCommit?: boolean; - orgInfo?: boolean; - disableSourcePackageOverride?: boolean; - disableParallelTestExecution?: boolean; + installExternalDependencies?: boolean; + validateAgainst: ValidateAgainst; + validationMode: ValidationMode; + releaseConfigPaths?: string[]; + coverageThreshold: number; + logsGroupSymbol: string[]; + targetOrg?: string; + hubOrg?: Org; + pools?: string[]; + shapeFile?: string; + isDeleteScratchOrg?: boolean; + keys?: string; + branch?: string; + baseBranch?: string; + diffcheck?: boolean; + disableArtifactCommit?: boolean; + orgInfo?: boolean; + disableSourcePackageOverride?: boolean; + disableParallelTestExecution?: boolean; } export default class ValidateImpl implements PostDeployHook, PreDeployHook { + private logger = new ConsoleLogger(); + private orgAsSFPOrg: SFPOrg; + private impactedPackagesAsPerBranch: Map; + + constructor(private props: ValidateProps) {} + + public async exec(): Promise { + rimraf.sync('artifacts'); + + let deploymentResult: DeploymentResult; + let targetUserName: string; + try { + if (this.props.validateAgainst === ValidateAgainst.PROVIDED_ORG) { + targetUserName = this.props.targetOrg; + } else if (this.props.validateAgainst === ValidateAgainst.PRECREATED_POOL) { + if (process.env.SFPOWERSCRIPTS_DEBUG_PREFETCHED_SCRATCHORG) + targetUserName = process.env.SFPOWERSCRIPTS_DEBUG_PREFETCHED_SCRATCHORG; + else targetUserName = await this.fetchScratchOrgFromPool(this.props.pools, this.props.orgInfo); + } else throw new Error(`Unknown mode ${this.props.validateAgainst}`); + + //Create Org + this.orgAsSFPOrg = await SFPOrg.create({ + aliasOrUsername: targetUserName, + }); + + //Print Org Info for validateAgainstOrg modes + //TODO: Not ideal need to unify sfpOrg and scratchOrg and then make this a global method + if (this.props.orgInfo && this.props.validateAgainst === ValidateAgainst.PROVIDED_ORG) { + OrgInfoDisplayer.printOrgInfo(this.orgAsSFPOrg); + OrgInfoDisplayer.writeOrgInfoToMarkDown(this.orgAsSFPOrg); + } + + //Fetch Artifacts in the org + let packagesInstalledInOrgMappedToCommits: { [p: string]: string }; + + if (this.props.validationMode !== ValidationMode.INDIVIDUAL) { + let installedArtifacts = await this.orgAsSFPOrg.getInstalledArtifacts(); + if (installedArtifacts.length == 0) { + SFPLogger.log(COLOR_ERROR('Failed to query org for sfp Artifacts')); + } + packagesInstalledInOrgMappedToCommits = await mapInstalledArtifactstoPkgAndCommits(installedArtifacts); + this.printArtifactVersions(this.orgAsSFPOrg, installedArtifacts); + } + //In individual mode, always build changed packages only especially for validateAgainstOrg + if (this.props.validationMode == ValidationMode.INDIVIDUAL) { + this.props.diffcheck = true; + } + // Figure Impacted packages as per the PR + this.impactedPackagesAsPerBranch = await this.computePackagesChangedAgainstBaseBranch(); + + if (this.impactedPackagesAsPerBranch) { + SFPLogger.log( + COLOR_KEY_MESSAGE(`Packages impacted in ${this.props.branch} vs ${this.props.baseBranch}`), + LoggerLevel.INFO, + new ConsoleLogger() + ); + ImpactedPackagesDisplayer.displayImpactedPackages(this.impactedPackagesAsPerBranch, this.logger); + } + + await this.buildImpactedPackages(packagesInstalledInOrgMappedToCommits); + deploymentResult = await this.deployPackages(targetUserName); + + if (deploymentResult.failed.length > 0 || deploymentResult.error) + throw new ValidateError('Validation failed', { deploymentResult }); + + return { + deploymentResult, + }; + } catch (error) { + if (error.message?.includes(`No changes detected in the packages to be built`)) { + SFPLogger.log( + `WARNING: No changes detected in any of the packages, Validation is treated as a success`, + LoggerLevel.WARN + ); + return; + } else if (error instanceof ValidateError) + SFPLogger.log(`Validation failed due to : ${error}`, LoggerLevel.DEBUG); + else SFPLogger.log(`Failure Reason: ${error}`, LoggerLevel.ERROR); + throw error; + } finally { + await this.handleScratchOrgStatus(targetUserName, deploymentResult, this.props.isDeleteScratchOrg); + } + } + + private async computePackagesChangedAgainstBaseBranch() { + if (this.props.branch && this.props.baseBranch) { + const impactedPackageDiffProps = { + branch: this.props.branch, + currentStage: Stage.VALIDATE, + baseBranch: this.props.baseBranch, + diffOptions: { + useLatestGitTags: false, + skipPackageDescriptorChange: true, + useBranchCompare: true, + branch: this.props.branch, + baseBranch: this.props.baseBranch, + }, + }; + + SFPLogger.log(COLOR_KEY_MESSAGE(`Computing impacted packages as per the PR.Please wait..`), LoggerLevel.INFO,this.logger); + const impactedPackageResolver = new ImpactedPackageResolver(impactedPackageDiffProps,this.logger); + return impactedPackageResolver.getImpactedPackages(); + } + } + + private async printArtifactVersions(orgAsSFPOrg: SFPOrg, installedArtifacts: any) { + let groupSection = new GroupConsoleLogs(`Artifacts installed in the Org ${orgAsSFPOrg.getUsername()}`).begin(); + + InstalledArtifactsDisplayer.printInstalledArtifacts(installedArtifacts, null); + + groupSection.end(); + } + + private async installPackageDependencies( + sfdxProjectConfig: any, + scratchOrgAsSFPOrg: SFPOrg, + sfpPackage: SfpPackage, + deployedPackages?: SfpPackage[] + ) { + let deployedPackagesAsStringArray: Array = []; + for (const deployedPackage of deployedPackages) { + deployedPackagesAsStringArray.push(deployedPackage.package_name); + } + + //Resolve external package dependencies + let externalPackageResolver = new ExternalPackage2DependencyResolver( + this.props.hubOrg.getConnection(), + sfdxProjectConfig, + this.props.keys + ); + let externalPackage2s = await externalPackageResolver.resolveExternalPackage2DependenciesToVersions( + [sfpPackage.packageName], + deployedPackagesAsStringArray + ); + + SFPLogger.log( + `Installing package dependencies of this ${sfpPackage.packageName} in ${scratchOrgAsSFPOrg.getUsername()}`, + LoggerLevel.INFO, + new ConsoleLogger() + ); + //Display resolved dependenencies + let externalDependencyDisplayer = new ExternalDependencyDisplayer(externalPackage2s, new ConsoleLogger()); + externalDependencyDisplayer.display(); + + let packageCollectionInstaller = new InstallUnlockedPackageCollection(scratchOrgAsSFPOrg, new ConsoleLogger()); + await packageCollectionInstaller.install(externalPackage2s, true, true); + + SFPLogger.log( + COLOR_KEY_MESSAGE( + `Successfully completed external dependencies of this ${ + sfpPackage.packageName + } in ${scratchOrgAsSFPOrg.getUsername()}` + ) + ); + } + + private async handleScratchOrgStatus( + scratchOrgUsername: string, + deploymentResult: DeploymentResult, + isToDelete: boolean + ) { + //No scratch org available.. just return + if (scratchOrgUsername == undefined) return; + + if (isToDelete) { + //If deploymentResult is not available, or there is 0 packages deployed, we can reuse the org + if (!deploymentResult || deploymentResult.deployed.length == 0) { + SFPLogger.log(`Attempting to return scratch org ${scratchOrgUsername} back to pool`, LoggerLevel.INFO); + const scratchOrgInfoAssigner = new ScratchOrgInfoAssigner(this.props.hubOrg); + try { + const result = await scratchOrgInfoAssigner.setScratchOrgStatus(scratchOrgUsername, 'Return'); + if (result) + SFPLogger.log(`Succesfully returned ${scratchOrgUsername} back to pool`, LoggerLevel.INFO); + else + SFPLogger.log( + COLOR_WARNING( + `Unable to return scratch org to pool, Please check permissions or update sfpower-pool-package to latest` + ) + ); + } catch (error) { + SFPLogger.log( + COLOR_WARNING( + `Unable to return scratch org to pool, Please check permissions or update sfpower-pool-package to latest` + ) + ); + } + } else { + try { + if (scratchOrgUsername && this.props.hubOrg.getUsername()) { + await deleteScratchOrg(this.props.hubOrg, scratchOrgUsername); + } + } catch (error) { + SFPLogger.log(COLOR_WARNING(error.message)); + } + } + } + async function deleteScratchOrg(hubOrg: Org, scratchOrgUsername: string) { + SFPLogger.log(`Deleting scratch org ${scratchOrgUsername}`, LoggerLevel.INFO); + const poolOrgDeleteImpl = new PoolOrgDeleteImpl(hubOrg, scratchOrgUsername); + await poolOrgDeleteImpl.execute(); + } + } + + private async deployPackages(scratchOrgUsername: string): Promise { + const deployStartTime: number = Date.now(); + + const deployProps: DeployProps = { + targetUsername: scratchOrgUsername, + artifactDir: 'artifacts', + waitTime: 120, + deploymentMode: + this.props.disableSourcePackageOverride == true ? DeploymentMode.NORMAL : DeploymentMode.SOURCEPACKAGES, + isTestsToBeTriggered: true, + skipIfPackageInstalled: false, + logsGroupSymbol: this.props.logsGroupSymbol, + currentStage: Stage.VALIDATE, + disableArtifactCommit: false, //always set to false, let post deploy determine + impactedPackagesAsPerBranch: this.impactedPackagesAsPerBranch, + selectiveComponentDeployment: + this.props.validationMode == ValidationMode.FAST_FEEDBACK || + this.props.validationMode == ValidationMode.FASTFEEDBACK_LIMITED_BY_RELEASE_CONFIG, + }; + + const deployImpl: DeployImpl = new DeployImpl(deployProps); + deployImpl.postDeployHook = this; + deployImpl.preDeployHook = this; + + const deploymentResult = await deployImpl.exec(); + + const deploymentElapsedTime: number = Date.now() - deployStartTime; + printDeploySummary(deploymentResult, deploymentElapsedTime); + + return deploymentResult; + + function printDeploySummary(deploymentResult: DeploymentResult, totalElapsedTime: number): void { + let groupSection = new GroupConsoleLogs(`Deployment Summary`).begin(); + SFPLogger.printHeaderLine('', COLOR_HEADER, LoggerLevel.INFO); + SFPLogger.log( + COLOR_SUCCESS( + `${deploymentResult.deployed.length} packages deployed in ${COLOR_TIME( + getFormattedTime(totalElapsedTime) + )} with {${COLOR_ERROR(deploymentResult.failed.length)}} failed deployments` + ) + ); + + if (deploymentResult.failed.length > 0) { + SFPLogger.log( + COLOR_ERROR( + `\nPackages Failed to Deploy`, + deploymentResult.failed.map((packageInfo) => packageInfo.sfpPackage.packageName) + ) + ); + + FileOutputHandler.getInstance().appendOutput(`validation-error.md`, `### 💣 Deployment Failed 💣`); + let firstPackageFailedToValdiate = deploymentResult.failed[0]; + FileOutputHandler.getInstance().appendOutput( + `validation-error.md`, + `Package validation failed for **${firstPackageFailedToValdiate.sfpPackage.packageName}** due to` + ); + FileOutputHandler.getInstance().appendOutput(`validation-error.md`, ''); + FileOutputHandler.getInstance().appendOutput(`validation-error.md`, deploymentResult.error); + + FileOutputHandler.getInstance().appendOutput(`validation-error.md`, `Package that are not validated:`); + deploymentResult.failed.map((packageInfo, index) => { + if (index != 0) + FileOutputHandler.getInstance().appendOutput( + `validation-error.md`, + `**${packageInfo.sfpPackage.packageName}**` + ); + }); + } + + SFPLogger.printHeaderLine('', COLOR_HEADER, LoggerLevel.INFO); + groupSection.end(); + } + } + + private async buildImpactedPackages(packagesInstalledInOrgMappedToCommits: { + [p: string]: string; + }): Promise { + let groupSection = new GroupConsoleLogs('Building packages for Synchronization+Validation').begin(); + + const buildStartTime: number = Date.now(); + + const buildProps: BuildProps = { + buildNumber: 1, + executorcount: 10, + waitTime: 120, + isDiffCheckEnabled: this.props.diffcheck, + isQuickBuild: true, + isBuildAllAsSourcePackages: !this.props.disableSourcePackageOverride, + currentStage: Stage.VALIDATE, + baseBranch: this.props.baseBranch, + devhubAlias: this.props.hubOrg?.getUsername(), + baselineOrgAlias: this.props.targetOrg, + impactedPackagesAsPerBranch: this.impactedPackagesAsPerBranch, + ref: this.props.branch, + }; + + //Build DiffOptions + const diffOptions: PackageDiffOptions = buildDiffOption(this.props); + buildProps.diffOptions = diffOptions; + + //compute pkg overides + buildProps.overridePackageTypes = computePackageOverrides(this.props); + + //Compute packages to be included + buildProps.includeOnlyPackages = fetchPackagesAsPerReleaseConfig(this.logger, this.props); + + const buildImpl: BuildImpl = new BuildImpl(buildProps); + const { generatedPackages, failedPackages } = await buildImpl.exec(); + + if (failedPackages.length > 0) throw new Error(`Failed to create packages ${failedPackages}`); + + if (generatedPackages.length === 0) { + throw new Error( + `No changes detected in the packages to be built\nvalidate will only execute if there is a change in atleast one of the packages` + ); + } + + for (const generatedPackage of generatedPackages) { + try { + await ArtifactGenerator.generateArtifact(generatedPackage, process.cwd(), 'artifacts'); + } catch (error) { + SFPLogger.log(COLOR_ERROR(`Unable to create artifact for ${generatedPackage.packageName}`)); + throw error; + } + } + const buildElapsedTime: number = Date.now() - buildStartTime; - private logger = new ConsoleLogger(); - private orgAsSFPOrg: SFPOrg; - - constructor(private props: ValidateProps) { } - - public async exec(): Promise { - rimraf.sync("artifacts"); - - let deploymentResult: DeploymentResult; - let scratchOrgUsername: string; - try { - - if (this.props.validateAgainst === ValidateAgainst.PROVIDED_ORG) { - scratchOrgUsername = this.props.targetOrg; - } else if ( - this.props.validateAgainst === ValidateAgainst.PRECREATED_POOL - ) { - if (process.env.SFPOWERSCRIPTS_DEBUG_PREFETCHED_SCRATCHORG) - scratchOrgUsername = - process.env.SFPOWERSCRIPTS_DEBUG_PREFETCHED_SCRATCHORG; - else - scratchOrgUsername = await this.fetchScratchOrgFromPool( - this.props.pools, - this.props.orgInfo, - ); - } else throw new Error(`Unknown mode ${this.props.validateAgainst}`); - - //Create Org - this.orgAsSFPOrg = await SFPOrg.create({ - aliasOrUsername: scratchOrgUsername, - }); - - - //Print Org Info for validateAgainstOrg modes - //TODO: Not ideal need to unify sfpOrg and scratchOrg and then make this a global method - if (this.props.orgInfo && this.props.validateAgainst === ValidateAgainst.PROVIDED_ORG){ - OrgInfoDisplayer.printOrgInfo(this.orgAsSFPOrg); - OrgInfoDisplayer.writeOrgInfoToMarkDown(this.orgAsSFPOrg); - } - - - - //Fetch Artifacts in the org - let packagesInstalledInOrgMappedToCommits: { [p: string]: string }; - - if (this.props.validationMode !== ValidationMode.INDIVIDUAL) { - let installedArtifacts = await this.orgAsSFPOrg.getInstalledArtifacts(); - if (installedArtifacts.length == 0) { - SFPLogger.log( - COLOR_ERROR("Failed to query org for sfp Artifacts"), - ); - } - packagesInstalledInOrgMappedToCommits = - await mapInstalledArtifactstoPkgAndCommits(installedArtifacts); - this.printArtifactVersions(this.orgAsSFPOrg, installedArtifacts); - } - //In individual mode, always build changed packages only especially for validateAgainstOrg - if (this.props.validationMode == ValidationMode.INDIVIDUAL) - this.props.diffcheck = true; - - let builtSfpPackages = await this.buildChangedSourcePackages( - packagesInstalledInOrgMappedToCommits, - ); - deploymentResult = await this.deploySourcePackages(scratchOrgUsername); - - if (deploymentResult.failed.length > 0 || deploymentResult.error) - throw new ValidateError("Validation failed", { deploymentResult }); - - return { - deploymentResult - } - } catch (error) { - if ( - error.message?.includes( - `No changes detected in the packages to be built`, - ) - ) { - SFPLogger.log( - `WARNING: No changes detected in any of the packages, Validation is treated as a success`, - LoggerLevel.WARN, - ); - return; - } else if (error instanceof ValidateError) - SFPLogger.log(`Validation failed due to : ${error}`, LoggerLevel.DEBUG); - else SFPLogger.log(`Failure Reason: ${error}`, LoggerLevel.ERROR); - throw error; - } finally { - await this.handleScratchOrgStatus( - scratchOrgUsername, - deploymentResult, - this.props.isDeleteScratchOrg, - ); - } - } - - private async printArtifactVersions( - orgAsSFPOrg: SFPOrg, - installedArtifacts: any, - ) { - let groupSection = new GroupConsoleLogs( - `Artifacts installed in the Org ${orgAsSFPOrg.getUsername()}`, - ).begin(); - - InstalledArtifactsDisplayer.printInstalledArtifacts( - installedArtifacts, - null, - ); - - groupSection.end(); - } - - - private async installPackageDependencies( - sfdxProjectConfig: any, - scratchOrgAsSFPOrg: SFPOrg, - sfpPackage: SfpPackage, - deployedPackages?: SfpPackage[], - ) { - let deployedPackagesAsStringArray: Array = []; - for (const deployedPackage of deployedPackages) { - deployedPackagesAsStringArray.push(deployedPackage.package_name); - } - - //Resolve external package dependencies - let externalPackageResolver = new ExternalPackage2DependencyResolver( - this.props.hubOrg.getConnection(), - sfdxProjectConfig, - this.props.keys, - ); - let externalPackage2s = - await externalPackageResolver.resolveExternalPackage2DependenciesToVersions( - [sfpPackage.packageName], - deployedPackagesAsStringArray, - ); - - SFPLogger.log( - `Installing package dependencies of this ${sfpPackage.packageName - } in ${scratchOrgAsSFPOrg.getUsername()}`, - LoggerLevel.INFO, - new ConsoleLogger(), - ); - //Display resolved dependenencies - let externalDependencyDisplayer = new ExternalDependencyDisplayer( - externalPackage2s, - new ConsoleLogger(), - ); - externalDependencyDisplayer.display(); - - let packageCollectionInstaller = new InstallUnlockedPackageCollection( - scratchOrgAsSFPOrg, - new ConsoleLogger(), - ); - await packageCollectionInstaller.install(externalPackage2s, true, true); - - SFPLogger.log( - COLOR_KEY_MESSAGE( - `Successfully completed external dependencies of this ${sfpPackage.packageName - } in ${scratchOrgAsSFPOrg.getUsername()}`, - ), - ); - } - - private async handleScratchOrgStatus( - scratchOrgUsername: string, - deploymentResult: DeploymentResult, - isToDelete: boolean, - ) { - //No scratch org available.. just return - if (scratchOrgUsername == undefined) return; - - if (isToDelete) { - //If deploymentResult is not available, or there is 0 packages deployed, we can reuse the org - if (!deploymentResult || deploymentResult.deployed.length == 0) { - SFPLogger.log( - `Attempting to return scratch org ${scratchOrgUsername} back to pool`, - LoggerLevel.INFO, - ); - const scratchOrgInfoAssigner = new ScratchOrgInfoAssigner( - this.props.hubOrg, - ); - try { - const result = await scratchOrgInfoAssigner.setScratchOrgStatus( - scratchOrgUsername, - "Return", - ); - if (result) - SFPLogger.log( - `Succesfully returned ${scratchOrgUsername} back to pool`, - LoggerLevel.INFO, - ); - else - SFPLogger.log( - COLOR_WARNING( - `Unable to return scratch org to pool, Please check permissions or update sfpower-pool-package to latest`, - ), - ); - } catch (error) { - SFPLogger.log( - COLOR_WARNING( - `Unable to return scratch org to pool, Please check permissions or update sfpower-pool-package to latest`, - ), - ); - } - } else { - try { - if (scratchOrgUsername && this.props.hubOrg.getUsername()) { - await deleteScratchOrg(this.props.hubOrg, scratchOrgUsername); - } - } catch (error) { - SFPLogger.log(COLOR_WARNING(error.message)); - } - } - } - async function deleteScratchOrg(hubOrg: Org, scratchOrgUsername: string) { - SFPLogger.log( - `Deleting scratch org ${scratchOrgUsername}`, - LoggerLevel.INFO, - ); - const poolOrgDeleteImpl = new PoolOrgDeleteImpl( - hubOrg, - scratchOrgUsername, - ); - await poolOrgDeleteImpl.execute(); - } - } - - private async deploySourcePackages( - scratchOrgUsername: string, - ): Promise { - const deployStartTime: number = Date.now(); - - const deployProps: DeployProps = { - targetUsername: scratchOrgUsername, - artifactDir: "artifacts", - waitTime: 120, - deploymentMode: - this.props.disableSourcePackageOverride == true - ? DeploymentMode.NORMAL - : DeploymentMode.SOURCEPACKAGES, - isTestsToBeTriggered: true, - skipIfPackageInstalled: false, - logsGroupSymbol: this.props.logsGroupSymbol, - currentStage: Stage.VALIDATE, - disableArtifactCommit: this.props.disableArtifactCommit, - selectiveComponentDeployment: - this.props.validationMode == ValidationMode.FAST_FEEDBACK || this.props.validationMode == ValidationMode.FASTFEEDBACK_LIMITED_BY_RELEASE_CONFIG - - }; - - - - - const deployImpl: DeployImpl = new DeployImpl(deployProps); - deployImpl.postDeployHook = this; - deployImpl.preDeployHook = this; - - const deploymentResult = await deployImpl.exec(); - - const deploymentElapsedTime: number = Date.now() - deployStartTime; - printDeploySummary(deploymentResult, deploymentElapsedTime); - - return deploymentResult; - - function printDeploySummary( - deploymentResult: DeploymentResult, - totalElapsedTime: number, - ): void { - let groupSection = new GroupConsoleLogs(`Deployment Summary`).begin(); - SFPLogger.printHeaderLine('',COLOR_HEADER,LoggerLevel.INFO); - SFPLogger.log( - COLOR_SUCCESS( - `${deploymentResult.deployed.length - } packages deployed in ${COLOR_TIME( - getFormattedTime(totalElapsedTime), - )} with {${COLOR_ERROR( - deploymentResult.failed.length, - )}} failed deployments`, - ), - ); - - if (deploymentResult.failed.length > 0) { - SFPLogger.log( - COLOR_ERROR( - `\nPackages Failed to Deploy`, - deploymentResult.failed.map( - (packageInfo) => packageInfo.sfpPackage.packageName, - ), - ), - ); - - FileOutputHandler.getInstance().appendOutput(`validation-error.md`,`### 💣 Deployment Failed 💣`); - let firstPackageFailedToValdiate = deploymentResult.failed[0]; - FileOutputHandler.getInstance().appendOutput(`validation-error.md`,`Package validation failed for **${firstPackageFailedToValdiate.sfpPackage.packageName}** due to`); - FileOutputHandler.getInstance().appendOutput(`validation-error.md`,""); - FileOutputHandler.getInstance().appendOutput(`validation-error.md`,deploymentResult.error); - - FileOutputHandler.getInstance().appendOutput(`validation-error.md`,`Package that are not validated:`); - deploymentResult.failed.map( - (packageInfo, index) => { - if (index!=0) - FileOutputHandler.getInstance().appendOutput(`validation-error.md`,`**${packageInfo.sfpPackage.packageName}**`); - } - ); - } - - SFPLogger.printHeaderLine('',COLOR_HEADER,LoggerLevel.INFO); - groupSection.end(); - } - } - - private async buildChangedSourcePackages(packagesInstalledInOrgMappedToCommits: { - [p: string]: string; - }): Promise { - let groupSection = new GroupConsoleLogs("Building Packages").begin(); - - const buildStartTime: number = Date.now(); - - - - - const buildProps: BuildProps = { - buildNumber: 1, - executorcount: 10, - waitTime: 120, - isDiffCheckEnabled: this.props.diffcheck, - isQuickBuild: true, - isBuildAllAsSourcePackages: !this.props.disableSourcePackageOverride, - currentStage: Stage.VALIDATE, - baseBranch: this.props.baseBranch, - devhubAlias: this.props.hubOrg?.getUsername(), - baselineOrgAlias: this.props.targetOrg - }; - - //Build DiffOptions - const diffOptions: PackageDiffOptions = buildDiffOption(this.props); - buildProps.diffOptions = diffOptions; - - - //compute pkg overides - buildProps.overridePackageTypes = computePackageOverrides(this.props) - - - //Compute packages to be included - buildProps.includeOnlyPackages = fetchPackagesAsPerReleaseConfig( - this.logger, - this.props, - ); - - const buildImpl: BuildImpl = new BuildImpl(buildProps); - const { generatedPackages, failedPackages } = await buildImpl.exec(); - - if (failedPackages.length > 0) - throw new Error(`Failed to create packages ${failedPackages}`); - - if (generatedPackages.length === 0) { - throw new Error( - `No changes detected in the packages to be built\nvalidate will only execute if there is a change in atleast one of the packages`, - ); - } - - for (const generatedPackage of generatedPackages) { - try { - await ArtifactGenerator.generateArtifact( - generatedPackage, - process.cwd(), - "artifacts", - ); - } catch (error) { - SFPLogger.log( - COLOR_ERROR( - `Unable to create artifact for ${generatedPackage.packageName}`, - ), - ); - throw error; - } - } - const buildElapsedTime: number = Date.now() - buildStartTime; - - printBuildSummary(generatedPackages, failedPackages, buildElapsedTime); - - groupSection.end(); - - return generatedPackages; - - - function computePackageOverrides(props: ValidateProps): { [key: string]: PackageType } { + printBuildSummary(generatedPackages, failedPackages, buildElapsedTime); + + groupSection.end(); + + return generatedPackages; + + function computePackageOverrides(props: ValidateProps): { [key: string]: PackageType } { let overridedPackages: { [key: string]: PackageType } = {}; const allPackages = ProjectConfig.getAllPackages(null); const projectConfig = ProjectConfig.getSFDXProjectConfig(null); for (const pkg of allPackages) { if (ProjectConfig.getPackageType(projectConfig, pkg) !== PackageType.Data) { - if ( - props.validationMode === ValidationMode.FASTFEEDBACK_LIMITED_BY_RELEASE_CONFIG || - props.validationMode === ValidationMode.FAST_FEEDBACK - ) { - overridedPackages[pkg] = PackageType.Diff; - } else { - if (!props.disableSourcePackageOverride) { - if (ProjectConfig.getPackageType(projectConfig, pkg) == PackageType.Unlocked) { - overridedPackages[pkg] = PackageType.Source; - } + if (!props.disableSourcePackageOverride) { + if (ProjectConfig.getPackageType(projectConfig, pkg) == PackageType.Unlocked) { + overridedPackages[pkg] = PackageType.Source; } } } @@ -498,242 +431,237 @@ export default class ValidateImpl implements PostDeployHook, PreDeployHook { return overridedPackages; } - function fetchPackagesAsPerReleaseConfig( - logger: Logger, - props: ValidateProps, - ) { - if ( - props.validationMode === - ValidationMode.FASTFEEDBACK_LIMITED_BY_RELEASE_CONFIG || - props.validationMode === - ValidationMode.THOROUGH_LIMITED_BY_RELEASE_CONFIG - ) { - let includeOnlyPackages = []; - if (props.releaseConfigPaths?.length > 0) { - let releaseConfigAggregatedLoader = new ReleaseConfigAggregator(logger); - releaseConfigAggregatedLoader.addReleaseConfigs(props.releaseConfigPaths,true); - includeOnlyPackages = releaseConfigAggregatedLoader.getAllPackages(); - printIncludeOnlyPackages(includeOnlyPackages); - } - return includeOnlyPackages; - - function printIncludeOnlyPackages(includeOnlyPackages: string[]) { - SFPLogger.log( - COLOR_KEY_MESSAGE(`Validate will include the below packages release configs (domain(s))(domain)`), - LoggerLevel.INFO - ); - SFPLogger.log(COLOR_KEY_VALUE(`${includeOnlyPackages.toString()}`), LoggerLevel.INFO); - } - - } - } - - //generate diff Option - function buildDiffOption(props: ValidateProps) { - const diffOptions: PackageDiffOptions = new PackageDiffOptions(); - //In fast feedback ignore package descriptor changes - if ( - props.validationMode === ValidationMode.FAST_FEEDBACK - ) { - diffOptions.skipPackageDescriptorChange = true; - diffOptions.useLatestGitTags = false; - diffOptions.packagesMappedToLastKnownCommitId = - packagesInstalledInOrgMappedToCommits; - } - else if (props.validationMode === ValidationMode.THOROUGH) { - diffOptions.skipPackageDescriptorChange = false; - diffOptions.useLatestGitTags = false; - diffOptions.packagesMappedToLastKnownCommitId = - packagesInstalledInOrgMappedToCommits; - } else if (props.validationMode === ValidationMode.INDIVIDUAL) { - diffOptions.skipPackageDescriptorChange = false; - //Dont send whats installed in orgs, use only the changed package from last know git tags - diffOptions.useLatestGitTags = true; - diffOptions.packagesMappedToLastKnownCommitId = null; - } else if ( - props.validationMode === - ValidationMode.THOROUGH_LIMITED_BY_RELEASE_CONFIG - ) { - diffOptions.skipPackageDescriptorChange = false; - diffOptions.useLatestGitTags = false; - diffOptions.packagesMappedToLastKnownCommitId = - packagesInstalledInOrgMappedToCommits; - } else if ( - props.validationMode === - ValidationMode.FASTFEEDBACK_LIMITED_BY_RELEASE_CONFIG - ) { - diffOptions.skipPackageDescriptorChange = true; - diffOptions.useLatestGitTags = false; - diffOptions.packagesMappedToLastKnownCommitId = - packagesInstalledInOrgMappedToCommits; - } - return diffOptions; - } - - function printBuildSummary( - generatedPackages: SfpPackage[], - failedPackages: string[], - totalElapsedTime: number, - ): void { - SFPLogger.printHeaderLine('',COLOR_HEADER,LoggerLevel.INFO); - SFPLogger.log( - COLOR_SUCCESS( - `${generatedPackages.length} artifacts created in ${COLOR_TIME( - getFormattedTime(totalElapsedTime), - )} with {${COLOR_ERROR(failedPackages.length)}} errors`, - ), - ); - - if (failedPackages.length > 0) { - SFPLogger.log(COLOR_ERROR(`Packages Failed To Build`, failedPackages)); - } - SFPLogger.printHeaderLine('',COLOR_HEADER,LoggerLevel.INFO); - } - - } - - private async fetchScratchOrgFromPool( - pools: string[], - displayOrgInfo?: boolean, - ): Promise { - let scratchOrgUsername: string; - - for (const pool of pools) { - let scratchOrg: ScratchOrg; - try { - const poolFetchImpl = new PoolFetchImpl( - this.props.hubOrg, - pool.trim(), - false, - true, - ); - scratchOrg = (await poolFetchImpl.execute()) as ScratchOrg; - } catch (error) { - SFPLogger.log(error.message, LoggerLevel.TRACE); - } - if (scratchOrg && scratchOrg.status === "Assigned") { - scratchOrgUsername = scratchOrg.username; - SFPLogger.log( - COLOR_KEY_MESSAGE( - `Fetched scratch org ${scratchOrgUsername} from ${COLOR_KEY_VALUE( - pool, - )}`, - ), - LoggerLevel.INFO, - this.logger, - ); - - if (displayOrgInfo) { - OrgInfoDisplayer.printScratchOrgInfo(scratchOrg); - OrgInfoDisplayer.writeScratchOrgInfoToMarkDown(scratchOrg); - } - - this.getCurrentRemainingNumberOfOrgsInPoolAndReport(scratchOrg.tag); - break; - } - } - - if (scratchOrgUsername) return scratchOrgUsername; - else - throw new Error( - `Failed to fetch scratch org from ${pools}, Are you sure you created this pool using a DevHub authenticated using auth:sfdxurl or auth:web or auth:accesstoken:store`, - ); - - - } - - private async getCurrentRemainingNumberOfOrgsInPoolAndReport(tag: string) { - try { - const results = await new ScratchOrgInfoFetcher( - this.props.hubOrg, - ).getScratchOrgsByTag(tag, false, true); - - const availableSo = results.records.filter( - (soInfo) => soInfo.Allocation_status__c === "Available", - ); - - SFPStatsSender.logGauge("pool.available", availableSo.length, { - poolName: tag, - }); - } catch (error) { - //do nothing, we are not reporting anything if anything goes wrong here - } - } - - async preDeployPackage( - sfpPackage: SfpPackage, - targetUsername: string, - deployedPackages?: SfpPackage[], - devhubUserName?: string, -): Promise<{ isToFailDeployment: boolean; message?: string }> { - - const shouldInstallDependencies = (mode: ValidationMode) => { - if (this.props.validateAgainst === ValidateAgainst.PROVIDED_ORG && - !this.props.installExternalDependencies) { - return false; + function fetchPackagesAsPerReleaseConfig(logger: Logger, props: ValidateProps) { + if ( + props.validationMode === ValidationMode.FASTFEEDBACK_LIMITED_BY_RELEASE_CONFIG || + props.validationMode === ValidationMode.THOROUGH_LIMITED_BY_RELEASE_CONFIG + ) { + let includeOnlyPackages = []; + if (props.releaseConfigPaths?.length > 0) { + let releaseConfigAggregatedLoader = new ReleaseConfigAggregator(logger); + releaseConfigAggregatedLoader.addReleaseConfigs(props.releaseConfigPaths,true); + includeOnlyPackages = releaseConfigAggregatedLoader.getAllPackages(); + printIncludeOnlyPackages(includeOnlyPackages); + } + return includeOnlyPackages; + + function printIncludeOnlyPackages(includeOnlyPackages: string[]) { + SFPLogger.log( + COLOR_KEY_MESSAGE( + `Validate will include the below packages release configs (domain(s))(domain)` + ), + LoggerLevel.INFO + ); + SFPLogger.log(COLOR_KEY_VALUE(`${includeOnlyPackages.toString()}`), LoggerLevel.INFO); + } + } + } + + //generate diff Option + function buildDiffOption(props: ValidateProps) { + const diffOptions: PackageDiffOptions = new PackageDiffOptions(); + //In fast feedback ignore package descriptor changes + if (props.validationMode === ValidationMode.FAST_FEEDBACK) { + diffOptions.skipPackageDescriptorChange = true; + diffOptions.useLatestGitTags = false; + diffOptions.packagesMappedToLastKnownCommitId = packagesInstalledInOrgMappedToCommits; + } else if (props.validationMode === ValidationMode.THOROUGH) { + diffOptions.skipPackageDescriptorChange = false; + diffOptions.useLatestGitTags = false; + diffOptions.packagesMappedToLastKnownCommitId = packagesInstalledInOrgMappedToCommits; + } else if (props.validationMode === ValidationMode.INDIVIDUAL) { + diffOptions.skipPackageDescriptorChange = false; + //Dont send whats installed in orgs, use only the changed package from last know git tags + diffOptions.useLatestGitTags = true; + diffOptions.packagesMappedToLastKnownCommitId = null; + } else if (props.validationMode === ValidationMode.THOROUGH_LIMITED_BY_RELEASE_CONFIG) { + diffOptions.skipPackageDescriptorChange = false; + diffOptions.useLatestGitTags = false; + diffOptions.packagesMappedToLastKnownCommitId = packagesInstalledInOrgMappedToCommits; + } else if (props.validationMode === ValidationMode.FASTFEEDBACK_LIMITED_BY_RELEASE_CONFIG) { + diffOptions.skipPackageDescriptorChange = true; + diffOptions.useLatestGitTags = false; + diffOptions.packagesMappedToLastKnownCommitId = packagesInstalledInOrgMappedToCommits; + } + //It's validate.. review orgs may have bad commits.. so fall back + diffOptions.fallBackToNoTag=true; + return diffOptions; } - const isThoroughValidation = mode === ValidationMode.THOROUGH || - mode === ValidationMode.THOROUGH_LIMITED_BY_RELEASE_CONFIG; - - const isFastFeedbackWithExternalDependencies = - (mode === ValidationMode.FASTFEEDBACK_LIMITED_BY_RELEASE_CONFIG || - mode === ValidationMode.FAST_FEEDBACK) && - this.props.installExternalDependencies; - - return isThoroughValidation || - mode === ValidationMode.INDIVIDUAL || - isFastFeedbackWithExternalDependencies; - }; - - if (shouldInstallDependencies(this.props.validationMode)) { - const projectConfig = this.props.validationMode === ValidationMode.INDIVIDUAL ? - ProjectConfig.cleanupMPDFromProjectDirectory(null, sfpPackage.package_name) : - ProjectConfig.getSFDXProjectConfig(null); - - await this.installPackageDependencies( - projectConfig, - this.orgAsSFPOrg, - sfpPackage, - deployedPackages, - ); + function printBuildSummary( + generatedPackages: SfpPackage[], + failedPackages: string[], + totalElapsedTime: number + ): void { + SFPLogger.printHeaderLine('', COLOR_HEADER, LoggerLevel.INFO); + SFPLogger.log( + COLOR_SUCCESS( + `${generatedPackages.length} artifacts created in ${COLOR_TIME( + getFormattedTime(totalElapsedTime) + )} with {${COLOR_ERROR(failedPackages.length)}} errors` + ) + ); + + if (failedPackages.length > 0) { + SFPLogger.log(COLOR_ERROR(`Packages Failed To Build`, failedPackages)); + } + SFPLogger.printHeaderLine('', COLOR_HEADER, LoggerLevel.INFO); + } } - return { isToFailDeployment: false }; - } - - - async postDeployPackage( - sfpPackage: SfpPackage, - packageInstallationResult: PackageInstallationResult, - targetUsername: string, - deployedPackages?: SfpPackage[], - devhubUserName?: string, - ): Promise<{ isToFailDeployment: boolean; message?: string }> { - //Trigger Tests after installation of each package - if (sfpPackage.packageType && sfpPackage.packageType != PackageType.Data) { - if ( - packageInstallationResult.result === PackageInstallationStatus.Succeeded - ) { - //Get Changed Components - const apextestValidator = new ApexTestValidator(targetUsername, sfpPackage, this.props, this.logger); - const testResult = await apextestValidator.validateApexTests(); - - if (!testResult.result) { - FileOutputHandler.getInstance().appendOutput(`validation-error.md`,`### 💣 Validation Failed 💣`); - FileOutputHandler.getInstance().appendOutput(`validation-error.md`,`Package validation failed for **${sfpPackage.packageName}**`); - FileOutputHandler.getInstance().appendOutput(`validation-error.md`,`Reasons:`); - FileOutputHandler.getInstance().appendOutput(`validation-error.md`,`${testResult.message}`); - } - - return { - isToFailDeployment: !testResult.result, - message: testResult.message, - }; - } - } - return { isToFailDeployment: false }; - } + private async fetchScratchOrgFromPool(pools: string[], displayOrgInfo?: boolean): Promise { + let scratchOrgUsername: string; + for (const pool of pools) { + let scratchOrg: ScratchOrg; + try { + const poolFetchImpl = new PoolFetchImpl(this.props.hubOrg, pool.trim(), false, true); + scratchOrg = (await poolFetchImpl.execute()) as ScratchOrg; + } catch (error) { + SFPLogger.log(error.message, LoggerLevel.TRACE); + } + if (scratchOrg && scratchOrg.status === 'Assigned') { + scratchOrgUsername = scratchOrg.username; + SFPLogger.log( + COLOR_KEY_MESSAGE(`Fetched scratch org ${scratchOrgUsername} from ${COLOR_KEY_VALUE(pool)}`), + LoggerLevel.INFO, + this.logger + ); + + if (displayOrgInfo) { + OrgInfoDisplayer.printScratchOrgInfo(scratchOrg); + OrgInfoDisplayer.writeScratchOrgInfoToMarkDown(scratchOrg); + } + + this.getCurrentRemainingNumberOfOrgsInPoolAndReport(scratchOrg.tag); + break; + } + } + if (scratchOrgUsername) return scratchOrgUsername; + else + throw new Error( + `Failed to fetch scratch org from ${pools}, Are you sure you created this pool using a DevHub authenticated using auth:sfdxurl or auth:web or auth:accesstoken:store` + ); + } + + private async getCurrentRemainingNumberOfOrgsInPoolAndReport(tag: string) { + try { + const results = await new ScratchOrgInfoFetcher(this.props.hubOrg).getScratchOrgsByTag(tag, false, true); + + const availableSo = results.records.filter((soInfo) => soInfo.Allocation_status__c === 'Available'); + + SFPStatsSender.logGauge('pool.available', availableSo.length, { + poolName: tag, + }); + } catch (error) { + //do nothing, we are not reporting anything if anything goes wrong here + } + } + + async preDeployPackage( + sfpPackage: SfpPackage, + targetUsername: string, + deployedPackages?: SfpPackage[], + devhubUserName?: string + ): Promise<{ isToFailDeployment: boolean; message?: string }> { + const shouldInstallDependencies = (mode: ValidationMode) => { + if ( + this.props.validateAgainst === ValidateAgainst.PROVIDED_ORG && + !this.props.installExternalDependencies + ) { + return false; + } + + const isThoroughValidation = + mode === ValidationMode.THOROUGH || mode === ValidationMode.THOROUGH_LIMITED_BY_RELEASE_CONFIG; + + const isFastFeedbackWithExternalDependencies = + (mode === ValidationMode.FASTFEEDBACK_LIMITED_BY_RELEASE_CONFIG || + mode === ValidationMode.FAST_FEEDBACK) && + this.props.installExternalDependencies; + + return isThoroughValidation || mode === ValidationMode.INDIVIDUAL || isFastFeedbackWithExternalDependencies; + }; + + if (shouldInstallDependencies(this.props.validationMode)) { + const projectConfig = + this.props.validationMode === ValidationMode.INDIVIDUAL + ? ProjectConfig.cleanupMPDFromProjectDirectory(null, sfpPackage.package_name) + : ProjectConfig.getSFDXProjectConfig(null); + + await this.installPackageDependencies(projectConfig, this.orgAsSFPOrg, sfpPackage, deployedPackages); + } + + return { isToFailDeployment: false }; + } + + async postDeployPackage( + sfpPackage: SfpPackage, + packageInstallationResult: PackageInstallationResult, + targetUsername: string, + deployedPackages?: SfpPackage[], + devhubUserName?: string + ): Promise<{ isToFailDeployment: boolean; message?: string }> { + //Trigger Tests after installation of each package + let isPackageImpacted = this.impactedPackagesAsPerBranch + ? this.impactedPackagesAsPerBranch.get(sfpPackage.package_name) + : true; + + if (isPackageImpacted) { + if (sfpPackage.packageType && sfpPackage.packageType != PackageType.Data) { + if (packageInstallationResult.result === PackageInstallationStatus.Succeeded) { + //Get Changed Components + const apextestValidator = new ApexTestValidator( + targetUsername, + sfpPackage, + this.props, + this.logger + ); + const testResult = await apextestValidator.validateApexTests(); + + if (!testResult.result) { + FileOutputHandler.getInstance().appendOutput( + `validation-error.md`, + `### 💣 Validation Failed 💣` + ); + FileOutputHandler.getInstance().appendOutput( + `validation-error.md`, + `Package validation failed for **${sfpPackage.packageName}**` + ); + FileOutputHandler.getInstance().appendOutput(`validation-error.md`, `Reasons:`); + FileOutputHandler.getInstance().appendOutput(`validation-error.md`, `${testResult.message}`); + } + else + { + await this.updateOrgWithArtifact(targetUsername, sfpPackage); + } + + return { + isToFailDeployment: !testResult.result, + message: testResult.message, + }; + } + } + else if (sfpPackage.packageType && sfpPackage.packageType == PackageType.Data) { + await this.updateOrgWithArtifact(targetUsername, sfpPackage); + } + } else { + SFPLogger.log( + COLOR_KEY_MESSAGE(`Syncing ${sfpPackage.package_name} to the org, Tests will be skipped`), + LoggerLevel.INFO, + this.logger + ); + await this.updateOrgWithArtifact(targetUsername, sfpPackage); + } + return { isToFailDeployment: false }; + } + + private async updateOrgWithArtifact(targetUsername: string, sfpPackage: SfpPackage) { + if(!this.props.disableArtifactCommit) + { + const sfpOrg = await SFPOrg.create({ aliasOrUsername: targetUsername }); + await sfpOrg.updateArtifactInOrg(this.logger, sfpPackage); + } + } }