From ec884dceeb2c4e14b2c346a3f41c85e90667b39d Mon Sep 17 00:00:00 2001 From: azlam Date: Sat, 28 Oct 2023 18:32:23 +1100 Subject: [PATCH] feat(impact): add impact:package and impact:releaseconfig commands Add two new helper commands that can be used to figure out impacted packages or impacted releaseconfigs by comparing against last know gith t tags --- .../messages/impact_package.json | 4 + .../messages/impact_release_config.json | 6 + packages/sfpowerscripts-cli/package.json | 4 + .../src/commands/impact/package.ts | 80 ++++++++++ .../src/commands/impact/releaseconfig.ts | 128 ++++++++++++++++ .../impl/impact/ImpactedPackagesResolver.ts | 140 ++++++++++++++++++ .../src/impl/impact/ImpactedReleaseConfig.ts | 74 +++++++++ 7 files changed, 436 insertions(+) create mode 100644 packages/sfpowerscripts-cli/messages/impact_package.json create mode 100644 packages/sfpowerscripts-cli/messages/impact_release_config.json create mode 100644 packages/sfpowerscripts-cli/src/commands/impact/package.ts create mode 100644 packages/sfpowerscripts-cli/src/commands/impact/releaseconfig.ts create mode 100644 packages/sfpowerscripts-cli/src/impl/impact/ImpactedPackagesResolver.ts create mode 100644 packages/sfpowerscripts-cli/src/impl/impact/ImpactedReleaseConfig.ts diff --git a/packages/sfpowerscripts-cli/messages/impact_package.json b/packages/sfpowerscripts-cli/messages/impact_package.json new file mode 100644 index 000000000..991ac7d66 --- /dev/null +++ b/packages/sfpowerscripts-cli/messages/impact_package.json @@ -0,0 +1,4 @@ +{ + "commandDescription": "Figures out impacted packages of a project, due to a change from the last known tags", + "baseCommitOrBranchFlagDescription": "The base branch on which the git tags should be used" +} diff --git a/packages/sfpowerscripts-cli/messages/impact_release_config.json b/packages/sfpowerscripts-cli/messages/impact_release_config.json new file mode 100644 index 000000000..e53051cc9 --- /dev/null +++ b/packages/sfpowerscripts-cli/messages/impact_release_config.json @@ -0,0 +1,6 @@ +{ + "commandDescription": "Figures out impacted release configurations of a project, due to a change,from the last known tags", + "releaseConfigFileFlagDescription":"Path to the directory containing release defns", + "baseCommitOrBranchFlagDescription": "The base branch on which the git tags should be used from", + "filterByFlagDescription": "Filter by a specific release config name" +} diff --git a/packages/sfpowerscripts-cli/package.json b/packages/sfpowerscripts-cli/package.json index 6943e2f43..a1a4fe283 100644 --- a/packages/sfpowerscripts-cli/package.json +++ b/packages/sfpowerscripts-cli/package.json @@ -112,6 +112,10 @@ } } }, + "impact" : { + "description": "Figures out the impact of various components of sfpowerscripts", + "external": true + }, "analyze": { "description": "Analyze your projects using static analysis tools such as PMD", "external": true diff --git a/packages/sfpowerscripts-cli/src/commands/impact/package.ts b/packages/sfpowerscripts-cli/src/commands/impact/package.ts new file mode 100644 index 000000000..a550b7d3f --- /dev/null +++ b/packages/sfpowerscripts-cli/src/commands/impact/package.ts @@ -0,0 +1,80 @@ +import { Messages } from '@salesforce/core'; +import SfpowerscriptsCommand from '../../SfpowerscriptsCommand'; +import { Stage } from '../../impl/Stage'; +import SFPLogger, { COLOR_KEY_MESSAGE, ConsoleLogger } from '@dxatscale/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'; + + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@dxatscale/sfpowerscripts', 'impact_package'); + +export default class Package extends SfpowerscriptsCommand { + public static flags = { + loglevel, + basebranch: Flags.string({ + description: messages.getMessage('baseCommitOrBranchFlagDescription'), + required: true, + }) + }; + + public static description = messages.getMessage('commandDescription'); + private props: ImpactedPackageProps; + + async execute(): Promise { + // Read Manifest + + this.props = { + currentStage: Stage.BUILD, + baseBranch: this.flags.basebranch, + diffOptions: { + useLatestGitTags: true, + skipPackageDescriptorChange: false, + }, + }; + + 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()); + + + const outputPath = path.join(process.cwd(), 'impacted-package.json'); + if (packagesToBeBuilt && packagesToBeBuilt.length > 0) + fs.writeFileSync(outputPath, JSON.stringify(packagesToBeBuilt, null, 2)); + else fs.writeFileSync(outputPath, JSON.stringify([], null, 2)); + SFPLogger.log(`Impacted packages if any written to ${outputPath}`); + + + 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/sfpowerscripts-cli/src/commands/impact/releaseconfig.ts b/packages/sfpowerscripts-cli/src/commands/impact/releaseconfig.ts new file mode 100644 index 000000000..6a13b33b6 --- /dev/null +++ b/packages/sfpowerscripts-cli/src/commands/impact/releaseconfig.ts @@ -0,0 +1,128 @@ +import { Messages } from '@salesforce/core'; +import SfpowerscriptsCommand from '../../SfpowerscriptsCommand'; +import { Stage } from '../../impl/Stage'; +import * as fs from 'fs-extra'; +import SFPLogger, { COLOR_KEY_MESSAGE, ConsoleLogger } from '@dxatscale/sfp-logger'; +import { Flags } from '@oclif/core'; +import { loglevel } from '../../flags/sfdxflags'; +import { ZERO_BORDER_TABLE } from '../../ui/TableConstants'; +import path from 'path'; +import ImpactedPackageResolver, { ImpactedPackageProps } from '../../impl/impact/ImpactedPackagesResolver'; +import ImpactedRelaseConfigResolver from '../../impl/impact/ImpactedReleaseConfig'; +const Table = require('cli-table'); + + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@dxatscale/sfpowerscripts', 'impact_release_config'); + +export default class ReleaseConfig extends SfpowerscriptsCommand { + public static flags = { + loglevel, + basebranch: Flags.string({ + description: messages.getMessage('baseCommitOrBranchFlagDescription'), + required: true, + }), + releaseconfig: Flags.string({ + description: messages.getMessage('releaseConfigFileFlagDescription'), + default: 'config', + }), + filterBy: Flags.string({ + description: messages.getMessage('filterByFlagDescription'), + }), + }; + + public static description = messages.getMessage('commandDescription'); + private props: ImpactedPackageProps; + isMultiConfigFilesEnabled: boolean; + + async execute(): Promise { + // Read Manifest + + this.props = { + branch: this.flags.branch, + currentStage: Stage.VALIDATE, + baseBranch: this.flags.basebranch, + diffOptions: { + useLatestGitTags: true, + skipPackageDescriptorChange: false, + }, + }; + + 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()); + + const impactedReleaseConfigResolver = new ImpactedRelaseConfigResolver(); + + let impactedReleaseConfigs = impactedReleaseConfigResolver.getImpactedReleaseConfigs( + packagesToBeBuilt, + this.flags.releaseconfig, + this.flags.filterBy + ); + + let impactedReleaseConfigTable = this.createImpactedReleaseConfigsAsATable(impactedReleaseConfigs.include); + //Log Packages to be built + SFPLogger.log(COLOR_KEY_MESSAGE('Release Configs impacted...')); + SFPLogger.log(impactedReleaseConfigTable.toString()); + + const outputPath = path.join(process.cwd(), 'impacted-release-configs.json'); + if (impactedReleaseConfigs && impactedReleaseConfigs.include.length > 0) + fs.writeFileSync(outputPath, JSON.stringify(impactedReleaseConfigs, null, 2)); + else fs.writeFileSync(outputPath, JSON.stringify([], null, 2)); + if (!this.flags.filterBy) SFPLogger.log(`Impacted release configs written to ${outputPath}`); + else + SFPLogger.log( + `Impacted release configs written to ${outputPath},${ + impactedReleaseConfigs.include[0]?.releaseName + ? `filtered impacted release config found for ${impactedReleaseConfigs.include[0]?.releaseName}` + : `no impacted release config found for ${this.flags.filterBy}` + }` + ); + + return impactedReleaseConfigs.include; + } + + private createDiffPackageScheduledDisplayedAsATable(packagesToBeBuilt: Map) { + let tableHead = ['Package', 'Reason', 'Last Known Tag']; + if (this.isMultiConfigFilesEnabled && this.props.currentStage == Stage.BUILD) { + tableHead.push('Scratch Org Config File'); + } + 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; + } + + private createImpactedReleaseConfigsAsATable(impacatedReleaseConfigs: any[]) { + let tableHead = ['Release/Domain Name', 'Pools', 'ReleaseConfig Path']; + let table = new Table({ + head: tableHead, + chars: ZERO_BORDER_TABLE, + }); + for (const impactedReleaseConfig of impacatedReleaseConfigs) { + let item = [ + impactedReleaseConfig.releaseName, + impactedReleaseConfig.domainNameUsedForPools, + impactedReleaseConfig.filePath, + ]; + table.push(item); + } + return table; + } +} diff --git a/packages/sfpowerscripts-cli/src/impl/impact/ImpactedPackagesResolver.ts b/packages/sfpowerscripts-cli/src/impl/impact/ImpactedPackagesResolver.ts new file mode 100644 index 000000000..a3640b6c9 --- /dev/null +++ b/packages/sfpowerscripts-cli/src/impl/impact/ImpactedPackagesResolver.ts @@ -0,0 +1,140 @@ +import PackageDiffImpl, { PackageDiffOptions } from '@dxatscale/sfpowerscripts.core/lib/package/diff/PackageDiffImpl'; +import { Stage } from '../Stage'; +import ProjectConfig from '@dxatscale/sfpowerscripts.core/lib/project/ProjectConfig'; +import { PackageType } from '@dxatscale/sfpowerscripts.core/lib/package/SfpPackage'; +import * as fs from 'fs-extra'; +import { Logger } from '@dxatscale/sfp-logger'; +import BuildCollections from '../parallelBuilder/BuildCollections'; + +export interface ImpactedPackageProps { + projectDirectory?: string; + branch?: string; + configFilePath?: string; + currentStage: Stage; + baseBranch?: string; + diffOptions?: PackageDiffOptions; + includeOnlyPackages?: string[]; +} + +export default class ImpactedPackageResolver { + + + constructor(private props: ImpactedPackageProps, private logger: Logger) { + } + + async getImpactedPackages(): Promise> { + let projectConfig = ProjectConfig.getSFDXProjectConfig(this.props.projectDirectory); + let packagesToBeBuilt = this.getPackagesToBeBuilt(this.props.projectDirectory); + let packagesToBeBuiltWithReasons = await this.filterPackagesToBeBuiltByChanged( + this.props.projectDirectory, + projectConfig, + packagesToBeBuilt + ); + + return packagesToBeBuiltWithReasons; + } + + /** + * Get the file path of the forceignore for current stage, from project config. + * Returns null if a forceignore path is not defined in the project config for the current stage. + * + * @param projectConfig + * @param currentStage + */ + private getPathToForceIgnoreForCurrentStage(projectConfig: any, currentStage: Stage): string { + let stageForceIgnorePath: string; + + let ignoreFiles: { [key in Stage]: string } = projectConfig.plugins?.sfpowerscripts?.ignoreFiles; + if (ignoreFiles) { + Object.keys(ignoreFiles).forEach((key) => { + if (key.toLowerCase() == currentStage) { + stageForceIgnorePath = ignoreFiles[key]; + } + }); + } + + if (stageForceIgnorePath) { + if (fs.existsSync(stageForceIgnorePath)) { + return stageForceIgnorePath; + } else throw new Error(`${stageForceIgnorePath} forceignore file does not exist`); + } else return null; + } + + private async filterPackagesToBeBuiltByChanged(projectDirectory: string,projectConfig:any, allPackagesInRepo: any) { + let packagesToBeBuilt = new Map(); + let buildCollections = new BuildCollections(projectDirectory); + if (this.props.diffOptions) + this.props.diffOptions.pathToReplacementForceIgnore = this.getPathToForceIgnoreForCurrentStage( + projectConfig, + this.props.currentStage + ); + + for await (const pkg of allPackagesInRepo) { + let diffImpl: PackageDiffImpl = new PackageDiffImpl( + this.logger, + pkg, + this.props.projectDirectory, + this.props.diffOptions + ); + let packageDiffCheck = await diffImpl.exec(); + + if (packageDiffCheck.isToBeBuilt) { + packagesToBeBuilt.set(pkg, { + reason: packageDiffCheck.reason, + tag: packageDiffCheck.tag, + }); + //Add Bundles + if (buildCollections.isPackageInACollection(pkg)) { + buildCollections.listPackagesInCollection(pkg).forEach((packageInCollection) => { + if (!packagesToBeBuilt.has(packageInCollection)) { + packagesToBeBuilt.set(packageInCollection, { + reason: 'Part of a build collection', + }); + } + }); + } + } + } + return packagesToBeBuilt; + } + + private getPackagesToBeBuilt(projectDirectory: string, includeOnlyPackages?: string[]): string[] { + let projectConfig = ProjectConfig.getSFDXProjectConfig(projectDirectory); + let sfdxpackages = []; + + let packageDescriptors = projectConfig['packageDirectories'].filter((pkg) => { + if ( + pkg.ignoreOnStage?.find((stage) => { + stage = stage.toLowerCase(); + return stage === this.props.currentStage; + }) + ) + return false; + else return true; + }); + + //Filter Packages + if (includeOnlyPackages) { + packageDescriptors = packageDescriptors.filter((pkg) => { + if ( + includeOnlyPackages.find((includedPkg) => { + return includedPkg == pkg.package; + }) + ) + return true; + else return false; + }); + } + + // Ignore aliasfied packages on stages fix #1289 + packageDescriptors = packageDescriptors.filter((pkg) => { + return !(this.props.currentStage === 'prepare' && pkg.aliasfy && pkg.type !== PackageType.Data); + }); + + for (const pkg of packageDescriptors) { + if (pkg.package && pkg.versionNumber) sfdxpackages.push(pkg['package']); + } + return sfdxpackages; + } + +} diff --git a/packages/sfpowerscripts-cli/src/impl/impact/ImpactedReleaseConfig.ts b/packages/sfpowerscripts-cli/src/impl/impact/ImpactedReleaseConfig.ts new file mode 100644 index 000000000..6e756ecf3 --- /dev/null +++ b/packages/sfpowerscripts-cli/src/impl/impact/ImpactedReleaseConfig.ts @@ -0,0 +1,74 @@ +import * as fs from 'fs-extra'; +const yaml = require('js-yaml'); +import path from 'path'; + +export default class ImpactedRelaseConfigResolver { + + public getImpactedReleaseConfigs(impactedPackages, configDir, filterBy?: string) { + const impactedReleaseDefs = []; + + fs.readdirSync(configDir).forEach((file) => { + const filePath = path.join(configDir, file); + const fileContent = fs.readFileSync(filePath, 'utf8'); + const releaseConfig = yaml.load(fileContent); + + if (releaseConfig.releaseName) { + let releaseImpactedPackages = []; + //Its a releasedefn, + if (releaseConfig.includeOnlyArtifacts) { + releaseImpactedPackages = releaseConfig.includeOnlyArtifacts.filter((artifact) => + impactedPackages.includes(artifact) + ); + } else if (releaseConfig.excludeArtifacts) { + releaseImpactedPackages = impactedPackages.filter( + (artifact) => !releaseConfig.excludeArtifacts.includes(artifact) + ); + } + + if (releaseImpactedPackages.length > 0) { + if (filterBy) { + if (releaseConfig.releaseName.includes(filterBy)) { + impactedReleaseDefs.push({ + releaseName: releaseConfig.releaseName, + domainNameUsedForPools: releaseConfig.domainNameUsedForPools + ? releaseConfig.domainNameUsedForPools + : releaseConfig.releaseName, + filePath: filePath, + impactedPackages: releaseImpactedPackages, // Including the impacted packages + }); + } + } else { + impactedReleaseDefs.push({ + releaseName: releaseConfig.releaseName, + domainNameUsedForPools: releaseConfig.domainNameUsedForPools + ? releaseConfig.domainNameUsedForPools + : releaseConfig.releaseName, + filePath: filePath, + impactedPackages: releaseImpactedPackages, // Including the impacted packages + }); + } + } + } + }); + + const sortedImpactedReleaseDefs = impactedReleaseDefs.sort((a, b) => { + if (!a.impactedPackages.length && !b.impactedPackages.length) return 0; + if (!a.impactedPackages.length) return 1; // Move releases with no impacted packages to the end + if (!b.impactedPackages.length) return -1; // Same as above + + const indexA = impactedPackages.indexOf(a.impactedPackages[0]); + const indexB = impactedPackages.indexOf(b.impactedPackages[0]); + + if (indexA === -1 && indexB === -1) return 0; // Neither package is in impactedPackages + if (indexA === -1) return 1; // Move releases with unknown impacted packages to the end + if (indexB === -1) return -1; // Same as above + + return indexA - indexB; // Sort based on index in impactedPackages + }); + + const output = { + include: sortedImpactedReleaseDefs, + }; + return output; + } +}