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

259 lines (251 loc) • 12.2 kB
/* jscpd:ignore-start */ import { SfCommand, Flags, requiredOrgFlagWithDeprecations } from '@salesforce/sf-plugins-core'; import { Messages, SfError } from '@salesforce/core'; import { getConfig } from '../../../../../config/index.js'; import c from 'chalk'; // import * as path from "path"; import { ensureGitRepository, gitHasLocalUpdates, execCommand, git, uxLog, isCI, } from '../../../../../common/utils/index.js'; import { CleanOptions } from 'simple-git'; import CleanReferences from '../../../project/clean/references.js'; import SaveTask from '../../../work/save.js'; import CleanXml from '../../../project/clean/xml.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('sfdx-hardis', 'org'); export default class Retrofit extends SfCommand { static DEFAULT_SOURCES_TO_RETROFIT = [ 'CompactLayout', 'CustomApplication', 'CustomField', 'CustomLabel', 'CustomLabels', 'CustomMetadata', 'CustomObject', 'CustomObjectTranslation', 'CustomTab', 'DuplicateRule', 'EmailTemplate', 'FlexiPage', 'GlobalValueSet', 'Layout', 'ListView', 'MatchingRules', 'PermissionSet', 'RecordType', 'StandardValueSet', 'Translations', 'ValidationRule', ]; static title = 'Retrofit changes from an org'; static description = `Retrieve changes from org link to a ref branch not present in sources This command need to be triggered from a branch that is connected to a SF org. It will then retrieve all changes not present in that branch sources, commit them and create a merge request against the default branch. If a merge request already exists, it will simply add a new commit. Define the following properties in **.sfdx-hardis.yml** - **productionBranch** : Name of the git branch that is corresponding to production org - **retrofitBranch** : Name of the git branch that will be used as merge request target List of metadata to retrieve can be set in three way, in order of priority : - \`CI_SOURCES_TO_RETROFIT\`: env variable (can be defined in CI context) - \`sourcesToRetrofit\` property in \`.sfdx-hardis.yml\` - Default list:\n\n - ${Retrofit.DEFAULT_SOURCES_TO_RETROFIT.join('\n - ')} You can also ignore some files even if they have been updated in production. To do that, define property **retrofitIgnoredFiles** in .sfdx-hardis.yml Example of full retrofit configuration: \`\`\`yaml productionBranch: master retrofitBranch: preprod retrofitIgnoredFiles: - force-app/main/default/applications/MyApp.app-meta.xml - force-app/main/default/applications/MyOtherApp.app-meta.xml - force-app/main/default/flexipages/MyFlexipageContainingDashboards.flexipage-meta.xml \`\`\` `; static examples = [ '$ sf hardis:org:retrieve:sources:retrofit', 'sf hardis:org:retrieve:sources:retrofit --productionbranch master --commit --commitmode updated', 'sf hardis:org:retrieve:sources:retrofit --productionbranch master --retrofitbranch preprod --commit --commitmode updated --push --pushmode mergerequest', ]; static flags = { commit: Flags.boolean({ default: false, description: 'If true, a commit will be performed after the retrofit', }), commitmode: Flags.string({ default: 'updated', options: ['updated', 'all'], description: 'Defines if we commit all retrieved updates, or all updates including creations', }), push: Flags.boolean({ default: false, description: 'If true, a push will be performed after the retrofit', }), pushmode: Flags.string({ default: 'default', options: ['default', 'mergerequest'], description: 'Defines if we send merge request options to git push arguments', }), productionbranch: Flags.string({ description: 'Name of the git branch corresponding to the org we want to perform the retrofit on.\nCan be defined in productionBranch property in .sfdx-hardis.yml', }), retrofittargetbranch: Flags.string({ description: 'Name of branch the merge request will have as target\nCan be defined in retrofitBranch property in .sfdx-hardis.yml', }), 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; configInfo = {}; debugMode = false; commit = false; commitMode = 'updated'; push = false; pushMode = 'default'; productionBranch; retrofitTargetBranch; /* jscpd:ignore-end */ async run() { const { flags } = await this.parse(Retrofit); this.commit = flags.commit || false; this.commitMode = flags.commitmode || false; this.push = flags.push || false; this.pushMode = flags.pushmode || 'default'; this.productionBranch = flags.productionbranch || null; this.retrofitTargetBranch = flags.retrofittargetbranch || null; this.debugMode = flags.debug || false; this.configInfo = await getConfig('branch'); // check git repo before processing await ensureGitRepository(); // set commit & merge request author await this.setDefaultGitConfig(); // checkout to retrofit branch, retrieve changes & push them if any await this.processRetrofit(flags); return { outputString: 'Merge request created/updated' }; } async processRetrofit(flags) { const config = await getConfig('branch'); this.productionBranch = this.productionBranch || config.productionBranch || process.env.CI_COMMIT_REF_NAME || 'master'; const retrofitWorkBranch = `retrofit/${this.productionBranch}`; this.retrofitTargetBranch = this.retrofitTargetBranch || config.retrofitBranch || 'retrofitTargetBranch MUST BE SET'; await git().fetch(['--prune']); const branches = await git().branch(); if (branches.all.find((branch) => branch.includes(retrofitWorkBranch))) { // If manual command (not CI), force user to remove previous retrofit branches if (!isCI) { throw new SfError(`You must delete local and remote branch ${c.yellow(retrofitWorkBranch)} before running this command`); } uxLog(this, c.cyan(`Checkout to existing branch ${retrofitWorkBranch}`)); await git().checkout(retrofitWorkBranch, ['--force']); } else { uxLog(this, c.cyan(`Create a new branch ${retrofitWorkBranch} from ${this.productionBranch}`)); await git().checkoutBranch(retrofitWorkBranch, `origin/${this.productionBranch}`); } const currentHash = await git().revparse(['HEAD']); uxLog(this, c.grey(`HEAD currently at ${currentHash}`)); // Retrieve sources from target org const hasChangedSources = await this.retrieveSources(flags); if (hasChangedSources) { // Commit and push if requested if (this.commit) { await this.commitChanges(flags); // Update package.xml files and clean if necessary await SaveTask.run(['--targetbranch', this.retrofitTargetBranch || '', '--auto']); if (this.push) { await this.pushChanges(retrofitWorkBranch); } } } else { uxLog(this, c.yellow('No changes to commit')); // Delete locally created branch if we are within CI process if (isCI) { uxLog(this, c.yellow('Deleting local retrofit branch...')); await git().branch([`-D ${retrofitWorkBranch}`]); } } } // Commit all changes or only updated files async commitChanges(flags) { if (this.commitMode === 'updated') { uxLog(this, c.cyan('Stage and commit only updated files... ')); await git().add(['--update']); await this.doCommit(flags); uxLog(this, c.cyan('Removing created files... ')); await git().reset(['--hard']); await git().clean([CleanOptions.FORCE, CleanOptions.RECURSIVE]); } else { uxLog(this, c.cyan('Stage and commit all files... ')); await git().add(['--all']); await this.doCommit(flags); } } async doCommit(flags) { await git().commit(`[sfdx-hardis] Changes retrofited from ${flags['target-org'].getUsername()}`); } // Push changes and add merge request options if requested async pushChanges(retrofitWorkBranch) { const origin = `https://root:${process.env.CI_TOKEN}@${process.env.CI_SERVER_HOST}/${process.env.CI_PROJECT_PATH}.git`; const pushOptions = []; if (this.pushMode === 'mergerequest') { const mrOptions = [ '-o merge_request.create', `-o merge_request.target ${this.retrofitTargetBranch}`, `-o merge_request.title='[sfdx-hardis][RETROFIT] Created by pipeline #${process.env.CI_PIPELINE_ID}'`, '-o merge_request.merge_when_pipeline_succeeds', '-o merge_request.remove_source_branch', ]; pushOptions.push(...mrOptions); } const pushResult = await execCommand(`git push ${origin} ${retrofitWorkBranch} ${pushOptions.join(' ')}`, this, { fail: true, debug: this.debugMode, output: true, }); uxLog(this, c.yellow(JSON.stringify(pushResult))); } async setDefaultGitConfig() { // Just do that in CI, because this config should already exist in local if (isCI) { // either use values from variables from CI or use predefined variables from gitlab const USERNAME = process.env.CI_USER_NAME || process.env.GITLAB_USER_NAME || ''; const EMAIL = process.env.CI_USER_EMAIL || process.env.GITLAB_USER_EMAIL || ''; await git().addConfig('user.name', USERNAME, false, 'local'); await git().addConfig('user.email', EMAIL, false, 'local'); } } async retrieveSources(flags) { uxLog(this, c.cyan(`Retrieving sources from ${c.green(flags['target-org'].getUsername())} ...`)); const RETROFIT_MDT = process.env.CI_SOURCES_TO_RETROFIT || this.configInfo.sourcesToRetrofit || Retrofit.DEFAULT_SOURCES_TO_RETROFIT; const retrieveCommand = `sf project retrieve start -m "${RETROFIT_MDT.join(',')}" -o ${flags['target-org'].getUsername()}`; await execCommand(retrieveCommand, this, { fail: true, debug: this.debugMode, output: true }); // Discard ignored changes await this.discardIgnoredChanges(); // Clean sources await CleanReferences.run(['--type', 'all']); await CleanXml.run([]); // display current changes to commit return gitHasLocalUpdates(); } // Discard ignored changes from retrofitIgnoredFiles async discardIgnoredChanges() { const config = await getConfig('branch'); const ignoredFiles = config.retrofitIgnoredFiles || []; if (ignoredFiles.length > 0) { uxLog(this, c.cyan(`Discarding ignored changes from .sfdx-hardis.yml ${c.bold('retrofitIgnoredFiles')} property...`)); for (const ignoredFile of ignoredFiles) { // Reset file state await git().checkout(['--', ignoredFile]); } } } } //# sourceMappingURL=retrofit.js.map