UNPKG

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

378 lines (377 loc) • 20.1 kB
/* jscpd:ignore-start */ import { SfCommand, Flags, requiredHubFlagWithDeprecations } from '@salesforce/sf-plugins-core'; import { AuthInfo, Messages, SfError } from '@salesforce/core'; import c from 'chalk'; import { assert } from 'console'; import fs from 'fs-extra'; import moment from 'moment'; import * as os from 'os'; import * as path from 'path'; import { clearCache } from '../../../common/cache/index.js'; import { elapseEnd, elapseStart, execCommand, execSfdxJson, getCurrentGitBranch, isCI, uxLog, } from '../../../common/utils/index.js'; import { initApexScripts, initOrgData, initOrgMetadatas, initPermissionSetAssignments, installPackages, promptUserEmail, } from '../../../common/utils/orgUtils.js'; import { addScratchOrgToPool, fetchScratchOrg } from '../../../common/utils/poolUtils.js'; import { prompts } from '../../../common/utils/prompts.js'; import { WebSocketClient } from '../../../common/websocketClient.js'; import { getConfig, setConfig } from '../../../config/index.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('sfdx-hardis', 'org'); export default class ScratchCreate extends SfCommand { static title = 'Create and initialize scratch org'; static description = `Create and initialize a scratch org or a source-tracked sandbox (config can be defined using \`config/.sfdx-hardis.yml\`): - **Install packages** - Use property \`installedPackages\` - **Push sources** - **Assign permission sets** - Use property \`initPermissionSets\` - **Run apex initialization scripts** - Use property \`scratchOrgInitApexScripts\` - **Load data** - Use property \`dataPackages\` `; static examples = ['$ sf hardis:scratch:create']; // public static args = [{name: 'file'}]; static flags = { forcenew: Flags.boolean({ char: 'n', default: false, description: messages.getMessage('forceNewScratch'), }), pool: Flags.boolean({ default: false, description: 'Creates the scratch org for a scratch org pool', }), 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; // List required plugins, their presence will be tested before running the command static requiresSfdxPlugins = ['sfdmu', 'texei-sfdx-plugin']; forceNew = false; /* jscpd:ignore-end */ debugMode = false; pool = false; configInfo; devHubAlias; scratchOrgAlias; scratchOrgDuration; userEmail; gitBranch; projectScratchDef; scratchOrgInfo; scratchOrgUsername; scratchOrgPassword; scratchOrgSfdxAuthUrl; authFileJson; projectName; scratchOrgFromPool; async run() { const { flags } = await this.parse(ScratchCreate); this.pool = flags.pool || false; this.debugMode = flags.debug || false; this.forceNew = flags.forcenew || false; elapseStart(`Create and initialize scratch org`); await this.initConfig(); await this.createScratchOrg(flags); try { await this.updateScratchOrgUser(); await installPackages(this.configInfo.installedPackages || [], this.scratchOrgAlias); if (this.pool === false) { await initOrgMetadatas(this.configInfo, this.scratchOrgUsername, this.scratchOrgAlias, this.projectScratchDef, this.debugMode, { scratch: true, }); await initPermissionSetAssignments(this.configInfo.initPermissionSets || [], this.scratchOrgUsername); await initApexScripts(this.configInfo.scratchOrgInitApexScripts || [], this.scratchOrgUsername); await initOrgData(path.join('.', 'scripts', 'data', 'ScratchInit'), this.scratchOrgUsername); } } catch (e) { elapseEnd(`Create and initialize scratch org`); uxLog(this, c.grey('Error: ' + e.message + '\n' + e.stack)); if (isCI && this.scratchOrgFromPool) { this.scratchOrgFromPool.failures = this.scratchOrgFromPool.failures || []; this.scratchOrgFromPool.failures.push(JSON.stringify(e, null, 2)); uxLog(this, '[pool] ' + c.yellow('Put back scratch org in the scratch orgs pool. ') + c.grey({ result: this.scratchOrgFromPool })); await addScratchOrgToPool(this.scratchOrgFromPool, { position: 'first' }); } else if (isCI && this.scratchOrgUsername) { await execCommand(`sf org delete scratch --no-prompt --target-org ${this.scratchOrgUsername}`, this, { fail: false, output: true, }); uxLog(this, c.red('Deleted scratch org as we are in CI and its creation has failed')); } throw e; } // Show password to user if (this.scratchOrgPassword) { uxLog(this, c.cyan(`You can connect to your scratch using username ${c.green(this.scratchOrgUsername)} and password ${c.green(this.scratchOrgPassword)}`)); } elapseEnd(`Create and initialize scratch org`); // Return an object to be displayed with --json return { status: 0, scratchOrgAlias: this.scratchOrgAlias, scratchOrgInfo: this.scratchOrgInfo, scratchOrgUsername: this.scratchOrgUsername, scratchOrgPassword: this.scratchOrgPassword, scratchOrgSfdxAuthUrl: this.scratchOrgSfdxAuthUrl, authFileJson: this.authFileJson, outputString: 'Created and initialized scratch org', }; } // Initialize configuration from .sfdx-hardis.yml + .gitbranch.sfdx-hardis.yml + .username.sfdx-hardis.yml async initConfig() { this.configInfo = await getConfig('user'); this.gitBranch = (await getCurrentGitBranch({ formatted: true })) || ''; const newScratchName = os.userInfo().username + '-' + (this.gitBranch.split('/').pop() || '').slice(0, 15) + '_' + moment().format('YYYYMMDD_hhmm'); this.scratchOrgAlias = process.env.SCRATCH_ORG_ALIAS || (!this.forceNew && this.pool == false ? this.configInfo.scratchOrgAlias : null) || newScratchName; if (isCI && !this.scratchOrgAlias.startsWith('CI-')) { this.scratchOrgAlias = 'CI-' + this.scratchOrgAlias; } if (this.pool === true) { this.scratchOrgAlias = 'PO-' + Math.random().toString(36).substr(2, 2) + this.scratchOrgAlias; } // Verify that the user wants to resume scratch org creation if (!isCI && this.scratchOrgAlias !== newScratchName && this.pool === false) { const checkRes = await prompts({ type: 'confirm', name: 'value', message: c.cyanBright(`You are about to reuse scratch org ${c.green(this.scratchOrgAlias)}. Are you sure that's what you want to do ?\n${c.grey('(if not, run again hardis:work:new or use hardis:scratch:create --forcenew)')}`), default: false, }); if (checkRes.value === false) { process.exit(0); } } this.projectName = process.env.PROJECT_NAME || this.configInfo.projectName; this.devHubAlias = process.env.DEVHUB_ALIAS || this.configInfo.devHubAlias; this.scratchOrgDuration = process.env?.SCRATCH_ORG_DURATION ? process.env.SCRATCH_ORG_DURATION // Priority to global variable if defined : isCI && this.pool === false ? 1 // If CI and not during pool feed job, default is 1 day because the scratch will not be used after the job : this.configInfo?.scratchOrgDuration ? this.configInfo.scratchOrgDuration // Override default value in scratchOrgDuration : 30; // Default value: 30 this.userEmail = process.env.USER_EMAIL || process.env.GITLAB_USER_EMAIL || this.configInfo.userEmail; // If not found, prompt user email and store it in user config file if (this.userEmail == null) { if (this.pool === true) { throw new SfError(c.red('You need to define userEmail property in .sfdx-hardis.yml')); } this.userEmail = await promptUserEmail(); } } // Create a new scratch org or reuse existing one async createScratchOrg(flags) { // Build project-scratch-def-branch-user.json uxLog(this, c.cyan('Building custom project-scratch-def.json...')); this.projectScratchDef = JSON.parse(fs.readFileSync('./config/project-scratch-def.json', 'utf-8')); this.projectScratchDef.orgName = this.scratchOrgAlias; this.projectScratchDef.adminEmail = this.userEmail; // Keep only first 15 and last 15 chars if scratch org alias is too long const aliasForUsername = this.scratchOrgAlias.length > 30 ? this.scratchOrgAlias.slice(0, 15) + this.scratchOrgAlias.slice(-15) : this.scratchOrgAlias; this.projectScratchDef.username = `${this.userEmail.split('@')[0].slice(0, 20)}@hardis-scratch-${aliasForUsername}.com`; const projectScratchDefLocal = `./config/user/project-scratch-def-${this.scratchOrgAlias}.json`; await fs.ensureDir(path.dirname(projectScratchDefLocal)); await fs.writeFile(projectScratchDefLocal, JSON.stringify(this.projectScratchDef, null, 2)); // Check current scratch org const orgListResult = await execSfdxJson('sf org list', this); const hubOrgUsername = flags['target-dev-hub'].getUsername(); const matchingScratchOrgs = orgListResult?.result?.scratchOrgs?.filter((org) => { return org.alias === this.scratchOrgAlias && org.status === 'Active' && org.devHubUsername === hubOrgUsername; }) || []; // Reuse existing scratch org if (matchingScratchOrgs?.length > 0 && !this.forceNew && this.pool == false) { this.scratchOrgInfo = matchingScratchOrgs[0]; this.scratchOrgUsername = this.scratchOrgInfo.username; uxLog(this, c.cyan(`Reusing org ${c.green(this.scratchOrgAlias)} with user ${c.green(this.scratchOrgUsername)}`)); return; } // Try to fetch a scratch org from the pool if (this.pool === false && this.configInfo.poolConfig) { this.scratchOrgFromPool = await fetchScratchOrg({ devHubConn: flags['target-dev-hub'].getConnection(), devHubUsername: flags['target-dev-hub'].getUsername(), }); if (this.scratchOrgFromPool) { this.scratchOrgAlias = this.scratchOrgFromPool.scratchOrgAlias; this.scratchOrgInfo = this.scratchOrgFromPool.scratchOrgInfo; this.scratchOrgUsername = this.scratchOrgFromPool.scratchOrgUsername; this.scratchOrgPassword = this.scratchOrgFromPool.scratchOrgPassword; await setConfig('user', { scratchOrgAlias: this.scratchOrgAlias }); uxLog(this, '[pool] ' + c.cyan(`Fetched org ${c.green(this.scratchOrgAlias)} from pool with user ${c.green(this.scratchOrgUsername)}`)); if (!isCI) { uxLog(this, c.cyan('Now opening org...') + ' ' + c.yellow('(The org is not ready to work in until this script is completed !)')); await execSfdxJson('sf org open', this, { fail: true, output: false, debug: this.debugMode, }); // Trigger a status refresh on VsCode WebSocket Client WebSocketClient.sendMessage({ event: 'refreshStatus' }); } return; } } // Fix @salesforce/cli bug: remove shape.zip if found const tmpShapeFolder = path.join(os.tmpdir(), 'shape'); if (fs.existsSync(tmpShapeFolder) && this.pool === false) { await fs.remove(tmpShapeFolder); uxLog(this, c.grey('Deleted ' + tmpShapeFolder)); } // Create new scratch org uxLog(this, c.cyan('Creating new scratch org...')); const waitTime = process.env.SCRATCH_ORG_WAIT || '15'; const createCommand = 'sf org create scratch --set-default ' + `--definition-file ${projectScratchDefLocal} ` + `--alias ${this.scratchOrgAlias} ` + `--wait ${waitTime} ` + `--target-dev-hub ${this.devHubAlias} ` + `--duration-days ${this.scratchOrgDuration}`; const createResult = await execSfdxJson(createCommand, this, { fail: false, output: false, debug: this.debugMode, }); await clearCache('sf org list'); assert(createResult.status === 0 && createResult.result, this.buildScratchCreateErrorMessage(createResult)); this.scratchOrgInfo = createResult.result; this.scratchOrgUsername = this.scratchOrgInfo.username; await setConfig('user', { scratchOrgAlias: this.scratchOrgAlias, scratchOrgUsername: this.scratchOrgUsername, }); // Generate password const passwordCommand = `sf org generate password --target-org ${this.scratchOrgUsername}`; const passwordResult = await execSfdxJson(passwordCommand, this, { fail: true, output: false, debug: this.debugMode, }); this.scratchOrgPassword = passwordResult.result.password; await setConfig('user', { scratchOrgPassword: this.scratchOrgPassword, }); // Trigger a status refresh on VsCode WebSocket Client WebSocketClient.sendMessage({ event: 'refreshStatus' }); if (isCI || this.pool === true) { // Try to store sfdxAuthUrl for scratch org reuse during CI const displayOrgCommand = `sf org display -o ${this.scratchOrgAlias} --verbose`; const displayResult = await execSfdxJson(displayOrgCommand, this, { fail: true, output: false, debug: this.debugMode, }); if (displayResult.result.sfdxAuthUrl) { await setConfig('user', { scratchOrgSfdxAuthUrl: displayResult.result.sfdxAuthUrl, }); this.scratchOrgSfdxAuthUrl = displayResult.result.sfdxAuthUrl; } else { // Try to get sfdxAuthUrl with workaround try { const authInfo = await AuthInfo.create({ username: displayResult.result.username }); this.scratchOrgSfdxAuthUrl = authInfo.getSfdxAuthUrl(); displayResult.result.sfdxAuthUrl = this.scratchOrgSfdxAuthUrl; await setConfig('user', { scratchOrgSfdxAuthUrl: this.scratchOrgSfdxAuthUrl, }); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { uxLog(this, c.yellow(`Unable to fetch sfdxAuthUrl for ${displayResult.result.username}. Only Scratch Orgs created from DevHub using authenticated using sf org login sfdx-url or sf org login web will have access token and enabled for autoLogin\nYou may need to define SFDX_AUTH_URL_DEV_HUB or SFDX_AUTH_URL_devHubAlias in your CI job running sf hardis:scratch:pool:refresh`)); this.scratchOrgSfdxAuthUrl = null; } } if (this.pool) { await setConfig('user', { authFileJson: displayResult, }); this.authFileJson = displayResult; } // Display org URL const openRes = await execSfdxJson('sf org open --url-only', this, { fail: true, output: false, debug: this.debugMode, }); uxLog(this, c.cyan(`Open scratch org with url: ${c.green(openRes?.result?.url)}`)); } else { // Open scratch org for user if not in CI await execSfdxJson('sf org open', this, { fail: true, output: false, debug: this.debugMode, }); } uxLog(this, c.cyan(`Created scratch org ${c.green(this.scratchOrgAlias)} with user ${c.green(this.scratchOrgUsername)}`)); } buildScratchCreateErrorMessage(createResult) { if (createResult.status === 0 && createResult.result) { return c.green('Scratch create OK'); } else if (createResult.status === 1 && createResult.errorMessage.includes('Socket timeout occurred while listening for results')) { return c.red(`[sfdx-hardis] Error creating scratch org. ${c.bold('This is probably a Salesforce error, try again manually or launch again CI job')}\n${JSON.stringify(createResult, null, 2)}`); } else if (createResult.status === 1 && createResult.errorMessage.includes('LIMIT_EXCEEDED')) { return c.red(`[sfdx-hardis] Error creating scratch org. ${c.bold('It seems you have no more scratch orgs available, go delete some in "Active Scratch Orgs" tab in the Dev Hub org')}\n${JSON.stringify(createResult, null, 2)}`); } return c.red(`[sfdx-hardis] Error creating scratch org. Maybe try ${c.yellow(c.bold('sf hardis:scratch:create --forcenew'))} ?\n${JSON.stringify(createResult, null, 2)}`); } // Update scratch org user async updateScratchOrgUser() { const config = await getConfig('user'); // Update scratch org main user uxLog(this, c.cyan('Update / fix scratch org user ' + this.scratchOrgUsername)); const userQueryCommand = `sf data get record --sobject User --where "Username=${this.scratchOrgUsername}" --target-org ${this.scratchOrgAlias}`; const userQueryRes = await execSfdxJson(userQueryCommand, this, { fail: true, output: false, debug: this.debugMode, }); let updatedUserValues = `LastName='SFDX-HARDIS' FirstName='Scratch Org'`; if (config.userEmail !== userQueryRes.result.CountryCode) { updatedUserValues += ` Email='${config.userEmail}'`; } // Fix country value is State & Country picklist activated if ((this.projectScratchDef.features || []).includes('StateAndCountryPicklist') && userQueryRes.result.CountryCode == null) { updatedUserValues += ` CountryCode='${config.defaultCountryCode || 'FR'}' Country='${config.defaultCountry || 'France'}'`; } if ((this.projectScratchDef.features || []).includes('MarketingUser') && userQueryRes.result.UserPermissionsMarketingUser === false) { // Make sure MarketingUser is checked on scratch org user if it is supposed to be updatedUserValues += ' UserPermissionsMarketingUser=true'; } const userUpdateCommand = `sf data update record --sobject User --record-id ${userQueryRes.result.Id} --values "${updatedUserValues}" --target-org ${this.scratchOrgAlias}`; await execSfdxJson(userUpdateCommand, this, { fail: false, output: true, debug: this.debugMode }); } } //# sourceMappingURL=create.js.map