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
JavaScript
/* 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