diff --git a/packages/core/src/display/DeployErrorDisplayer.ts b/packages/core/src/display/DeployErrorDisplayer.ts index e6c0b6480..8d6a8a68a 100644 --- a/packages/core/src/display/DeployErrorDisplayer.ts +++ b/packages/core/src/display/DeployErrorDisplayer.ts @@ -2,6 +2,7 @@ const Table = require('cli-table'); import { CodeCoverageWarnings, DeployMessage, Failures, MetadataApiDeployStatus } from '@salesforce/source-deploy-retrieve'; import SFPLogger, { Logger, LoggerLevel } from '@dxatscale/sfp-logger'; import { ZERO_BORDER_TABLE } from './TableConstants'; +import { ReleaseStreamService } from '../eventStream/release'; export default class DeployErrorDisplayer { private static printMetadataFailedToDeploy(componentFailures: DeployMessage | DeployMessage[], logger: Logger) { @@ -19,6 +20,7 @@ export default class DeployErrorDisplayer { componentFailure.problemType, componentFailure.problem, ]; + ReleaseStreamService.buildDeployErrorsMsg(componentFailure.componentType, componentFailure.fullName, componentFailure.problemType, componentFailure.problem); table.push(item); }; diff --git a/packages/core/src/display/PackageDependencyDisplayer.ts b/packages/core/src/display/PackageDependencyDisplayer.ts index ecd64d6b4..2d2d92f8e 100644 --- a/packages/core/src/display/PackageDependencyDisplayer.ts +++ b/packages/core/src/display/PackageDependencyDisplayer.ts @@ -1,12 +1,14 @@ import SFPLogger, { Logger, LoggerLevel } from '@dxatscale/sfp-logger'; import { ZERO_BORDER_TABLE } from './TableConstants'; const Table = require('cli-table'); +import { BuildStreamService } from '../eventStream/build'; export default class PackageDependencyDisplayer { public static printPackageDependencies( dependencies: { package: string; versionNumber?: string }[], projectConfig: any, - logger: Logger + logger: Logger, + pck?: string ) { if (Array.isArray(dependencies)) { SFPLogger.log('Package Dependencies:', LoggerLevel.INFO, logger); @@ -27,6 +29,9 @@ export default class PackageDependencyDisplayer { const row = [order,dependency.package, versionNumber]; table.push(row); + if(pck){ + BuildStreamService.buildPackageDependencies(pck,{order:order, pck: dependency.package, version: versionNumber}); + } order++; } SFPLogger.log(table.toString(), LoggerLevel.INFO, logger); diff --git a/packages/core/src/eventStream/build.ts b/packages/core/src/eventStream/build.ts new file mode 100644 index 000000000..74ae1d4a4 --- /dev/null +++ b/packages/core/src/eventStream/build.ts @@ -0,0 +1,264 @@ +import fs from 'fs'; +import { PROCESSNAME, PATH, EVENTTYPE, BuildProps, BuildHookSchema, BuildPackageDependencies } from './types'; +import SfpPackage from '../package/SfpPackage'; +import { HookService } from './hooks'; + +export class BuildStreamService { + public static buildPackageInitialitation(pck: string, reason: string, tag: string): void { + BuildLoggerBuilder.getInstance().buildPackageInitialitation(pck, reason, tag); + } + + public static sendPackageError(sfpPackage: SfpPackage, message: string, isEvent?: boolean): void { + const file = BuildLoggerBuilder.getInstance().buildPackageError(sfpPackage, message).build(); + if (!isEvent) HookService.getInstance().logEvent(file.payload.events[sfpPackage.package_name]); + } + + public static buildPackageErrorList(pck: string): void { + BuildLoggerBuilder.getInstance().buildPackageErrorList(pck); + } + + public static buildPackageSuccessList(pck: string): void { + BuildLoggerBuilder.getInstance().buildPackageSuccessList(pck); + } + + public static buildPackageAwaitingList(pck: string[]): void { + BuildLoggerBuilder.getInstance().buildPackageAwaitingList(pck); + } + + public static buildPackageCurrentlyProcessedList(pck: string[]): void { + BuildLoggerBuilder.getInstance().buildPackageCurrentlyProcessedList(pck); + } + + public static sendPackageCompletedInfos(sfpPackage: SfpPackage): void { + const file = BuildLoggerBuilder.getInstance().buildPackageCompletedInfos(sfpPackage).build(); + HookService.getInstance().logEvent(file.payload.events[sfpPackage.package_name]); + } + + public static buildPackageDependencies(pck: string, dependencies: BuildPackageDependencies): void { + BuildLoggerBuilder.getInstance().buildPackageDependencies(pck, dependencies); + } + + public static buildProps(props: BuildProps): void { + BuildLoggerBuilder.getInstance().buildProps(props); + } + + public static buildStatus(status: 'success' | 'failed' | 'inprogress', message: string): void { + BuildLoggerBuilder.getInstance().buildStatus(status, message); + } + + public static sendStatistics(scheduled: number, success: number, failed: number, elapsedTime: number): void { + const file = BuildLoggerBuilder.getInstance().buildStatistics(scheduled, success, failed, elapsedTime).build(); + } + + public static buildReleaseConfig(pcks: string[]): void { + BuildLoggerBuilder.getInstance().buildReleaseConfig(pcks); + } + + public static buildPackageStatus(pck: string, status: 'success' | 'inprogress', elapsedTime?: number): void { + BuildLoggerBuilder.getInstance().buildPackageStatus(pck, status, elapsedTime); + } + + public static buildJobAndOrgId(jobId: string, orgId: string, devhubAlias: string, commitId: string): void { + BuildLoggerBuilder.getInstance().buildOrgAndJobId(orgId, jobId, devhubAlias, commitId); + } + + public static writeArtifatcs(): void { + const file = BuildLoggerBuilder.getInstance().build(); + if (!fs.existsSync(PATH.DEFAULT)) { + fs.mkdirSync(PATH.DEFAULT); + } + if (!fs.existsSync(PATH.BUILD)) { + // File doesn't exist, create it + fs.writeFileSync(PATH.BUILD, JSON.stringify(file, null, 4), 'utf-8'); + } + } +} + +class BuildLoggerBuilder { + private file: BuildHookSchema; + private static instance: BuildLoggerBuilder; + + private constructor() { + this.file = { + payload: { + processName: PROCESSNAME.BUILD, + scheduled: 0, + success: 0, + failed: 0, + elapsedTime: 0, + status: 'inprogress', + message: '', + releaseConfig: [], + awaitingDependencies: [], + currentlyProcessed: [], + successfullyProcessed: [], + failedToProcess: [], + instanceUrl: '', + events: {}, + }, + eventType: 'sfpowerscripts.build', + jobId: '', + devhubAlias: '', + commitId: '', + }; + } + + public static getInstance(): BuildLoggerBuilder { + if (!BuildLoggerBuilder.instance) { + BuildLoggerBuilder.instance = new BuildLoggerBuilder(); + } + + return BuildLoggerBuilder.instance; + } + + buildOrgAndJobId(orgId: string, jobId: string, devhubAlias: string, commitId: string): BuildLoggerBuilder { + this.file.jobId = jobId; + this.file.payload.instanceUrl = orgId; + this.file.devhubAlias = devhubAlias; + this.file.commitId = commitId; + return this; + } + + buildPackageInitialitation(pck: string, reason: string, tag: string): BuildLoggerBuilder { + this.file.payload.events[pck] = { + event: 'sfpowerscripts.build.progress', + context: { + command: 'sfpowerscript:orchestrator:build', + eventId: `${this.file.jobId}_${Date.now().toString()}`, + jobId: this.file.jobId, + timestamp: new Date(), + instanceUrl: this.file.payload.instanceUrl, + branch: this.file.payload.buildProps.branch, + commitId: this.file.commitId, + devHubAlias: this.file.devhubAlias, + eventType: EVENTTYPE.BUILD, + }, + metadata: { + package: pck, + message: [], + elapsedTime: 0, + reasonToBuild: reason, + lastKnownTag: tag, + type: '', + versionNumber: '', + versionId: '', + testCoverage: 0, + coverageCheckPassed: false, + metadataCount: 0, + apexInPackage: false, + profilesInPackage: false, + sourceVersion: '', + packageDependencies: [], + }, + }; + HookService.getInstance().logEvent(this.file.payload.events[pck]); + return this; + } + + buildPackageCompletedInfos(sfpPackage: SfpPackage): BuildLoggerBuilder { + this.file.payload.events[sfpPackage.package_name].event = 'sfpowerscripts.build.success'; + this.file.payload.events[sfpPackage.package_name].metadata.type = sfpPackage.package_type; + this.file.payload.events[sfpPackage.package_name].metadata.versionNumber = sfpPackage.package_version_number; + this.file.payload.events[sfpPackage.package_name].metadata.versionId = sfpPackage.package_version_id; + this.file.payload.events[sfpPackage.package_name].metadata.testCoverage = sfpPackage.test_coverage; + this.file.payload.events[sfpPackage.package_name].metadata.coverageCheckPassed = + sfpPackage.has_passed_coverage_check; + this.file.payload.events[sfpPackage.package_name].metadata.metadataCount = sfpPackage.metadataCount; + this.file.payload.events[sfpPackage.package_name].metadata.apexInPackage = sfpPackage.isApexFound; + this.file.payload.events[sfpPackage.package_name].metadata.profilesInPackage = sfpPackage.isProfilesFound; + this.file.payload.events[sfpPackage.package_name].metadata.sourceVersion = sfpPackage.sourceVersion; + this.file.payload.events[sfpPackage.package_name].context.timestamp = new Date(); + return this; + } + + buildPackageError(sfpPackage: SfpPackage, message: string): BuildLoggerBuilder { + this.file.payload.events[sfpPackage.package_name].event = 'sfpowerscripts.build.failed'; + this.file.payload.events[sfpPackage.package_name].metadata.type = sfpPackage.package_type; + this.file.payload.events[sfpPackage.package_name].context.timestamp = new Date(); + if (message) { + this.file.payload.events[sfpPackage.package_name].metadata.message.push(message); + } + return this; + } + + buildPackageErrorList(pcks: string): BuildLoggerBuilder { + this.file.payload.failedToProcess.push(pcks); + return this; + } + + buildPackageSuccessList(pcks: string): BuildLoggerBuilder { + this.file.payload.successfullyProcessed.push(pcks); + return this; + } + + buildPackageAwaitingList(pcks: string[]): BuildLoggerBuilder { + this.file.payload.awaitingDependencies = pcks; + return this; + } + + buildPackageCurrentlyProcessedList(pcks: string[]): BuildLoggerBuilder { + this.file.payload.currentlyProcessed = pcks; + return this; + } + + buildPackageDependencies(pck: string, dependencies: BuildPackageDependencies): BuildLoggerBuilder { + this.file.payload.events[pck].metadata.packageDependencies.push(dependencies); + return this; + } + + buildProps(props: BuildProps): BuildLoggerBuilder { + this.file.payload.buildProps = { ...props }; + return this; + } + + buildStatus(status: 'inprogress' | 'success' | 'failed', message: string): BuildLoggerBuilder { + this.file.payload.status = status; + this.file.payload.message = message; + if (status === 'failed') { + Object.values(this.file.payload.events).forEach((value) => { + if ( + value.event === 'sfpowerscripts.build.awaiting' || + value.event === 'sfpowerscripts.build.progress' + ) { + value.metadata.message.push(message); + value.event = 'sfpowerscripts.build.failed'; + //HookService.getInstance().logEvent(this.file.payload.events[value.metadata.package]); + } + }); + } + return this; + } + + buildStatistics(scheduled: number, success: number, failed: number, elapsedTime: number): BuildLoggerBuilder { + this.file.payload.scheduled = success + failed; + this.file.payload.success = success; + this.file.payload.failed = failed; + this.file.payload.elapsedTime = elapsedTime; + this.file.payload.awaitingDependencies = []; + // set status to success when scheduled = success + if (this.file.payload.scheduled > 1 && this.file.payload.scheduled === this.file.payload.success) { + this.file.payload.status = 'success'; + } else { + this.file.payload.status = 'failed'; + } + return this; + } + + buildReleaseConfig(pcks: string[]): BuildLoggerBuilder { + this.file.payload.releaseConfig = pcks; + return this; + } + + buildPackageStatus(pck: string, status: 'success' | 'inprogress', elapsedTime?: number): BuildLoggerBuilder { + this.file.payload.events[pck].event = + status === 'success' ? 'sfpowerscripts.build.success' : 'sfpowerscripts.build.progress'; + if (elapsedTime) { + this.file.payload.events[pck].metadata.elapsedTime = elapsedTime; + } + return this; + } + + build(): BuildHookSchema { + return this.file; + } +} diff --git a/packages/core/src/eventStream/hooks.ts b/packages/core/src/eventStream/hooks.ts new file mode 100644 index 000000000..17625e56c --- /dev/null +++ b/packages/core/src/eventStream/hooks.ts @@ -0,0 +1,92 @@ +import axios from 'axios'; +import SFPLogger, { LoggerLevel, COLOR_TRACE, COLOR_WARNING } from '@dxatscale/sfp-logger'; +import SFPOrg from '../org/SFPOrg'; +import { SfPowerscriptsEvent__c } from './types'; +import 'dotenv/config' + +export class HookService { + private static instance: HookService; + + public static getInstance(): HookService { + if (!HookService.instance) { + HookService.instance = new HookService(); + } + return HookService.instance; + } + + public async logEvent(event: T) { + //###send webkooks### only when the env variables are set + if (process.env.EVENT_STREAM_WEBHOOK_URL) { + const axiosInstance = axios.create(); + axiosInstance.defaults.headers.common['Authorization'] = process.env.EVENT_STREAM_WEBHOOK_TOKEN; + axiosInstance.defaults.baseURL = process.env.EVENT_STREAM_WEBHOOK_URL; + // datetime not enough , so we need math.random to make it unique + const payload = { eventType: event['context']['eventType'], eventId: `${event['context']['eventId']}_${Math.floor(10000 + Math.random() * 90000)}`, payload: event }; + + + try { + const commitResponse = await axiosInstance.post(``, JSON.stringify(payload)); + + if (commitResponse.status === 201) { + SFPLogger.log(COLOR_TRACE(`Commit successful.`), LoggerLevel.TRACE); + } else { + SFPLogger.log( + COLOR_TRACE(`Commit failed. Status code: ${commitResponse.status}`), + LoggerLevel.TRACE + ); + } + } catch (error) { + SFPLogger.log(COLOR_TRACE(`An error happens for the webkook callout: ${error}`), LoggerLevel.INFO); + } + } + + if(!event['context']['devHubAlias'] && event['context']['jobId'].includes('NO_DEV_HUB_IMPL')){ + return; + } + + const sfpOrg = await SFPOrg.create({ + aliasOrUsername: event['context']['devHubAlias'], + }); + + const connection = sfpOrg.getConnection(); + + const sfpEvent: SfPowerscriptsEvent__c[] = [ + { + Name: `${event['context']['jobId']}-${event['metadata']['package']}`, + Command__c: event['context']['command'], + JobId__c: event['context']['jobId'], + Branch__c: event['context']['branch'], + Commit__c: event['context']['commitId'], + EventId__c: event['context']['eventId'], + InstanceUrl__c: event['context']['instanceUrl'], + JobTimestamp__c: event['context']['timestamp'], + EventName__c: event['event'], + Package__c: event['metadata']['package'], + ErrorMessage__c: event['metadata']['message'].length > 0 ? JSON.stringify(event['metadata']['message']) : '' + }, + ]; + + const upsertGitEvents = async () => { + try { + const result = await connection.sobject('SfPowerscriptsEvent__c').upsert(sfpEvent, 'Name'); + onResolved(result); + } catch (error) { + onReject(error); + } + }; + + const onResolved = (res) => { + SFPLogger.log(COLOR_TRACE('Upsert successful:', res), LoggerLevel.TRACE); + // Implement your custom logic here for resolved cases + }; + + const onReject = (err) => { + SFPLogger.log(COLOR_TRACE('Error:', err), LoggerLevel.TRACE); + SFPLogger.log(COLOR_WARNING('We cannot send the events to your DevHub. Please check that the package id 04t2o000001B1jzAAC is installed on DevHub and the username has the permissions.'), LoggerLevel.TRACE); + }; + + await upsertGitEvents() + .then(() => SFPLogger.log(COLOR_TRACE('Promise resolved successfully.'), LoggerLevel.TRACE)) + .catch((err) => SFPLogger.log(COLOR_TRACE('Promise rejected:', err), LoggerLevel.TRACE)); + } +} diff --git a/packages/core/src/eventStream/release.ts b/packages/core/src/eventStream/release.ts new file mode 100644 index 000000000..4cbdb66da --- /dev/null +++ b/packages/core/src/eventStream/release.ts @@ -0,0 +1,309 @@ +import { PROCESSNAME,PATH,EVENTTYPE, ReleaseHookSchema, ReleaseProps, ReleaseDeployError, ReleaseTestResult, ReleaseTestCoverage } from './types'; +import { HookService } from './hooks'; +import SfpPackage from '../package/SfpPackage'; +import fs from 'fs'; + +export class ReleaseStreamService { + public static buildPackageInitialitation( + pck: string, + targetVersion: string, + orgVersion: string, + type: string + ): void { + ReleaseLoggerBuilder.getInstance().buildPackageInitialitation(pck, targetVersion, orgVersion, type); + } + + public static buildProps(props: ReleaseProps): void { + ReleaseLoggerBuilder.getInstance().buildProps(props); + } + + public static buildStatus(message: string): void { + ReleaseLoggerBuilder.getInstance().buildCommandError(message); + } + + public static buildCommandError(message: string): void { + ReleaseLoggerBuilder.getInstance().buildCommandError(message); + } + + public static sendPackageError(pck: string, message: string): void { + const file = ReleaseLoggerBuilder.getInstance().buildPackageError(pck, message).build(); + HookService.getInstance().logEvent(file.payload.events[pck]); + //EventService.getInstance().logEvent(file.payload.events[pck]); + } + + public static sendPackageSuccess(sfpPackage: SfpPackage): void { + const file = ReleaseLoggerBuilder.getInstance().buildPackageCompleted(sfpPackage).build(); + //EventService.getInstance().logEvent(file.payload.events[sfpPackage.packageName]); + HookService.getInstance().logEvent(file.payload.events[sfpPackage.packageName]); + + } + + public static buildDeployErrorsMsg( + metadataType: string, + apiName: string, + problemType: string, + problem: string + ): void { + ReleaseLoggerBuilder.getInstance().buildDeployErrorsMsg({ + metadataType: metadataType, + apiName: apiName, + problemType: problemType, + problem: problem, + }); + } + + public static buildDeployErrorsPkg(pck: string): void { + ReleaseLoggerBuilder.getInstance().buildDeployErrorsPkg(pck); + } + + public static buildStatusProgress(sfpPackage: SfpPackage): void { + ReleaseLoggerBuilder.getInstance().buildStatusProgress(sfpPackage); + } + + public static buildTestResult(name: string, outcome: string, message: string, runtime: number): void { + ReleaseLoggerBuilder.getInstance().buildTestResult({name: name, outcome: outcome, message: message || 'N/A', runtime: runtime}); + } + + public static buildTestCoverage(cls: string, coverage: number): void { + ReleaseLoggerBuilder.getInstance().buildTestCoverage({class: cls, coverage: coverage}); + } + + public static buildTestSummary(key: string, message: string | number): void { + ReleaseLoggerBuilder.getInstance().buildTestSummary(key, message); + } + + public static buildStatistik(elapsedTime: number, failed: number, success: number, scheduled: number): void { + ReleaseLoggerBuilder.getInstance().buildStatistik(elapsedTime,failed,success, scheduled); + } + + public static buildOrgInfo(orgInfo: string, devHubAlias: string): void { + ReleaseLoggerBuilder.getInstance().buildOrgInfo(orgInfo, devHubAlias); + } + + public static buildJobandBranchId(jobId: string, branch: string): void { + ReleaseLoggerBuilder.getInstance().buildJobandBranchId(jobId,branch); + } + + public static writeArtifatcs(): void { + const file = ReleaseLoggerBuilder.getInstance().build(); + if (!fs.existsSync(PATH.DEFAULT)) { + fs.mkdirSync(PATH.DEFAULT); + } + if (!fs.existsSync(PATH.RELEASE)) { + // File doesn't exist, create it + fs.writeFileSync(PATH.RELEASE, JSON.stringify(file, null, 4), 'utf-8'); + } + } +} + + +class ReleaseLoggerBuilder { + private file: ReleaseHookSchema; + private static instance: ReleaseLoggerBuilder; + + private constructor() { + this.file = { + payload: { + processName: PROCESSNAME.VALIDATE, + scheduled: 0, + success: 0, + failed: 0, + elapsedTime: 0, + status: 'inprogress', + message: '', + releaseConfig: [], + instanceUrl: '', + events: {}, + }, + eventType: 'sfpowerscripts.release', + jobId: '', + devHubAlias: '', + branch: '', + }; + } + + public static getInstance(): ReleaseLoggerBuilder { + if (!ReleaseLoggerBuilder.instance) { + ReleaseLoggerBuilder.instance = new ReleaseLoggerBuilder(); + } + + return ReleaseLoggerBuilder.instance; + } + + buildPackageInitialitation( + pck: string, + targetVersion: string, + orgVersion: string, + type: string + ): ReleaseLoggerBuilder { + this.file.payload.events[pck] = { + event: 'sfpowerscripts.release.progress', + context: { + command: 'sfpowerscript:orchestrator:release', + instanceUrl: this.file.payload.instanceUrl, + timestamp: new Date(), + jobId: this.file.jobId, + eventId: `${this.file.jobId}_${Date.now().toString()}`, + eventType: EVENTTYPE.RELEASE, + branch: this.file.branch, + commitId: '', + devHubAlias: this.file.devHubAlias, + }, + metadata: { + package: pck, + message: [], + elapsedTime: 0, + reasonToBuild: '', + type: type, + targetVersion: targetVersion, + orgVersion: orgVersion, + versionId: '', + packageCoverage: 0, + coverageCheckPassed: false, + metadataCount: 0, + apexInPackage: false, + profilesInPackage: false, + permissionSetGroupInPackage: false, + isPayLoadContainTypesSupportedByProfiles: false, + isPickListsFound: false, + isDependencyValidated: false, + creationDetails: {}, + sourceVersion: '', + deployErrors: [], + testResults: [], + testCoverages: [], + testSummary: {}, + }, + orgId: '', + }; + HookService.getInstance().logEvent(this.file.payload.events[pck]); + return this; + } + + buildProps(props: ReleaseProps): ReleaseLoggerBuilder { + this.file.payload.releaseProps = props; + return this; + } + + buildOrgInfo(orgInfo: string, devHubAlias: string): ReleaseLoggerBuilder { + this.file.payload.instanceUrl = orgInfo; + this.file.devHubAlias = devHubAlias; + return this; + } + + buildJobandBranchId(jobId: string, branch: string): ReleaseLoggerBuilder { + this.file.jobId = jobId; + this.file.branch = branch; + return this; + } + + buildPackageError(pck: string, message: string): ReleaseLoggerBuilder { + this.file.payload.events[pck].event = 'sfpowerscripts.release.failed'; + this.file.payload.events[pck].context.timestamp = new Date(); + if (message) { + this.file.payload.events[pck].metadata.message.push(message); + } + return this; + } + + buildPackageCompleted(sfpPackage: SfpPackage): ReleaseLoggerBuilder { + this.file.payload.events[sfpPackage.packageName].event = 'sfpowerscripts.release.success'; + this.file.payload.events[sfpPackage.packageName].context.timestamp = new Date(); + this.file.payload.events[sfpPackage.packageName].metadata.apexInPackage = sfpPackage.isApexFound; + this.file.payload.events[sfpPackage.packageName].metadata.profilesInPackage = sfpPackage.isProfilesFound; + this.file.payload.events[sfpPackage.packageName].metadata.metadataCount = sfpPackage.metadataCount; + this.file.payload.events[sfpPackage.packageName].metadata.sourceVersion = sfpPackage.sourceVersion; + this.file.payload.events[sfpPackage.packageName].metadata.packageCoverage = sfpPackage.test_coverage; + this.file.payload.events[sfpPackage.packageName].metadata.coverageCheckPassed = sfpPackage.has_passed_coverage_check; + this.file.payload.events[sfpPackage.packageName].metadata.versionId = sfpPackage.package_version_id; + this.file.payload.events[sfpPackage.packageName].metadata.permissionSetGroupInPackage = sfpPackage.isPermissionSetGroupFound; + this.file.payload.events[sfpPackage.packageName].metadata.isPayLoadContainTypesSupportedByProfiles = sfpPackage.isPayLoadContainTypesSupportedByProfiles; + this.file.payload.events[sfpPackage.packageName].metadata.isPickListsFound = sfpPackage.isPickListsFound; + this.file.payload.events[sfpPackage.packageName].metadata.isDependencyValidated = sfpPackage.isDependencyValidated; + this.file.payload.events[sfpPackage.packageName].metadata.creationDetails = sfpPackage.creation_details; + return this; + } + + buildDeployErrorsMsg(deployError: ReleaseDeployError): ReleaseLoggerBuilder { + Object.values(this.file.payload.events).forEach((value) => { + if (value.event === 'sfpowerscripts.release.awaiting' || value.event === 'sfpowerscripts.release.failed') { + value.metadata.deployErrors.push(deployError); + } + }); + return this; + } + + buildDeployErrorsPkg(pck: string): ReleaseLoggerBuilder { + Object.values(this.file.payload.events).forEach((value) => { + if (value.event === 'sfpowerscripts.release.awaiting' || value.event === 'sfpowerscripts.release.progress' || value.event === 'sfpowerscripts.release.failed') { + for (const err of value.metadata.deployErrors) { + err.package = pck; + } + } + }); + return this; + } + + buildTestResult(testResult: ReleaseTestResult): ReleaseLoggerBuilder { + Object.values(this.file.payload.events).forEach((value) => { + if (value.event === 'sfpowerscripts.release.progress') { + value.metadata.testResults.push(testResult); + } + }); + return this; + } + + buildTestCoverage(testCoverage: ReleaseTestCoverage): ReleaseLoggerBuilder { + Object.values(this.file.payload.events).forEach((value) => { + if (value.event === 'sfpowerscripts.release.progress') { + value.metadata.testCoverages.push(testCoverage); + } + }); + return this; + } + + buildTestSummary(key: string, message: string | number): ReleaseLoggerBuilder { + Object.values(this.file.payload.events).forEach((value) => { + if (value.event === 'sfpowerscripts.release.progress') { + value.metadata.testSummary[key] = message; + } + }); + return this; + } + + buildStatistik(elapsedTime: number, failed: number, success: number, scheduled: number): ReleaseLoggerBuilder { + this.file.payload.elapsedTime = elapsedTime; + this.file.payload.status = this.file.payload.status === 'inprogress' ? 'success' : 'failed'; + this.file.payload.failed = failed; + this.file.payload.success = success; + this.file.payload.scheduled = scheduled; + return this; + } + + buildStatusProgress(sfpPackage: SfpPackage): ReleaseLoggerBuilder { + this.file.payload.events[sfpPackage.packageName].event = 'sfpowerscripts.release.progress'; + this.file.payload.events[sfpPackage.packageName].metadata.apexInPackage = sfpPackage.isApexFound; + this.file.payload.events[sfpPackage.packageName].metadata.profilesInPackage = sfpPackage.isProfilesFound; + this.file.payload.events[sfpPackage.packageName].metadata.metadataCount = sfpPackage.metadataCount; + this.file.payload.events[sfpPackage.packageName].metadata.sourceVersion = sfpPackage.sourceVersion; + this.file.payload.events[sfpPackage.packageName].metadata.packageCoverage = sfpPackage.test_coverage; + this.file.payload.events[sfpPackage.packageName].metadata.coverageCheckPassed = sfpPackage.has_passed_coverage_check; + this.file.payload.events[sfpPackage.packageName].metadata.versionId = sfpPackage.package_version_id; + this.file.payload.events[sfpPackage.packageName].metadata.permissionSetGroupInPackage = sfpPackage.isPermissionSetGroupFound; + this.file.payload.events[sfpPackage.packageName].metadata.isPayLoadContainTypesSupportedByProfiles = sfpPackage.isPayLoadContainTypesSupportedByProfiles; + this.file.payload.events[sfpPackage.packageName].metadata.isPickListsFound = sfpPackage.isPickListsFound; + this.file.payload.events[sfpPackage.packageName].metadata.isDependencyValidated = sfpPackage.isDependencyValidated; + this.file.payload.events[sfpPackage.packageName].metadata.creationDetails = sfpPackage.creation_details; + return this; + } + + buildCommandError(message: string): ReleaseLoggerBuilder { + this.file.payload.status = 'failed'; + this.file.payload.message = message; + return this; + } + + build(): ReleaseHookSchema { + return this.file; + } +} diff --git a/packages/core/src/eventStream/types.ts b/packages/core/src/eventStream/types.ts new file mode 100644 index 000000000..b9f6ac227 --- /dev/null +++ b/packages/core/src/eventStream/types.ts @@ -0,0 +1,446 @@ +import { Org } from "@salesforce/core"; +// default types for file logger + +export enum PROCESSNAME { + PREPARE = "prepare", + BUILD = "build", + VALIDATE = "validate", + RELEASE = "release", +} + +export enum PATH { + DEFAULT = ".sfpowerscripts", + PREPARE = ".sfpowerscripts/eventStreamPrepare.json", + BUILD = ".sfpowerscripts/eventStreamBuild.json", + VALIDATE = ".sfpowerscripts/eventStreamValidate.json", + RELEASE = ".sfpowerscripts/eventStreamRelease.json" +} + +export enum EVENTTYPE { + BUILD = "sfpowerscripts.build", + RELEASE = "sfpowerscripts.release", + VALIDATE = "sfpowerscripts.validate", + PREPARE = "sfpowerscripts.prepare" +} + + +export interface Context { + command: string; + eventId: string; + jobId: string; + instanceUrl: string; + timestamp: Date; + commitId: string; + branch: string; + devHubAlias: string; + eventType: string; + } + +// types for file logger prepare +export interface PrepareHookSchema { + eventType: string; + eventId: string; + payload: PrepareFile; +} +export interface PrepareFile { + processName: string; + success: number; + failed: number; + status: 'success' | 'failed' | 'inprogress'; + message: string; + errorCode: string; + poolDefinition: PoolDefinition; + poolInfo: Poolinfo; + externalDependencies: ExternalDependency[]; + releaseConfig?: string[]; +} + +export interface Poolinfo { + activeOrgs: number; + maxOrgs: number; + prepareDuration: number; + events: OrgDetails[]; +} + +export interface OrgDetails { + event: 'sfpowerscripts.prepare.success' | 'sfpowerscripts.prepare.failed'; + context: Context; + metadata: OrgInfo; + orgId: string; +} + +export interface OrgInfo { + alias: string; + orgId: string; + username: string; + loginURL: string; + elapsedTime: number; + password: string; + status?: 'success' | 'failed'; + message?: string; +} + +export interface ExternalDependency { + order: number; + pck: string; + version?: string; + subscriberVersionId: string; +} + +export interface PoolDefinition { + tag: string; + waitTime?: number; + expiry?: number; + maxAllocation: number; + batchSize?: number; + configFilePath?: string; + releaseConfigFile?: string; + succeedOnDeploymentErrors?: boolean; + installAll?: boolean; + enableVlocity?: boolean; + enableSourceTracking?: boolean; + relaxAllIPRanges?: boolean; + ipRangesToBeRelaxed?: string[]; + retryOnFailure?: boolean; + maxRetryCount?: number; + snapshotPool?: string; + postDeploymentScriptPath?: string; + preDependencyInstallationScriptPath?: string; + disableSourcePackageOverride?: boolean; + } + +// types for file logger build + +export interface BuildHookSchema { + eventType: string; + jobId: string; + devhubAlias: string; + commitId: string; + payload: BuildFile; +} + +export interface BuildFile { + processName: string; + scheduled: number; + success: number; + failed: number; + elapsedTime: number; + status: 'success' | 'failed' | 'inprogress'; + message: string; + buildProps?: BuildProps; + releaseConfig: string[]; + awaitingDependencies: string[]; + currentlyProcessed: string[]; + successfullyProcessed: string[]; + failedToProcess: string[]; + instanceUrl: string; + events: BuildPackage; +} + +export interface BuildPackage { + [key: string]: BuildPackageDetails +} + +export interface BuildPackageDetails { + event: 'sfpowerscripts.build.success' | 'sfpowerscripts.build.failed' | 'sfpowerscripts.build.progress' | 'sfpowerscripts.build.awaiting'; + context: Context; + metadata: BuildPackageMetadata; +} + +export interface BuildPackageDependencies { + order: number; + pck: string; + version: string; +} + + +export interface BuildPackageMetadata { + package: string; + message: string[]; + elapsedTime: number; + reasonToBuild: string; + lastKnownTag: string; + type: string; + versionNumber: string; + versionId: string; + testCoverage: number; + coverageCheckPassed: boolean; + metadataCount: number; + apexInPackage: boolean; + profilesInPackage: boolean; + sourceVersion?: string; + packageDependencies: BuildPackageDependencies[]; +} + +export interface BuildProps { + projectDirectory?: string; + devhubAlias?: string; + repourl?: string; + waitTime: number; + isQuickBuild: boolean; + isDiffCheckEnabled: boolean; + buildNumber: number; + executorcount: number; + isBuildAllAsSourcePackages: boolean; + branch?: string; + baseBranch?: string; + includeOnlyPackages?: string[]; +} + +// types for file logger validate + +export interface ValidateHookSchema { + eventType: string; + eventId: string; + payload: ValidateFile; +} + +export interface ValidateFile { + processName: string; + scheduled: number; + success: number; + failed: number; + elapsedTime: number; + status: 'success' | 'failed' | 'inprogress'; + message: string; + validateProps?: ValidateProps; + releaseConfig?: string[]; + events: ValidatePackage; +} + +export interface ValidatePackage { + [key: string]: ValidatePackageDetails +} + +export interface ValidatePackageDetails { + event: 'sfpowerscripts.validate.success' | 'sfpowerscripts.validate.failed' | 'sfpowerscripts.validate.awaiting' | 'sfpowerscripts.validate.progress'; + context: Context; + metadata: ValidatePackageMetadata; + orgId: string; +} + +export interface ValidatePackageMetadata { + package: string; + message: string[]; + elapsedTime: number; + reasonToBuild: string; + type: string; + targetVersion: string; + orgVersion: string; + versionId: string; + packageCoverage: number; + coverageCheckPassed: boolean; + metadataCount: number; + apexInPackage: boolean; + profilesInPackage: boolean; + permissionSetGroupInPackage: boolean; + isPayLoadContainTypesSupportedByProfiles: boolean; + isPickListsFound: boolean; + isDependencyValidated: boolean; + creationDetails: {[key: string]: number}; + sourceVersion?: string; + deployErrors: ValidateDeployError[]; + testResults: ValidateTestResult[]; + testCoverages: ValidateTestCoverage[]; + testSummary: ValidateTestSummary; +} + +export interface ValidateTestResult { + name: string; + outcome: string; + message: string; + runtime: number; +} + +export interface ValidateTestCoverage { + class: string; + coverage: number; +} + +export interface ValidateTestSummary { + [key: string]: string | number; +} + +export interface ValidateDeployError { + package?: string; + metadataType: string; + apiName: string; + problemType: string; + problem: string; +} + +export enum ValidateAgainst { + 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", +} + +export interface ValidateProps { + installExternalDependencies?: boolean; + validateAgainst: ValidateAgainst; + validationMode: ValidationMode; + releaseConfigPath?: string; + coverageThreshold: number; + logsGroupSymbol: string[]; + targetOrg?: string; + hubOrg?: Org; + pools?: string[]; + shapeFile?: string; + isDeleteScratchOrg?: boolean; + keys?: string; + baseBranch?: string; + isImpactAnalysis?: boolean; + isDependencyAnalysis?: boolean; + diffcheck?: boolean; + disableArtifactCommit?: boolean; + orgInfo?: boolean; + disableSourcePackageOverride?: boolean; + disableParallelTestExecution?: boolean; +} + +// types for file logger release + +export interface ReleaseHookSchema { + eventType: string; + jobId: string; + devHubAlias: string; + branch: string; + payload: ReleaseFile; +} + +export interface ReleaseFile { + processName: string; + scheduled: number; + success: number; + failed: number; + elapsedTime: number; + status: 'success' | 'failed' | 'inprogress'; + message: string; + releaseProps?: ReleaseProps; + releaseConfig?: string[]; + instanceUrl: string; + events: ReleasePackage; +} + +export interface ReleasePackage { + [key: string]: ReleasePackageDetails +} + +export interface ReleasePackageDetails { + event: 'sfpowerscripts.release.success' | 'sfpowerscripts.release.failed' | 'sfpowerscripts.release.awaiting' | 'sfpowerscripts.release.progress'; + context: Context; + metadata: ReleasePackageMetadata; + orgId: string; +} + +export interface ReleasePackageMetadata { + package: string; + message: string[]; + elapsedTime: number; + reasonToBuild: string; + type: string; + targetVersion: string; + orgVersion: string; + versionId: string; + packageCoverage: number; + coverageCheckPassed: boolean; + metadataCount: number; + apexInPackage: boolean; + profilesInPackage: boolean; + permissionSetGroupInPackage: boolean; + isPayLoadContainTypesSupportedByProfiles: boolean; + isPickListsFound: boolean; + isDependencyValidated: boolean; + creationDetails: {[key: string]: number}; + sourceVersion?: string; + deployErrors: ValidateDeployError[]; + testResults: ValidateTestResult[]; + testCoverages: ValidateTestCoverage[]; + testSummary: ValidateTestSummary; +} + +export interface ReleaseTestResult { + name: string; + outcome: string; + message: string; + runtime: number; +} + +export interface ReleaseTestCoverage { + class: string; + coverage: number; +} + +export interface ReleaseTestSummary { + [key: string]: string | number; +} + +export interface ReleaseDeployError { + package?: string; + metadataType: string; + apiName: string; + problemType: string; + problem: string; +} + + +export interface ReleaseProps { + releaseDefinitions: ReleaseDefinitionSchema[]; + targetOrg: string; + fetchArtifactScript: string; + isNpm: boolean; + scope: string; + npmrcPath: string; + logsGroupSymbol: string[]; + tags: any; + isDryRun: boolean; + waitTime: number; + keys: string; + isGenerateChangelog: boolean; + devhubUserName: string; + branch: string; + directory: string; +} + +export interface SfPowerscriptsEvent__c { + Name: string; + Command__c: string; + EventId__c: string; + JobId__c: string; + Branch__c: string; + Commit__c: string; + InstanceUrl__c: string; + JobTimestamp__c: Date; + EventName__c: string; + Package__c: string; + ErrorMessage__c: string; +} + +export default interface ReleaseDefinitionSchema { + release: string; + skipIfAlreadyInstalled: boolean; + skipArtifactUpdate:boolean; + baselineOrg?: string; + artifacts: { + [p: string]: string; + }; + packageDependencies?: { + [p: string]: string; + }; + promotePackagesBeforeDeploymentToOrg?: string; + changelog?: { + repoUrl?: string; + workItemFilter?:string; + workItemFilters?: string[]; + workItemUrl?: string; + limit?: number; + showAllArtifacts?: boolean; + }; +} + diff --git a/packages/core/src/package/packageCreators/CreateDataPackageImpl.ts b/packages/core/src/package/packageCreators/CreateDataPackageImpl.ts index 24998370d..68cd501d1 100644 --- a/packages/core/src/package/packageCreators/CreateDataPackageImpl.ts +++ b/packages/core/src/package/packageCreators/CreateDataPackageImpl.ts @@ -4,6 +4,7 @@ import FileSystem from '../../utils/FileSystem'; import { CreatePackage } from './CreatePackage'; import SfpPackage, { PackageType, SfpPackageParams } from '../SfpPackage'; import { PackageCreationParams } from '../SfpPackageBuilder'; +import { BuildStreamService } from '../../eventStream/build'; const SFDMU_CONFIG = 'export.json'; const VLOCITY_CONFIG = 'VlocityComponents.yaml'; @@ -62,6 +63,7 @@ export default class CreateDataPackageImpl extends CreatePackage { } if (isSfdmu && isVlocity) { + BuildStreamService.sendPackageError(this.sfpPackage,`Data package '${this.sfpPackage.packageName}' contains both SFDMU & Vlocity configuration`) throw new Error( `Data package '${this.sfpPackage.packageName}' contains both SFDMU & Vlocity configuration` ); @@ -78,6 +80,7 @@ export default class CreateDataPackageImpl extends CreatePackage { this.logger ); } else { + BuildStreamService.sendPackageError(this.sfpPackage,`Could not find export.json or VlocityComponents.yaml in ${packageDirectory}. sfpowerscripts only support vlocity or sfdmu based data packages`) throw new Error( `Could not find export.json or VlocityComponents.yaml in ${packageDirectory}. sfpowerscripts only support vlocity or sfdmu based data packages` ); diff --git a/packages/core/src/package/packageCreators/CreateDiffPackageImpl.ts b/packages/core/src/package/packageCreators/CreateDiffPackageImpl.ts index a0bacd531..b69a46b6c 100644 --- a/packages/core/src/package/packageCreators/CreateDiffPackageImpl.ts +++ b/packages/core/src/package/packageCreators/CreateDiffPackageImpl.ts @@ -16,6 +16,7 @@ import MetadataCount from '../components/MetadataCount'; import * as rimraf from 'rimraf'; import Component from '../../dependency/Component'; import PackageComponentDiff from '../diff/PackageComponentDiff'; +import { BuildStreamService } from '../../eventStream/build'; export default class CreateDiffPackageImp extends CreateSourcePackageImpl { public constructor( @@ -80,6 +81,7 @@ export default class CreateDiffPackageImp extends CreateSourcePackageImpl { } catch (error) { //if both are same after git resolution.. just do nothing, treat is a normal source package if (error.message.includes('Unable to compute diff, as both commits are same')) { + BuildStreamService.sendPackageError(this.sfpPackage,`Unable to compute diff, as both commits are same`) SFPLogger.log( `Both commits are same, treating it as an empty package`, LoggerLevel.WARN, @@ -88,7 +90,10 @@ export default class CreateDiffPackageImp extends CreateSourcePackageImpl { //Create an empty diff directory to force skip of packages const diffSrcDir = path.join(sfpPackage.workingDirectory, `diff/${sfpPackage.packageDirectory}`); fs.mkdirpSync(diffSrcDir); - } else throw error; + } else { + BuildStreamService.sendPackageError(this.sfpPackage,`Unable to create diff package`) + throw error; + } } await this.introspectDiffPackageCreated(sfpPackage, this.params, this.logger); diff --git a/packages/core/src/package/packageCreators/CreatePackage.ts b/packages/core/src/package/packageCreators/CreatePackage.ts index 8d9133dcd..67eaa8cac 100644 --- a/packages/core/src/package/packageCreators/CreatePackage.ts +++ b/packages/core/src/package/packageCreators/CreatePackage.ts @@ -2,6 +2,7 @@ import SFPLogger, { COLOR_HEADER, COLOR_KEY_MESSAGE, COLOR_WARNING, Logger, Logg import SFPStatsSender from '../../stats/SFPStatsSender'; import SfpPackage, { PackageType, SfpPackageParams } from '../SfpPackage'; import { PackageCreationParams } from '../SfpPackageBuilder'; +import { BuildStreamService } from '../../eventStream/build'; export abstract class CreatePackage { private startTime: number; @@ -20,7 +21,7 @@ export abstract class CreatePackage { public async exec(): Promise { //Capture Start TimegetSFDXProjectConfig this.startTime = Date.now(); - + BuildStreamService.buildPackageStatus(this.sfpPackage.package_name, 'inprogress'); //Print Header this.printHeader(); @@ -48,6 +49,7 @@ export abstract class CreatePackage { private sendMetricsWhenSuccessfullyCreated() { let elapsedTime = Date.now() - this.startTime; + BuildStreamService.buildPackageStatus(this.sfpPackage.package_name, 'success', elapsedTime); this.sfpPackage.creation_details = { creation_time: elapsedTime, @@ -82,20 +84,28 @@ export abstract class CreatePackage { if (packageDescriptor.assignPermSetsPreDeployment) { if (packageDescriptor.assignPermSetsPreDeployment instanceof Array) this.sfpPackage.assignPermSetsPreDeployment = packageDescriptor.assignPermSetsPreDeployment; - else throw new Error("Property 'assignPermSetsPreDeployment' must be of type array"); + else { + BuildStreamService.sendPackageError(this.sfpPackage, `Property 'assignPermSetsPreDeployment' must be of type array`); + throw new Error("Property 'assignPermSetsPreDeployment' must be of type array"); + } } if (packageDescriptor.assignPermSetsPostDeployment) { if (packageDescriptor.assignPermSetsPostDeployment instanceof Array) this.sfpPackage.assignPermSetsPostDeployment = packageDescriptor.assignPermSetsPostDeployment; - else throw new Error("Property 'assignPermSetsPostDeployment' must be of type array"); + else { + BuildStreamService.sendPackageError(this.sfpPackage, `Property 'assignPermSetsPostDeployment' must be of type array`); + throw new Error("Property 'assignPermSetsPostDeployment' must be of type array"); + } } } private async checkWhetherProvidedPackageIsEmpty(packageDirectory: string) { if (await this.isEmptyPackage(this.projectDirectory, packageDirectory)) { - if (this.packageCreationParams.breakBuildIfEmpty) + if (this.packageCreationParams.breakBuildIfEmpty){ + BuildStreamService.sendPackageError(this.sfpPackage, `Package directory ${packageDirectory} is empty`); throw new Error(`Package directory ${packageDirectory} is empty`); + } else this.printEmptyArtifactWarning(); } } diff --git a/packages/core/src/package/packageCreators/CreateUnlockedPackageImpl.ts b/packages/core/src/package/packageCreators/CreateUnlockedPackageImpl.ts index c70e44967..113fb2cab 100644 --- a/packages/core/src/package/packageCreators/CreateUnlockedPackageImpl.ts +++ b/packages/core/src/package/packageCreators/CreateUnlockedPackageImpl.ts @@ -15,6 +15,7 @@ import { PackageVersion, PackageVersionCreateRequestResult } from '@salesforce/p import { Duration } from '@salesforce/kit'; import PackageDependencyDisplayer from '../../display/PackageDependencyDisplayer'; const path = require('path'); +import { BuildStreamService } from '../../eventStream/build'; export default class CreateUnlockedPackageImpl extends CreatePackage { private static packageTypeInfos: PackageTypeInfo[]; @@ -60,6 +61,7 @@ export default class CreateUnlockedPackageImpl extends CreatePackage { LoggerLevel.WARN, this.logger ); + BuildStreamService.sendPackageError(this.sfpPackage,'Unable to find a package info for this particular package, Are you sure you created this package?') throw new Error('Unable to fetch Package Info'); } @@ -89,7 +91,7 @@ export default class CreateUnlockedPackageImpl extends CreatePackage { } //Print Dependencies - PackageDependencyDisplayer.printPackageDependencies(sfpPackage.dependencies,sfpPackage.projectConfig, this.logger); + PackageDependencyDisplayer.printPackageDependencies(sfpPackage.dependencies,sfpPackage.projectConfig, this.logger, this.sfpPackage.packageName); } @@ -147,6 +149,7 @@ export default class CreateUnlockedPackageImpl extends CreatePackage { errorMessage = 'Creation errors: '; for (let i = 0; i < errors.length; i++) { errorMessage += `\n${i + 1}) ${errors[i]}`; + BuildStreamService.sendPackageError(this.sfpPackage,errors[i],true) } } throw new Error(`Unable to create ${this.sfpPackage.packageName} due to \n` + errorMessage); @@ -160,6 +163,9 @@ export default class CreateUnlockedPackageImpl extends CreatePackage { sfpPackage.package_version_id = currentPackageCreationStatus.SubscriberPackageVersionId; await this.getPackageInfo(sfpPackage); } else { + BuildStreamService.sendPackageError(this.sfpPackage,`The build for ${this.sfpPackage.packageName} was not completed in the wait time ${this.packageCreationParams.waitTime} provided.${EOL} + You might want to increase the wait time or better check the dependencies or convert to different package type ${EOL} + Read more about it here https://docs.dxatscale.io/development-practices/types-of-packaging/unlocked-packages#build-options-with-unlocked-packages`) throw new Error( `The build for ${this.sfpPackage.packageName} was not completed in the wait time ${this.packageCreationParams.waitTime} provided.${EOL} You might want to increase the wait time or better check the dependencies or convert to different package type ${EOL} @@ -169,8 +175,10 @@ export default class CreateUnlockedPackageImpl extends CreatePackage { //Break if coverage is low if (this.packageCreationParams.isCoverageEnabled && !this.isOrgDependentPackage) { - if (!sfpPackage.has_passed_coverage_check) + if (!sfpPackage.has_passed_coverage_check){ + BuildStreamService.sendPackageError(this.sfpPackage,'This package has not meet the minimum coverage requirement of 75%') throw new Error('This package has not meet the minimum coverage requirement of 75%'); + } } } diff --git a/packages/core/src/package/packageInstallers/InstallUnlockedPackageImpl.ts b/packages/core/src/package/packageInstallers/InstallUnlockedPackageImpl.ts index fac5e2abf..47f24a95c 100644 --- a/packages/core/src/package/packageInstallers/InstallUnlockedPackageImpl.ts +++ b/packages/core/src/package/packageInstallers/InstallUnlockedPackageImpl.ts @@ -4,6 +4,7 @@ import SFPOrg from '../../org/SFPOrg'; import { PackageInstallCreateRequest, PackagingSObjects, SubscriberPackageVersion } from '@salesforce/packaging'; import { delay } from '../../utils/Delay'; import { SfpPackageInstallationOptions } from './InstallPackage'; +import { ReleaseStreamService } from '../../eventStream/release'; @@ -88,6 +89,7 @@ export default class InstallUnlockedPackageImpl { errorMessage = 'Installation errors: '; for (let i = 0; i < errors.length; i++) { errorMessage += `\n${i + 1}) ${errors[i].message}`; + ReleaseStreamService.buildDeployErrorsMsg(pkgName, '', 'Installation Error', errors[i].message); } } throw new Error(`Unable to install ${pkgName} due to \n` + errorMessage); diff --git a/packages/sfpowerscripts-cli/messages/build.json b/packages/sfpowerscripts-cli/messages/build.json index fddd4ee44..7a499fddb 100644 --- a/packages/sfpowerscripts-cli/messages/build.json +++ b/packages/sfpowerscripts-cli/messages/build.json @@ -14,5 +14,6 @@ "branchFlagDescription": "The git branch that this build is triggered on, Useful for metrics and general identification purposes", "tagFlagDescription": "Tag the build with a label, useful to identify in metrics", "logsGroupSymbolFlagDescription": "Symbol used by CICD platform to group/collapse logs in the console. Provide an opening group, and an optional closing group symbol.", - "releaseConfigFileFlagDescription":"Path to the release config file which determines what packages should be built" + "releaseConfigFileFlagDescription":"Path to the release config file which determines what packages should be built", + "jobIdFlagDescription":"The job id of the current build job, identifier for event streams and webhooks" } diff --git a/packages/sfpowerscripts-cli/messages/release.json b/packages/sfpowerscripts-cli/messages/release.json index dfe21fdc9..53241f70f 100644 --- a/packages/sfpowerscripts-cli/messages/release.json +++ b/packages/sfpowerscripts-cli/messages/release.json @@ -15,5 +15,6 @@ "allowUnpromotedPackagesFlagDescription": "Allow un-promoted packages to be installed in production", "devhubAliasFlagDescription": "Provide the alias of the devhub previously authenticated, default value is HubOrg", "directoryFlagDescription": "Relative path to directory to which the changelog should be generated, if the directory doesnt exist, it will be created", - "branchNameFlagDescription": "Repository branch in which the changelog files are located" + "branchNameFlagDescription": "Repository branch in which the changelog files are located", + "jobIdFlagDescription":"The job id of the current build job, identifier for event streams and webhooks" } diff --git a/packages/sfpowerscripts-cli/src/BuildBase.ts b/packages/sfpowerscripts-cli/src/BuildBase.ts index d6eae1e1b..c892938d4 100644 --- a/packages/sfpowerscripts-cli/src/BuildBase.ts +++ b/packages/sfpowerscripts-cli/src/BuildBase.ts @@ -25,6 +25,7 @@ import SfpPackage from '@dxatscale/sfpowerscripts.core/lib/package/SfpPackage'; import ReleaseConfig from './impl/release/ReleaseConfig'; import { Flags } from '@oclif/core'; import { loglevel, orgApiVersionFlagSfdxStyle, targetdevhubusername } from './flags/sfdxflags'; +import { BuildStreamService } from '@dxatscale/sfpowerscripts.core/lib/eventStream/build'; // Initialize Messages with the current plugin directory @@ -88,7 +89,11 @@ export default abstract class BuildBase extends SfpowerscriptsCommand { }), releaseconfig: Flags.string({ description: messages.getMessage('releaseConfigFileFlagDescription'), - }) + }), + jobid: Flags.string({ + char: 'j', + description: messages.getMessage('jobIdFlagDescription'), + }), }; public async execute() { @@ -147,6 +152,7 @@ export default abstract class BuildBase extends SfpowerscriptsCommand { SFPLogger.log(`${EOL}${EOL}`); SFPLogger.log(COLOR_INFO('No packages found to be built.. Exiting.. ')); SFPLogger.printHeaderLine('',COLOR_HEADER,LoggerLevel.INFO); + BuildStreamService.buildStatus('failed','No packages to be found to be built') return; } @@ -164,14 +170,17 @@ export default abstract class BuildBase extends SfpowerscriptsCommand { totalElapsedTime = Date.now() - executionStartTime; - if (artifactCreationErrors.length > 0 || buildExecResult.failedPackages.length > 0) + if (artifactCreationErrors.length > 0 || buildExecResult.failedPackages.length > 0){ + BuildStreamService.buildStatus('failed','Build Failed'); throw new Error('Build Failed'); + } SFPStatsSender.logGauge('build.duration', Date.now() - executionStartTime, tags); SFPStatsSender.logCount('build.succeeded', tags); } catch (error) { SFPStatsSender.logCount('build.failed', tags); + BuildStreamService.buildStatus('failed',error.toString()) SFPLogger.log(COLOR_ERROR(error)); process.exitCode = 1; } finally { @@ -226,8 +235,9 @@ export default abstract class BuildBase extends SfpowerscriptsCommand { buildResult['summary'].elapsed_time = totalElapsedTime; buildResult['summary'].succeeded = buildExecResult.generatedPackages.length; buildResult['summary'].failed = buildExecResult.failedPackages.length; + BuildStreamService.sendStatistics(buildResult['summary'].scheduled_packages,buildResult['summary'].succeeded,buildResult['summary'].failed,buildResult['summary'].elapsed_time) - fs.writeFileSync(`buildResult.json`, JSON.stringify(buildResult, null, 4)); + BuildStreamService.writeArtifatcs(); } } } @@ -236,6 +246,7 @@ export default abstract class BuildBase extends SfpowerscriptsCommand { if (releaseConfigFilePath) { let releaseConfig:ReleaseConfig = new ReleaseConfig(logger, releaseConfigFilePath); buildProps.includeOnlyPackages = releaseConfig.getPackagesAsPerReleaseConfig(); + BuildStreamService.buildReleaseConfig(buildProps.includeOnlyPackages); printIncludeOnlyPackages(buildProps.includeOnlyPackages); } return buildProps; diff --git a/packages/sfpowerscripts-cli/src/commands/orchestrator/build.ts b/packages/sfpowerscripts-cli/src/commands/orchestrator/build.ts index 0d2c20cff..f7dd1f3c3 100644 --- a/packages/sfpowerscripts-cli/src/commands/orchestrator/build.ts +++ b/packages/sfpowerscripts-cli/src/commands/orchestrator/build.ts @@ -27,6 +27,7 @@ export default class Build extends BuildBase { isDiffCheckEnabled: this.flags.diffcheck, buildNumber: this.flags.buildnumber, executorcount: this.flags.executorcount, + jobId: this.flags.jobid ?? `NO_DEV_HUB_IMPL_${Date.now().toString()}`, branch: this.flags.branch, currentStage: Stage.BUILD, isBuildAllAsSourcePackages: false, diff --git a/packages/sfpowerscripts-cli/src/commands/orchestrator/release.ts b/packages/sfpowerscripts-cli/src/commands/orchestrator/release.ts index 930184777..8b1136cd4 100644 --- a/packages/sfpowerscripts-cli/src/commands/orchestrator/release.ts +++ b/packages/sfpowerscripts-cli/src/commands/orchestrator/release.ts @@ -17,6 +17,7 @@ import SFPLogger, { import ReleaseDefinitionSchema from '../../impl/release/ReleaseDefinitionSchema'; import { arrayFlagSfdxStyle, loglevel, logsgroupsymbol, optionalDevHubFlag, requiredUserNameFlag } from '../../flags/sfdxflags'; import { Flags } from '@oclif/core'; +import { ReleaseStreamService } from '@dxatscale/sfpowerscripts.core/lib/eventStream/release'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@dxatscale/sfpowerscripts', 'release'); @@ -95,12 +96,18 @@ export default class Release extends SfpowerscriptsCommand { message: '--allowunpromotedpackages is deprecated, All packages are allowed', }, }), + jobid: Flags.string({ + char: 'j', + description: messages.getMessage('jobIdFlagDescription'), + dependsOn: ['devhubalias'], + }), devhubalias: optionalDevHubFlag, loglevel }; public async execute() { this.validateFlags(); + ReleaseStreamService.buildJobandBranchId(this.flags.jobid ?? `NO_DEV_HUB_IMPL_${Date.now().toString()}`,this.flags.branchname); let tags = { targetOrg: this.flags.targetorg, @@ -163,6 +170,8 @@ export default class Release extends SfpowerscriptsCommand { directory:this.flags.directory, }; + ReleaseStreamService.buildProps(props); + let releaseImpl: ReleaseImpl = new ReleaseImpl(props,new ConsoleLogger()); releaseResult = await releaseImpl.exec(); @@ -171,7 +180,11 @@ export default class Release extends SfpowerscriptsCommand { } catch (err) { if (err instanceof ReleaseError) { releaseResult = err.data; - } else SFPLogger.log(err.message); + ReleaseStreamService.buildCommandError(err.message); + } else { + ReleaseStreamService.buildCommandError(err.message); + SFPLogger.log(err.message) + }; if(!this.flags.dryrun) SFPStatsSender.logCount('release.failed', tags); @@ -210,6 +223,8 @@ export default class Release extends SfpowerscriptsCommand { packagesFailed += deploymentResults.result.failed.length; } + ReleaseStreamService.buildStatistik(totalElapsedTime, packagesFailed, packagesSucceeded, packagesScheduled); + SFPStatsSender.logGauge('release.packages.scheduled', packagesScheduled, tags); SFPStatsSender.logGauge('release.packages.succeeded', packagesSucceeded, tags); SFPStatsSender.logGauge('release.packages.failed', packagesFailed, tags); diff --git a/packages/sfpowerscripts-cli/src/impl/deploy/DeployImpl.ts b/packages/sfpowerscripts-cli/src/impl/deploy/DeployImpl.ts index 4e83da473..5afe846ce 100644 --- a/packages/sfpowerscripts-cli/src/impl/deploy/DeployImpl.ts +++ b/packages/sfpowerscripts-cli/src/impl/deploy/DeployImpl.ts @@ -27,6 +27,7 @@ import ReleaseConfig from '../release/ReleaseConfig'; import fs from 'fs-extra'; import { Align, getMarkdownTable } from 'markdown-table-ts'; import FileOutputHandler from '../../outputs/FileOutputHandler'; +import { ReleaseStreamService } from '@dxatscale/sfpowerscripts.core/lib/eventStream/release'; const Table = require('cli-table'); @@ -153,6 +154,7 @@ export default class DeployImpl { for (let i = 0; i < queue.length; i++) { let packageInfo = packagesToPackageInfo[queue[i].packageName]; let sfpPackage: SfpPackage = packageInfo.sfpPackage; + ReleaseStreamService.buildStatusProgress(sfpPackage); let packageType: string = sfpPackage.packageType; @@ -184,7 +186,16 @@ export default class DeployImpl { this.props.logger ); if (preHookStatus?.isToFailDeployment) { - failed = queue.slice(i).map((pkg) => packagesToPackageInfo[pkg.packageName]); + ReleaseStreamService.buildDeployErrorsPkg(sfpPackage.packageName); + failed = queue.slice(i).map((pkg) => { + ReleaseStreamService.sendPackageError( + pkg.packageName, + preHookStatus.message + ? preHookStatus.message + : 'Hook Failed to execute, but didnt provide proper message' + ); + return packagesToPackageInfo[pkg.packageName]; + }); throw new Error( preHookStatus.message ? preHookStatus.message @@ -268,7 +279,10 @@ export default class DeployImpl { } else if (packageInstallationResult.result === PackageInstallationStatus.Skipped) { continue; } else if (packageInstallationResult.result === PackageInstallationStatus.Failed) { - failed = queue.slice(i).map((pkg) => packagesToPackageInfo[pkg.packageName]); + ReleaseStreamService.buildDeployErrorsPkg(sfpPackage.packageName); + failed = queue.slice(i).map((pkg) => { + return packagesToPackageInfo[pkg.packageName] + }); } // Only deploy post hook when package installation is successful @@ -283,7 +297,16 @@ export default class DeployImpl { ); if (postHookStatus?.isToFailDeployment) { - failed = queue.slice(i).map((pkg) => packagesToPackageInfo[pkg.packageName]); + ReleaseStreamService.buildDeployErrorsPkg(sfpPackage.packageName); + failed = queue.slice(i).map((pkg) => { + ReleaseStreamService.sendPackageError( + pkg.packageName, + postHookStatus.message + ? postHookStatus.message + : 'Hook Failed to execute, but didnt provide proper message' + ); + return packagesToPackageInfo[pkg.packageName]; + }); throw new Error( postHookStatus.message ? postHookStatus.message @@ -293,10 +316,16 @@ export default class DeployImpl { } if (packageInstallationResult.result === PackageInstallationStatus.Failed) { - failed = queue.slice(i).map((pkg) => packagesToPackageInfo[pkg.packageName]); + ReleaseStreamService.buildDeployErrorsPkg(sfpPackage.packageName); + failed = queue.slice(i).map((pkg) => { + ReleaseStreamService.sendPackageError(pkg.packageName, packageInstallationResult.message); + return packagesToPackageInfo[pkg.packageName] + }); throw new Error(packageInstallationResult.message); } + ReleaseStreamService.sendPackageSuccess(packageInfo.sfpPackage); + groupSection.end(); } @@ -491,7 +520,7 @@ export default class DeployImpl { }); queue.forEach((pkg) => { - if (!packagesToPackageInfo[pkg.packageName].isPackageInstalled) + if (!packagesToPackageInfo[pkg.packageName].isPackageInstalled){ minTable.push([ COLOR_KEY_MESSAGE(pkg.packageName), COLOR_KEY_MESSAGE(pkg.versionNumber), @@ -499,6 +528,15 @@ export default class DeployImpl { ? COLOR_KEY_MESSAGE(packagesToPackageInfo[pkg.packageName].versionInstalledInOrg) : COLOR_KEY_MESSAGE('N/A'), ]); + ReleaseStreamService.buildPackageInitialitation( + pkg.packageName, + pkg.versionNumber, + packagesToPackageInfo[pkg.packageName].versionInstalledInOrg + ? COLOR_KEY_MESSAGE(packagesToPackageInfo[pkg.packageName].versionInstalledInOrg) + : COLOR_KEY_MESSAGE('N/A'), + pkg.package_type + ); + } }); SFPLogger.log(minTable.toString(), LoggerLevel.INFO, this.props.logger); groupSection.end(); @@ -610,6 +648,12 @@ export default class DeployImpl { queue.forEach((pkg) => { table.push([pkg.packageName, pkg.versionNumber]); + ReleaseStreamService.buildPackageInitialitation( + pkg.packageName, + pkg.versionNumber, + 'N/A', + pkg.package_type + ); }); SFPLogger.log(table.toString(), LoggerLevel.INFO, this.props.logger); groupSection.end(); diff --git a/packages/sfpowerscripts-cli/src/impl/parallelBuilder/BuildImpl.ts b/packages/sfpowerscripts-cli/src/impl/parallelBuilder/BuildImpl.ts index aa11d3f68..f7e48632a 100644 --- a/packages/sfpowerscripts-cli/src/impl/parallelBuilder/BuildImpl.ts +++ b/packages/sfpowerscripts-cli/src/impl/parallelBuilder/BuildImpl.ts @@ -37,6 +37,7 @@ import TransitiveDependencyResolver from "@dxatscale/sfpowerscripts.core/lib/pac import GroupConsoleLogs from "../../ui/GroupConsoleLogs"; import UserDefinedExternalDependency from "@dxatscale/sfpowerscripts.core/lib/project/UserDefinedExternalDependency"; import PackageDependencyDisplayer from "@dxatscale/sfpowerscripts.core/lib/display/PackageDependencyDisplayer"; +import { BuildStreamService } from '@dxatscale/sfpowerscripts.core/lib/eventStream/build'; const PRIORITY_UNLOCKED_PKG_WITH_DEPENDENCY = 1; const PRIORITY_UNLOCKED_PKG_WITHOUT_DEPENDENCY = 3; @@ -55,6 +56,7 @@ export interface BuildProps { buildNumber: number; executorcount: number; isBuildAllAsSourcePackages: boolean; + jobId?: string; branch?: string; currentStage: Stage; baseBranch?: string; @@ -106,11 +108,11 @@ export default class BuildImpl { aliasOrUsername: this.props.devhubAlias, }); - + BuildStreamService.buildProps(this.props); let git = await Git.initiateRepo(new ConsoleLogger()); this.repository_url = await git.getRemoteOriginUrl(this.props.repourl); this.commit_id = await git.getHeadCommit(); - + BuildStreamService.buildJobAndOrgId(this.props.jobId, this.sfpOrg?.getConnection().getAuthInfoFields().instanceUrl,this.props.devhubAlias,this.commit_id); this.packagesToBeBuilt = this.getPackagesToBeBuilt( this.props.projectDirectory, this.props.includeOnlyPackages, @@ -309,6 +311,7 @@ export default class BuildImpl { }); for (const pkg of this.packagesToBeBuilt) { let item = [pkg, "Activated as part of all package build"]; + BuildStreamService.buildPackageInitialitation(pkg,'Activated as part of all package build',''); if ( this.isMultiConfigFilesEnabled && this.props.currentStage == Stage.BUILD @@ -351,6 +354,7 @@ export default class BuildImpl { reason: packageDiffCheck.reason, tag: packageDiffCheck.tag, }); + BuildStreamService.buildPackageInitialitation(pkg,packageDiffCheck.reason,packageDiffCheck.tag); //Add Bundles if (buildCollections.isPackageInACollection(pkg)) { buildCollections @@ -360,6 +364,7 @@ export default class BuildImpl { packagesToBeBuilt.set(packageInCollection, { reason: "Part of a build collection", }); + BuildStreamService.buildPackageInitialitation(packageInCollection,'Part of a build collection',''); } }); } @@ -435,14 +440,34 @@ export default class BuildImpl { SFPLogger.log( `${EOL}Packages currently processed:{${this.packagesInQueue.length}} + ${this.packagesInQueue}`, ); + BuildStreamService.buildPackageCurrentlyProcessedList(this.packagesInQueue); SFPLogger.log( `Awaiting Dependencies to be resolved:{${this.packagesToBeBuilt.length}} + ${this.packagesToBeBuilt}`, ); + BuildStreamService.buildPackageAwaitingList(this.packagesToBeBuilt); } private handlePackageError(reason: any, pkg: string): any { SFPLogger.printHeaderLine('', COLOR_HEADER, LoggerLevel.INFO); SFPLogger.log(COLOR_ERROR(`Package Creation Failed for ${pkg}, Here are the details:`)); + const sfpPackageMain:SfpPackage = { + projectDirectory: this.props.projectDirectory, packageDirectory: pkg, + workingDirectory: "", + mdapiDir: "", + destructiveChangesPath: "", + resolvedPackageDirectory: "", + version: "", + packageName: "", + versionNumber: "", + packageType: "", + toJSON() { + return this; + }, + package_name: pkg + }; + + BuildStreamService.sendPackageError(sfpPackageMain,reason.message); + try { // Append error to log file fs.appendFileSync(`.sfpowerscripts/logs/${pkg}`, reason.message, "utf8"); @@ -458,6 +483,7 @@ export default class BuildImpl { SFPLogger.log(data); } catch (e) { + BuildStreamService.sendPackageError(sfpPackageMain,`Unable to display logs for pkg ${pkg}`); SFPLogger.log(`Unable to display logs for pkg ${pkg}`); } @@ -466,19 +492,39 @@ export default class BuildImpl { if (el == pkg) return false; else return true; }); + BuildStreamService.buildPackageAwaitingList(this.packagesToBeBuilt); this.packagesInQueue = this.packagesInQueue.filter((pkg_name) => { if (pkg == pkg_name) return false; else return true; }); + BuildStreamService.buildPackageCurrentlyProcessedList(this.packagesInQueue); //Remove myself and my childs this.failedPackages.push(pkg); + BuildStreamService.buildPackageErrorList(pkg); SFPStatsSender.logCount("build.failed.packages", { package: pkg }); this.packagesToBeBuilt = this.packagesToBeBuilt.filter((pkgBuild) => { if (this.childs[pkg].includes(pkgBuild)) { SFPStatsSender.logCount("build.failed.packages", { package: pkgBuild, }); + const sfpPackage:SfpPackage = { + projectDirectory: this.props.projectDirectory, packageDirectory: pkgBuild, + workingDirectory: "", + mdapiDir: "", + destructiveChangesPath: "", + resolvedPackageDirectory: "", + version: "", + packageName: "", + versionNumber: "", + packageType: "", + toJSON() { + return this; + }, + package_name: pkgBuild + }; + BuildStreamService.sendPackageError(sfpPackage,reason.message); + BuildStreamService.buildPackageErrorList(pkgBuild); this.failedPackages.push(pkgBuild); return false; } @@ -492,6 +538,8 @@ export default class BuildImpl { private queueChildPackages(sfpPackage: SfpPackage): any { this.packagesBuilt.push(sfpPackage.packageName); + BuildStreamService.buildPackageSuccessList(sfpPackage.packageName); + BuildStreamService.sendPackageCompletedInfos(sfpPackage); this.printPackageDetails(sfpPackage); this.packagesToBeBuilt.forEach((pkg) => { @@ -556,9 +604,11 @@ export default class BuildImpl { this.packagesToBeBuilt = this.packagesToBeBuilt.filter((el) => { return !pushedPackages.includes(el); }); + BuildStreamService.buildPackageAwaitingList(this.packagesToBeBuilt); this.packagesInQueue = this.packagesInQueue.filter( (pkg_name) => pkg_name !== sfpPackage.packageName, ); + BuildStreamService.buildPackageCurrentlyProcessedList(this.packagesInQueue); } private resolveDependenciesOnCompletedPackage( diff --git a/packages/sfpowerscripts-cli/src/impl/release/ReleaseImpl.ts b/packages/sfpowerscripts-cli/src/impl/release/ReleaseImpl.ts index dc7eddbff..032d312e2 100644 --- a/packages/sfpowerscripts-cli/src/impl/release/ReleaseImpl.ts +++ b/packages/sfpowerscripts-cli/src/impl/release/ReleaseImpl.ts @@ -13,6 +13,7 @@ import Package2Detail from '@dxatscale/sfpowerscripts.core/lib/package/Package2D import InstallUnlockedPackageCollection from '@dxatscale/sfpowerscripts.core/lib/package/packageInstallers/InstallUnlockedPackageCollection'; import FetchImpl from '../artifacts/FetchImpl'; import GroupConsoleLogs from '../../ui/GroupConsoleLogs'; +import { ReleaseStreamService } from '@dxatscale/sfpowerscripts.core/lib/eventStream/release'; export interface ReleaseProps { releaseDefinitions: ReleaseDefinitionSchema[]; @@ -279,6 +280,7 @@ export default class ReleaseImpl { externalPackage2s.push(dependendentPackage); } let sfpOrg = await SFPOrg.create({ aliasOrUsername: targetOrg }); + ReleaseStreamService.buildOrgInfo(sfpOrg.getConnection().getAuthInfoFields().instanceUrl, this.props.devhubUserName); let packageCollectionInstaller = new InstallUnlockedPackageCollection(sfpOrg, new ConsoleLogger(),this.props.isDryRun); await packageCollectionInstaller.install(externalPackage2s, true, true);