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
276 lines • 11.9 kB
JavaScript
import c from 'chalk';
import fs from 'fs-extra';
import { glob } from 'glob';
import puppeteer from 'puppeteer-core';
import sortArray from 'sort-array';
import * as chromeLauncher from 'chrome-launcher';
import * as yaml from 'js-yaml';
import { uxLog } from './index.js';
import { SfError } from '@salesforce/core';
import { GLOB_IGNORE_PATTERNS } from './projectUtils.js';
const listViewRegex = /objects\/(.*)\/listViews\/(.*)\.listView-meta\.xml/gi;
export async function restoreListViewMine(listViewStrings, conn, options = { debug: false }) {
const listViewItems = [];
for (const listViewStr of listViewStrings) {
// Format Object:ListViewName
const splits = listViewStr.split(':');
if (splits.length === 2) {
listViewItems.push({ object: splits[0], listViewName: splits[1] });
}
else {
// sfdx file path format
listViewRegex.lastIndex = 0;
const m = listViewRegex.exec(listViewStr);
if (!m) {
uxLog("error", this, c.red(`Unable to find list view object and name from ${listViewStr}. Use format ${c.bold('Object:ListViewName')} , or ${c.bold('.../objects/OBJECT/listViews/LISTVIEWNAME.listview-meta.xml')}`));
continue;
}
listViewItems.push({ object: m[1], listViewName: m[2] });
}
}
// Build login url
const instanceUrl = conn.instanceUrl;
const loginUrl = `${instanceUrl}/secur/frontdoor.jsp?sid=${conn.accessToken}`;
// Get chrome/chromium executable path using the utility function
const chromeExecutablePath = getChromeExecutablePath();
// Start puppeteer
let browser;
try {
browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
headless: !(options.debug === true),
executablePath: chromeExecutablePath
});
}
catch (e) {
uxLog("error", this, c.red("List view 'Mine' has not been restored: error while trying to launch Puppeteer (browser simulator)."));
uxLog("error", this, c.red(e.message));
uxLog("error", this, c.red("You might need to set the PUPPETEER_EXECUTABLE_PATH environment variable to a Chrome/Chromium executable path. Example: /usr/bin/chromium-browser."));
return { error: e };
}
const page = await browser.newPage();
// Process login page
await page.goto(loginUrl, { waitUntil: ['domcontentloaded', 'networkidle0'] });
const success = [];
const failed = [];
const unnecessary = [];
// Restore list views with Mine option
for (const listView of listViewItems) {
const objectName = listView.object;
const listViewName = listView.listViewName;
// Build start URL to see the listview
const instanceUrl = conn.instanceUrl;
const setupObjectUrl = `${instanceUrl}/lightning/o/${objectName}/list?filterName=${listViewName}`;
try {
// Open ListView url in org
const navigationPromise = page.waitForNavigation();
await page.goto(setupObjectUrl);
await navigationPromise;
// Open ListView settings
const filterButton = await page.waitForSelector('.filterButton');
if (filterButton) {
await filterButton.click();
}
else {
throw new SfError('Puppeteer: .filterButton not found');
}
// Open Filter by owner popup
const filterByOwnerButtons = await page.waitForSelector("xpath///div[contains(text(), 'Filter by Owner')]");
if (filterByOwnerButtons) {
await filterByOwnerButtons.click();
}
else {
throw new SfError('Puppeteer: .filterByOwnerButtons not found');
}
// Select Mine value
const mineValue = await page.waitForSelector('input[value="mine"]');
if (mineValue) {
const mineValueClickableLabel = await mineValue.$('following-sibling::*');
if (mineValueClickableLabel) {
await mineValueClickableLabel[0].click();
}
}
else {
throw new SfError('Puppeteer: input[value="mine"] not found');
}
// Click done
const doneButtons = await page.waitForSelector("xpath///span[contains(text(), 'Done')]");
if (doneButtons) {
await doneButtons.click();
}
else {
throw new SfError('Puppeteer: Done button not found');
}
// Save
try {
const saveButton = await page.waitForSelector('.saveButton', { timeout: 3000 });
if (saveButton) {
await saveButton.click();
}
else {
throw new SfError('Puppeteer: .saveButton not found');
}
}
catch {
unnecessary.push(`${objectName}:${listViewName}`);
uxLog("warning", this, c.yellow(`Unable to hit save button, but it's probably because ${objectName}.${listViewName} was already set to "Mine"`));
continue;
}
// Confirmed saved toast
await page.waitForSelector("xpath///span[contains(text(), 'List view updated.')]");
success.push(`${objectName}:${listViewName}`);
uxLog("success", this, c.green(`Successfully set ${objectName}.${listViewName} as "Mine"`));
}
catch (e) {
// Unexpected puppeteer error
failed.push(`${objectName}:${listViewName}`);
uxLog("error", this, c.red(`Puppeteer error while processing ${objectName}:${listViewName}: ${e.message}`));
}
}
// Close puppeteer browser
await browser.close();
return { success, failed, unnecessary };
}
// List all yml files in config/branches and build list of major orgs from them
export async function listMajorOrgs() {
const majorOrgs = [];
const branchConfigPattern = '**/config/branches/.sfdx-hardis.*.yml';
const configFiles = await glob(branchConfigPattern, { ignore: GLOB_IGNORE_PATTERNS });
for (const configFile of configFiles) {
const props = (yaml.load(fs.readFileSync(configFile, 'utf-8')) || {});
listViewRegex.lastIndex = 0;
const branchNameRegex = /\.sfdx-hardis\.(.*)\.yml/gi;
const m = branchNameRegex.exec(configFile);
if (m) {
props.branchName = m[1];
}
majorOrgs.push(props);
}
// Clumsy sorting but not other way :/
const majorOrgsSorted = [];
// Main
for (const majorOrg of majorOrgs) {
if (isProduction(majorOrg?.branchName || "")) {
majorOrg.level = majorOrg.level || 100;
majorOrgsSorted.push(majorOrg);
}
}
// Preprod
for (const majorOrg of majorOrgs) {
if (isPreprod(majorOrg?.branchName || "")) {
majorOrg.level = majorOrg.level || 90;
majorOrgsSorted.push(majorOrg);
}
}
// uat run
for (const majorOrg of majorOrgs) {
if (isUatRun(majorOrg?.branchName || "")) {
majorOrg.level = majorOrg.level || 80;
majorOrgsSorted.push(majorOrg);
}
}
// uat
for (const majorOrg of majorOrgs) {
if (isUat(majorOrg?.branchName || "")) {
majorOrg.level = majorOrg.level || 70;
majorOrgsSorted.push(majorOrg);
}
}
// integration
for (const majorOrg of majorOrgs) {
if (isIntegration(majorOrg?.branchName || "")) {
majorOrg.level = majorOrg.level || 50;
majorOrgsSorted.push(majorOrg);
}
}
// Add remaining major branches
for (const majorOrg of sortArray(majorOrgs, { by: ['branchName'], order: ['asc'] })) {
if (majorOrgsSorted.filter(org => org.branchName === majorOrg.branchName).length === 0) {
majorOrg.level = majorOrg.level || 40;
majorOrgsSorted.push(majorOrg);
}
}
const completedMajorOrgs = majorOrgsSorted.map((majorOrg) => {
if (majorOrg?.mergeTargets?.length > 0) {
return majorOrg;
}
majorOrg.mergeTargets = guessMatchingMergeTargets(majorOrg.branchName, majorOrgs);
return majorOrg;
});
return completedMajorOrgs;
}
function guessMatchingMergeTargets(branchName, majorOrgs) {
if (isProduction(branchName)) {
return [];
}
else if (isPreprod(branchName)) {
return majorOrgs.filter(org => isProduction(org.branchName)).map(org => org.branchName);
}
else if (isUat(branchName)) {
return majorOrgs.filter(org => isPreprod(org.branchName)).map(org => org.branchName);
}
else if (isUatRun(branchName)) {
return majorOrgs.filter(org => isPreprod(org.branchName)).map(org => org.branchName);
}
else if (isIntegration(branchName)) {
return majorOrgs.filter(org => isUat(org.branchName)).map(org => org.branchName);
}
if (branchName.toLowerCase().includes('training')) {
uxLog('log', this, c.grey(`Branch ${branchName} appears to be a training branch, that's probably ok to have no merge targets.`));
return [];
}
uxLog("warning", this, c.yellow(`Unable to guess merge targets for ${branchName}.
Please set them manually in config/branches/.sfdx-hardis.${branchName}.yml
Example:
mergeTargets:
- preprod
`));
return [];
}
export function isProduction(branchName) {
return branchName.toLowerCase().startsWith("prod") || branchName.toLowerCase().startsWith("main");
}
export function isPreprod(branchName) {
return branchName.toLowerCase().startsWith("preprod") || branchName.toLowerCase().startsWith("staging");
}
export function isUat(branchName) {
return (branchName.toLowerCase().startsWith("uat") || branchName.toLowerCase().startsWith("recette")) && !branchName.toLowerCase().includes("run");
}
export function isIntegration(branchName) {
return branchName.toLowerCase().startsWith("integ");
}
export function isUatRun(branchName) {
return (branchName.toLowerCase().startsWith("uat") || branchName.toLowerCase().startsWith("recette")) && branchName.toLowerCase().includes("run");
}
export async function checkSfdxHardisTraceAvailable(conn) {
let traceObject;
try {
traceObject = await conn.sobject("SfdxHardisTrace__c").describe();
}
catch (e) {
throw new SfError("You need a Custom Setting of type List (activate through Schema Settings), named SfdxHardisTrace__c, with Type__c and Key__c fields (both string, length 80)\n" + e.message);
}
const traceObjectFields = traceObject.fields;
if (traceObjectFields.filter(field => field.name === "Type__c").length === 0) {
throw new SfError("You need a field Type__c (string, length 80) on SfdxHardisTrace__c in target org");
}
if (traceObjectFields.filter(field => field.name === "Key__c").length === 0) {
throw new SfError("You need a field Key__c (string, length 80) on SfdxHardisTrace__c in target org");
}
}
/**
* Get the Chrome/Chromium executable path for Puppeteer
* This is used by various commands that need browser automation
* @returns string - Path to Chrome executable, or empty string if not found
*/
export function getChromeExecutablePath() {
let chromeExecutablePath = process.env?.PUPPETEER_EXECUTABLE_PATH || "";
if (chromeExecutablePath === "" || !fs.existsSync(chromeExecutablePath)) {
const chromePaths = chromeLauncher.Launcher.getInstallations();
if (chromePaths && chromePaths.length > 0) {
chromeExecutablePath = chromePaths[0];
}
}
return chromeExecutablePath;
}
//# sourceMappingURL=orgConfigUtils.js.map