SFDX plugin to generate Incremental Salesforce deployments manifests and artifacts
Report Bug
·
Request Feature
·
Become a Sponsor
Warning
Potentially breaking changes in v6: Check out the v6 migration guide to see how you could be impacted by some changes in the new major v6 version of the plugin, and how to migrate to this version.
Warning
Breaking change for programmatic consumers: Config.mergeBase is now a required field. Add mergeBase: false to your config to preserve today's behavior. See the merge-base migration guide for details.
Table of Contents
- TL;DR
- What is SFDX-Git-Delta?
- Is SGD for you?
- Getting Started
- How to use it?
sf sgd source delta- Walkthrough
- Advanced use-cases
- Test Changes Without Committing
- Generate a folder containing only the added/modified sources
- Exclude some metadata only from destructiveChanges.xml
- Explicitly including specific files for inclusion or destruction regardless of diff
- Scoping delta generation to a specific folder
- Scope delta generation to the sfdx-project.json package directories
- Run specified tests using the SGD manifest
- Condition deployment on package.xml and destructiveChange content
- Use the module in your own node application
- Handle flow deletion
- Decomposed Metadata Types
- Debugging
- Changelog
- Built With
- Used by
- Versioning
- Authors
- Contributing
- License
sf plugins install sfdx-git-deltasf sgd source delta --to "HEAD" --from "HEAD~1" --output-dir "."sf project deploy start -x package/package.xml --post-destructive-changes destructiveChanges/destructiveChanges.xmlSFDX-Git-Delta (a.k.a. SGD) helps Salesforce Architects and Developers do 2 things with their source deployments:
-
Make deployments faster: identify the changed metadata since a reference commit.
-
Automate destructive deployments: build the destructiveChanges.xml from the deleted (or renamed) metadata
Have a look at this post on the Salesforce Developers Blog to dive into it: Optimizing Unpackaged Deployments Using a Delta Generation Tool.
If you are not a Salesforce Architect or Developer, probably not, sorry.
If you are a Technical Architect or Developer, then it’s a very useful tool for you, when meeting the 4 conditions below:
- Your Salesforce project uses a git repo as the source of truth.
- You use the Source (DX) format in the repo.
- Your metadata is unmanaged (in other words, you are not building a managed or unlocked package).
- Your repository is deployable at both
--fromand--to. A state that cannot be deployed (for example, two files declaring the same component in one package directory) gives an unspecified delta.
SGD is designed to be part of a CI/CD pipeline (Jenkins, Bitbucket Pipelines, GitLab CI, GitHub Actions, Azure DevOps...) that handles the deployment of the sources to the Salesforce org(s).
Pro tip: Make sure your pipeline works before implementing incremental deployments. Otherwise, it will just make it harder to debug your pipeline. It's also important to implement a way to switch back to full deployment in case the incremental deployment does not behave as expected.
DISCLAIMER:
👷 Use it at your own risk, wear a helmet, and test it first before adding it to your pipeline 🔥
No git binary is required: the plugin reads the repository's git object store in-process, including --ref-format=reftable and --object-format=sha256 repositories (the latter isn't hosted by GitHub or GitLab today, so you're unlikely to run into one).
Node v22 or above is required.
To check if Salesforce CLI runs under a supported node version for SGD, run sf --version. You should see a node version of v22 or above to use SGD.
If you encounter this issue whereas the node version is OK on the running environment, try to install the Salesforce CLI via npm (npm install @salesforce/cli --global).
SGD is a Salesforce CLI plugin (sf sgd source delta). Run the following command to install it:
sf plugins install sfdx-git-deltaBecause this plugin is not signed, you will get a warning saying: "This plugin is not digitally signed and its authenticity cannot be verified". This is expected, and you will have to answer y (yes) to proceed.
If you run your CI/CD jobs inside a Docker image, you can add the plugin to your image (such as in this example). If you use GitHub Actions, you can find some examples of using SGD here.
Generate incremental package manifest and source content
USAGE
$ sf sgd source delta -f <value> [--json] [--flags-dir <value>] [-t <value>] [-b] [-d] [-o <value>] [-r <value>] [-s
<value>...] [-i <value>] [-D <value>] [-n <value>] [-N <value>] [-M <value>] [-c <value>] [-W] [-a <value>]
FLAGS
-D, --ignore-destructive-file=<value> file listing paths to explicitly ignore for any destructive actions
-M, --additional-metadata-registry=<value> file containing additional metadata definitions to add to the registry
-N, --include-destructive-file=<value> file listing paths to explicitly include for any destructive actions
-W, --ignore-whitespace ignore git diff whitespace (space, tab, eol) changes
-a, --api-version=<value> salesforce metadata API version, default to sfdx-project.json
"sourceApiVersion" at --to or latest version
-b, --merge-base diff from the merge base of --from and --to instead of --from itself (git
three-dot semantics)
-c, --changes-manifest=<value> path to a JSON file grouping changed components by kind (add, modify,
delete, rename); setting this flag also enables git rename detection
-d, --generate-delta generate delta files in [--output-dir] folder
-f, --from=<value> (required) commit sha from where the diff is done
-i, --ignore-file=<value> file listing paths to explicitly ignore for any diff actions
-n, --include-file=<value> file listing paths to explicitly include for any diff actions
-o, --output-dir=<value> [default: ./output] source package specific output
-r, --repo-dir=<value> [default: ./] git repository location
-s, --source-dir=<value>... [default: ./] source folders focus location relative to --repo-dir
-t, --to=<value> [default: HEAD] commit sha to where the diff is done
GLOBAL FLAGS
--flags-dir=<value> Import flag values from a directory.
--json Format output as json.
DESCRIPTION
Generate incremental package manifest and source content
Use two git commit reference to generate the package corresponding to what has changed in between
EXAMPLES
- Build incremental manifest from the previous commit
$ sf sgd source delta --from "origin/development" --output-dir incremental
- Build incremental manifest and source from the development branch
$ sf sgd source delta --from "origin/development" --generate-delta --output-dir incremental
- Build incremental manifest from where the current branch diverged from main
$ sf sgd source delta --from "main" --merge-base --output-dir incremental
FLAG DESCRIPTIONS
-a, --api-version=<value>
salesforce metadata API version, default to sfdx-project.json "sourceApiVersion" at --to or latest version
Override the api version used for api requests made by this command
-s, --source-dir=<value>... source folders focus location relative to --repo-dir
You can use this flag multiple times to include different folders that contain source files. Each path should be
relative to --repo-dir.
The folder can exist or not.
* If the folder exists, its contents will be processed.
* If the folder doesn't exist, it usually won't show any output—unless the folder was recently deleted and is part
of a diff, in which case changes may still be picked up.
Each value must be a literal repository-relative path: wildcards (`*`, `?`, `[`), git pathspec magic (`:(...)`),
absolute paths, `..` and the empty string are all rejected. Matching is rooted at the repository root, so
`--source-dir force-app` does not match `nested/force-app/...`.
See code: src/commands/sgd/source/delta.ts
If you run SGD on a Windows system, use double quotes to prevent the terminal to interpret parameters
You should also avoid using the "^" character (shorthand for parent commit in git) because it is the escape character in Windows. So instead of:
sf sgd source delta --from "HEAD^" # wrong git shortcut with windows because it uses "^" syntaxYou should write:
sf sgd source delta --from "HEAD~1" # right git shortcut with windows because it does not use "^", it uses "~n" syntaxIn CI/CD pipelines, for most of the CI/CD providers, the checkout operation fetches only the last commit of the branch currently evaluated.
You need to fetch all the needed commits as the plugin needs access to the branch being compared.
Example for Github action checkout here.
With --merge-base [-b], a shallow clone is an even sharper trap: --from and --to can each resolve to a valid commit while their histories still don't connect within the fetched depth, so SGD reports "no merge base found" even though the refs themselves are fine. Fetch enough history for the two branches to actually share an ancestor, not just enough for each ref to resolve.
GitHub Actions' pull_request trigger checks out a synthetic merge commit whose parent already is the base branch tip, so merge-base(HEAD, main) resolves to main's tip and --merge-base silently becomes a no-op (--from main --merge-base behaves like --from main). If you need true divergence semantics in that context, target the PR head commit directly — --to <head sha> (from the pull_request event payload) — or check out ref: <head sha> instead of the default merge ref.
In CI/CD pipelines, branches are not checked out locally when the repository is cloned, so you must specify the remote prefix.
If you do not specify the remote in CI context, the git pointer check will raise an error (as the branch is not created locally).
This applies to both --from and --to parameters as they both accept git pointers.
Example comparing HEAD with a development branch when the CI clones the repository with origin set as reference to the remote:
sf sgd source delta --to "HEAD" --from "origin/development" --output-dir .Use a global variable when you need to easily switch sgd version (vX.X.X format) or channel (stable, latest, latest-rc) in your pipeline, without having to commit a new version of your pipeline.
Example with github action, create a variable SGD_VERSION and use it in the plugin installation phase
- name: Install SGD
run: echo y | sf plugins install "sfdx-git-delta@${{ vars.SGD_VERSION }}"The plugin is compatible with git LFS.
It can read content from LFS locally.
It is the user responsibility to ensure LFS content is present when the plugin is executed.
Any git sha pointer is supported as long as it resolves to a commit: commit sha, branch, tag (an annotated tag is followed to the commit it points at), git expression (HEAD, HEAD~1, etc.). A tree or blob expression such as HEAD^{tree} or HEAD:path is rejected with an error naming the flag and the value you typed.
--from parameter is the base commit (the first, the oldest, the closest)
--to parameter is the target commit (the last, the youngest, the farthest)
If you want to deploy incrementally the content of a PR, --from parameter will be the base branch the PR branch wants to merge to, and --to parameter will be the PR branch.
Here are examples of how to compare the content of different branches:
- Comparing between commits in different branches
For example, if you have commit
fbc3ade6in branchdevelopand commit61f235b1in branchmain:
sf sgd source delta --to fbc3ade6 --from 61f235b1 --output-dir .- Comparing branches (all changes)
Comparing all changes between the
developbranch and themainbranch:
sf sgd source delta --to develop --from main --output-dir .- Comparing branches (from a common ancestor)
To compare the
developbranch since its common ancestor with themainbranch (i.e. ignoring the changes performed in themainbranch afterdevelopcreation), use--merge-base [-b]:
sf sgd source delta --to develop --from main --merge-base --output-dir .This is equivalent to git diff main...develop (three-dot, common-ancestor semantics). Unlike the $(git merge-base ...) shell form, --merge-base resolves inside --repo-dir and needs no shell — the shell form always resolves in the current directory regardless of --repo-dir.
Consider the following scenario:
The CI pipeline deploys the sources to Production anytime there is a new commit in the main branch.
In our example, the latest commit to main is composed of:
- Apex Class added: TriggerHandler
- Apex Class added: TriggerHandler_Test
- Apex Class modified: TestDataFactory
- Apex Class deleted: AnotherTriggerFramework
In this situation, we would expect the CI pipeline to:
- Deploy to Production only 3 classes (no matter how much metadata is present in the force-app folder):
TriggerHandler,TriggerHandler_Test, andTestDataFactory - Delete from Production 1 class:
AnotherTriggerFramework
So let’s do it!
From the project repo folder, the CI pipeline will run the following command:
sf sgd source delta --to "HEAD" --from "HEAD~1" --output-dir .which means:
Analyze the difference between HEAD (latest commit) and HEAD~1 (previous commit), and output the result in the current folder.
The sf sgd source delta command produces 2 useful artifacts:
1) A package.xml file, inside a package folder. This package.xml file contains just the added/changed metadata to deploy to the target org.
Content of the package.xml file in our scenario:

2) A destructiveChanges.xml file, inside a destructiveChanges folder. This destructiveChanges.xml file contains just the removed/renamed metadata to delete from the target org. Note: the destructiveChanges folder also contains a minimal package.xml file, because deploying destructive changes requires a package.xml (even an empty one).
Content of the destructiveChanges.xml file in our scenario:

Note: it is also possible to generate a source folder containing added/changed metadata with the --generate-delta (-d) parameter. See the "Advanced use-cases" section for more examples.
The simplest option to deploy the incremental changes is to use force:source:deploy command with -x parameter:
sf project deploy start -x package/package.xml --post-destructive-changes destructiveChanges/destructiveChanges.xmlAnd voilà! 🥳
However, keep in mind that the above command will fail if the destructive change was supposed to be executed before the deployment (i.e. as --pre-destructive-changes), or if a warning occurs during deployment. Make sure to protect your CI/CD pipeline from those scenarios, so that it doesn't get stuck by a failed destructive change.
If needed, you can also split the added/modified metadata deployment from the deleted/renamed metadata deployment, as in the below examples:
Use the package/package.xml file to deploy only the added/modified metadata:
echo "--- package.xml generated with added and modified metadata ---"
cat package/package.xml
echo
echo "---- Deploying added and modified metadata ----"
sf project deploy start -x package/package.xmlUse the destructiveChanges folder to deploy only the destructive changes:
echo "--- destructiveChanges.xml generated with deleted metadata ---"
cat destructiveChanges/destructiveChanges.xml
echo
echo "--- Deleting removed metadata ---"
sf project deploy start --pre-destructive-changes destructiveChanges/destructiveChanges.xml --manifest destructiveChanges/package.xml --ignore-warningsTo check changes that aren't committed yet:
- Make a temporary commit:
git add '<files>'
git commit -m 'temp: testing changes'- Run SGD:
sf sgd source delta --from <sha-pointer>- Undo the temporary commit while keeping your changes:
git reset --soft HEAD~1Tip: For extra safety, do this in a temporary branch.
Using a package.xml for deployment is the simplest approach to delta deployments. But in some cases you may want to have only the actual recently changed source files.
One example is to speed up object deployments: the package.xml approach will deploy the entire sub-folder for a given object. Having a copy of the actual sources added/modified allows you to deploy only those components.
This is where the --generate-delta (-d) option comes handy!
Let's use this option with our previous example:
mkdir changed-sources
sf sgd source delta --to "HEAD" --from "HEAD~1" --output-dir changed-sources/ --generate-deltaIt generates the package and destructiveChanges folders, and copies added/changed files in the output folder.
Content of the output folder when using the --generate-delta option, with the same scenario as above:
⚠️ Use--generate-delta (-d)when--to (-t)value is set to "HEAD" or to the "HEAD commit SHA". If you need to use it with--to (-t)pointing to another commit than "HEAD", checkout that commit first. Example:# move HEAD to the wanted past commit $ git checkout <not-HEAD-commit-sha> # You can omit --to, it will take "HEAD" as default value $ sf sgd source delta --from "HEAD~1" --output-dir changed-sources/ --generate-delta
Then it is possible to deploy the change-sources folder using force:source:deploy command with -p parameter:
sf project deploy start -p change-sourcesThe --ignore-file [-i] parameter allows you to specify an ignore file to filter the
element on the diff to ignore. SGD ignores every diff line matching the pattern from the ignore file specified in the --ignore-file [-i]. package.xml generation, destructiveChanges.xml generation and --delta-generate will ignore those lines.
Moving a component into a directory --ignore-file [-i] covers is treated as a move, not a deletion: SGD holds the ignored addition and, when nothing else of that component is visible at --to, cancels the matching deletion so neither manifest gets an entry. If the component still has a file outside the ignored patterns at --to, the ignored copy is stale and the deletion is reported as usual.
# ignorefile
force-app/recycle-bin/**
$ git mv force-app/main/default/classes/AccountService.cls force-app/recycle-bin/classes/AccountService.cls
$ git mv force-app/main/default/classes/AccountService.cls-meta.xml force-app/recycle-bin/classes/AccountService.cls-meta.xml
$ git commit -m "retire AccountService"
$ sf sgd source delta --from HEAD~1 --ignore-file ignorefileBefore this behavior, destructiveChanges.xml listed ApexClass/AccountService and package.xml was empty. Now both are empty: the class moved into a directory SGD ignores, it wasn't deleted.
Sometimes you may need to have two different ignore policies. One for the package.xml and another one for destructiveChanges.xml files. This is where the --ignore-destructive-file [-D] option comes handy!
Use the --ignore-destructive-file parameter to specify a dedicated ignore file to handle deletions. It will apply to metadata listed in the destructiveChanges.xml. In other words, this will override the --ignore-file [-i] parameter for deleted items.
Consider the following:
- a repository containing many sub-folders (force-app/main, force-app/sample, etc)
- a commit deleting the Custom__c object from one folder and modifying the Custom__c object from another folder. This is a Modification and a Deletion events.
The Custom__c object appears in the package.xml and in destructiveChanges.xml and fail the deployment. This is a situation where your may want to use the --ignore-destructive-file [-D] parameter! Add the Custom__c object pattern in an ignore file and pass it in the CLI parameter:
# destructiveignore
*Custom\_\_c.object-meta.xml
$ sf sgd source delta --from commit --ignore-destructive-file destructiveignore
--ignore-destructive-file [-D] also covers sub-components deleted inside a modified file (a label removed from CustomLabels.labels-meta.xml, a workflow rule removed from a .workflow-meta.xml, etc.), while the additions and modifications of that same file still reach package.xml. Patterns match file paths, so a pattern matching CustomLabels.labels-meta.xml keeps every label deleted from that file out of destructiveChanges.xml, not a single label.
--ignore-destructive-file [-D] does not affect the move detection described above. Whether a moved addition can cancel its deletion is decided purely against the --ignore-file [-i] patterns — the same file that held the addition in the first place — never the destructive-only one. A permissive --ignore-destructive-file [-D] does not turn the move back into a destructive change: as long as the component has no file outside --ignore-file [-i]'s patterns at --to, the deletion still cancels.
If SGD cannot read the file listing at --to, the move check is skipped, the deletion is reported as usual, and SGD emits a warning noting the check could not run.
Note: when only using the --ignore-file [-i] parameter (and not --ignore-destructive-file [-D]) the plugin will apply it to added/changed/deleted elements.
Independently of any ignore file, SGD also keeps components of a type the Metadata API cannot delete — today only RecordType — out of destructiveChanges.xml on every route: an ordinary deletion, a rename, and --include-destructive-file alike. A destructive entry for one of these is a component failure (Cannot delete record type through API) that fails the whole deployment. The component is not deleted anywhere; it is only omitted from the manifest SGD would otherwise emit, so it remains in the target org until you remove it by hand in Setup. SGD raises a warning naming the type and the affected members:
Warning: The Metadata API cannot delete 'RecordType' components, so destructiveChanges.xml omits 'Account.Alpha'. Remove them from the target org manually in Setup.
A component moved to another directory is not a change: SGD recognizes the move and leaves it out of package.xml and destructiveChanges.xml, so a refactoring commit does not redeploy (or delete) what did not change. A move is suppressed when all of these hold:
- the deleted and the added files declare the same type and member;
- every file is identical byte for byte, and the files keep the same layout relative to each other;
- the component is complete: the move carries every file of the component;
- the component stays in the same package directory of
sfdx-project.json, each side being read at its own revision (when neither revision has ansfdx-project.json, all paths count as one package directory); - the source is visible to the ignore files: a source under
--ignore-file [-i], or only under--ignore-destructive-file [-D], is deployed as usual. A destination under--ignore-destructive-file [-D]alone is still suppressed.
$ git mv force-app/main/default/classes/AccountService.cls force-app/shared/classes/AccountService.cls
$ git mv force-app/main/default/classes/AccountService.cls-meta.xml force-app/shared/classes/AccountService.cls-meta.xml
$ git commit -m "move AccountService"
$ sf sgd source delta --from HEAD~1Both manifests are empty. A move that also edits a file, leaves a file behind, or crosses package directories is not a pure move: it is reported as an addition and a deletion, as before.
Labels are different, because a CustomLabels file holds many components. When a labels file is moved inside a package directory (deleted at one path, added at another), SGD compares the labels of all the CustomLabels files of that package directory before and after the change: labels that only moved are not redeployed, changed or added labels reach package.xml, and a label gone from every file reaches destructiveChanges.xml.
<!-- before: force-app/main/labels/CustomLabels.labels-meta.xml has L1, L2 -->
<!-- after: force-app/shared/labels/CustomLabels.labels-meta.xml has L1 -->The file moved and L2 was removed: package.xml stays empty and destructiveChanges.xml lists CustomLabel/L2. Merging two moved labels files into one gives the same result: only the labels that were changed or added reach package.xml, only the labels that vanished reach destructiveChanges.xml. Labels files edited in place, with no file moved, are compared file by file, as before.
A label moved to a file of another package directory is not a move: every label of the moved file deploys. With --ignore-file [-i], a labels file matched by the pattern is out of the comparison and a label moved into it emits nothing; with --ignore-destructive-file [-D], labels still deploy and only the deletions are filtered. A labels file whose package directory changed between --from and --to is left out of the comparison. Other types stored in one file (workflows, sharing rules...) follow the same rule file by file.
If sfdx-project.json or the file listing cannot be read at either revision, the move check is skipped for the run, moved components are deployed as if they had changed, and SGD emits a warning that names the cause.
The --include-file [-n] parameter allows you to specify a file based on gitignore glob matching to include specific files. Regardless whether they appears in the diff or not.
Like the --ignore-file flag, this file defines a list of glob file matchers to always include git aware files in the package.xml package.
SGD will include every metadata from the repo at the to parameter state, matching the pattern from the include file specified in the --include-file [-n].
As with --ignore-file, you may need different policies for the package.xml and destructiveChanges.xml files. This is where the --include-destructive-file [-N] option comes handy!
Use the --include-destructive-file parameter to specify a dedicated include file to handle deletions. Related metadata will appear in the destructiveChanges.xml output.
For metadata contained inside files (Labels, Workflow, MatchingRules, etc.), every element the file holds at --to is included: in package.xml with --include-file, in destructiveChanges.xml with --include-destructive-file. The file's history plays no part, so a shallow clone is enough.
Consider the following:
- a repository containing many sub-folders (force-app/main,force-app/sample, etc)
- a CI/CD platform generating a
force-app/generated/foofile thesource:deploycommand should not include. You can create a file with a line matching this new file and specify this file using the--include-destructive-file [-N]parameter.
# .destructiveinclude
*generated/foo
$ sf sgd source delta --from commit --include-destructive-file .destructiveincludeThe path matchers in includes file must follow gitignore spec and accept only unix path separator / (even for windows system).
The --source-dir [-s] parameter allows you to specify one or more folders to focus on, making any other folders ignored.
It means the delta generation will only focus on the dedicated folders.
For example, consider a repository containing many sub-folders (force-app/packaged, force-app/unpackaged, etc).
This repository contains packaged (deployed via package) and unpackaged (deployed via CLI) sources.
You only want to apply delta generation for the admin and ui folder of the unpackaged sources.
$ tree
.
├── force-app
│ ├── packaged
│ │ └── classes
│ │ └── PackagedClass.cls
│ └── unpackaged
│ ├── admin
│ │ └── profile
│ │ └── Admin.profile-meta.xml
│ ├── legacy
│ │ └── classes
│ │ └── LegacyClass.cls
│ └── ui
│ └── lightningExperienceThemes
│ └── MyCustomTheme.lightningExperienceTheme-meta.xml
└── ...
# scope the delta generation only to the unpackaged folders
$ sf sgd source delta --from commit --source-dir force-app/unpackaged/admin --source-dir force-app/unpackaged/uiEach --source-dir value is a literal repository-relative folder (or file) path, matched from the repository root — --source-dir force-app matches force-app/... but never nested/force-app/.... A trailing slash or a leading ./ is accepted and canonicalised (force-app/, ./force-app and force-app all scope the same folder).
Wildcards (*, ?, [), git pathspec magic (:(exclude)…, :!…, :(glob)…, …), absolute paths, .., and the empty string are all rejected with an error — --source-dir never matches a glob pattern. For pattern matching, repeat --source-dir once per folder, or use --include-file [-n] / --ignore-file [-i] (gitignore-spec patterns) instead.
The ignored patterns specified using
--ignore-file [-i]and--ignore-destructive-file [-D]still apply. The--source-dirpath must be relative to the--repo-dirpath
SGD intentionally works without an sfdx-project.json and treats the whole repository as in scope. As a result, files living outside your declared package directories (for example .claude/ metadata templates, samples or tooling fixtures) are picked up as soon as they change.
When you want the delta to cover only the package directories declared in sfdx-project.json, feed each of them to the repeatable --source-dir [-s] parameter. Anything outside those directories is then excluded:
sf sgd source delta --from baseline --to HEAD --output-dir ./delta --generate-delta \
$(jq -r '.packageDirectories[] | "--source-dir", .path' sfdx-project.json)jq reads every packageDirectories[].path entry and expands it into a --source-dir argument. sfdx-project.json package directory paths commonly carry a trailing slash (e.g. "force-app/"); since --source-dir canonicalises trailing slashes away, this recipe works unchanged whether or not your packageDirectories[].path entries end in /.
See Scoping delta generation to specific folders for the
--source-dirbehavior. Prefer--ignore-file [-i]when you only need to skip a few known paths (e.g.**/.claude/**) without coupling tosfdx-project.json.
Depending on your testing strategy, you may want to run only the Apex tests related to the delta instead of every local test. Three options use the package.xml produced by SGD to drive the test selection:
Generate a space-separated list of the added and modified Apex classes by parsing the package.xml produced by SGD with yq:
TESTS=$(xq . < package/package.xml | jq -r '.Package.types | [.] | flatten | map(select(.name=="ApexClass")) | .[] | .members | [.] | flatten | map(select(. | index("*") | not)) | unique | join(" ")')
sf project deploy start -x package/package.xml --test-level RunSpecifiedTests --tests $TESTS--tests expects a space-separated list, so $TESTS is left unquoted to expand into one argument per class. This only works when the delta contains the test classes themselves, or when your test class names can be derived from the changed classes.
The apex-test-list plugin (by renatoliveira) reads @Tests: / @TestSuites: annotations in your Apex classes and resolves the tests to run for the components listed in the SGD manifest:
// @Tests: AccountServiceTest, AccountTriggerTest
public class AccountService { }echo y | sf plugins install apextestlist
sf sgd source delta --to "HEAD" --from "HEAD~1" --output-dir "."
sf project deploy start -x package/package.xml --test-level RunSpecifiedTests $(sf apextests list -x package/package.xml --format sf)Salesforce's RunRelevantTests test level (beta, Spring '26) analyzes the deployment payload server-side and runs only the tests relevant to it. Deploy the SGD manifest with it.
Package managers (1GP, unlocked, managed) and release reviewers often need to distinguish newly added components from modified ones — adding a new component to a managed package is a 1-way door that binds all subscribers. SGD groups git diff into Salesforce components, but its package.xml bundles additions and modifications together (as required for deployment).
Use --changes-manifest [-c] to emit an additional JSON file that groups components by change kind. The file is written alongside package.xml / destructiveChanges.xml (no changes to the deployment manifests themselves).
Two forms:
# Bare flag: writes to <output-dir>/changes.manifest.json
sf sgd source delta --from "origin/development" --to HEAD --output-dir incremental --changes-manifest
# Explicit path: resolved against cwd (or used as-is when absolute), same as --ignore-file
sf sgd source delta --from "origin/development" --to HEAD --changes-manifest reports/changes.jsonProduces (bare form example) incremental/changes.manifest.json. Setting --changes-manifest also turns on git's -M rename detection for this run, so components renamed at the file level show up in their own bucket instead of being split into a fake delete+add pair. A rename lands in the rename bucket only when both sides are components the run itself detected; renaming a file that is not a component (e.g. a .txt inside classes/) leaves both manifests and buckets exactly as without the flag. Default sgd runs (without the flag) keep the pre-feature behaviour — renames still appear as a delete on the old path + an add on the new path.
{
"add": { "ApexClass": ["BrandNewClass"], "CustomObject": ["NewObject__c"] },
"modify": { "ApexClass": ["ExistingClass"], "CustomLabels": ["MyApp.Label1"] },
"delete": { "ApexTrigger": ["OldTrigger"] },
"rename": { "ApexClass": [{ "from": "OldName", "to": "NewName" }] }
}package.xml still lists NewName and destructiveChanges.xml still lists OldName for renames — the deployment contract is unchanged, and the manifests are identical whether or not --changes-manifest is set. Both sides of a rename still pass through the ignore files: a from path covered by --ignore-destructive-file keeps its member out of destructiveChanges.xml, a to path covered by --ignore-file keeps its member out of package.xml, and the change is then reported in changes.manifest.json as a plain addition or deletion rather than a rename: an ignored from leaves to in the add bucket, an ignored to leaves from in the delete bucket. When the move keeps the component name, the deletion is cancelled against the ignored addition and neither side is reported at all. A pure move (see Moved components) appears in no bucket.
Two default-configuration behaviours changed with no dedicated changelog entry — the kind of thing you notice the first time you diff a JSON manifest across the upgrade:
- a renamed record type is still reported in the
renamebucket, but its former name is no longer emitted intodestructiveChanges.xml— that entry previously made the whole deployment fail; - a deleted record type now appears in the
deletebucket, where previously it appeared nowhere.
Both are consequences of the undeletable-component warning described above: RecordType (today's only such type) is kept out of destructiveChanges.xml, but still reported in changes.manifest.json for review.
Works for file-backed metadata, in-file sub-components (CustomLabels members, Workflow rules, etc.), decomposed metadata, in-resource bundles and in-folder metadata.
SGD does not always generate content in the package.xml (or destructiveChanges.xml). Sometimes the commit range contains changes only within files to ignore (using .sgdignore and --i parameter).
Deploying empty package.xml can lead to deployment errors.
To avoid starting a failing deployment, test files content before execution:
# run deploy command only if the generated package contains metadata
if grep -q '<types>' ./package/package.xml ; then
echo "---- Deploying added and modified metadata ----"
sf project deploy start -x package/package.xml
else
echo "---- No changes to deploy ----"
fiIf you want to embed sgd in your node application, install it as a dependency for your application
npm install sfdx-git-deltaThen use the JavaScript module
// sample/app.js
import sgd from 'sfdx-git-delta'
const work = await sgd({
to: '', // commit sha to where the diff is done. [default : "HEAD"]
from: '', // (required) commit sha from where the diff is done. [default : git rev-list --max-parents=0 HEAD]
mergeBase: false, // (required) resolve `from` to the merge base of `from`/`to` first. [default : false]
output: '', // source package specific output. [default : "./output"]
apiVersion: undefined, // salesforce API version, as a number (e.g. 67). [default : sfdx-project.json "sourceApiVersion" at --to, else latest]
repo: '', // git repository location. [default : "."]
source: ['.'], // (required) one or more repo-relative folders to scope the diff to; '.' (or an empty array) means the whole repository
})
console.log(JSON.stringify(work))
/* {
* config: config,
* diffs: { package: {...}, destructiveChanges: {...} },
* warnings: []
* }
*/Deleting a flow cannot be done by adding the flow in the destructiveChanges.xml and deploy.
A known issue exist to cover this feature.
Please do not assume committing a flow metadata deletion to the repo, and then run sgd will allow you to delete a flow.
We suggest to deal with flow deletion in one go by following those steps (it requires the FlowDefinition metadata which is not available in API v44+)
- Set the
FlowDefinitionactiveVersionNumberto0 - List the
FlowDefinitionin apackage.xml - List all the existing version of the
Flowin adestructiveChangesPost.xml(can be fetch via SOQL using this query :SELECT FlowDefinitionView.ApiName, VersionNumber, Status FROM FlowVersionView WHERE FlowDefinitionView.ApiName='<FLOW_API_NAME>') - Deploy this
FlowDefinitionwith apackage.xmland post delete all theFlowversions with a postdestructiveChangesPost.xml
Example to delete the Flow Set_Account_Description :
- Set the
FlowDefinitionactiveVersionNumberto0
<!--Set_Account_Description.flowDefinition-meta.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<FlowDefinition xmlns="http://soap.sforce.com/2006/04/metadata">
<activeVersionNumber>0</activeVersionNumber>
</FlowDefinition>- List the
FlowDefinitionin apackage.xml
<!--package.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>Set_Account_Description</members>
<name>FlowDefinition</name>
</types>
<version>61.0</version>
</Package>- List all the existing version of the
Flowin adestructiveChangesPost.xml
<!--destructiveChangesPost.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>Set_Account_Description-1</members>
<members>Set_Account_Description-2</members>
<members>Set_Account_Description-...</members>
<members>Set_Account_Description-n</members>
<name>Flow</name>
</types>
</Package>- Deploy this
package.xml,destructiveChangesPost.xmlandFlowDefinition
# add `--ignore-warnings` parameter if you listed a deleted Flow version in the destructiveChangesPost.xml
sf project deploy start -x package.xml --post-destructive-changes destructiveChangesPost.xmlIf you need to handle metadata types that are not (yet) supported by the standard registry, you can provide a JSON file containing the definitions using the --additional-metadata-registry flag.
The file must contain an array of metadata definitions:
[
{
"xmlName": "CustomThing",
"suffix": "thing",
"directoryName": "things",
"inFolder": false,
"metaFile": false
}
]Then run the command:
sf sgd source delta --from "HEAD~1" --additional-metadata-registry ./my-registry.jsonSGD supports Salesforce decomposed metadata source format beta features. When these features are enabled in your sfdx-project.json, metadata types are split into individual files instead of a single monolithic file. SGD detects the file layout automatically and generates the correct package.xml entries.
Supported decomposed presets:
| Preset | Parent Type | Notes |
|---|---|---|
decomposePermissionSetBeta |
PermissionSet | Each child (field permissions, class accesses, etc.) in its own subdirectory |
decomposePermissionSetBeta2 |
PermissionSet | Flat files + objectSettings/ subdirectory |
decomposeCustomLabelsBeta |
CustomLabels | Individual .label files |
decomposeCustomLabelsBeta2 |
CustomLabels | Individual .label files (flat layout) |
decomposeWorkflowBeta |
Workflow | Each child (alerts, field updates, rules, etc.) in its own subdirectory |
decomposeSharingRulesBeta |
SharingRules | Each child (criteria, owner, guest rules) in its own subdirectory |
decomposeExternalServiceRegistrationBeta |
ExternalServiceRegistration | .yaml schema + -meta.xml definition; a change to either redeploys the registration, both files are copied with --generate-delta |
No additional configuration is needed — SGD reads the file structure from the git diff and resolves the metadata types accordingly.
SGD is using the Salesforce CLI logging system to log information. You can control the logging level by setting the SF_LOG_LEVEL environment variable. You can redirect the logging in the terminal using DEBUG=sfdx-git-delta.
example:
DEBUG=sfdx-git-delta SF_LOG_LEVEL=trace sf sgd source delta --from "HEAD~1" --to "HEAD"changelog.md is available for consultation.
- fast-xml-parser - Validate XML, Parse XML to JS/JSON and vise versa, or parse XML to Nimn rapidly without C/C++ based libraries and no callback
- flexible-xml-parser - Fastest XML parser in pure JS with pluggable output builders and value parsers
- fs-extra - Node.js: extra methods for the fs object like copy(), remove(), mkdirs().
- ignore - is a manager, filter and parser which implemented in pure JavaScript according to the .gitignore spec 2.22.1.
- lodash - A modern JavaScript utility library delivering modularity, performance & extras.
- MegaLinter - Open-Source tool for CI/CD workflows that analyzes the consistency of your code, IAC, configuration, and scripts
- simple-git - A light weight interface for running git commands in any node.js application.
-sfdx-hardis Toolbox for Salesforce DX, by Cloudity & friends, natively compliant with most platforms and tools.
Versioning follows SemVer specification.
- Sebastien Colladon - Developer - scolladon
- Mehdi Cherfaoui - Tester - mehdicherf
Contributions are what make the trailblazer community such an amazing place. I regard this component as a way to inspire and learn from others. Any contributions you make are appreciated.
See contributing.md for sgd contribution principles.
This project license is MIT - see the LICENSE.md file for details


