Skip to Content

CI/CD for Salesforce using Github Actions

How to Guide on setting up DevOps for Salesforce with no expensive tools
28 January 2026 by
CI/CD for Salesforce using Github Actions
Bitmoq Technologies Private Limited

Use Case

I need my Salesforce development workflow fully automated through GitHub Actions CI. The goal is to move from manual deployments to a reliable CI/CD pipeline that covers every step - from code commit to production release - while following Salesforce best practices (SFDX, scratch orgs, unlocked packages). 

Here is what I want to accomplish:

  • Pipeline configuration: create or refine .gitlab-ci.yml so that feature branches trigger scratch-org builds, run Apex tests, static code analysis, and package creation.
  • Environment setup: connect GitLab runners to my Salesforce orgs, manage authentication with JWT-based connections, and handle secrets securely in GitLab variables.
  • Automated testing: ensure unit tests run in scratch orgs on every merge request, with code-coverage gates that block merges if thresholds are not met. 

Acceptance is met when a merge to main automatically:

  1. Spins up a scratch org, installs dependencies, and passes all tests.
  2. Generates an unlocked package or deployment artifact.
  3. Promotes the artifact to a staging sandbox with change-set validation.
  4. Provides clear success/failure feedback inside Github. 

The solution has to remain maintainable and documented. Detail out every single step so that my team can handle post implementation.

Solution

This setup uses a JWT-based authentication flow, which is the gold standard for secure, headless Salesforce deployments.

Prerequisites & Security Setup

Before the pipeline can run, you must establish a secure "handshake" between GitHub and Salesforce.

  1. Create a Connected App: In your Dev Hub, create a Connected App with OAuth scopes (api, web, refresh_token).

  2. Generate a JWT Certificate: Create a self-signed digital certificate.

    • Upload the .crt to the Connected App.

    • Store the .key (private key) as a GitHub Secret.

  3. GitHub Secrets: Add the following to your repository:

    • SFDX_JWT_KEY: The content of your server.key.

    • SFDX_HUB_URL: The username for your Dev Hub.

    • SFDX_CONSUMER_KEY: The Client ID from your Connected App.

Initial Project & Repo Setup

To start from a clean slate, you need to initialize your local environment and the GitHub repository correctly.

Step 1: Create the Local Project

Run these commands in your terminal:

# Create the project using the standard template
sf project generate --name my-salesforce-app --template standard
cd my-salesforce-app

# Initialize Git
git init
echo "node_modules/\n.sfdx/\n.sf/\nserver.key" >> .gitignore
Step 2: Professional Folder Structure

A professional structure separates configuration, source, and automation scripts.

force-app/: Your main source code (LWC, Apex, Metadata).

config/: Scratch org definition files.

scripts/: Bash or Python scripts for data loading or custom CI steps.

.github/workflows/: Your CI/CD pipeline definitions.

Configure GitHub Branch Protection

This is the "human" side of the automation.

  1. Go to Settings > Branches in GitHub.

  2. Add a Branch Protection Rule for main.

  3. Check "Require status checks to pass before merging".

  4. Search for and select validate-change (the name of the job in our YAML).

  5. Check "Require branches to be up to date before merging".

The Delta-Based Pipeline (main.yml)

This pipeline uses sfdx-git-delta to calculate exactly what changed between the current commit and the previous one. This reduces deployment times from minutes to seconds.

Create .github/workflows/main.yml:

name: Production Deployment (Delta)

on:
push:
branches: [ main ]

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
fetch-depth: 0 # Critical for git-delta to see history

- name: Install Salesforce CLI & Plugins
run: |
npm install @salesforce/cli --global
echo 'y' | sf plugins install sfdx-git-delta

- name: Authenticate Production
run: |
echo "${{ secrets.SFDX_JWT_KEY }}" > server.key
sf org login jwt --username ${{ secrets.SFDX_PROD_USER }} \
--key-file server.key --client-id ${{ secrets.SFDX_CONSUMER_KEY }} \
--alias production --set-default

- name: Generate Delta (Incremental Changes)
run: |
mkdir delta
sf sgd source delta --to HEAD --from HEAD^ --output delta/ --generate-delta

- name: Deploy Incremental Changes
run: |
sf project deploy start --manifest delta/package/package.xml \
--post-destructive-changes delta/destructiveChanges/destructiveChanges.xml \
--target-org production --wait 30 --test-level RunLocalTests

- name: Create Release Artifact (For Rollback)
if: success()
uses: actions/upload-artifact@v3
with:
name: deployment-package-${{ github.sha }}
path: delta/

Managing "what goes in" and "how to undo" is the most critical part of a professional setup.

Incremental Logic (sfdx-git-delta)

Instead of deploying the whole force-app folder, the pipeline above:

  1. Compares HEAD (current commit) with HEAD^ (previous commit).

  2. Creates a package.xml containing only the new/changed files.

  3. Creates a destructiveChanges.xml for any files deleted in the commit.

Provisions for Rollback

Salesforce does not have a "native" one-click rollback for metadata. To handle rollbacks professionally, follow this dual-layered approach:

  1. Git Revert (Recommended):

    • If a deployment fails or causes bugs, perform a git revert <commit_id> on your main branch.

    • The pipeline will treat the revert as a "new change" and deploy the previous state of the metadata back to the org.

  2. Artifact Storage:

    • The pipeline step Create Release Artifact saves the exact package.xml and source files deployed for every successful run.

    • If GitHub is down or you need to see exactly what touched the org, you can download these from the Actions tab.

The PR Validation Workflow (pr_validation.yml)

This Pull Request (PR) Validation pipeline is the most critical gate in your workflow. It ensures that no "broken" code—whether it's a syntax error, a failing test, or poor code coverage - ever touches your main branch.

Create this file in .github/workflows/pr_validation.yml. This pipeline triggers on every PR targeting main. It provisions a fresh, isolated environment (Scratch Org), deploys the changes, and runs the full battery of tests.

name: PR Validation & Quality Gate

on:
pull_request:
branches: [ main ]
types: [opened, synchronize, reopened]

jobs:
static-analysis:
runs-on:ubuntu-latest
steps:
- name: Checkout Code
uses:actions/checkout@v4

- name: Install Salesforce CLI & Scanner
run: |
npm install @salesforce/cli --global
echo 'y' | sf plugins install @salesforce/sfdx-scanner
- name: Run Salesforce Code Analyzer
run: |
​ # Scans for security, performance, and best practices​
# --severity-threshold 3 will fail the build for any 'High' or 'Medium' issues
sf scanner run --target "force-app" --engine "pmd,eslint" --severity-threshold 3

validate-change:
needs: static-analysis # This ensures tests only run if code is clean
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install Salesforce CLI
run: npm install @salesforce/cli --global

- name: Authenticate Dev Hub
run: |
echo "${{ secrets.SFDX_JWT_KEY }}" > server.key
sf org login jwt --username ${{ secrets.SFDX_HUB_URL }} \
--key-file server.key --client-id ${{ secrets.SFDX_CONSUMER_KEY }} \
--alias devhub --set-default-dev-hub

- name: Create Ephemeral Scratch Org
run: |
sf org create scratch --definition-file config/project-scratch-def.json \
--alias temp-org --set-default --duration-days 1 --wait 20

- name: Deploy Changes to Scratch Org
run: sf project deploy start --target-org temp-org

- name: Run Apex Tests & Check Coverage
id: run-tests
run: |
# Generate JSON results to allow for logic-based gating
sf apex run test --target-org temp-org --wait 20 \
--result-format json --code-coverage --detailed-coverage > test-results.json

- name: Enforce Coverage Gate (75%)
run: |
# Professional gate using jq to parse coverage
COVERAGE=$(jq '.result.summary.testRunCoverage' test-results.json | tr -d '"%')
echo "Current Code Coverage: $COVERAGE%"
if (( $(echo "$COVERAGE < 75" | bc -l) )); then
echo "Error: Code coverage is below the 75% threshold."
exit 1
fi

- name: Clean Up Scratch Org
if: always()
run: sf org delete scratch --target-org temp-org --no-prompt


Guidelines for Salesforce Team to Follow

Repository Setup

To ensure your team can maintain this, include these steps in your repo's README.md:

  1. JWT Certificate: Rotate the server.key every 12 months. Update the SFDX_JWT_KEY secret in GitHub.

  2. Scratch Org Def: If you enable new Salesforce features (e.g., Person Accounts, Multi-Currency), update config/project-scratch-def.json.

  3. Bypass Analysis: If you have legacy code that fails the scanner, you can ignore files by creating a .eslintignore or adding // nopmd to Apex classes, though this should be a last resort.

  4. Rollback: To undo a release, use the "Revert" button on the merged Pull Request in GitHub. The Delta pipeline will automatically handle the "inverse" deployment.

Best Practices

  • Never deploy manually to Production. All changes must pass through a Pull Request.

  • Feature Branches: Name your branches feature/JIRA-123. Pushing to these will trigger a scratch org validation.

  • Fixing a Release: If main is broken, do not push a "fix" directly. Revert the PR using the GitHub "Revert" button to trigger an automated rollback deployment.

  • Secrets: If the JWT certificate expires, update the SFDX_JWT_KEY in GitHub Settings > Secrets.

Handling Unlocked Packages

To incorporate those into this flow:

  • The main.yml would use sf package version create instead of sf project deploy start.

  • You would then use sf package version promote once the package is validated in your Staging sandbox.

This is a very powerful setup for a Managed Services Provider (MSP) or a professional Salesforce practice, as it guarantees a consistent, documented trail for every client change.

Share this post
Tags
Archive
Master the Lead-to-Cash Flow: Why Visibility is the Engine of Revenue