Read the screenplay: FANNIEGATE — $7 trillion. 17 years. The biggest fraud in American capital markets.
🚀 DeploymentAdvanced2026-03-04

CI/CD for Salesforce: GitHub Actions Pipeline That Validates Every PR

Most Salesforce teams deploy by clicking "Deploy" in VS Code or using change sets. Both are manual, error-prone, and impossible to audit. A proper CI/CD pipeline validates every pull request against a scratch org, runs Apex tests, and blocks merge if anything fails. GitHub Actions makes this straightforward.

The pipeline has three stages: authenticate, deploy, and test. Authentication uses a JWT bearer flow with a connected app and server key. The key is stored as a GitHub secret. The deploy step creates a scratch org (or deploys to a sandbox), pushes all metadata, and runs all local tests. If any test fails, the PR gets a red X and cannot merge. After merge to main, a separate workflow deploys to the staging sandbox automatically.

The ROI is immediate. No more "it worked in my org" problems. No more deploying untested code. No more manual validation steps. The first time this pipeline catches a breaking change before it reaches production, it pays for the entire setup time. I run this exact pipeline for Mobilization Funding deployments.

Code Example

# .github/workflows/validate-pr.yml
name: Validate PR
on:
  pull_request:
    branches: [main]
    paths:
      - 'force-app/**'
      - 'config/**'

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

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

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

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

      - name: Push Source
        run: sf project deploy start --target-org ci-scratch

      - name: Run Apex Tests
        run: |
          sf apex run test \
            --target-org ci-scratch \
            --code-coverage \
            --result-format human \
            --wait 20 \
            --test-level RunLocalTests

      - name: Delete Scratch Org
        if: always()
        run: sf org delete scratch --target-org ci-scratch --no-prompt

Need this implemented in your org?

I've shipped these patterns in production for 10+ years.

View Consulting →

Enjoyed this? Get more like it.

Glen's Musings — AI, investing, and building things. Occasional. Free.

More Deployment Tips