Skip to content

Commit ec884dc

Browse files
committedOct 28, 2023
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
1 parent 018e283 commit ec884dc

File tree

7 files changed

+436
-0
lines changed

7 files changed

+436
-0
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"commandDescription": "Figures out impacted packages of a project, due to a change from the last known tags",
3+
"baseCommitOrBranchFlagDescription": "The base branch on which the git tags should be used"
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"commandDescription": "Figures out impacted release configurations of a project, due to a change,from the last known tags",
3+
"releaseConfigFileFlagDescription":"Path to the directory containing release defns",
4+
"baseCommitOrBranchFlagDescription": "The base branch on which the git tags should be used from",
5+
"filterByFlagDescription": "Filter by a specific release config name"
6+
}

‎packages/sfpowerscripts-cli/package.json

+4
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@
112112
}
113113
}
114114
},
115+
"impact" : {
116+
"description": "Figures out the impact of various components of sfpowerscripts",
117+
"external": true
118+
},
115119
"analyze": {
116120
"description": "Analyze your projects using static analysis tools such as PMD",
117121
"external": true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Messages } from '@salesforce/core';
2+
import SfpowerscriptsCommand from '../../SfpowerscriptsCommand';
3+
import { Stage } from '../../impl/Stage';
4+
import SFPLogger, { COLOR_KEY_MESSAGE, ConsoleLogger } from '@dxatscale/sfp-logger';
5+
import { Flags } from '@oclif/core';
6+
import { loglevel } from '../../flags/sfdxflags';
7+
import { ZERO_BORDER_TABLE } from '../../ui/TableConstants';
8+
import ImpactedPackageResolver, { ImpactedPackageProps } from '../../impl/impact/ImpactedPackagesResolver';
9+
const Table = require('cli-table');
10+
import path from 'path';
11+
import * as fs from 'fs-extra';
12+
13+
14+
Messages.importMessagesDirectory(__dirname);
15+
const messages = Messages.loadMessages('@dxatscale/sfpowerscripts', 'impact_package');
16+
17+
export default class Package extends SfpowerscriptsCommand {
18+
public static flags = {
19+
loglevel,
20+
basebranch: Flags.string({
21+
description: messages.getMessage('baseCommitOrBranchFlagDescription'),
22+
required: true,
23+
})
24+
};
25+
26+
public static description = messages.getMessage('commandDescription');
27+
private props: ImpactedPackageProps;
28+
29+
async execute(): Promise<any> {
30+
// Read Manifest
31+
32+
this.props = {
33+
currentStage: Stage.BUILD,
34+
baseBranch: this.flags.basebranch,
35+
diffOptions: {
36+
useLatestGitTags: true,
37+
skipPackageDescriptorChange: false,
38+
},
39+
};
40+
41+
const impactedPackageResolver = new ImpactedPackageResolver(this.props, new ConsoleLogger());
42+
43+
let packagesToBeBuiltWithReasons = await impactedPackageResolver.getImpactedPackages();
44+
let packageDiffTable = this.createDiffPackageScheduledDisplayedAsATable(packagesToBeBuiltWithReasons);
45+
const packagesToBeBuilt = Array.from(packagesToBeBuiltWithReasons.keys());
46+
47+
//Log Packages to be built
48+
SFPLogger.log(COLOR_KEY_MESSAGE('Packages impacted...'));
49+
SFPLogger.log(packageDiffTable.toString());
50+
51+
52+
const outputPath = path.join(process.cwd(), 'impacted-package.json');
53+
if (packagesToBeBuilt && packagesToBeBuilt.length > 0)
54+
fs.writeFileSync(outputPath, JSON.stringify(packagesToBeBuilt, null, 2));
55+
else fs.writeFileSync(outputPath, JSON.stringify([], null, 2));
56+
SFPLogger.log(`Impacted packages if any written to ${outputPath}`);
57+
58+
59+
return packagesToBeBuilt;
60+
}
61+
62+
private createDiffPackageScheduledDisplayedAsATable(packagesToBeBuilt: Map<string, any>) {
63+
let tableHead = ['Package', 'Reason', 'Last Known Tag'];
64+
let table = new Table({
65+
head: tableHead,
66+
chars: ZERO_BORDER_TABLE,
67+
});
68+
for (const pkg of packagesToBeBuilt.keys()) {
69+
let item = [
70+
pkg,
71+
packagesToBeBuilt.get(pkg).reason,
72+
packagesToBeBuilt.get(pkg).tag ? packagesToBeBuilt.get(pkg).tag : '',
73+
];
74+
table.push(item);
75+
}
76+
return table;
77+
}
78+
79+
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { Messages } from '@salesforce/core';
2+
import SfpowerscriptsCommand from '../../SfpowerscriptsCommand';
3+
import { Stage } from '../../impl/Stage';
4+
import * as fs from 'fs-extra';
5+
import SFPLogger, { COLOR_KEY_MESSAGE, ConsoleLogger } from '@dxatscale/sfp-logger';
6+
import { Flags } from '@oclif/core';
7+
import { loglevel } from '../../flags/sfdxflags';
8+
import { ZERO_BORDER_TABLE } from '../../ui/TableConstants';
9+
import path from 'path';
10+
import ImpactedPackageResolver, { ImpactedPackageProps } from '../../impl/impact/ImpactedPackagesResolver';
11+
import ImpactedRelaseConfigResolver from '../../impl/impact/ImpactedReleaseConfig';
12+
const Table = require('cli-table');
13+
14+
15+
Messages.importMessagesDirectory(__dirname);
16+
const messages = Messages.loadMessages('@dxatscale/sfpowerscripts', 'impact_release_config');
17+
18+
export default class ReleaseConfig extends SfpowerscriptsCommand {
19+
public static flags = {
20+
loglevel,
21+
basebranch: Flags.string({
22+
description: messages.getMessage('baseCommitOrBranchFlagDescription'),
23+
required: true,
24+
}),
25+
releaseconfig: Flags.string({
26+
description: messages.getMessage('releaseConfigFileFlagDescription'),
27+
default: 'config',
28+
}),
29+
filterBy: Flags.string({
30+
description: messages.getMessage('filterByFlagDescription'),
31+
}),
32+
};
33+
34+
public static description = messages.getMessage('commandDescription');
35+
private props: ImpactedPackageProps;
36+
isMultiConfigFilesEnabled: boolean;
37+
38+
async execute(): Promise<any> {
39+
// Read Manifest
40+
41+
this.props = {
42+
branch: this.flags.branch,
43+
currentStage: Stage.VALIDATE,
44+
baseBranch: this.flags.basebranch,
45+
diffOptions: {
46+
useLatestGitTags: true,
47+
skipPackageDescriptorChange: false,
48+
},
49+
};
50+
51+
const impactedPackageResolver = new ImpactedPackageResolver(this.props, new ConsoleLogger());
52+
53+
let packagesToBeBuiltWithReasons = await impactedPackageResolver.getImpactedPackages();
54+
let packageDiffTable = this.createDiffPackageScheduledDisplayedAsATable(packagesToBeBuiltWithReasons);
55+
const packagesToBeBuilt = Array.from(packagesToBeBuiltWithReasons.keys());
56+
57+
//Log Packages to be built
58+
SFPLogger.log(COLOR_KEY_MESSAGE('Packages impacted...'));
59+
SFPLogger.log(packageDiffTable.toString());
60+
61+
const impactedReleaseConfigResolver = new ImpactedRelaseConfigResolver();
62+
63+
let impactedReleaseConfigs = impactedReleaseConfigResolver.getImpactedReleaseConfigs(
64+
packagesToBeBuilt,
65+
this.flags.releaseconfig,
66+
this.flags.filterBy
67+
);
68+
69+
let impactedReleaseConfigTable = this.createImpactedReleaseConfigsAsATable(impactedReleaseConfigs.include);
70+
//Log Packages to be built
71+
SFPLogger.log(COLOR_KEY_MESSAGE('Release Configs impacted...'));
72+
SFPLogger.log(impactedReleaseConfigTable.toString());
73+
74+
const outputPath = path.join(process.cwd(), 'impacted-release-configs.json');
75+
if (impactedReleaseConfigs && impactedReleaseConfigs.include.length > 0)
76+
fs.writeFileSync(outputPath, JSON.stringify(impactedReleaseConfigs, null, 2));
77+
else fs.writeFileSync(outputPath, JSON.stringify([], null, 2));
78+
if (!this.flags.filterBy) SFPLogger.log(`Impacted release configs written to ${outputPath}`);
79+
else
80+
SFPLogger.log(
81+
`Impacted release configs written to ${outputPath},${
82+
impactedReleaseConfigs.include[0]?.releaseName
83+
? `filtered impacted release config found for ${impactedReleaseConfigs.include[0]?.releaseName}`
84+
: `no impacted release config found for ${this.flags.filterBy}`
85+
}`
86+
);
87+
88+
return impactedReleaseConfigs.include;
89+
}
90+
91+
private createDiffPackageScheduledDisplayedAsATable(packagesToBeBuilt: Map<string, any>) {
92+
let tableHead = ['Package', 'Reason', 'Last Known Tag'];
93+
if (this.isMultiConfigFilesEnabled && this.props.currentStage == Stage.BUILD) {
94+
tableHead.push('Scratch Org Config File');
95+
}
96+
let table = new Table({
97+
head: tableHead,
98+
chars: ZERO_BORDER_TABLE,
99+
});
100+
for (const pkg of packagesToBeBuilt.keys()) {
101+
let item = [
102+
pkg,
103+
packagesToBeBuilt.get(pkg).reason,
104+
packagesToBeBuilt.get(pkg).tag ? packagesToBeBuilt.get(pkg).tag : '',
105+
];
106+
107+
table.push(item);
108+
}
109+
return table;
110+
}
111+
112+
private createImpactedReleaseConfigsAsATable(impacatedReleaseConfigs: any[]) {
113+
let tableHead = ['Release/Domain Name', 'Pools', 'ReleaseConfig Path'];
114+
let table = new Table({
115+
head: tableHead,
116+
chars: ZERO_BORDER_TABLE,
117+
});
118+
for (const impactedReleaseConfig of impacatedReleaseConfigs) {
119+
let item = [
120+
impactedReleaseConfig.releaseName,
121+
impactedReleaseConfig.domainNameUsedForPools,
122+
impactedReleaseConfig.filePath,
123+
];
124+
table.push(item);
125+
}
126+
return table;
127+
}
128+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import PackageDiffImpl, { PackageDiffOptions } from '@dxatscale/sfpowerscripts.core/lib/package/diff/PackageDiffImpl';
2+
import { Stage } from '../Stage';
3+
import ProjectConfig from '@dxatscale/sfpowerscripts.core/lib/project/ProjectConfig';
4+
import { PackageType } from '@dxatscale/sfpowerscripts.core/lib/package/SfpPackage';
5+
import * as fs from 'fs-extra';
6+
import { Logger } from '@dxatscale/sfp-logger';
7+
import BuildCollections from '../parallelBuilder/BuildCollections';
8+
9+
export interface ImpactedPackageProps {
10+
projectDirectory?: string;
11+
branch?: string;
12+
configFilePath?: string;
13+
currentStage: Stage;
14+
baseBranch?: string;
15+
diffOptions?: PackageDiffOptions;
16+
includeOnlyPackages?: string[];
17+
}
18+
19+
export default class ImpactedPackageResolver {
20+
21+
22+
constructor(private props: ImpactedPackageProps, private logger: Logger) {
23+
}
24+
25+
async getImpactedPackages(): Promise<Map<string, any>> {
26+
let projectConfig = ProjectConfig.getSFDXProjectConfig(this.props.projectDirectory);
27+
let packagesToBeBuilt = this.getPackagesToBeBuilt(this.props.projectDirectory);
28+
let packagesToBeBuiltWithReasons = await this.filterPackagesToBeBuiltByChanged(
29+
this.props.projectDirectory,
30+
projectConfig,
31+
packagesToBeBuilt
32+
);
33+
34+
return packagesToBeBuiltWithReasons;
35+
}
36+
37+
/**
38+
* Get the file path of the forceignore for current stage, from project config.
39+
* Returns null if a forceignore path is not defined in the project config for the current stage.
40+
*
41+
* @param projectConfig
42+
* @param currentStage
43+
*/
44+
private getPathToForceIgnoreForCurrentStage(projectConfig: any, currentStage: Stage): string {
45+
let stageForceIgnorePath: string;
46+
47+
let ignoreFiles: { [key in Stage]: string } = projectConfig.plugins?.sfpowerscripts?.ignoreFiles;
48+
if (ignoreFiles) {
49+
Object.keys(ignoreFiles).forEach((key) => {
50+
if (key.toLowerCase() == currentStage) {
51+
stageForceIgnorePath = ignoreFiles[key];
52+
}
53+
});
54+
}
55+
56+
if (stageForceIgnorePath) {
57+
if (fs.existsSync(stageForceIgnorePath)) {
58+
return stageForceIgnorePath;
59+
} else throw new Error(`${stageForceIgnorePath} forceignore file does not exist`);
60+
} else return null;
61+
}
62+
63+
private async filterPackagesToBeBuiltByChanged(projectDirectory: string,projectConfig:any, allPackagesInRepo: any) {
64+
let packagesToBeBuilt = new Map<string, any>();
65+
let buildCollections = new BuildCollections(projectDirectory);
66+
if (this.props.diffOptions)
67+
this.props.diffOptions.pathToReplacementForceIgnore = this.getPathToForceIgnoreForCurrentStage(
68+
projectConfig,
69+
this.props.currentStage
70+
);
71+
72+
for await (const pkg of allPackagesInRepo) {
73+
let diffImpl: PackageDiffImpl = new PackageDiffImpl(
74+
this.logger,
75+
pkg,
76+
this.props.projectDirectory,
77+
this.props.diffOptions
78+
);
79+
let packageDiffCheck = await diffImpl.exec();
80+
81+
if (packageDiffCheck.isToBeBuilt) {
82+
packagesToBeBuilt.set(pkg, {
83+
reason: packageDiffCheck.reason,
84+
tag: packageDiffCheck.tag,
85+
});
86+
//Add Bundles
87+
if (buildCollections.isPackageInACollection(pkg)) {
88+
buildCollections.listPackagesInCollection(pkg).forEach((packageInCollection) => {
89+
if (!packagesToBeBuilt.has(packageInCollection)) {
90+
packagesToBeBuilt.set(packageInCollection, {
91+
reason: 'Part of a build collection',
92+
});
93+
}
94+
});
95+
}
96+
}
97+
}
98+
return packagesToBeBuilt;
99+
}
100+
101+
private getPackagesToBeBuilt(projectDirectory: string, includeOnlyPackages?: string[]): string[] {
102+
let projectConfig = ProjectConfig.getSFDXProjectConfig(projectDirectory);
103+
let sfdxpackages = [];
104+
105+
let packageDescriptors = projectConfig['packageDirectories'].filter((pkg) => {
106+
if (
107+
pkg.ignoreOnStage?.find((stage) => {
108+
stage = stage.toLowerCase();
109+
return stage === this.props.currentStage;
110+
})
111+
)
112+
return false;
113+
else return true;
114+
});
115+
116+
//Filter Packages
117+
if (includeOnlyPackages) {
118+
packageDescriptors = packageDescriptors.filter((pkg) => {
119+
if (
120+
includeOnlyPackages.find((includedPkg) => {
121+
return includedPkg == pkg.package;
122+
})
123+
)
124+
return true;
125+
else return false;
126+
});
127+
}
128+
129+
// Ignore aliasfied packages on stages fix #1289
130+
packageDescriptors = packageDescriptors.filter((pkg) => {
131+
return !(this.props.currentStage === 'prepare' && pkg.aliasfy && pkg.type !== PackageType.Data);
132+
});
133+
134+
for (const pkg of packageDescriptors) {
135+
if (pkg.package && pkg.versionNumber) sfdxpackages.push(pkg['package']);
136+
}
137+
return sfdxpackages;
138+
}
139+
140+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import * as fs from 'fs-extra';
2+
const yaml = require('js-yaml');
3+
import path from 'path';
4+
5+
export default class ImpactedRelaseConfigResolver {
6+
7+
public getImpactedReleaseConfigs(impactedPackages, configDir, filterBy?: string) {
8+
const impactedReleaseDefs = [];
9+
10+
fs.readdirSync(configDir).forEach((file) => {
11+
const filePath = path.join(configDir, file);
12+
const fileContent = fs.readFileSync(filePath, 'utf8');
13+
const releaseConfig = yaml.load(fileContent);
14+
15+
if (releaseConfig.releaseName) {
16+
let releaseImpactedPackages = [];
17+
//Its a releasedefn,
18+
if (releaseConfig.includeOnlyArtifacts) {
19+
releaseImpactedPackages = releaseConfig.includeOnlyArtifacts.filter((artifact) =>
20+
impactedPackages.includes(artifact)
21+
);
22+
} else if (releaseConfig.excludeArtifacts) {
23+
releaseImpactedPackages = impactedPackages.filter(
24+
(artifact) => !releaseConfig.excludeArtifacts.includes(artifact)
25+
);
26+
}
27+
28+
if (releaseImpactedPackages.length > 0) {
29+
if (filterBy) {
30+
if (releaseConfig.releaseName.includes(filterBy)) {
31+
impactedReleaseDefs.push({
32+
releaseName: releaseConfig.releaseName,
33+
domainNameUsedForPools: releaseConfig.domainNameUsedForPools
34+
? releaseConfig.domainNameUsedForPools
35+
: releaseConfig.releaseName,
36+
filePath: filePath,
37+
impactedPackages: releaseImpactedPackages, // Including the impacted packages
38+
});
39+
}
40+
} else {
41+
impactedReleaseDefs.push({
42+
releaseName: releaseConfig.releaseName,
43+
domainNameUsedForPools: releaseConfig.domainNameUsedForPools
44+
? releaseConfig.domainNameUsedForPools
45+
: releaseConfig.releaseName,
46+
filePath: filePath,
47+
impactedPackages: releaseImpactedPackages, // Including the impacted packages
48+
});
49+
}
50+
}
51+
}
52+
});
53+
54+
const sortedImpactedReleaseDefs = impactedReleaseDefs.sort((a, b) => {
55+
if (!a.impactedPackages.length && !b.impactedPackages.length) return 0;
56+
if (!a.impactedPackages.length) return 1; // Move releases with no impacted packages to the end
57+
if (!b.impactedPackages.length) return -1; // Same as above
58+
59+
const indexA = impactedPackages.indexOf(a.impactedPackages[0]);
60+
const indexB = impactedPackages.indexOf(b.impactedPackages[0]);
61+
62+
if (indexA === -1 && indexB === -1) return 0; // Neither package is in impactedPackages
63+
if (indexA === -1) return 1; // Move releases with unknown impacted packages to the end
64+
if (indexB === -1) return -1; // Same as above
65+
66+
return indexA - indexB; // Sort based on index in impactedPackages
67+
});
68+
69+
const output = {
70+
include: sortedImpactedReleaseDefs,
71+
};
72+
return output;
73+
}
74+
}

0 commit comments

Comments
 (0)
Please sign in to comment.