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

655 lines (645 loc) 35.6 kB
/* jscpd:ignore-start */ import { SfCommand, Flags, requiredOrgFlagWithDeprecations } from '@salesforce/sf-plugins-core'; import { Messages } from '@salesforce/core'; import c from 'chalk'; import fs from 'fs-extra'; import open from 'open'; import * as path from 'path'; import { createTempDir, execCommand, getCurrentGitBranch, getGitRepoUrl, git, gitHasLocalUpdates, gitPush, normalizeFileStatusPath, uxLog, uxLogTable, } from '../../../common/utils/index.js'; import { exportData } from '../../../common/utils/dataUtils.js'; import { forceSourcePull } from '../../../common/utils/deployUtils.js'; import { callSfdxGitDelta, getGitDeltaScope, selectTargetBranch } from '../../../common/utils/gitUtils.js'; import { prompts } from '../../../common/utils/prompts.js'; import { appendPackageXmlFilesContent, parseXmlFile, removePackageXmlFilesContent, writeXmlFile, } from '../../../common/utils/xmlUtils.js'; import { WebSocketClient } from '../../../common/websocketClient.js'; import { CONSTANTS, getApiVersion, getConfig, setConfig } from '../../../config/index.js'; import CleanReferences from '../project/clean/references.js'; import CleanXml from '../project/clean/xml.js'; import { GitProvider } from '../../../common/gitProvider/index.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('sfdx-hardis', 'org'); export default class SaveTask extends SfCommand { static title = 'Save User Story'; static description = ` ## Command Behavior **Guides the user through the process of saving their work, preparing it for a Merge Request (also named Pull Request), and pushing changes to the remote Git repository.** This command automates several critical steps involved in finalizing a development User Story and integrating it into the main codebase. It ensures that your local changes are properly synchronized, cleaned, and committed before being pushed. Key functionalities include: - **Git Status Management:** Ensures a clean Git working directory by handling ongoing merges and unstaging files. - **Org Synchronization (Optional):** Prompts the user to pull the latest metadata updates from their scratch org or source-tracked sandbox, ensuring local files reflect the org's state. - **Package.xml Updates:** Automatically generates \`package.xml\` and \`destructiveChanges.xml\` files based on the Git delta between your current branch and the target branch, reflecting added, modified, and deleted metadata. - **Automated Source Cleaning:** Applies predefined cleaning operations to your local Salesforce sources, such as removing unwanted references, minimizing profiles, or cleaning XML files based on configurations in your \`.sfdx-hardis.yml\`. - \`autoCleanTypes\`: A list of automated source cleanings, configurable via [hardis:project:clean:references]($\{CONSTANTS.DOC_URL_ROOT}/hardis/project/clean/references/). - \`autoRemoveUserPermissions\`: A list of user permissions to automatically remove from profile metadata. - **Deployment Plan Generation:** Builds an automated deployment plan based on the updated \`package.xml\` and configured deployment splits. - **Commit and Push:** Guides the user to commit the changes and push them to the remote Git repository, optionally handling force pushes if a branch reset occurred. - **Merge Request Guidance:** Provides information and links to facilitate the creation of a merge request after the changes are pushed. Example \`.sfdx-hardis.yml\` configuration: \`\`\`yaml autoCleanTypes: - checkPermissions - destructivechanges - datadotcom - minimizeProfiles - listViewsMine autoRemoveUserPermissions: - EnableCommunityAppLauncher - FieldServiceAccess - OmnichannelInventorySync - SendExternalEmailAvailable - UseOmnichannelInventoryAPIs - ViewDataLeakageEvents - ViewMLModels - ViewPlatformEvents - WorkCalibrationUser \`\`\` Advanced instructions are available in the [Publish a User Story documentation]($\{CONSTANTS.DOC_URL_ROOT}/salesforce-ci-cd-publish-task/). <details markdown="1"> <summary>Technical explanations</summary> The command's technical implementation involves a series of orchestrated steps: - **Git Integration:** Extensively uses the \`git\` utility for status checks, adding files, committing, and pushing. It also leverages \`sfdx-git-delta\` for generating metadata differences between Git revisions. - **Interactive Prompts:** Employs the \`prompts\` library to interact with the user for decisions like pulling sources or pushing commits. - **Configuration Management:** Reads and updates project and user configurations using \`getConfig\` and \`setConfig\` to store preferences and deployment plans. - **Metadata Synchronization:** Calls \`forceSourcePull\` to retrieve metadata from the org and \`callSfdxGitDelta\` to generate \`package.xml\` and \`destructiveChanges.xml\` based on Git changes. - **XML Manipulation:** Utilizes \`appendPackageXmlFilesContent\`, \`removePackageXmlFilesContent\`, \`parseXmlFile\`, and \`writeXmlFile\` for modifying \`package.xml\` and \`destructiveChanges.xml\` files. - **Automated Cleaning:** Integrates with \`CleanReferences.run\` and \`CleanXml.run\` commands to perform automated cleaning operations on the Salesforce source files. - **Deployment Plan Building:** Dynamically constructs a deployment plan by analyzing the \`package.xml\` content and applying configured deployment splits. - **WebSocket Communication:** Uses \`WebSocketClient.sendRefreshStatusMessage\` to notify connected VS Code clients about status updates. - **External Tool Integration:** Requires the \`sfdx-git-delta\` plugin to be installed for its core functionality. </details> `; static examples = ['$ sf hardis:work:task:save', '$ sf hardis:work:task:save --nopull --nogit --noclean']; // public static args = [{name: 'file'}]; static flags = { nopull: Flags.boolean({ char: 'n', default: false, description: 'No scratch pull before save', }), nogit: Flags.boolean({ char: 'g', default: false, description: 'No automated git operations', }), noclean: Flags.boolean({ char: 'c', default: false, description: 'No cleaning of local sources', }), auto: Flags.boolean({ default: false, description: 'No user prompts (when called from CI for example)', }), targetbranch: Flags.string({ description: 'Name of the Merge Request target branch. Will be guessed or prompted if not provided.', }), 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-org': requiredOrgFlagWithDeprecations, }; // 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 = ['sfdx-git-delta']; debugMode = false; noPull = false; noGit = false; noClean = false; auto = false; gitUrl; currentBranch; targetBranch; /* jscpd:ignore-end */ async run() { const { flags } = await this.parse(SaveTask); this.noPull = flags.nopull || false; this.noGit = flags.nogit || false; this.noClean = flags.noclean || false; this.auto = flags.auto || false; this.targetBranch = flags.targetbranch || null; this.debugMode = flags.debug || false; const localBranch = (await getCurrentGitBranch()) || ''; // Define current and target branches this.gitUrl = await getGitRepoUrl() || ''; this.currentBranch = (await getCurrentGitBranch()) || ''; if (this.targetBranch == null) { const userConfig = await getConfig('user'); if (userConfig?.localStorageBranchTargets && userConfig?.localStorageBranchTargets[localBranch]) { this.targetBranch = userConfig?.localStorageBranchTargets[localBranch]; } } if (this.targetBranch == null) { this.targetBranch = await selectTargetBranch({ message: `Please select the target branch of your future ${GitProvider.getMergeRequestName(this.gitUrl)}`, }); } // User log info uxLog("action", this, c.cyan(`Preparing ${GitProvider.getMergeRequestName(this.gitUrl)} from branch ${c.green(localBranch)} to ${c.green(this.targetBranch)}`)); // Make sure git is clean before starting operations await this.cleanGitStatus(); // Make sure commit is ready before starting operations const orgPullStateRes = await this.ensureCommitIsReady(flags); if (orgPullStateRes && orgPullStateRes.outputString) { return orgPullStateRes; } // Update package.xml files using sfdx-git-delta const gitStatusWithConfig = await this.upgradePackageXmlFilesWithDelta(); // Apply cleaning on sources await this.applyCleaningOnSources(); // Build automated deployment plan const gitStatusAfterDeployPlan = await this.buildDeploymentPlan(); // Push new commit(s) await this.manageCommitPush(gitStatusWithConfig, gitStatusAfterDeployPlan); let mergeRequestUrl = GitProvider.getMergeRequestCreateUrl(this.gitUrl, this.targetBranch || '', this.currentBranch); mergeRequestUrl = mergeRequestUrl || this.gitUrl.replace('.git', ''); // Merge request uxLog("action", this, c.cyan(`If your work is ${c.bold('completed')}, create a ${c.bold(GitProvider.getMergeRequestName(this.gitUrl))}, otherwise push new commits to the ${c.green(this.currentBranch)} branch.`)); let summaryMsg = c.grey(""); if (WebSocketClient.isAliveWithLwcUI()) { WebSocketClient.sendReportFileMessage(mergeRequestUrl, `Create ${GitProvider.getMergeRequestName(this.gitUrl)}`, 'actionUrl'); } else { summaryMsg += c.grey(`- New ${GitProvider.getMergeRequestName(this.gitUrl)} URL: ${c.green(mergeRequestUrl)}\n`); } const mergeRequestDoc = `${CONSTANTS.DOC_URL_ROOT}/salesforce-ci-cd-publish-task/#create-merge-request`; summaryMsg += c.grey(`- Repository: ${c.green(this.gitUrl.replace('.git', ''))}\n`); summaryMsg += c.grey(`- Source branch: ${c.green(this.currentBranch)}\n`); summaryMsg += c.grey(`- Target branch: ${c.green(this.targetBranch)}`); uxLog("log", this, summaryMsg); uxLog("log", this, `${c.yellow(`When your ${GitProvider.getMergeRequestName(this.gitUrl)} has been merged:`)} - ${c.yellow('DO NOT REUSE THE SAME BRANCH')} - Use New User Story menu (sf hardis:work:new), even if you work in the same sandbox or scratch org 😊`); // Manual actions file const config = await getConfig('project'); if (config.manualActionsFileUrl && config.manualActionsFileUrl !== '') { uxLog("warning", this, c.yellow(`If you have pre-deployment or post-deployment manual actions, record them in ${c.green(config.manualActionsFileUrl)}`)); if (WebSocketClient.isAliveWithLwcUI()) { WebSocketClient.sendReportFileMessage(config.manualActionsFileUrl, `Update Manual Actions file`, 'actionUrl'); } } else { uxLog("warning", this, c.yellow(`Define a manual actions file. Ask your release manager to create one and set its URL in .sfdx-hardis.yml under manualActionsFileUrl.`)); } if (!WebSocketClient.isAliveWithLwcUI()) { uxLog("log", this, c.grey(`${GitProvider.getMergeRequestName(this.gitUrl)} documentation is available here -> ${c.bold(mergeRequestDoc)}`)); } WebSocketClient.sendReportFileMessage(mergeRequestDoc, `View ${GitProvider.getMergeRequestName(this.gitUrl)} documentation`, 'docUrl'); // Return an object to be displayed with --json return { outputString: 'Saved the User Story' }; } // Clean git status async cleanGitStatus() { // Skip git stuff if requested if (this.noGit) { uxLog("action", this, c.cyan(`[Expert mode] Skipped git reset`)); return; } let gitStatusInit = await git().status(); // Cancel merge if ongoing merge if (gitStatusInit.conflicted.length > 0) { await git({ output: true }).merge(['--abort']); gitStatusInit = await git().status(); } // Unstage files if (gitStatusInit.staged.length > 0) { await execCommand('git reset', this, { output: true, fail: true }); } } async ensureCommitIsReady(flags) { // Manage project deploy start from scratch org if (this.noPull || this.auto) { // Skip pull uxLog("action", this, c.cyan(`Skipped sf project:retrieve:start from scratch org`)); return; } // Request user if commit is ready const commitReadyRes = await prompts({ type: 'select', name: 'value', message: c.cyanBright('Have you already committed the updated metadata you want to deploy?'), description: 'Select your current state regarding git commits and metadata updates', placeholder: 'Select commit status', choices: [ { title: '😎 Yes, my commit(s) are ready! I staged my files and created one or multiple commits.', value: 'commitReady', description: "You have already pulled updates from your org (or locally updated the files if you're a nerd), staged your files, and created a commit", }, { title: '😐 No, please pull my latest updates from my org so I can commit my metadata', value: 'pleasePull', description: 'Pull latest updates from org so you can stage files and create your commit', }, { title: '😱 What is a commit? What does pull mean? Help!', value: 'help', description: "Don't panic, just click on the link that will appear in the console (CTRL + Click) and you'll learn!", }, ], }); if (commitReadyRes.value === 'pleasePull') { // Process sf project retrieve start uxLog("action", this, c.cyan(`Pulling sources from scratch org ${flags['target-org'].getUsername()}...`)); await forceSourcePull(flags['target-org'].getUsername(), this.debugMode); uxLog("action", this, c.cyan(`Sources have been pulled from ${flags['target-org'].getUsername()}, now you can stage and commit your updates!`)); WebSocketClient.sendReportFileMessage("workbench.view.scm", "Commit your retrieved files", "actionCommand"); WebSocketClient.sendReportFileMessage(`${CONSTANTS.DOC_URL_ROOT}/salesforce-ci-cd-publish-task/#commit-your-updates`, "Retrieve and Commit documentation", 'docUrl'); return { outputString: 'Pull performed' }; } else if (commitReadyRes.value === 'help') { // Show pull commit stage help const commitHelpUrl = `${CONSTANTS.DOC_URL_ROOT}/hardis/scratch/pull/`; uxLog("action", this, c.cyan(`Opening help at ${commitHelpUrl} ...`)); await open(commitHelpUrl, { wait: true }); return { outputString: 'Help displayed at ' }; } // Extract data from org const dataSources = [ { label: 'Email templates', dataPath: './scripts/data/EmailTemplate', }, ]; for (const dataSource of dataSources) { if (fs.existsSync(dataSource.dataPath)) { const exportDataRes = await prompts({ type: 'confirm', name: 'value', message: c.cyan(`Did you update ${c.green(dataSource.label)} and want to export related data ?`), description: 'Confirm if you want to export data that may have been updated for this data source', }); if (exportDataRes.value === true) { await exportData(dataSource.dataPath, this, { sourceUsername: flags['target-org'].getUsername(), }); } } } } async upgradePackageXmlFilesWithDelta() { uxLog("action", this, c.cyan('Updating manifest/package.xml and manifest/destructiveChanges.xml using sfdx-git-delta...')); // Retrieving info about current branch latest commit and master branch latest commit const gitDeltaScope = await getGitDeltaScope(this.currentBranch, this.targetBranch || ''); // Build package.xml delta between most recent commit and developpement const localPackageXml = path.join('manifest', 'package.xml'); const toCommitMessage = gitDeltaScope.toCommit ? gitDeltaScope.toCommit.message : ''; uxLog("log", this, c.grey(`Calculating package.xml diff from [${c.green(this.targetBranch)}] to [${c.green(this.currentBranch)} - ${c.green(toCommitMessage)}]`)); const tmpDir = await createTempDir(); const packageXmlResult = await callSfdxGitDelta(gitDeltaScope.fromCommit, gitDeltaScope.toCommit ? gitDeltaScope.toCommit.hash : gitDeltaScope.fromCommit, tmpDir); if (packageXmlResult.status === 0) { // Upgrade local destructivePackage.xml const localDestructiveChangesXml = path.join('manifest', 'destructiveChanges.xml'); if (!fs.existsSync(localDestructiveChangesXml)) { // Create default destructiveChanges.xml if not defined const blankDestructiveChanges = `<?xml version="1.0" encoding="UTF-8"?> <Package xmlns="http://soap.sforce.com/2006/04/metadata"> <version>${getApiVersion()}</version> </Package> `; await fs.writeFile(localDestructiveChangesXml, blankDestructiveChanges); } const diffDestructivePackageXml = path.join(tmpDir, 'destructiveChanges', 'destructiveChanges.xml'); const destructivePackageXmlDiffStr = await fs.readFile(diffDestructivePackageXml, 'utf8'); uxLog("log", this, c.grey(c.bold(`Delta destructiveChanges.xml diff to be merged within ${c.green(localDestructiveChangesXml)}:\n`)) + c.red(destructivePackageXmlDiffStr)); await appendPackageXmlFilesContent([localDestructiveChangesXml, diffDestructivePackageXml], localDestructiveChangesXml); if ((await gitHasLocalUpdates()) && !this.noGit) { await git().add(localDestructiveChangesXml); } // Upgrade local package.xml const diffPackageXml = path.join(tmpDir, 'package', 'package.xml'); const packageXmlDiffStr = await fs.readFile(diffPackageXml, 'utf8'); uxLog("log", this, c.grey(c.bold(`Delta package.xml diff to be merged within ${c.green(localPackageXml)}:\n`)) + c.green(packageXmlDiffStr)); await appendPackageXmlFilesContent([localPackageXml, diffPackageXml], localPackageXml); await removePackageXmlFilesContent(localPackageXml, localDestructiveChangesXml, { outputXmlFile: localPackageXml, }); if ((await gitHasLocalUpdates()) && !this.noGit) { await git().add(localPackageXml); } } else { uxLog("log", this, `[error] ${c.grey(JSON.stringify(packageXmlResult))}`); uxLog("error", this, c.red(`Unable to build git diff.${c.yellow(c.bold('Please update package.xml and destructiveChanges.xml manually'))}`)); } // Commit updates let gitStatusWithConfig = await git().status(); if (gitStatusWithConfig.staged.length > 0 && !this.noGit) { const commitMessage = '[sfdx-hardis] Update package content'; uxLog("action", this, c.cyan(`🌿Adding new commit: ${commitMessage}\n- Contains package.xml and destructiveChanges.xml updates based on your metadata changes.`)); // Build files list for table const filesTable = gitStatusWithConfig.staged.map((file) => { const status = gitStatusWithConfig.files.find(f => f.path === file); const statusLabel = status?.working_dir === 'D' ? 'deleted' : status?.index === 'A' ? 'created' : 'modified'; return { Status: statusLabel, File: file }; }); uxLogTable(this, filesTable, ['Status', 'File']); try { await git({ output: true }).commit(commitMessage); } catch (e) { uxLog("warning", this, c.yellow(`There may be an issue while committing files, but it can be safely ignored\n${c.grey(e.message)}`)); gitStatusWithConfig = await git().status(); } } return gitStatusWithConfig; } // Apply automated cleaning to avoid to have to do it manually async applyCleaningOnSources() { const config = await getConfig('branch'); if (!this.noClean) { const gitStatusFilesBeforeClean = (await git().status()).files.map((file) => file.path); uxLog("other", this, JSON.stringify(gitStatusFilesBeforeClean, null, 2)); // References cleaning await CleanReferences.run(['--type', 'all']); if (globalThis?.displayProfilesWarning === true) { uxLog("warning", this, c.yellow(c.bold('Please make sure the attributes removed from Profiles are defined on Permission Sets 😊'))); } // Xml cleaning if (config.cleanXmlPatterns && config.cleanXmlPatterns.length > 0) { uxLog("action", this, c.cyan('Cleaning project using patterns defined in cleanXmlPatterns...')); await CleanXml.run([]); } // Manage git after cleaning const gitStatusAfterClean = await git().status(); uxLog("other", this, JSON.stringify(gitStatusAfterClean, null, 2)); const cleanedFiles = gitStatusAfterClean.files .filter((file) => !gitStatusFilesBeforeClean.includes(file.path)) .map((file) => normalizeFileStatusPath(file.path, config)); if (cleanedFiles.length > 0) { if (!this.noGit) { try { await git().add(cleanedFiles); const commitMessage = '[sfdx-hardis] Clean sfdx project'; uxLog("action", this, c.cyan(`🌿Adding new commit: ${commitMessage}\n- Contains files that have been automatically cleaned per your configuration.`)); // Build files list for table const filesTable = cleanedFiles.map((file) => ({ Status: 'modified', File: file })); uxLogTable(this, filesTable, ['Status', 'File']); await git({ output: true }).commit(commitMessage); } catch (e) { uxLog("warning", this, c.yellow(`There may be an issue while adding cleaned files but it can be ok to ignore it\n${c.grey(e.message)}`)); } } } } } async buildDeploymentPlan() { const configProject = await getConfig('project'); if (!(configProject?.enableDeprecatedDeploymentPlan === true)) { uxLog("log", this, c.cyan('Deployment plan generation is disabled in project configuration. Enable it with enableDeprecatedDeploymentPlan: true in .sfdx-hardis.yml')); return await git().status(); } // Build deployment plan splits let splitConfig = await this.getSeparateDeploymentsConfig(); const localPackageXml = path.join('manifest', 'package.xml'); const packageXml = await parseXmlFile(localPackageXml); for (const type of packageXml.Package.types || []) { const typeName = type.name[0]; splitConfig = splitConfig.map((split) => { if (split.types.includes(typeName) && type.members[0] !== '*') { split.content[typeName] = type.members; } return split; }); } // Generate deployment plan items const config = await getConfig('project'); const deploymentPlan = config?.deploymentPlan || {}; let packages = deploymentPlan?.packages || []; const blankPackageXml = packageXml; blankPackageXml.Package.types = []; for (const split of splitConfig) { if (Object.keys(split.content).length > 0) { // data case if (split.data) { const label = `Import ${split.types.join('-')} records`; packages = this.addToPlan(packages, { label: label, dataPath: split.data, order: split.dataPos, waitAfter: split.waitAfter, }); } // single split file case if (split.file) { const splitPackageXml = blankPackageXml; blankPackageXml.Package.types = []; for (const type of Object.keys(split.content)) { splitPackageXml.Package.types.push({ name: [type], members: split.content[type], }); } await writeXmlFile(split.file, splitPackageXml); const label = `Deploy ${split.types.join('-')}`; packages = this.addToPlan(packages, { label: label, packageXmlFile: split.file, order: split.filePos, waitAfter: split.waitAfter, }); } // Multiple split file case if (split.files) { let pos = split.filePos; for (const mainTypeMember of split.content[split.mainType] || []) { const splitFile = split.files.replace(`{{name}}`, mainTypeMember); const splitPackageXml = blankPackageXml; blankPackageXml.Package.types = []; for (const type of Object.keys(split.content)) { if (type !== split.mainType) { const filteredMembers = split.content[type].filter((member) => member.includes(`${mainTypeMember}.`)); splitPackageXml.Package.types.push({ name: [type], members: filteredMembers, }); } } splitPackageXml.Package.types.push({ name: [split.mainType], members: [mainTypeMember], }); await writeXmlFile(splitFile, splitPackageXml); const label = `Deploy ${split.mainType} - ${mainTypeMember}`; packages = this.addToPlan(packages, { label: label, packageXmlFile: splitFile, order: pos, waitAfter: split.waitAfter, }); pos++; } } } } // Update deployment plan in config deploymentPlan.packages = packages.sort((a, b) => (a.order > b.order ? 1 : -1)); await setConfig('project', { deploymentPlan: deploymentPlan }); if (!this.noGit) { await git({ output: true }).add(['./config']); await git({ output: true }).add(['./manifest']); } let gitStatusAfterDeployPlan = await git().status(); if (gitStatusAfterDeployPlan.staged.length > 0 && !this.noGit) { const commitMessage = '[sfdx-hardis] Update deployment plan'; uxLog("action", this, c.cyan(`🌿Adding new commit: ${commitMessage}\n- Contains deployment plan updates (legacy feature).`)); // Build files list for table const filesTable = gitStatusAfterDeployPlan.staged.map((file) => { const status = gitStatusAfterDeployPlan.files.find(f => f.path === file); const statusLabel = status?.working_dir === 'D' ? 'deleted' : status?.index === 'A' ? 'created' : 'modified'; return { Status: statusLabel, File: file }; }); uxLogTable(this, filesTable, ['Status', 'File']); try { await git({ output: true }).commit(commitMessage); } catch (e) { uxLog("warning", this, c.yellow(`There may be an issue while committing files, but it can be safely ignored\n${c.grey(e.message)}`)); gitStatusAfterDeployPlan = await git().status(); } } return gitStatusAfterDeployPlan; } // Manage push from user async manageCommitPush(gitStatusWithConfig, gitStatusAfterDeployPlan) { if ((gitStatusWithConfig.staged.length > 0 || gitStatusAfterDeployPlan.staged.length > 0 || gitStatusAfterDeployPlan?.ahead > 0 || gitStatusAfterDeployPlan.tracking == null) && !this.noGit && !this.auto) { const pushResponse = await prompts({ type: 'confirm', name: 'push', default: true, message: c.cyanBright(`Do you want to push your commit(s) on git server ? (git push in remote git branch ${c.green(this.currentBranch)})`), description: 'Choose whether to push your commits to the remote git repository', }); if (pushResponse.push === true) { uxLog("action", this, c.cyan(`Pushing commit(s) to remote branch ${c.green(`origin/${this.currentBranch}`)}...`)); const configUSer = await getConfig('user'); let pushResult; if (configUSer.canForcePush === true) { // Force push if hardis:work:resetselection has been called before pushResult = await gitPush({ output: true }, ['-u', 'origin', this.currentBranch, '--force']); await setConfig('user', { canForcePush: false }); } else { pushResult = await gitPush({ output: true }, ['-u', 'origin', this.currentBranch]); } // Update merge request info if (pushResult && pushResult.remoteMessages) { let mergeRequestsStored = configUSer.mergeRequests || []; if (mergeRequestsStored.filter((mergeRequest) => mergeRequest?.branch === this.currentBranch).length === 1) { mergeRequestsStored = mergeRequestsStored.map((mergeRequestStored) => { if (mergeRequestStored?.branch === this.currentBranch) { return this.updateMergeRequestInfo(mergeRequestStored, pushResult); } }); } else { mergeRequestsStored.push(this.updateMergeRequestInfo({ branch: this.currentBranch }, pushResult)); } // Update user config file & send Websocket event await setConfig('user', { mergeRequests: mergeRequestsStored.filter((mr) => mr !== null) }); WebSocketClient.sendRefreshStatusMessage(); } } } } updateMergeRequestInfo(mergeRequestStored, mergeRequestInfo) { if (this.debugMode) { uxLog("log", this, c.grey(JSON.stringify(mergeRequestInfo, null, 2))); } if (mergeRequestInfo?.remoteMessages?.id) { mergeRequestStored.id = mergeRequestInfo.remoteMessages.id; } else { delete mergeRequestStored.id; } if (mergeRequestInfo?.remoteMessages?.pullRequestUrl) { mergeRequestStored.urlCreate = mergeRequestInfo.remoteMessages.pullRequestUrl; } else { delete mergeRequestStored.urlCreate; } if (mergeRequestInfo?.remoteMessages?.all[0] && mergeRequestInfo?.remoteMessages?.all[0].includes('View merge request')) { mergeRequestStored.url = mergeRequestInfo?.remoteMessages?.all[1]; } else { delete mergeRequestStored.url; } return mergeRequestStored; } async getSeparateDeploymentsConfig() { const config = await getConfig('project'); if (config.separateDeploymentsConfig || config.separateDeploymentsConfig === false) { return config.separateDeploymentConfig || []; } const separateDeploymentConfig = [ /* NV: Commented because seems to be now useless { types: ["EmailTemplate"], file: "manifest/splits/packageXmlEmails.xml", filePos: -20, data: "scripts/data/EmailTemplate", dataPos: -21, content: {}, }, { types: ["Flow", "Workflow"], file: "manifest/splits/packageXmlFlowWorkflow.xml", filePos: 6, content: {}, }, */ { types: ['SharingRules', 'SharingOwnerRule'], files: 'manifest/splits/packageXmlSharingRules{{name}}.xml', filePos: 30, mainType: 'SharingRules', waitAfter: 30, content: {}, }, ]; return separateDeploymentConfig; } // Add item to .sfdx-hardis.yml deploymentPlan property addToPlan(packages, item) { let updated = false; if (item.waitAfter === null) { delete item.waitAfter; } packages = packages.map((pckg) => { if (pckg.label === item.label) { pckg = item; updated = true; } return pckg; }); if (updated === false) { packages.push(item); } return packages; } } //# sourceMappingURL=save.js.map