ci-sf-plugin
Version:
Set of commands making CI and dev's life easier.
268 lines • 13.3 kB
JavaScript
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import * as os from 'os';
import * as util from 'util';
import * as childProcess from 'child_process';
import { readFileSync, writeFileSync } from 'fs';
import { SfCommand, Flags, requiredHubFlagWithDeprecations } from '@salesforce/sf-plugins-core';
import { Messages, SfError } from '@salesforce/core';
import { ux } from '@oclif/core';
// promisify child process
const execSync = util.promisify(childProcess.exec);
// Initialize Messages with the current plugin directory
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
// Load the specific messages for this file. Messages from @salesforce/command, @salesforce/core,
// or any library that is using the messages framework can also be loaded this way.
const messages = Messages.loadMessages('ci-sf-plugin', 'org.create');
export default class Create extends SfCommand {
static description = messages.getMessage('commandDescription');
static examples = messages.getMessage('examples').split(os.EOL);
static flags = {
'target-dev-hub': requiredHubFlagWithDeprecations,
// flag with a value (-n, --name=VALUE)
'set-alias': Flags.string({
char: 'a',
description: messages.getMessage('setaliasFlagDescription')
}),
'duration-days': Flags.integer({
char: 'd',
default: 30,
description: messages.getMessage('durationdaysFlagDescription')
}),
'definition-file': Flags.string({
char: 'f',
default: 'config/project-scratch-def.json',
description: messages.getMessage('definitionfileFlagDescription')
}),
'ci': Flags.boolean({
default: false,
description: messages.getMessage('ciFlagDescription')
}),
'validation-timeout': Flags.integer({
char: 't',
default: 30,
description: messages.getMessage('validationtimeoutFlagDescription')
}),
'ci-config-file': Flags.file({
char: 'x',
default: 'ciconfig.json',
description: messages.getMessage('ciconfigfileFlagDescription')
}),
'wait': Flags.integer({
char: 'w',
default: 60,
description: messages.getMessage('waitFlagDescription')
})
};
// Set this to true if your command requires a project workspace; 'requiresProject' is false by default
static requiresProject = false;
async run() {
try {
const { flags } = await this.parse(Create); // fetch needed flags
const targetDevHubUsername = flags['target-dev-hub'].getUsername();
const setAliasFlag = flags['set-alias'];
const durationDaysFlag = flags['duration-days'];
const definitionFileFlag = flags['definition-file'];
const ciFlag = flags['ci'];
const validationTimeoutFlag = flags['validation-timeout'];
const ciConfigFileFlag = flags['ci-config-file'];
const waitFlag = flags['wait'];
const setAliasStr = setAliasFlag ? `--alias ${setAliasFlag}` : ''; // alias is optional
ux.action.start(messages.getMessage('infoCreatingNewScratchOrg', [targetDevHubUsername]), 'in progress', { stdout: true });
// load project configuration
const config = JSON.parse(readFileSync(ciConfigFileFlag, 'utf-8'));
if (!config.projectName) {
throw new Error(`'projectName' property is missing in the '${ciConfigFileFlag}' file.`);
}
const output = { stdout: [], stderr: [] };
// definition file can be different for CI environment
const definitionFilePath = ciFlag ? prepareDefinitionFileForCi(definitionFileFlag, config) : definitionFileFlag;
let username = '';
// e.g. for CI environment we need to have 100% valid org which does not always happen on the first try
do {
// build the command, with --json to see details
const createStr = `sf org create scratch ${setAliasStr} --duration-days ${durationDaysFlag} --definition-file ${definitionFilePath} --set-default --target-dev-hub ${targetDevHubUsername} --wait ${waitFlag} --json`;
// execute and print output
const createPromise = execSync(createStr, { encoding: 'utf8', maxBuffer: 1024 * 1024 });
const createOutput = await createPromise;
username = JSON.parse(createOutput.stdout).result?.username;
process.stdout.write(`Created scratch org with username '${username}'.\n`);
output.stdout.push(createOutput.stdout);
output.stderr.push(createOutput.stderr);
} while (ciFlag && await isCreatedSratchOrgCorrupted(username, validationTimeoutFlag, config, output));
// update user values to prevent errors such as 'FIELD_INTEGRITY_EXCEPTION, There's a problem with this country, even though it may appear correct. Please select a country/territory from the list of valid countries.: Country: [Country]'
const updateValuesStr = `sf data update record --sobject User --where "Username=${username}" --values "Country='Czechia' City='Prague' TimeZoneSidKey='Europe/Prague'" --target-org ${username}`;
const updateValuesPromise = execSync(updateValuesStr, { encoding: 'utf8', maxBuffer: 1024 * 1024 });
updateValuesPromise.child.stdout.on('data', (data) => {
if (data?.trim()) {
process.stdout.write(data);
}
});
updateValuesPromise.child.stderr.on('data', (data) => {
if (data?.trim() && data.trim() !== 'Success') {
process.stderr.write(`Update and complete fields of '${username}': ${data.trim()}`);
}
});
const updateValuesOutput = await updateValuesPromise;
output.stdout.push(updateValuesOutput.stdout);
output.stderr.push(updateValuesOutput.stderr);
printManualSteps(ciFlag, config);
ux.action.stop('done');
// Return an object
return { output };
}
catch (error) {
ux.action.stop('failed');
throw new SfError(messages.getMessage('errorCreationFailed', [JSON.stringify(error, null, 2)]));
}
}
}
/*
* Load existing definition file, add extra elements based on ciconfig.json file and store this to a new file.
*
* @param definitionfilePath original def file path
* @param config holding extra configuration
*
* @return a new definition file path
*/
function prepareDefinitionFileForCi(definitionfilePath, config) {
// is there any special configuration at all?
if (!config.hasOwnProperty('postCreateManualSteps')) {
return definitionfilePath;
}
// prepare special def file
const definitionfilePathNew = definitionfilePath.slice(0, -5) + '_ci.json'; // new filename
// load original file
const definitionfileNew = JSON.parse(readFileSync(definitionfilePath, 'utf-8'));
const stepsConfig = config['postCreateManualSteps'];
// update def file features
if (Array.isArray(stepsConfig.features)) {
if (!Array.isArray(definitionfileNew.features)) {
definitionfileNew.features = [];
}
stepsConfig.features.forEach(feature => {
if (feature.name) {
definitionfileNew.features.push(feature.name);
}
});
}
// update def file settings
if (stepsConfig.settings) {
if (definitionfileNew.settings === null || typeof definitionfileNew.settings !== 'object' || Array.isArray(definitionfileNew.settings)) {
definitionfileNew.settings = {};
}
for (const [key, value] of Object.entries(stepsConfig.settings)) {
if (definitionfileNew.settings.hasOwnProperty(key)) {
Object.assign(definitionfileNew.settings[key], value);
}
else {
definitionfileNew.settings[key] = value;
}
}
}
// create updated file
writeFileSync(definitionfilePathNew, JSON.stringify(definitionfileNew));
return definitionfilePathNew;
}
/*
* Checks newly created scratch org for all required features whether these were enabled properly.
* Deletes corrupted scratch org if needed.
*
* @param username of a new scratch org
* @param validationTimeout seconds to wait before org validation starts
* @param config holding extra configuration
* @param output shared object = { stdout: [], stderr: [] };
*
* @return true for corrupted scratch org, false otherwise
*/
async function isCreatedSratchOrgCorrupted(username, validationTimeout, config, output) {
// do we have anything to check at all?
if (!config.hasOwnProperty('postCreateManualSteps') || !config['postCreateManualSteps'].apexOrgVerification) {
return false;
}
process.stdout.write(`Waiting ${validationTimeout} seconds before org validation starts...\n`);
await new Promise((f) => setTimeout(f, validationTimeout * 1000)); // wait for features
// check required features in the new scratch org
let errorMessage = '';
const commandApexStr = `sf apex run --file ${config['postCreateManualSteps'].apexOrgVerification} --target-org ${username}`;
// execute and print output
const commandApexPromise = execSync(commandApexStr, { encoding: 'utf8', maxBuffer: 1024 * 1024 });
const sfLogUselessLinesKeywords = ['|HEAP_ALLOCATE|', '_STARTED', '_FINISHED', 'LIMIT_', 'out of', 'Compiled'];
commandApexPromise.child.stdout.on('data', (data) => {
if (data?.trim()) {
const dataOutput = [];
data.trim().split('\n').filter(line => {
return line.trim() !== '' && !sfLogUselessLinesKeywords.some(keyword => line.includes(keyword));
}).forEach(line => {
if (line.startsWith('Error')) { // apex code failure
errorMessage += line + '\n';
}
dataOutput.push('\t' + line);
});
process.stdout.write(dataOutput.join('\n') + '\nValidation of enabled features has failed, scratch org to be deleted...\n'); // prints simplified SF console log
}
});
commandApexPromise.child.stderr.on('data', (data) => {
if (data?.trim()) {
// nothing to do
}
});
const commandApexOutput = await commandApexPromise;
output.stdout.push(commandApexOutput.stdout);
output.stderr.push(commandApexOutput.stderr);
// org validity check results
if (errorMessage) {
// delete corrupted scratch org
const commandOrgDeleteStr = `sf org delete scratch --target-org ${username} --no-prompt`;
// execute and print output
const commandOrgDeletePromise = execSync(commandOrgDeleteStr, { encoding: 'utf8', maxBuffer: 1024 * 1024 });
commandOrgDeletePromise.child.stdout.on('data', (data) => {
if (data?.trim()) {
process.stdout.write(data);
}
});
commandOrgDeletePromise.child.stderr.on('data', (data) => {
if (data?.trim()) {
process.stderr.write(data.trim());
}
});
const commandOrgDeleteOutput = await commandOrgDeletePromise;
output.stdout.push(commandOrgDeleteOutput.stdout);
output.stderr.push(commandOrgDeleteOutput.stderr);
return true; // corrupted org which has been automatically marked as deleted too
}
else {
return false; // org successfully validated
}
}
/*
* Check if there are any manual steps in ciconfig.json to be announced to the CLI. Does not apply for CI environment.
*
* @param ci flag identifies CI environment
* @param config holding extra configuration
*/
function printManualSteps(ci, config) {
if (!ci && config.hasOwnProperty('postCreateManualSteps')) {
const stepsConfig = config['postCreateManualSteps'];
const stepsUserInstructions = [];
if (Array.isArray(stepsConfig.features)) {
stepsConfig.features.forEach(feature => {
if (feature.manualStepDescription) {
stepsUserInstructions.push(feature.manualStepDescription);
}
});
}
if (stepsConfig.continueInstruction) {
stepsUserInstructions.push('\n' + stepsConfig.continueInstruction);
}
// make the output nice
if (stepsUserInstructions.length > 0) {
process.stdout.write(`###################################################################################\n\x1b[35m############################### Manual Steps Needed ###############################\x1b[0m\n###################################################################################\n${stepsUserInstructions.join('\n')}\n###################################################################################`);
}
}
}
//# sourceMappingURL=create.js.map