UNPKG

ci-sf-plugin

Version:

Set of commands making CI and dev's life easier.

268 lines 13.3 kB
/* * 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