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

276 lines • 11.9 kB
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