Skip to content
This repository has been archived by the owner on Mar 18, 2024. It is now read-only.

feat(events): new event stream for build command #1446

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/core/src/display/DeployErrorDisplayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
};

Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/display/PackageDependencyDisplayer.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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);
Expand Down
264 changes: 264 additions & 0 deletions packages/core/src/eventStream/build.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
92 changes: 92 additions & 0 deletions packages/core/src/eventStream/hooks.ts
Original file line number Diff line number Diff line change
@@ -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<T> {
private static instance: HookService<any>;

public static getInstance(): HookService<any> {
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));
}
}
Loading