sfdy is a command line tool to work with the Salesforce Metadata API. It has been built to work around strange behaviors and known limitations of the API, and to simplify the implementation of a continuous integration process. It applies useful patches to avoid common problems when deploying metadata, and it exposes a simple interface to build your own plugins
- Node.js at least
10.x.x
sfdy
is meant to be mainly used as a command line tool. It can also be used as a library since it exposes a small API.
Type sfdy --help
to see available commands. Type sfdy [command] --help
to see available options for a specific command
SFDX is a tool to work with scratch orgs and with modular projects.
In a typical salesforce project, development starts in a scratch org or a classic sandbox. Even if you use scratch orgs, however, sooner or later you'll have to deploy your sfdx project to a classic sandbox (a shared, persistent development sandbox used to test integrations, a UAT sandbox, etc.). After the code is deployed to a classic sandbox, we are right back to where we started.
Moreover, DX requires the developer to break down their entire Enterprise org into individual projects, but sometimes this is not possible/advisable or the sub-projects are still too big to be worked on by a single developer. Salesforce metadata are deeply interconnected, and every module is very likely to use a subset of common functionalities (standard objects, layout, flexipages). It is often a nightmare to divide an enterprise project in modules because those modules are not independent of each other.
Finally, this tool solves some problems that SFDX does not address, and gives the developer an easy way to customize a Salesforce CI process the way HE/SHE wants. To have the best possible experience, use this tool in conjunction with the VSCode plugin fast-sfdc. patches and even your custom plugins are automatically applied in both your CI flow and your local development environment!
npm install -g sfdy
then go to the root folder of a Salesforce project, and type
sfdy init
this command creates a .sfdy.json
file within the root folder of your current workspace with the configuration of the 'standard' patches (more on this later)
- Authenticate to Salesforce
- Retrieve full metadata (based on package.xml)
- Retrieve partial metadata (glob pattern or metadata-based)
- Deploy full metadata (based on package.xml)
- Deploy partial metadata (glob pattern or diff between 2 git branches)
- Perform a quick deploy
- Deploy a destructive changeset (glob pattern or metadata-based)
- Publish a community
- Apply 'standard' patches and renderers to metadata
- Build your own plugins (pre-deploy and after-retrieve)
- Build your own renderers
- Use
sfdy
as a library
You can pass a username and password in all the available commands. For example:
sfdy retrieve -u USERNAME -p PASSWORD ...
Otherwise, just type:
sfdy auth -s
This command will start an oauth2 web server flow and will output a refresh token and an instance URL.
The -s
flag should be used when connecting to a sandbox.
The refresh token
can be used in all the available commands as an authentication method instead of username+password. Example:
sfdy retrieve --refresh-token REFRESH_TOKEN --instance-url INSTANCE_URL -s ...
If you want to avoid passing refresh token
and instance url
all the time, you can use the auth command in this way:
eval $(sfdy auth -s -e)
This will set the returned refresh token
and instance url
as environment variables. The subsequent commands will read them from the environment. Example:
sfdy retrieve -s ...
Otherwise, you can just export them manually:
sfdy auth -s
export SFDY_REFRESH_TOKEN=refreshtoken
export SFDY_INSTANCE_URL=instanceurl
By default, the SFDY connected app will be used. If you want to use yours, you can pass a user-defined client_id
and client_secret
in all the available commands.
sfdy auth --client-id CLIENT_ID --client-secret CLIENT_SECRET -s ...
sfdy retrieve --refresh-token REFRESH_TOKEN --instance-url INSTANCE_URL --client-id CLIENT_ID --client-secret CLIENT_SECRET -s ...
Or you can use the env vars:
export SFDY_REFRESH_TOKEN=refreshtoken
export SFDY_INSTANCE_URL=instanceurl
export SFDY_CLIENT_ID=clientid
export SFDY_CLIENT_SECRET=clientsecret
sfdy retrieve -s ...
Warning: You must add
http://localhost:3000/callback
as redirect_uri in your connected app setup
From the root folder of your salesforce project, type:
sfdy retrieve -u USERNAME -p PASSWORD -s
This command will retrieve all metadata specified in package.xml and will apply any enabled patch.
The -s
flag should be used when connecting to a sandbox.
sfdy retrieve -u USERNAME -p PASSWORD -s --files='objects/*,!objects/{Account,Contact}*,site*/**/*'
This command will retrieve all objects present in the local objects
folder, except those whose name starts with Account
or Contact
, and will retrieve all metadata (present in the local project) whose folder starts with site
(for example sites
, siteDotCom
)
The --files consist of a comma-separated list of glob patterns
Warning: Negated patterns always have the highest precedence
sfdy retrieve -u USERNAME -p PASSWORD -s --meta='CustomObject/Account,FlexiPage/*'
This command will retrieve the Account object and all the flexipages present on the target salesforce environment
Warning: the --meta option builds an ad-hoc package.xml to retrieve the data. Glob patterns cannot be used in this case. You can use a wildcard only if that metadata type supports it
sfdy deploy -u USERNAME -p PASSWORD -s
This command will apply any enabled pre-deploy patch and will deploy all metadata specified in package.xml.
The -s
flag should be used when connecting to a sandbox.
sfdy deploy -u USERNAME -p PASSWORD -s --files='objects/*,!objects/Account*,site*/**/*'
This command will deploy all objects present in the local objects
folder, except those whose name starts with Account
, and will deploy all metadata (present in the local project) whose folder starts with site
(for example sites
, siteDotCom
)
The --files consist of a comma-separated list of glob patterns
Warning: Negated patterns always have the highest precedence
sfdy deploy -u USERNAME -p PASSWORD -s --diff='behindBranch..aheadBranch'
The --diff
flag is used to compute the list of files that needs to be deployed by comparing 2 git branches. (examples: --diff='origin/myBranch..HEAD'
or --diff='branch1..branch2
). As an example of a use case, you can trigger a deployment to the DEV environment when you create a pull request to the dev branch. The deployment will contain only the files that have been modified in the pull-request
Warning: the --diff option requires git. To use this feature you should be versioning your Salesforce project
sfdy deploy -u USERNAME -p PASSWORD -s --diff='behindBranch..aheadBranch' --files='!siteDotCom/**/*'
This is useful if you want to perform a delta deployment skipping some metadata that could be included by the --diff
option. Typical use case: production and sandbox orgs have different versions and some metadata can't be deployed
sfdy deploy -u USERNAME -p PASSWORD -s --quick-deploy=0Af1k000028frDD
This command will perform a quick deployment of the deployment identified by the passed id
Just run a partial deployment passing the --destructive
flag
sfdy deploy -u USERNAME -p PASSWORD -s --files='objects/*,!objects/Account*,site*/**/*' --destructive
You can also run a destructive deploy changeset with a custom destructiveChanges.xml path:
sfdy deploy -u USERNAME -p PASSWORD -s --destructive <destructiveChanges.xml path>
Optionally you can pass the --ignoreWarnings
flag to ignore deploy warnings
sfdy deploy -u USERNAME -p PASSWORD -s --destructive <destructiveChanges.xml path> --ignoreWarnings
Warning: Full destructive deploy is deliberately not supported
Warning: This command deletes the metadata files from Salesforce, but they remain on the filesystem
sfdy community:publish -u USERNAME -p PASSWORD -s --community-name=myCoolCommunity
This command will programmatically publish your Experience Bundle. Execute it in a pipeline after a deployment to avoid the manual publish step!
Sfdy provides several ready-to-use patches that you may find useful. All these patches serve 2 purposes:
- Remove useless metadata (not translated fields, useless FLS in permission sets, roles that are automatically managed by salesforce, profile permissions in standard profiles, and stuff created by managed packages at installation time)
- Add useful metadata. We want our repo to be the 'source of truth' (ALL profile permissions, not only the enabled ones. ALL object permissions. Profile configuration of objects/applications/tabs that we DON'T want to version because they're not used or we're not the maintainer of that metadata)
All of these patches can be disabled, so you can incrementally adopt them or skip a specific patch if you don't find it useful.
In addition to metadata patching, sfdy
provides an out-of-the-box renderer to handle static resource bundles.
First of all, create the configuration file .sfdy.json
in the root folder of the salesforce project:
sfdy init
The configuration file is a JSON object:
{
"permissionSets": {
"stripUselessFls": true
},
"objectTranslations": {
"stripUntranslatedFields": true,
"stripNotVersionedFields": true
},
"preDeployPlugins": [],
"postRetrievePlugins": [],
"renderers": [],
"profiles": {
"addAllUserPermissions": false,
"addDisabledVersionedObjects": true,
"addExtraObjects": ["*", "!*__?", "!CommSubscription*"],
"addExtraTabVisibility": ["standard-*"],
"addExtraApplications": ["standard__*"],
"stripUserPermissionsFromStandardProfiles": true,
"stripUnversionedStuff": true
},
"roles": {
"stripPartnerRoles": true
},
"staticResources": {
"useBundleRenderer": ["*.resource"]
},
"stripManagedPackageFields": ["et4ae5"]
}
Patch | Description |
---|---|
stripUselessFls | if stripUselessFls is true , fieldPermissions in which both readable and editable tags are false are removed from the XML. They are totally redundand since a PermissionSet can only add permissions. |
Patch | Metadata | Description |
---|---|---|
stripUntranslatedFields | Translations, CustomObjectTranslation, GlobalValueSetTranslation, StandardValueSetTranslation | if stripUntranslatedFields is true , untranslated tags are removed from the XML. |
stripNotVersionedFields | CustomObjectTranslation | if stripNotVersionedFields is true , translated fields that are not present in the file system in the corresponding .object files, are removed from the XML. |
Patch | Description |
---|---|
addAllUserPermissions | Salesforce does not retrieve disabled userPermissions . If addAllUserPermissions is true , all permissions are retrieved |
addDisabledVersionedObjects | Salesforce does not retrieve totally disabled objects. If addDisabledVersionedObjects is true , sfdy retrieves also objectsPermissions of objects that are present in the file system (and of course tracked by version control systems)) but are disabled for the profile |
addExtraObjects | Sometimes you want to explicitly configure the access level to some objects even if you're not interested in versioning the whole object metadata. Now you can. addExtraObjects is an array of glob patterns of the objects of which objectPermissions you want to add to the profile (the glob patterns match against the <member> content in package.xml ) |
addExtraTabVisibility | Sometimes you want to explicitly set the TabVisibility of some tabs even if you're not interested in versioning the object/tab metadata. Now you can. addExtraTabVisibility is an array of glob patterns of the tabs whose tabVisibilities you want to add to the profile (the glob patterns match against the <member> content in package.xml ) |
stripUserPermissionsFromStandardProfiles | User Permissions are not editable in standard profiles, and they change almost every Salesforce release causing errors that can be avoided. Set this flag to true to automatically remove them |
stripUnversionedStuff | This flag 'sanitizes' the profiles, removing fieldPermissions , classAccesses , pageAccesses , layoutAssignments that are not related to stuff tracked under version control. I can't really see any reason not to enable this option, that can help avoiding errors made by developers during code/metadata versioning |
Patch | Description |
---|---|
stripPartnerRoles | if stripPartnerRoles is true , roles that end with PartnerUser[0-9]*.role are removed even if a * is used in package.xml . They are automatically created by Salesforce when you create a Partner Account, so there's no need to track them them using version control |
Renderer | Metadata | Description |
---|---|---|
useBundleRenderer | StaticResource | glob pattern to identify static resource files to handle as an uncompressed bundle. The contentType of the .resource-meta.xml file must be application/zip . This renderer retrieves directly the uncompressed folder instead of the .resource file. If you deploy a single file inside the bundle, the .resource file is rebuilded behind the scenes and deployed in place of the single specified file |
Patch | Metadata | Description |
---|---|---|
stripManagedPackageFields | CustomObject, PermissionSet, Profile | Array of namespaces of stuff created by managed packages (eg Marketing Cloud) that we don't want to track changes using Version Control. This plugin removes fields , picklistValues , weblinks from CustomObject and fieldPermissions from Profile and PermisissionSet |
sfdy
offers a convenient way to develop your own plugins. This is useful in many cases. A simple use case might be changing named credentials endpoints or email addresses in the workflow's email alerts based on the target org, but the possibilities are endless. You can even query salesforce (rest API or tooling API) to conditionally apply transformations to the metadata based on information coming from the target org.
All the standard plugins are built using the plugin engine of sfdy
, so the best reference to understand how to develop a custom plugin is to look at the plugins folder in which all the standard plugins reside.
A plugin is a .js
module that exports a function with this signature:
module.exports = async (context, helpers, utils) => {
//TODO -> Plugin implementation
}
sfdcConnector
- an instance of a Salesforce connector. It exposes 2 methods,query
andrest
environment
- The value of the environment variableenvironment
. It can be used to execute different patches in different sandboxesusername
- The username used to loginlog
- Alog
function that should be used instead ofconsole.log
if you want to log something. The reason is that, when used as a library,sfdy
can accept a custom logger implementation. When used as a command line tool, thelog
function fallbacks toconsole.log
pkg
- A JSON representation of thepackage.xml
config
- The content of.sfdy.json
(as a JSON object)
xmlTransformer (pattern, callback1)
- This helper allows the developer to easily transform one or more metadata (identified bypattern
), using acallback
function. See examples to understand how to use itmodifyRawContent (pattern, callback2)
- This helper allows the developer to manipulate the whole metadata file. It is useful if you want to edit a file that is not an XML, or if you want to apply drastic transformationsfilterMetadata (filterFn)
- This helper can be used in a post-retrieve plugin to filter out unwanted metadatarequireMetadata (pattern, callback3)
- This helper can be used to define dependencies between metadata. For example, aProfile
must be retrieved together withCustomObject
metadata to also get the list offieldPermissions
. By defining such a dependency usingrequireMetadata
, whenever you retrieve aProfile
, all dependent metadata are automatically included in thepackage.xml
and eventually discarded at the end of the retrieve operation, just to retrieve all the related parts of the original metadata you wanted to retrieveaddRemapper (regex, callback4)
- This helper can be used to map an arbitrary file to a file representing Salesforce metadata. For example, it can be used to instructsfdy
to deploy/retrieve a.resource
file when you deploy/retrieve a file inside an uncompressed bundle.regex
is aRegExp
object defining the matching patterns
filename
- The current filenamefJson
- JSON representation of the XML. You can modify the object to modify the XMLrequireFiles (filenames: string[]): Promise<Entry[]>
- An async function taking an array of glob patterns and returning an array of{ fileName: string, data: Buffer }
objects representing files. The files are taken from memory if you are requiring files that you are retrieving/deploying, otherwise, they are searched in the filesystem. These files will be added to the files that will be retrieved/deployed unless you specify a filter with thefilterMetadata
helper. This helper is useful when you want to act on metadata based on another one (for example you need to retrieve the versioned fields from a.object
file to add/delete FLS fromprofiles
).addFiles (entries: Entry[])
- A function taking an array of{ fileName: string, data: Buffer }
objects representing files. This function is similar torequireFiles
. The main difference is thatrequireFiles
looks for existing files, whileaddFiles
let you add arbitrary data to the retrieved/deployed files. For this reason, it is best suited to be used in arenderer
cleanFiles (filenames: string[])
- A function taking an array of glob patterns. This function lets you specify files that should be deleted. It should be used in the context of an after-retrieve plugin or a transform renderer (for example it is used to clean up an uncompressed staticresource bundle before uncompressing the.resource
file coming from Salesforce)
filename
- The current filenamefile
is an object containing adata
field.data
is a buffer containing the whole file. You can modifydata
to modify the filerequireFiles (filenames: string[]): Promise<Entry[]>
- See callback1addFiles (entries: Entry[])
- See callback1cleanFiles (filenames: string[])
- See callback1
filename: string
- The current filename, including the path (for exampleclasses/MyClass.cls
)
-
filterPackage (arrayOfMetadata: string[])
- A function taking an array of metadata that should be included together with metadata matched bypattern
. The 'companions' will be retrieved only if they are present in the storedpackage.xml
. For example, if you retrieve a profile, the profile will be retrieved together with the referencedCustomObject
-
requirePackage (arrayOfMetadata: string[])
- The same asfilterPackage
, but the included metadata will be added topackage.xml
whether they were present before or not. In this case,arrayOfMetadata
is an array of 'pseudo' glob patterns (ex.['CustomApplication/*', 'CustomObject/Account']
)
fileName: string
- The current filename, including the path (for exampleclasses/MyClass.cls
)regexp: RegExp
- The regexp originally passed to the helper- return value: a string representing the mapped filename
Helpers function. See here
To instruct sfdy
to use your plugin, you have to configure the path of your plugin in the .sfdy.json
file:
{
"preDeployPlugins": ["sfdy-plugins/my-awesome-plugin.js"],
"postRetrievePlugins": ["sfdy-plugins/my-wonderful-plugin.js"]
}
You have 2 different 'hooks' to choose from:
postRetrievePlugins
are executed just before the metadata retrieved from Salesforce is stored on the filesystempreDeployPlugins
are executed before deploying metadata to Salesforce
module.exports = async ({ environment, log }, helpers) => {
helpers.xmlTransformer('namedCredentials/*', async (filename, fJson) => {
log(`Patching ${filename}...`)
if(filename === 'idontwanttochangethis.NamedCredential') return
switch(environment) {
case 'uat':
fJson.endpoint = 'https://uat-endpoint.com/restservice'
break
case 'prod':
fJson.endpoint = 'https://prod-endpoint.com/restservice'
break
default:
fJson.endpoint = 'https://test-endpoint.com/restservice'
break
log('Done')
}
})
}
Remove every field and every apex class that starts with Test_
(better suited as a postRetrievePlugin
)
module.exports = async ({ environment, log }, helpers) => {
helpers.xmlTransformer('objects/*', async (filename, fJson) => {
log(`Patching ${filename}...`)
fJson.fields = (fJson.fields || []).filter(field => !field.FullName[0].startsWith('Test_'))
log('Done')
})
helpers.filterMetadata(fileName => !/classes\/Test_[^\/]+\.cls$/.test(fileName))
}
Warning: fJson contains the json representation of the metadata file. The root tag of the metadata is omitted for convenience. Every tag is treated as an array
Warning: The callback function ot the
xmlTransformer
helper MUST return aPromise
See this
See this
A renderer is a .js
file that exports an object with this signature:
module.exports = {
transform: async (context, helpers, utils) => {
//TODO -> Transform
},
untransform: async (context, helpers, utils) => {
//TODO -> Untransform
}
}
The transform
function is applied after the retrieve operation and after the execution of the post-retrieve plugins. The untransform
function is applied as soon as you start a deployment, before the application of the pre-deploy plugins and the actual deployment.
A renderer can be used to transform the metadata in the format you like. For example, you could think to split a .object
file into different files, one for fields
and one per recordtypes
, or to even convert everything in JSON, or represent some information as a .csv
file. You can do what best fit your needs.
To instruct sfdy
to use your renderer, you have to configure the path of your renderer in the .sfdy.json
file:
{
"renderers": ["sfdy-plugins/my-awesome-renderer.js"]
}
Tip: You do not have to include the renderer whithin your salesforce project to be able to use it, so you can use your plugin referencing it from all of your project workspaces!
module.exports = {
transform: async (context, helpers, { parseXmlNoArray }) => {
helpers.modifyRawContent('profiles/*', async (filename, file) => {
const fJson = await parseXmlNoArray(file.data)
file.data = Buffer.from(JSON.stringify(fJson, null, 2), 'utf8')
})
},
untransform: async (context, helpers, { buildXml }) => {
helpers.modifyRawContent('profiles/*', async (filename, file) => {
const fJson = JSON.parse(file.data.toString())
file.data = Buffer.from(buildXml(fJson), 'utf8')
})
}
}
See here
It's as simple as that:
npm i sfdy
const retrieve = require('sfdy/src/retrieve')
retrieve({
basePath: 'root/folder',
config: {
//.sfdy.json like config
},
files: [ /*specific files*/ ],
loginOpts: {
serverUrl: creds.url,
username: creds.username,
password: creds.password
},
meta: [/*specific meta*/]
logger: (msg: string) => logger.appendLine(msg)
}).then(() => console.log('Done!'))
const deploy = require('sfdy/src/deploy')
deploy({
logger: (msg: string) => logger.appendLine(msg),
preDeployPlugins,
renderers,
basePath: 'root/folder',
loginOpts: {
serverUrl: creds.url,
username: creds.username,
password: creds.password
},
checkOnly,
files: ['specific', 'files']
}).then(() => console.log('Done!'))
-
1.7.8
- Fix error when retrieving profiles that contain
'
- Fix error when retrieving profiles that contain
-
1.7.7
- Fix quick deploy polling
-
1.7.6
- Fix quick deploy polling
-
1.7.5
- Printing deployment id
- Better error reporting when package.xml is corrupt
-
1.7.4
- Bugfixing
-
1.7.3
- Bugfixing
-
1.7.2
- Handling
Territory2*
metadata, that works differently from everything else - Updated dependencies to fix vulnerabilities
- Handling
-
1.7.1
- Added
--ignoreWarnings
option tosfdy deploy
to ignore deployment warnings. If you don't pass this option, a warning during the deployment will cause the entire deployment to fail. Thanks, maiantialberto - Corrected several typos in the documentation
- Added
-
1.7.0
- Added
community:publish
command to publish an experience bundle after deployment - Added
--quick-deploy=deploymentId
option tosfdy deploy
to perform a quick deploy
- Added
-
1.6.5
- Fix instanceUrl redirection to new
*.sandbox.my.salesforce.com
domains when using oauth2
- Fix instanceUrl redirection to new
-
1.6.4
- Faster getListOfSrcFiles when using
**/*
- Added web scope to Oauth2 auth
- Faster
PermissionSet
retrieval
- Faster getListOfSrcFiles when using
-
1.6.3
- Fixed plugins context variable when OAuth2 flow is used
-
1.6.2
- Fixed broken soapLogin
-
1.6.1
- Minor bug fixing
-
1.6.0
- Oauth2 web server auth flow: get a refresh token using an oauth2 flow. If you have enabled MFA you should use this auth method
-
1.5.3
- Minor bug fixing
-
1.5.2
- Retrieve: When
--files
option is used, the real list of files is shown instead of the raw glob patterns - Bugfixing: glob expression parser now recognizes expressions with {}
- Bugfixing: negated glob patterns have the highest priority
- Bugfixing: fix retrieve of in-folder metadata (for example a report in a folder) when using
--meta
- Retrieve: When
-
1.5.1
- Bugfixing: fix deployment of static resource bundle
-
1.5.0
- Deploy: added the possibility to skip some files from deploying. To do that, add for example
"excludeFiles": ["lwc/**/__tests__/**/*"]
to.sfdy.json
- Deploy: added the possibility to skip some files from deploying. To do that, add for example
-
1.4.7
- Bugfixing: fix another regression handling folder in an in-folder metadata
-
1.4.6
- Bugfixing: fix regression handling folders in in-folder metadata
-
1.4.5
- Bugfixing: solved the 'multiple metadata types in same folder' issue. It is now possible to retrieve and deploy correctly wave metadata
-
1.4.4
-
1.4.3
- Bugfixing: fixed exclusion glob pattern when using
--files
option
- Bugfixing: fixed exclusion glob pattern when using
-
1.4.2
- Bugfixing: fixed issue when deploying ExperienceBundle
-
1.4.1
- Bugfixing: fixed issue when deploying with --diff a report in a nested folder
-
1.4.0
- Transformer API. API to load unrendered files in memory
-
1.3.6
- Bugfixing:
--diff
and--files
flag can be used together
- Bugfixing:
-
1.3.5
-
1.3.4
- Bugfixing: Delta deployment of in-folder metadata (Report, Document, Email, Dashboard) fails
- Minor Bugfixing
-
1.3.3
- Bugfixing
--files
option: now you can pass specific file paths (not glob patterns) even if the files are not present in the filesystem
-
1.3.2
- README.md fixes
- Static resource bundle renderer cleans
.resource
file when active, and the uncompressed folder when inactive
-
1.3.1
- README.md fixes
-
1.3.0
-
1.2.0
- destructive changesets support
README.md
improvements. Thanks, tr4uma
-
1.1.0
- First release