Skip to content

๐Ÿš€ SFDX Deploy โ€‹

This repository implements a simple GitHub composite action for deploying Salesforce metadata to a target org. It supports source directories, manifest (package.xml) and metadata component selectors, configurable Apex test levels, validation-only (--dry-run) deployments, and optional delta deployments (only the components that changed between two git refs) via the sfdx-git-delta plugin - which makes it equally suitable for pull request validation and for the actual deployment to higher environments.

Usage โ€‹

Validate a deployment on pull requests (dry-run) โ€‹

After installing the SF CLI and authorizing the relevant org, a check-only validation could look like this:

Check out with fetch-depth: 0 so metadata deletions can be detected (see Deleting metadata). Set destructive-changes: false if you don't need this - then a shallow checkout is fine.

yaml
# Validate pull requests with `pull_request`, never `pull_request_target` - the
# latter would run fork code with this repository's org credentials.
on:
  pull_request:
    branches: [main]

# Least-privilege token: this pipeline only reads the repository.
permissions:
  contents: read

jobs:
  validation:
    name: Validation
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v7.0.1
        with:
          fetch-depth: 0
          persist-credentials: false # don't leave the GITHUB_TOKEN in .git/config for later steps

      - name: Install SF CLI
        uses: svierk/sfdx-cli-setup@v1.1.2

      - name: Salesforce Org Login
        uses: svierk/sfdx-login@v1.4.2
        with:
          sfdx-url: ${{ secrets.SFDX_AUTH_URL }}
          alias: target-org

      - name: Validate Deployment
        uses: svierk/sfdx-deploy@v1.2.1
        with:
          source-dir: force-app
          target-org: target-org
          test-level: RunLocalTests
          dry-run: true

Deploy metadata to an org โ€‹

yaml
      - name: Checkout
        uses: actions/checkout@v7.0.1
        with:
          fetch-depth: 0            # required so metadata deletions can be detected
          persist-credentials: false

      - name: Deploy Metadata
        uses: svierk/sfdx-deploy@v1.2.1
        with:
          source-dir: force-app
          target-org: target-org
          test-level: RunLocalTests

Delta deployment (only changed components) โ€‹

Delta mode uses sfdx-git-delta to deploy only the components that changed between two git refs. This requires the full git history, so check out with fetch-depth: 0. The generated delta manifest takes precedence over source-dir, manifest and metadata, and component deletions are applied as post-destructive changes (see Deleting metadata). If no deployable changes are detected, the step succeeds without deploying.

yaml
      - name: Checkout
        uses: actions/checkout@v7.0.1
        with:
          fetch-depth: 0
          persist-credentials: false

      - name: Deploy Changed Components
        uses: svierk/sfdx-deploy@v1.2.1
        with:
          delta: true
          delta-from: origin/${{ github.base_ref }}
          delta-to: HEAD
          target-org: target-org
          test-level: RunLocalTests

Deleting metadata (destructive changes) โ€‹

A plain Salesforce source deploy is additive - it never deletes a component from the org just because its file was removed from the repo. This action improves on that: it detects metadata you deleted in the repository (via sfdx-git-delta) and applies those deletions to the target org after the deployment, for both delta and normal full deployments. This behaviour is on by default (destructive-changes: true); set it to false for an additive-only deploy.

yaml
      - name: Checkout
        uses: actions/checkout@v7.0.1
        with:
          fetch-depth: 0            # required so deletions can be detected
          persist-credentials: false

      - name: Deploy Metadata
        uses: svierk/sfdx-deploy@v1.2.1
        with:
          source-dir: force-app
          target-org: target-org
          test-level: RunLocalTests
          # destructive-changes: true  # default - components deleted in the repo are deleted on the org

How it works:

  • Deletions are derived from the git range delta-from..delta-to. In a full deployment delta-from defaults to the previous commit (HEAD~1) and delta-to to HEAD; set delta-from explicitly (e.g. a base branch or release tag) to compare against a different baseline.
  • The full git history is required (fetch-depth: 0). On a shallow clone deletions cannot be detected and the action emits a warning instead of deleting.
  • Everything runs as a single, atomic deploy (one test run): when deletions exist the action deploys via a generated package.xml plus the derived destructiveChanges.xml, since Salesforce requires post-destructive changes to be paired with a manifest.
  • With dry-run: true the deletions are validated (check-only) but not applied.

Inputs โ€‹

NameRequiredDefaultDescription
source-dirnoComma-separated list of source directories to deploy.
manifestnoPath to a manifest (package.xml) file specifying the components to deploy.
metadatanoComma-separated list of metadata component names, e.g. ApexClass,CustomObject:Account.
destructive-changesnotrueAuto-detect metadata deleted in the repo (via sfdx-git-delta) and delete it on the org after the deploy. Works for full and delta deployments. Needs fetch-depth: 0. Set false for additive-only.
deltanofalseDeploy only components changed between two git refs (via sfdx-git-delta). Needs fetch-depth: 0.
delta-fromnoGit ref to compare from when detecting changes/deletions, e.g. origin/main. Required in delta mode; defaults to HEAD~1 for destructive changes on full deployments.
delta-tonoHEADGit ref to compare to when detecting changes/deletions.
target-orgnoUsername or alias of the target org. Not required if the default org is set.
test-levelnoNoTestRun, RunSpecifiedTests, RunLocalTests or RunAllTestsInOrg.
testsnoComma-separated Apex tests for the RunSpecifiedTests level.
dry-runnofalseValidate the deployment without saving components (check-only deployment).
ignore-conflictsnofalseDeploy local files even if they overwrite changes in the org.
ignore-warningsnofalseAllow the deployment to complete successfully despite warnings.
waitno33Number of minutes to wait for the deployment to complete.
api-versionnoOverride the api version used for api requests, for example 59.0.
step-summarynotrueWrite a result section to the GitHub Actions job summary. Set to false to avoid collisions with a custom workflow summary.

When the tests input is provided, the CLI runs the specified tests and the test-level input is ignored.

If none of source-dir, manifest or metadata is provided (and delta mode is off), the CLI deploys the package directories defined in your sfdx-project.json (typically force-app).

Outputs โ€‹

NameDescription
deploy-idID of the deployment job.
statusFinal status of the deployment (e.g. Succeeded).

The step fails if the deployment fails, while still printing the deployment result and setting the outputs.

The action prints a concise, human-readable table of the affected components (created, changed and deleted) to the log and folds a detailed, human-readable breakdown of the deployment result - including component errors, test failures and code coverage warnings - into a collapsible "Deployment details" group (or "Validation details" for dry-runs). When step-summary is enabled, the job summary additionally lists the deployed, modified and deleted metadata - including deletions applied via destructive changes - plus a failures table when the deployment did not succeed.

Security & versioning โ€‹

Every uses: reference in the snippets above is pinned to an exact release version, e.g. svierk/sfdx-deploy@v1.2.1. Do the same in your own pipelines:

  • Never reference a mutable ref such as @main or @v1. It runs whatever code sits on that branch/tag at run time - with access to your org credentials - so a compromised or rewritten ref would run unnoticed.
  • Good - pin to an exact release tag (@v1.2.1). Readable, concrete, and bumped through reviewed pull requests.
  • Strictest - pin to a full-length commit SHA (@a1b2c3dโ€ฆ) with the version as a trailing comment. A SHA can never be re-pointed by the publisher; the cost is readability. Worth it for actions from publishers you don't control.
  • Enable Dependabot for github-actions so those pins are bumped for you instead of silently ageing. Note that Dependabot only scans .github/workflows and a root action.yml - copy-paste snippets in a README are not covered and have to be updated by hand.

This applies to all actions your workflows reference - this action as well as actions/* and any other third-party action. Latest versions at the time of writing: actions/checkout@v7.0.1, svierk/sfdx-cli-setup@v1.1.2, svierk/sfdx-login@v1.4.2, svierk/sfdx-deploy@v1.2.1.

Beyond pinning, a few rules are worth copying into the workflow that calls this action:

  • Least-privilege GITHUB_TOKEN - declare a permissions: block granting only what the job needs. This action itself needs no token at all, so contents: read is enough for the deploy job.
  • persist-credentials: false on checkout - the token is not written to .git/config, so later steps (SF CLI, third-party actions) cannot reuse it. The delta and destructive-change detection reads the local git history only and works fine without persisted credentials.
  • Secrets travel as secrets - pass them straight into an action input (sfdx-url: ${{ secrets.SFDX_AUTH_URL }}) and reference them in your own shell steps as environment variables ("$TARGET_ORG"), never by interpolating ${{ ... }} into the script itself - that would allow command injection and can leak values into the log. Every input of this action is handed to the shell through an env: mapping for exactly that reason.
  • Validate pull requests with pull_request, never pull_request_target - the latter runs with the base repository's secrets, which would let a fork execute its own code against your org.
  • Gate production behind a GitHub Environment - required reviewers and environment-scoped secrets keep a non-dry-run deploy from being triggered accidentally.

References โ€‹

The deployment option supported by this GitHub composite action can be found in the Salesforce CLI Command Reference here:

Releases โ€‹

Latest release notes can be found on the release page.

License โ€‹

The scripts and documentation in this project are released under the MIT License.


โžก๏ธ Full source & releases: svierk/sfdx-deploy

Released under the MIT License.