๐ 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: 0so metadata deletions can be detected (see Deleting metadata). Setdestructive-changes: falseif you don't need this - then a shallow checkout is fine.
# 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: trueDeploy metadata to an org โ
- 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: RunLocalTestsDelta 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.
- 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: RunLocalTestsDeleting 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.
- 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 orgHow it works:
- Deletions are derived from the git range
delta-from..delta-to. In a full deploymentdelta-fromdefaults to the previous commit (HEAD~1) anddelta-totoHEAD; setdelta-fromexplicitly (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.xmlplus the deriveddestructiveChanges.xml, since Salesforce requires post-destructive changes to be paired with a manifest. - With
dry-run: truethe deletions are validated (check-only) but not applied.
Inputs โ
| Name | Required | Default | Description |
|---|---|---|---|
source-dir | no | Comma-separated list of source directories to deploy. | |
manifest | no | Path to a manifest (package.xml) file specifying the components to deploy. | |
metadata | no | Comma-separated list of metadata component names, e.g. ApexClass,CustomObject:Account. | |
destructive-changes | no | true | Auto-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. |
delta | no | false | Deploy only components changed between two git refs (via sfdx-git-delta). Needs fetch-depth: 0. |
delta-from | no | Git 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-to | no | HEAD | Git ref to compare to when detecting changes/deletions. |
target-org | no | Username or alias of the target org. Not required if the default org is set. | |
test-level | no | NoTestRun, RunSpecifiedTests, RunLocalTests or RunAllTestsInOrg. | |
tests | no | Comma-separated Apex tests for the RunSpecifiedTests level. | |
dry-run | no | false | Validate the deployment without saving components (check-only deployment). |
ignore-conflicts | no | false | Deploy local files even if they overwrite changes in the org. |
ignore-warnings | no | false | Allow the deployment to complete successfully despite warnings. |
wait | no | 33 | Number of minutes to wait for the deployment to complete. |
api-version | no | Override the api version used for api requests, for example 59.0. | |
step-summary | no | true | Write a result section to the GitHub Actions job summary. Set to false to avoid collisions with a custom workflow summary. |
When the
testsinput is provided, the CLI runs the specified tests and thetest-levelinput is ignored.
If none of
source-dir,manifestormetadatais provided (and delta mode is off), the CLI deploys the package directories defined in yoursfdx-project.json(typicallyforce-app).
Outputs โ
| Name | Description |
|---|---|
deploy-id | ID of the deployment job. |
status | Final 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
@mainor@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-actionsso those pins are bumped for you instead of silently ageing. Note that Dependabot only scans.github/workflowsand a rootaction.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 apermissions:block granting only what the job needs. This action itself needs no token at all, socontents: readis enough for the deploy job. persist-credentials: falseon 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 anenv:mapping for exactly that reason. - Validate pull requests with
pull_request, neverpull_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
