From bd48247e8b1e25e22bd83aba758cc9c06e53ac41 Mon Sep 17 00:00:00 2001 From: Andrew Goodman Date: Thu, 26 Nov 2020 01:45:01 +0000 Subject: [PATCH] Squash history --- .gitignore | 2 + LICENSE | 21 ++++ README.md | 109 +++++++++++------- package.json | 126 +++++++++++---------- src/hooks/predeploy/destructive-changes.ts | 113 +++++++++++++++--- test/commands/hello/org.test.ts | 19 ---- yarn.lock | 2 +- 7 files changed, 251 insertions(+), 141 deletions(-) create mode 100644 LICENSE delete mode 100644 test/commands/hello/org.test.ts diff --git a/.gitignore b/.gitignore index 9d6ea2c..708bed3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ /package-lock.json /tmp node_modules +*.tgz +oclif.manifest.json \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..16f9d30 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Andrew Goodman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index a8e83c5..084c271 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,81 @@ sfdx-destruction ================ -Destructive changes support for sfdx force:source:deploy +Destructive changes support for `sfdx force:source:deploy` via a predeploy hook. + +At time of writing, the `sfdx force:source:deploy` command doesn't support destructive changes files out of the box. Therefore if deletions are required, either source metadata needs to be converted for use with `sfdx force:mdapi:deploy` or there'll need to be multiple deployments (with downtime, difficult rollbacks and multiple unit test cycles). + +This simple plugin hooks into source deploy and drops `destructiveChangesPre.xml` and/or `destructiveChangesPost.xml` deletions manifests into the package before the deployment initiates. [![Version](https://img.shields.io/npm/v/sfdx-destruction.svg)](https://npmjs.org/package/sfdx-destruction) -[![CircleCI](https://circleci.com/gh/gdman/sfdx-destruction/tree/master.svg?style=shield)](https://circleci.com/gh/gdman/sfdx-destruction/tree/master) -[![Appveyor CI](https://ci.appveyor.com/api/projects/status/github/gdman/sfdx-destruction?branch=master&svg=true)](https://ci.appveyor.com/project/heroku/sfdx-destruction/branch/master) -[![Codecov](https://codecov.io/gh/gdman/sfdx-destruction/branch/master/graph/badge.svg)](https://codecov.io/gh/gdman/sfdx-destruction) -[![Greenkeeper](https://badges.greenkeeper.io/gdman/sfdx-destruction.svg)](https://greenkeeper.io/) [![Known Vulnerabilities](https://snyk.io/test/github/gdman/sfdx-destruction/badge.svg)](https://snyk.io/test/github/gdman/sfdx-destruction) [![Downloads/week](https://img.shields.io/npm/dw/sfdx-destruction.svg)](https://npmjs.org/package/sfdx-destruction) [![License](https://img.shields.io/npm/l/sfdx-destruction.svg)](https://github.com/gdman/sfdx-destruction/blob/master/package.json) - -* [Debugging your plugin](#debugging-your-plugin) - - - +# Installation + ```sh-session -$ npm install -g sfdx-destruction -$ sfdx COMMAND -running command... -$ sfdx (-v|--version|version) -sfdx-destruction/0.0.0 darwin-x64 node-v12.9.0 -$ sfdx --help [COMMAND] -USAGE - $ sfdx COMMAND -... +$ sfdx plugins:install sfdx-destruction ``` - - - - - -# Debugging your plugin -We recommend using the Visual Studio Code (VS Code) IDE for your plugin development. Included in the `.vscode` directory of this plugin is a `launch.json` config file, which allows you to attach a debugger to the node process when running your commands. - -To debug the `hello:org` command: -1. Start the inspector - -If you linked your plugin to the sfdx cli, call your command with the `dev-suspend` switch: + +# Usage + ```sh-session -$ sfdx hello:org -u myOrg@example.com --dev-suspend +$ SFDX_DESTRUCTION_ENABLE=true sfdx force:source:deploy ``` - -Alternatively, to call your command using the `bin/run` script, set the `NODE_OPTIONS` environment variable to `--inspect-brk` when starting the debugger: -```sh-session -$ NODE_OPTIONS=--inspect-brk bin/run hello:org -u myOrg@example.com + +# Configuration + +Destructive changes functionality is disabled by default and therefore must be enabled by environment variable or in the project configuration file. + +If the `SFDX_DESTRUCTION_ENABLE` environment variable is set, it will be used to determine whether to run the plugin (true or false and regardless of configuration file settings). +Else the `plugins` -> `sfdx-destruction` -> `enabledByDefault` variable in the project configuration file will be used (if set). +If neither is configured, the plugin will remain disabled. + +The locations of the pre and post destructive changes files can also be specified by environment variable and/or configuration file. Environment variables take precedence over the configuration file. If neither is present, the plugin will have no effect. + +Recommended usage is to use the configuration file to store the locations of the destructive changes files and the environment variable to enable at build time. + +## Environment Variables + +`SFDX_DESTRUCTION_ENABLE` - true or false to enable or disable the plugin + +`SFDX_DESTRUCTIVE_PRE_FILE` - path to the destructive changes pre file i.e. deletions that will occur before the deployment + +`SFDX_DESTRUCTIVE_POST_FILE` - path to the destructive changes post file i.e. deletions that will occur after the deployment + +## Project Configuration File (sfdx-project.json) + +`enabledByDefault` - true or false - will be used to determine whether the plugin should run if no environment variable is present (default: false - disabled) + +`destructiveChangesPreFile` - path to the destructive changes pre file i.e. deletions that will occur before the deployment + +`destructiveChangesPostFile` - path to the destructive changes post file i.e. deletions that will occur after the deployment + +### Example: +```json +{ + "packageDirectories": [ + { + "default": true, + "path": "force-app" + } + ], + "namespace": "", + "sfdcLoginUrl": "https://login.salesforce.com", + "sourceApiVersion": "50.0", + "plugins": { + "sfdx-destruction": { + "enabledByDefault": false, + "destructiveChangesPreFile": "{path-to-destructiveChangesPre.xml}", + "destructiveChangesPostFile": "{path-to-destructiveChangesPost.xml}" + } + } +} ``` -2. Set some breakpoints in your command code -3. Click on the Debug icon in the Activity Bar on the side of VS Code to open up the Debug view. -4. In the upper left hand corner of VS Code, verify that the "Attach to Remote" launch configuration has been chosen. -5. Hit the green play button to the left of the "Attach to Remote" launch configuration window. The debugger should now be suspended on the first line of the program. -6. Hit the green play button at the top middle of VS Code (this play button will be to the right of the play button that you clicked in step #5). -

-Congrats, you are debugging! +# Can I enable the plugin by command line argument? + +No, unfortunately not. + +This functionality is implemented as a hook and although it is possible to access argv, it doesn't seem to be possible to define additional arguments. Passing in an extra argument will return an error of `Unexpected argument`. diff --git a/package.json b/package.json index cc50e3e..f1322c3 100644 --- a/package.json +++ b/package.json @@ -1,65 +1,67 @@ { - "name": "sfdx-destruction", - "description": "Destructive changes support for sfdx force:source:deploy", - "version": "0.0.0", - "author": "Andrew Goodman", - "bugs": "https://github.com/gdman/sfdx-destruction/issues", - "dependencies": { - "@oclif/command": "^1", - "@oclif/config": "^1", - "@oclif/errors": "^1", - "@salesforce/command": "^2", - "@salesforce/core": "^2", - "tslib": "^1" - }, - "devDependencies": { - "@oclif/dev-cli": "^1", - "@oclif/plugin-help": "^2", - "@oclif/test": "^1", - "@salesforce/dev-config": "1.4.1", - "@types/chai": "^4", - "@types/mocha": "^5", - "@types/node": "^10", - "chai": "^4", - "globby": "^8", - "mocha": "^5", - "nyc": "^14", - "ts-node": "^8", - "tslint": "^5" - }, - "engines": { - "node": ">=8.0.0" - }, - "files": [ - "/lib", - "/messages", - "/npm-shrinkwrap.json", - "/oclif.manifest.json" - ], - "homepage": "https://github.com/gdman/sfdx-destruction", - "keywords": [ - "sfdx-plugin" - ], - "license": "MIT", - "oclif": { - "commands": "./lib/commands", - "bin": "sfdx", - "topics": { - "hello": { - "description": "Commands to say hello." - } + "name": "sfdx-destruction", + "description": "Destructive changes support for sfdx force:source:deploy", + "version": "1.0.0-beta.0", + "author": "Andrew Goodman", + "bugs": "https://github.com/gdman/sfdx-destruction/issues", + "dependencies": { + "@oclif/command": "^1", + "@oclif/config": "^1", + "@oclif/errors": "^1", + "@salesforce/command": "^2", + "@salesforce/core": "^2", + "tslib": "^1" }, - "devPlugins": [ - "@oclif/plugin-help" - ] - }, - "repository": "gdman/sfdx-destruction", - "scripts": { - "lint": "tslint --project . --config tslint.json --format stylish", - "postpack": "rm -f oclif.manifest.json", - "posttest": "tslint -p test -t stylish", - "prepack": "rm -rf lib && tsc -b && oclif-dev manifest && oclif-dev readme", - "test": "nyc --extension .ts mocha --forbid-only \"test/**/*.test.ts\"", - "version": "oclif-dev readme && git add README.md" - } + "devDependencies": { + "@oclif/dev-cli": "^1", + "@oclif/plugin-help": "^2", + "@oclif/test": "^1", + "@salesforce/dev-config": "1.4.1", + "@types/chai": "^4", + "@types/mocha": "^5", + "@types/node": "^10", + "chai": "^4", + "globby": "^8", + "mocha": "^5", + "nyc": "^14", + "ts-node": "^8", + "tslint": "^5" + }, + "engines": { + "node": ">=8.0.0" + }, + "files": [ + "/lib", + "/messages", + "/npm-shrinkwrap.json", + "/oclif.manifest.json" + ], + "homepage": "https://github.com/gdman/sfdx-destruction", + "keywords": [ + "sfdx-plugin", "salesforce", "sfdc", "sfdx", "plugin", "destructive", "changes", "deploy" + ], + "license": "MIT", + "oclif": { + "bin": "sfdx", + "topics": { + "sfdx-destructive": { + "description": "Destructive changes support for sfdx force:source:deploy" + } + }, + "hooks": { + "predeploy": "./lib/hooks/predeploy/destructive-changes" + }, + "devPlugins": [ + "@oclif/plugin-help" + ] + }, + "repository": "gdman/sfdx-destruction", + "scripts": { + "lint": "tslint --project . --config tslint.json --format stylish", + "postpack": "rm -f oclif.manifest.json", + "posttest": "tslint -p test -t stylish", + "prepack": "rm -rf lib && tsc -b && oclif-dev manifest && oclif-dev readme", + "test": "nyc --extension .ts mocha --forbid-only \"test/**/*.test.ts\"", + "version": "oclif-dev readme && git add README.md" + } } diff --git a/src/hooks/predeploy/destructive-changes.ts b/src/hooks/predeploy/destructive-changes.ts index 329f248..f89e63d 100644 --- a/src/hooks/predeploy/destructive-changes.ts +++ b/src/hooks/predeploy/destructive-changes.ts @@ -1,7 +1,10 @@ import { Command, Hook } from '@oclif/config'; +import { UX } from '@salesforce/command'; +import { SfdxProject } from '@salesforce/core'; import * as fs from 'fs'; +import * as path from 'path'; -type HookFunction = (this: Hook.Context, options: HookOptions) => any; +type HookFunction = (this: Hook.Context, options: HookOptions) => unknown; type HookOptions = { Command: Command.Class; @@ -13,25 +16,103 @@ type HookOptions = { type PreDeployResult = { [aggregateName: string]: { mdapiFilePath: string; - workspaceElements: { - fullName: string; - metadataName: string; - sourcePath: string; - state: string; - deleteSupported: boolean; - }[]; }; }; -export const hook: HookFunction = async function(options) { - console.log('PreDepoy Hook Running'); +type PluginConfig = { + enabledByDefault?: boolean; + destructiveChangesPreFile?: string; + destructiveChangesPostFile?: string; +}; + +const TEMP_PACKAGE_DIR = 'sourceDeploy_pkg'; + +export const hook: HookFunction = async function(this, options): Promise { + + try { + if (options.commandId !== 'force:source:deploy' || !options.result) { + return; + } + + const ux = await UX.create(); + + if (!(await isPluginEnabled())) { + ux.log('SFDX destructive changes plugin installed but not active'); + return; + } + + const mdapiElementNames = Object.keys(options.result); + + if (mdapiElementNames.length === 0) { + return; + } - // Run only on the push command, not the deploy command - // if (options.commandId === 'force:source:push') { - if (options.result) { - console.log(options.result); + ux.log('SFDX destructive changes plugin enabled'); + + const mdapiFilePath = options.result[mdapiElementNames[0]].mdapiFilePath; + const packageDirPath = getPackagePath(mdapiFilePath); + + const preFile = await getDestructiveChangesPreFile(); + if (preFile) { + ux.log('Adding', preFile, 'to package'); + copyDestructiveChanges(preFile, path.join(packageDirPath, 'destructiveChangesPre.xml')); + } + + const postFile = await getDestructiveChangesPostFile(); + if (postFile) { + ux.log('Adding', postFile, 'to package'); + copyDestructiveChanges(postFile, path.join(packageDirPath, 'destructiveChangesPost.xml')); + } + } catch (ex) { + this.error(ex, { exit: 1 }); } - // } }; -export default hook; \ No newline at end of file +export default hook; + +const getPluginConfig = async (): Promise => { + const project = await SfdxProject.resolve(); + const projectJson = await project.resolveProjectConfig(); + + return projectJson?.plugins?.['sfdx-destruction'] || {}; +}; + +const isPluginEnabled = async (): Promise => { + if ('SFDX_DESTRUCTION_ENABLE' in process.env) { + return (process.env['SFDX_DESTRUCTION_ENABLE'] || '').toLowerCase() === 'true'; + } else { + const pluginConfig = await getPluginConfig(); + return pluginConfig.enabledByDefault === true; + } +}; + +const getDestructiveChangesPreFile = async (): Promise => { + if ('SFDX_DESTRUCTIVE_PRE_FILE' in process.env) { + return process.env['SFDX_DESTRUCTIVE_PRE_FILE']; + } else { + const pluginConfig = await getPluginConfig(); + return pluginConfig.destructiveChangesPreFile; + } +}; + +const getDestructiveChangesPostFile = async (): Promise => { + if ('SFDX_DESTRUCTIVE_POST_FILE' in process.env) { + return process.env['SFDX_DESTRUCTIVE_POST_FILE']; + } else { + const pluginConfig = await getPluginConfig(); + return pluginConfig.destructiveChangesPostFile; + } +}; + +const getPackagePath = (mdapiFilePath: string): string => { + const packageDirName = mdapiFilePath.split(path.sep).find(dir => dir.includes(TEMP_PACKAGE_DIR)); + return mdapiFilePath.substring(0, mdapiFilePath.indexOf(packageDirName) + packageDirName.length); +}; + +const copyDestructiveChanges = (destructiveChangesFile: string, packageDirPath: string): void => { + if (!fs.existsSync(destructiveChangesFile)) { + throw new Error('Destructive changes file not found! (' + destructiveChangesFile + ')'); + } + + fs.copyFileSync(destructiveChangesFile, packageDirPath); +}; diff --git a/test/commands/hello/org.test.ts b/test/commands/hello/org.test.ts deleted file mode 100644 index 1806a04..0000000 --- a/test/commands/hello/org.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { expect, test } from '@salesforce/command/lib/test'; -import { ensureJsonMap, ensureString } from '@salesforce/ts-types'; - -describe('hello:org', () => { - test - .withOrg({ username: 'test@org.com' }, true) - .withConnectionRequest(request => { - const requestMap = ensureJsonMap(request); - if (ensureString(requestMap.url).match(/Organization/)) { - return Promise.resolve({ records: [ { Name: 'Super Awesome Org', TrialExpirationDate: '2018-03-20T23:24:11.000+0000'}] }); - } - return Promise.resolve({ records: [] }); - }) - .stdout() - .command(['hello:org', '--targetusername', 'test@org.com']) - .it('runs hello:org --targetusername test@org.com', ctx => { - expect(ctx.stdout).to.contain('Hello world! This is org: Super Awesome Org and I will be around until Tue Mar 20 2018!'); - }); -}); diff --git a/yarn.lock b/yarn.lock index 357ac8f..2a38bce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -800,7 +800,7 @@ cli-ux@^4.9.3: treeify "^1.1.0" tslib "^1.9.3" -cli-ux@^5.2.1: +cli-ux@^5.2.1, cli-ux@^5.5.1: version "5.5.1" resolved "https://registry.yarnpkg.com/cli-ux/-/cli-ux-5.5.1.tgz#99d28dae0c3ef7845fa2ea56e066a1d5fcceca9e" integrity sha512-t3DT1U1C3rArLGYLpKa3m9dr/8uKZRI8HRm/rXKL7UTjm4c+Yd9zHNWg1tP8uaJkUbhmvx5SQHwb3VWpPUVdHQ==