
Andrew Hanna

Andrew Hanna

To set up a Salesforce CI/CD pipeline with GitHub Actions, you store your metadata in
Git in source format, authenticate GitHub to your org with a JWT-based Connected App,
and add a workflow that validates changes on every pull request and deploys them on
merge. The whole pipeline lives in a single .github/workflows YAML file
and runs on the Salesforce CLI. This guide walks the current, sf v2 way to do it, then
flags the parts that quietly break in production. It is part of our SF Guides library
at /serpent/resources.
A working pipeline assumes a few things are already in place:
force-app in source format, committed to a GitHub repo.
sf). The old sfdx CLI
is deprecated, so build on sf.
If your metadata is not in Git yet, that migration comes first. We cover the branching and environment mechanics in our companion guide to building a Salesforce CI/CD pipeline with GitHub Actions.
Use the JWT Bearer Flow. It is headless, needs no interactive login, and is what Salesforce recommends for CI. Generate a key pair, upload the certificate to a Connected App, then log in from the runner with the private key.
openssl genrsa -out server.key 2048 then
openssl req -new -x509 -nodes -sha256 -days 365 -key server.key -out
server.crt.
server.crt.
server.key and your consumer key, username, and instance URL as
GitHub repository secrets, never in the YAML.
sf org login jwt --client-id $SF_CONSUMER_KEY --jwt-key-file server.key
--username $SF_USERNAME --instance-url $SF_INSTANCE_URL --set-default-org.
The core pattern is one workflow with two behaviours, keyed off the event. Validate on a pull request so nothing merges that would fail; deploy for real when the change lands on your main branch.
pull_request against main for
validation, push to main for deployment.
sf project deploy validate --source-dir force-app --test-level RunLocalTests
--wait 30 --verbose. This is a check-only run, so nothing is written to the org.
sf project deploy start --source-dir force-app --test-level RunLocalTests --wait
30 --verbose.
Use an if: condition on each job so the same file handles both events.
Running local tests on validation is what makes this a real gate rather than a rubber
stamp, and it is the backbone of any serious
Salesforce test automation pipeline in CI.
Deploying the whole force-app directory every time is slow and grows
worse as the repo grows. The community tool sfdx-git-delta computes
the diff between two commits and produces a package of only the changed metadata:
sf sgd source delta --to HEAD --from HEAD~1 --output delta --generate-delta
--source-dir force-app
You then point the deploy command at the delta directory. It cuts deploy time sharply, but read the next section before you trust it blindly.
Most tutorials end at a green deploy. The failures show up later, and they are the reason we build tooling around this instead of leaving teams a raw YAML file:
If you would rather not own that pipeline forever, a managed platform handles delta, destructive changes, and the partial metadata edges for you. See what Serpent covers and how it is priced before committing a quarter of engineering time to a homegrown one.
Should I use sfdx or sf for a Salesforce GitHub Actions pipeline?
Use sf (Salesforce CLI v2). The legacy sfdx CLI is
deprecated and should not be the base of a new pipeline.
Why use JWT auth instead of a username and password?
JWT Bearer Flow is headless and needs no interactive login or MFA prompt, which is what a CI runner requires, and Salesforce recommends it for automation.
What is the difference between deploy validate and deploy start?
Validate is a check-only run that verifies and runs tests without changing the org, ideal on pull requests; start performs the actual deployment on merge.
Do I need sfdx-git-delta?
No, but it is worth it once deploys get slow. Just pair it with a destructive-changes step so deletions are handled, not silently skipped.
Commitment free!