The biggest bottleneck in most Salesforce CI/CD pipelines isn’t the tests - it’s the deployment itself. Deploying the entire metadata tree on every merge takes 40–90 minutes depending on org size. For a team doing 10–20 merges a day, that’s the whole day gone.
The fix is obvious: only deploy what changed. sf-metadata-delta automates that by diffing two git refs and producing a package.xml containing only the modified components.
The Problem with Full Deployments
A standard Salesforce DX pipeline looks like:
git push → CI trigger → sfdx force:source:deploy -p force-app/ → tests → done
force-app/ contains everything: all Apex classes, all flows, all custom objects, all layouts, all permission sets. A mid-size org might have 2,000+ metadata components. Deploying all of them validates all of them, runs all their associated tests, and waits for Salesforce’s deployment engine to process each one.
The result: a 60-minute deployment window during which you can’t merge anything else.
How Delta Deployments Work
A delta deployment only sends the components that changed between two commits:
sf-metadata-delta --from HEAD~1 --to HEAD --output delta/package.xml
sfdx force:source:deploy -x delta/package.xml
The output is a standard package.xml file. Everything downstream - Salesforce CLI, Copado, Gearset - understands it natively.
For a typical feature branch touching 3–5 components, the resulting deployment takes 3–5 minutes instead of 60.
Implementation
The tool is written in TypeScript and runs on Node.js. The core logic:
- Run
git diff --name-only <from>..<to>to get changed file paths - Map each path to its Salesforce metadata type using the DX source format conventions
- Group components by type and build the
package.xmlDOM - Write the output file
The metadata type mapping is the interesting part. Salesforce DX source format encodes type in the directory structure:
force-app/main/default/classes/AccountService.cls → ApexClass: AccountService
force-app/main/default/flows/Opportunity_After_Save.flow-meta.xml → Flow: Opportunity_After_Save
force-app/main/default/objects/Account/fields/SLA__c.field-meta.xml → CustomField: Account.SLA__c
The mapping table covers all standard metadata types. For object-level components (fields, validation rules, record types, list views), the tool extracts the parent object name from the path and uses the correct compound syntax (Account.SLA__c).
function classifyPath(filePath: string): MetadataComponent | null {
for (const [pattern, type] of metadataTypeMap) {
const match = filePath.match(pattern);
if (match) {
return { type, name: buildName(type, match) };
}
}
return null; // non-metadata file (e.g. README, .gitignore)
}
Handling Deletions
Deletions are handled separately via a destructiveChanges.xml - Salesforce’s mechanism for removing metadata from an org. The tool generates both files:
delta/
package.xml # components to deploy/update
destructiveChanges.xml # components to delete (if any)
destructiveChangesPost.xml # post-deployment deletions (e.g. obsolete fields)
Running the deployment with both:
sf project deploy start \
--manifest delta/package.xml \
--post-destructive-changes delta/destructiveChangesPost.xml
Git Ref Flexibility
The tool accepts any git ref for --from and --to:
# Feature branch vs main
sf-metadata-delta --from origin/main --to HEAD
# Last two commits
sf-metadata-delta --from HEAD~1 --to HEAD
# Two specific commits
sf-metadata-delta --from abc1234 --to def5678
# Tag-based releases
sf-metadata-delta --from v2.1.0 --to v2.2.0
CI Pipeline Integration
A typical GitHub Actions job:
- name: Generate delta package
run: |
npx sf-metadata-delta \
--from ${{ github.event.before }} \
--to ${{ github.sha }} \
--output delta/
- name: Deploy delta
run: |
sf project deploy start \
--manifest delta/package.xml \
--test-level RunLocalTests \
--wait 30
With Copado, you can call the CLI as a pre-promotion step and pass the generated package.xml path into Copado’s deployment options - bypassing its own (slower) delta calculation.
Real Results
On the implementation that prompted this tool - a Commerce Cloud org with ~2,400 metadata components:
| Before | After |
|---|---|
| 63 min average deployment | 4.5 min average deployment |
| Full org deployed on every merge | Only changed components |
| 2–3 parallel merges blocked | 10+ merges per day unblocked |
The 95% reduction holds for typical feature branches. A large refactor touching 50+ components will obviously take longer, but even then it’s faster than deploying the full org.
The project is on GitHub.