sfdx-hardis
Version:
Swiss-army-knife Toolbox for Salesforce. Allows you to define a complete CD/CD Pipeline. Orchestrate base commands and assist users with interactive wizards
159 lines • 8.6 kB
JavaScript
/* jscpd:ignore-start */
import { spawn } from 'child_process';
import c from 'chalk';
import which from 'which';
import { SfCommand, Flags, requiredHubFlagWithDeprecations } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';
import { addScratchOrgToPool, getPoolStorage, setPoolStorage } from '../../../../common/utils/poolUtils.js';
import { getConfig } from '../../../../config/index.js';
import { execCommand, stripAnsi, uxLog } from '../../../../common/utils/index.js';
import moment from 'moment';
import { authenticateWithSfdxUrlStore } from '../../../../common/utils/orgUtils.js';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('sfdx-hardis', 'org');
export default class ScratchPoolRefresh extends SfCommand {
static title = 'Refresh scratch org pool';
static description = 'Create enough scratch orgs to fill the pool';
static examples = ['$ sf hardis:scratch:pool:refresh'];
// public static args = [{name: 'file'}];
static flags = {
debug: Flags.boolean({
char: 'd',
default: false,
description: messages.getMessage('debugMode'),
}),
websocket: Flags.string({
description: messages.getMessage('websocket'),
}),
skipauth: Flags.boolean({
description: 'Skip authentication check when a default username is required',
}),
'target-dev-hub': requiredHubFlagWithDeprecations,
};
// Set this to true if your command requires a project workspace; 'requiresProject' is false by default
static requiresProject = true;
debugMode = false;
async run() {
const { flags } = await this.parse(ScratchPoolRefresh);
this.debugMode = flags.debug || false;
// Check pool configuration is defined on project
const config = await getConfig('project');
if (config.poolConfig == null) {
uxLog(this, c.yellow('Configuration file must contain a poolConfig property') +
'\n' +
c.grey(JSON.stringify(config, null, 2)));
return { outputString: 'Configuration file must contain a poolConfig property' };
}
const maxScratchOrgsNumber = config.poolConfig.maxScratchOrgsNumber || 5;
const maxScratchOrgsNumberToCreateOnce = config.poolConfig.maxScratchOrgsNumberToCreateOnce || 10;
uxLog(this, c.grey('Pool config: ' + JSON.stringify(config.poolConfig)));
// Get pool storage
const poolStorage = await getPoolStorage({
devHubConn: flags['target-dev-hub'].getConnection(),
devHubUsername: flags['target-dev-hub'].getUsername(),
});
let scratchOrgs = poolStorage.scratchOrgs || [];
/* jscpd:ignore-end */
// Clean expired orgs
const minScratchOrgRemainingDays = config.poolConfig.minScratchOrgRemainingDays || 25;
const scratchOrgsToDelete = [];
scratchOrgs = scratchOrgs.filter((scratchOrg) => {
const expiration = moment(scratchOrg?.authFileJson?.result?.expirationDate);
const today = moment();
const daysBeforeExpiration = expiration.diff(today, 'days');
if (daysBeforeExpiration < minScratchOrgRemainingDays) {
scratchOrg.daysBeforeExpiration = daysBeforeExpiration;
scratchOrgsToDelete.push(scratchOrg);
uxLog(this, c.grey(`Scratch org ${scratchOrg?.authFileJson?.result?.instanceUrl} will be deleted as it has only ${daysBeforeExpiration} remaining days (expiration on ${scratchOrg?.authFileJson?.result?.expirationDate})`));
return false;
}
uxLog(this, c.grey(`Scratch org ${scratchOrg?.authFileJson?.result?.instanceUrl} will be kept as it still has ${daysBeforeExpiration} remaining days (expiration on ${scratchOrg?.authFileJson?.result?.expirationDate})`));
return true;
});
// Delete expired orgs and update pool if found
if (scratchOrgsToDelete.length > 0) {
poolStorage.scratchOrgs = scratchOrgs;
await setPoolStorage(poolStorage, {
devHubConn: flags['target-dev-hub'].getConnection(),
devHubUsername: flags['target-dev-hub'].getUsername(),
});
for (const scratchOrgToDelete of scratchOrgsToDelete) {
// Authenticate to scratch org to delete
await authenticateWithSfdxUrlStore(scratchOrgToDelete);
// Delete scratch org
const deleteCommand = `sf org delete scratch --no-prompt --target-org ${scratchOrgToDelete.scratchOrgUsername}`;
await execCommand(deleteCommand, this, { fail: false, debug: this.debugMode, output: true });
uxLog(this, c.cyan(`Scratch org ${c.green(scratchOrgToDelete.scratchOrgUsername)} at ${scratchOrgToDelete?.authFileJson?.result?.instanceUrl} has been deleted because only ${scratchOrgToDelete.daysBeforeExpiration} days were remaining.`));
}
}
// Create new scratch orgs
const numberOfOrgsToCreate = Math.min(maxScratchOrgsNumber - scratchOrgs.length, maxScratchOrgsNumberToCreateOnce);
uxLog(this, c.cyan('Creating ' + numberOfOrgsToCreate + ' scratch orgs...'));
let numberCreated = 0;
let numberfailed = 0;
const subProcesses = [];
for (let i = 0; i < numberOfOrgsToCreate; i++) {
// eslint-disable-next-line no-async-promise-executor
const spawnPromise = new Promise(async (resolve) => {
// Run scratch:create command asynchronously
const commandArgs = ['hardis:scratch:create', '--pool', '--json'];
const sfdxPath = await which('sf');
const child = spawn(sfdxPath || 'sf', commandArgs, { cwd: process.cwd(), env: process.env });
uxLog(this, '[pool] ' + c.grey(`hardis:scratch:create (${i}) started`));
// handle errors
child.on('error', (err) => {
resolve({ code: 1, result: { error: err } });
});
// Store data
let stdout = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
if (this.debugMode === true) {
uxLog(this, data.toString());
}
});
// Handle end of command
child.on('close', async (code) => {
const colorFunc = code === 0 ? c.green : c.red;
uxLog(this, '[pool] ' + colorFunc(`hardis:scratch:create (${i}) exited with code ${c.bold(code)}`));
if (code !== 0) {
uxLog(this, `Return code is not 0 (${i}): ` + c.grey(stdout));
numberfailed++;
}
else {
numberCreated++;
}
let result = {};
stdout = stripAnsi(stdout);
try {
result = JSON.parse(stdout);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
}
catch (e) {
result = { result: { status: 1, rawLog: stdout } };
uxLog(this, c.yellow(`Error parsing stdout (${i}): ` + stdout));
}
await addScratchOrgToPool(result.result || result);
resolve({ code, result: result });
});
});
subProcesses.push(spawnPromise);
}
// Await parallel scratch org creations are completed
const createResults = await Promise.all(subProcesses);
if (this.debugMode) {
uxLog(this, c.grey('Create results: \n' + JSON.stringify(createResults, null, 2)));
}
const colorFunc = numberCreated === numberOfOrgsToCreate ? c.green : numberCreated === 0 ? c.red : c.yellow;
uxLog(this, '[pool] ' +
colorFunc(`Created ${c.bold(numberCreated)} scratch orgs (${c.bold(numberfailed)} creations(s) failed)`));
// Return an object to be displayed with --json
return {
outputString: 'Refreshed scratch orgs pool',
createResults: createResults,
numberCreated: numberCreated,
numberFailed: numberfailed,
};
}
}
//# sourceMappingURL=refresh.js.map