← All posts
· 3 min read ·
SalesforceCI/CDTypeScriptGitDevOpsOpen Source

sf-metadata-delta: Cutting Salesforce Deployment Time by 95%

How I built a TypeScript CLI that generates a minimal package.xml from git diffs - deploying only changed Salesforce metadata instead of the full org, dropping build times from 60 minutes to under 5.

Dark terminal screen showing git diff output and deployment commands

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:

  1. Run git diff --name-only <from>..<to> to get changed file paths
  2. Map each path to its Salesforce metadata type using the DX source format conventions
  3. Group components by type and build the package.xml DOM
  4. 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:

BeforeAfter
63 min average deployment4.5 min average deployment
Full org deployed on every mergeOnly changed components
2–3 parallel merges blocked10+ 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.

← All posts