Salesforce · · 22 min read

Setting Up a CI/CD Pipeline for Salesforce

A complete guide to CI/CD for Salesforce — what CI/CD is, what to include in your pipeline, popular tools, an introduction to YAML and shell scripting, and a hands-on project building a pipeline with GitHub Actions.

Part 70: Setting Up a CI/CD Pipeline for Salesforce

Welcome back to the Salesforce series. In Part 67, we explored the Salesforce CLI and learned how to interact with orgs from the terminal. In Part 68, we set up Scratch Orgs as disposable development environments. In Part 69, we packaged our metadata into Unlocked Packages with versioning and dependency management. Now we are going to tie all of those pieces together by building an automated pipeline that takes your code from a Git repository, validates it, tests it, and deploys it — without you having to touch a single button in Salesforce Setup.

This is CI/CD — Continuous Integration and Continuous Deployment — and it is the backbone of modern software delivery. If you have been manually deploying change sets, running tests by hand, and hoping that nothing breaks in production, this post is going to change how you think about Salesforce development. By the end, you will have a working GitHub Actions pipeline that spins up a scratch org, installs your unlocked package, runs your Apex tests, and reports the results back to your pull request. Let us get into it.


What is CI/CD?

CI/CD stands for Continuous Integration and Continuous Deployment (sometimes called Continuous Delivery). These are two related but distinct practices.

Continuous Integration

Continuous Integration is the practice of automatically building and testing your code every time a developer pushes changes to a shared repository. The goal is to catch problems early. Instead of waiting until the end of a sprint to merge everyone’s work together and discover conflicts, CI runs on every push or pull request and tells you immediately whether the new code integrates cleanly with the existing codebase.

In a Salesforce context, CI means that every time you push Apex classes, triggers, LWC components, or metadata changes to your Git repository, an automated process validates that code against a Salesforce org. It deploys the metadata, runs your Apex tests, checks for compile errors, and reports the results. If something fails, the developer knows within minutes — not days.

Continuous Deployment

Continuous Deployment takes it a step further. Once your code passes all the automated checks, it is automatically deployed to a target environment. In traditional software development, this might mean deploying to a staging server or even production. In Salesforce, it typically means deploying validated metadata to a sandbox, a scratch org, or — if your pipeline is mature enough — directly to production.

The “continuous” part is the key word. You are not batching up changes and deploying once a week. Every change that passes validation gets deployed automatically. This reduces the risk of large, complex deployments and gives your team faster feedback.

The CI/CD Cycle

Here is how a typical CI/CD cycle works for Salesforce:

  1. A developer creates a feature branch in Git and writes Apex code, LWC components, or declarative metadata changes.
  2. The developer pushes the branch to the remote repository and opens a pull request.
  3. The CI pipeline is triggered automatically. It authenticates with a Salesforce org, deploys the metadata, and runs all Apex tests.
  4. If the tests pass, the pull request gets a green check mark. If they fail, the developer sees the errors directly in the pull request.
  5. Once the pull request is reviewed and merged, the CD pipeline kicks in and deploys the validated code to the target environment.

This cycle repeats for every change, every developer, every day. The result is a codebase that is always in a deployable state.


Important Things to Include in a CI/CD Pipeline

Not all pipelines are created equal. A bare-minimum pipeline might just deploy code and check for compile errors. A production-grade pipeline includes much more. Here are the things you should consider including in your Salesforce CI/CD pipeline.

Source Validation

Before running any tests, your pipeline should validate that the source can be successfully deployed. This catches syntax errors in Apex, malformed XML in metadata files, and missing dependencies. Use the sf project deploy start command with the --dry-run flag (or --check-only in older CLI versions) to validate without actually persisting the deployment.

sf project deploy start --source-dir force-app --target-org my-org --dry-run

Apex Test Execution

Running Apex tests is non-negotiable. Your pipeline should execute all local tests (at minimum) and report the results. For production deployments, Salesforce requires 75% code coverage, but you should aim higher. A good pipeline runs tests at multiple levels:

  • Local tests — Tests defined in your project, excluding managed package tests.
  • Specific test classes — If you want to run only tests relevant to the changed code.
  • All tests — For production validation runs.
sf project deploy start --source-dir force-app --target-org my-org --test-level RunLocalTests

Code Coverage Reporting

Running tests is only useful if you actually look at the results. Your pipeline should extract code coverage numbers and either display them in the build output or post them as a comment on the pull request. The Salesforce CLI can output test results in JSON format, which you can parse in your pipeline scripts.

sf project deploy start --source-dir force-app --target-org my-org \
  --test-level RunLocalTests --results-dir test-results --coverage-formatters json

Static Code Analysis

Static analysis tools scan your code for common issues without executing it. For Salesforce, the most popular tool is PMD with Salesforce-specific rules (often called the Apex PMD ruleset). It catches things like empty catch blocks, hardcoded IDs, SOQL inside loops, and unused variables.

pmd check --dir force-app --rulesets apex-ruleset.xml --format csv

You can also use Salesforce Code Analyzer (formerly Salesforce Scanner), which wraps PMD along with other engines:

sf scanner run --target force-app --format csv --outfile scanner-results.csv

Scratch Org Provisioning

For a truly isolated test, your pipeline should create a fresh scratch org, push or deploy the source to it, and run tests there. This guarantees that your code works from a clean slate without relying on existing data or configuration in a persistent sandbox.

sf org create scratch --definition-file config/project-scratch-def.json \
  --alias pipeline-scratch --duration-days 1 --set-default

Package Version Creation

If you are using Unlocked Packages (as we covered in Part 69), your pipeline should create a new package version and optionally install it in a target org. This validates that your metadata is packageable and that the package installs cleanly.

sf package version create --package "My Package" --installation-key-bypass \
  --wait 20 --code-coverage

Notification and Reporting

When the pipeline finishes, your team needs to know the result. At a minimum, the CI status should appear on the pull request in GitHub (or your Git provider). Many teams also send notifications to Slack, Microsoft Teams, or email when a build fails.

Environment Cleanup

If your pipeline creates scratch orgs, it should also delete them when the build finishes. Scratch orgs count against your org’s limits, and leaving them around wastes resources.

sf org delete scratch --target-org pipeline-scratch --no-prompt

There are several tools and platforms that Salesforce teams use for CI/CD. Here is a rundown of the most common ones.

GitHub Actions

GitHub Actions is a CI/CD platform built directly into GitHub. You define your pipeline as a YAML file in your repository, and GitHub runs it on their hosted runners whenever you push code or open a pull request. It is free for public repositories and includes a generous free tier for private repositories. We will use GitHub Actions for the hands-on project later in this post.

Salesforce DevOps Center

Salesforce DevOps Center is Salesforce’s own tool for managing change tracking and deployments. It provides a visual interface for tracking changes across environments and integrates with GitHub for source control. It is a good option for teams that want a point-and-click experience, but it is less flexible than a custom pipeline for advanced workflows.

Gearset

Gearset is a third-party tool built specifically for Salesforce CI/CD. It offers a visual UI for comparing orgs, deploying metadata, and running automated tests. Gearset handles many of the complexities of Salesforce deployments (like dependency ordering and destructive changes) that you would otherwise have to script yourself. It is a paid tool, but many teams find it worth the cost for the time it saves.

Copado

Copado is another Salesforce-native DevOps platform. It runs inside your Salesforce org and provides release management, CI/CD pipelines, and environment management. Copado is popular with larger enterprises that want a managed solution with built-in compliance and governance features.

Jenkins

Jenkins is an open-source automation server that has been around for decades. It is highly configurable and can run Salesforce pipelines using the Salesforce CLI. The trade-off is that Jenkins requires you to host and maintain your own server, and configuration can be complex. If your organization already uses Jenkins for other projects, it makes sense to extend it for Salesforce.

GitLab CI/CD

Similar to GitHub Actions, GitLab CI/CD lets you define pipelines in a YAML file within your repository. If your team uses GitLab instead of GitHub, this is the natural choice. The syntax is slightly different from GitHub Actions, but the concepts are the same.

Azure DevOps

Azure DevOps (formerly VSTS) includes Azure Pipelines, which can run Salesforce CI/CD workflows. It integrates well with Microsoft’s ecosystem and is a strong choice for organizations that are already invested in Azure.


An Introduction to YAML

Before we build our pipeline, you need to understand YAML, because that is the language used to define GitHub Actions workflows (and most other CI/CD tools). YAML stands for “YAML Ain’t Markup Language,” and it is a human-readable data serialization format.

Basic Syntax

YAML uses indentation (spaces, not tabs) to represent structure. Here is a simple example:

name: My Pipeline
on: push

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Say hello
        run: echo "Hello from the pipeline"

Let us break down the key concepts.

Key-Value Pairs

The most basic YAML structure is a key-value pair, separated by a colon and a space:

name: My Pipeline
version: 1.0
enabled: true

Lists

Lists are represented by dashes. Each item starts with - (dash followed by a space):

fruits:
  - apple
  - banana
  - orange

Nested Objects

You nest objects by increasing the indentation level (typically two spaces):

database:
  host: localhost
  port: 5432
  credentials:
    username: admin
    password: secret

Multi-line Strings

YAML supports multi-line strings using the pipe | character (preserves newlines) or the greater-than > character (folds newlines into spaces):

description: |
  This is a multi-line
  description that preserves
  each line break.

summary: >
  This is a long sentence
  that gets folded into
  a single line.

Environment Variables and Expressions

In GitHub Actions YAML, you can reference environment variables and use expressions:

env:
  SF_ORG_ALIAS: my-scratch-org

steps:
  - name: Deploy
    run: sf project deploy start --target-org ${{ env.SF_ORG_ALIAS }}

Common YAML Mistakes

A few things that trip people up:

  • Tabs vs. spaces — YAML only allows spaces for indentation. A single tab will cause a parse error.
  • Inconsistent indentation — Once you pick an indentation level (two spaces is standard), stick with it throughout the file.
  • Missing space after colonkey:value is not valid. It must be key: value with a space.
  • Special characters in strings — If your value contains colons, curly braces, or other special characters, wrap it in quotes: message: "Error: something went wrong".

An Introduction to Shell Scripting

Your CI/CD pipeline will run shell commands — the same Bash commands you use in your terminal. Understanding shell scripting basics will help you write more powerful pipeline steps.

Variables

Shell variables are assigned without spaces around the equals sign:

ORG_ALIAS="pipeline-scratch"
PACKAGE_NAME="My Package"

echo "Creating scratch org: $ORG_ALIAS"

Conditionals

Use if statements to branch your logic:

if [ "$TEST_RESULT" -eq 0 ]; then
  echo "All tests passed"
else
  echo "Tests failed with exit code $TEST_RESULT"
  exit 1
fi

Command Substitution

Capture the output of a command into a variable:

PACKAGE_VERSION_ID=$(sf package version create \
  --package "My Package" \
  --installation-key-bypass \
  --wait 20 \
  --json | jq -r '.result.SubscriberPackageVersionId')

echo "Created package version: $PACKAGE_VERSION_ID"

Exit Codes

Every command returns an exit code. Zero means success, and anything else means failure. In a CI/CD pipeline, a non-zero exit code typically causes the pipeline to fail and stop. You can check exit codes explicitly:

sf project deploy start --source-dir force-app --target-org my-org
DEPLOY_STATUS=$?

if [ "$DEPLOY_STATUS" -ne 0 ]; then
  echo "Deployment failed"
  exit 1
fi

Piping and Redirection

Chain commands together with pipes and redirect output to files:

# Pipe JSON output through jq to extract a value
sf org display --target-org my-org --json | jq -r '.result.instanceUrl'

# Redirect test results to a file
sf apex run test --target-org my-org --result-format json > test-results.json

The set Command

Two flags you will see in CI/CD scripts:

set -e  # Exit immediately if any command fails
set -o pipefail  # Catch errors in piped commands

These are important in pipelines because without them, a failing command might be silently ignored, and the pipeline would continue as if nothing went wrong.


How to Set Up a Custom CI/CD Pipeline Using GitHub Actions

Now let us put everything together. We will build a GitHub Actions workflow that does the following:

  1. Triggers on pull requests to the main branch.
  2. Installs the Salesforce CLI.
  3. Authenticates with a Salesforce Dev Hub.
  4. Creates a scratch org.
  5. Deploys the source code.
  6. Runs all Apex tests.
  7. Reports the results.
  8. Cleans up the scratch org.

Step 1: Repository Structure

Your Salesforce DX project should already have this structure from our earlier posts:

my-salesforce-project/
  force-app/
    main/
      default/
        classes/
        triggers/
        lwc/
        objects/
  config/
    project-scratch-def.json
  sfdx-project.json

GitHub Actions workflows live in a .github/workflows/ directory at the root of your repository.

Step 2: Set Up Authentication

GitHub Actions needs to authenticate with your Salesforce Dev Hub. The recommended approach is to use a JWT Bearer Flow with a connected app. Here is what you need:

  1. Create a Connected App in your Dev Hub org. Enable OAuth settings, select the “Use digital signatures” option, and upload the public key from a self-signed certificate.
  2. Generate a server key and certificate:
openssl genrsa -out server.key 2048
openssl req -new -x509 -key server.key -out server.crt -days 365
  1. Store the private key as a GitHub Secret. Go to your repository settings, navigate to Secrets and Variables, then Actions, and create a new secret called SF_JWT_KEY with the contents of server.key.
  2. Store your Dev Hub credentials. Create additional secrets:
    • SF_CLIENT_ID — The Consumer Key from your Connected App.
    • SF_USERNAME — The username of your Dev Hub admin user.

Step 3: Create the Workflow File

Create the file .github/workflows/salesforce-ci.yml:

name: Salesforce CI

on:
  pull_request:
    branches:
      - main
    paths:
      - "force-app/**"
      - "config/**"
      - "sfdx-project.json"

jobs:
  validate:
    name: Validate and Test
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source code
        uses: actions/checkout@v4

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

      - name: Authenticate Dev Hub
        run: |
          echo "${{ secrets.SF_JWT_KEY }}" > server.key
          sf org login jwt \
            --client-id ${{ secrets.SF_CLIENT_ID }} \
            --jwt-key-file server.key \
            --username ${{ secrets.SF_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: Deploy Source to Scratch Org
        run: |
          sf project deploy start \
            --source-dir force-app \
            --target-org ci-scratch

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

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

Let us walk through each section of this workflow.

Understanding the Workflow

The on block defines when the pipeline triggers. We trigger on pull requests to the main branch, but only when files in force-app/, config/, or sfdx-project.json change. This prevents the pipeline from running when you edit a README or other non-Salesforce files.

The runs-on field specifies the runner. ubuntu-latest gives you a fresh Linux virtual machine for each run. GitHub provides these for free (with usage limits).

The Checkout step pulls your repository code onto the runner. Without this, the runner has nothing to work with.

The Install Salesforce CLI step installs the sf CLI globally using npm. The runner starts with Node.js pre-installed, so this works out of the box.

The Authenticate Dev Hub step writes the JWT private key to a temporary file, authenticates using the JWT bearer flow, and then deletes the key file. The secrets are injected by GitHub and never appear in the logs.

The Create Scratch Org step provisions a fresh scratch org using the scratch org definition file from your project. The --duration-days 1 flag ensures the org expires quickly, since we only need it for this pipeline run.

The Deploy Source step pushes all the metadata from force-app/ to the scratch org. If there are compile errors or missing dependencies, this step fails.

The Run Apex Tests step executes all local Apex tests and reports code coverage. The --wait 20 flag tells the CLI to poll for up to 20 minutes while tests run.

The Delete Scratch Org step uses if: always() to ensure it runs even if previous steps failed. This prevents orphaned scratch orgs from piling up.


PROJECT: Set Up a CI/CD Pipeline to Generate a Scratch Org and Deploy Your Unlocked Packages to It

Now let us build a more complete pipeline that not only deploys source code but also creates an unlocked package version and installs it in a scratch org. This is the pattern you would use for a real production workflow.

Prerequisites

Before you begin, make sure you have:

  • A Salesforce Dev Hub org with scratch org and unlocked package features enabled.
  • A Salesforce DX project with an unlocked package already defined in sfdx-project.json (we did this in Part 69).
  • A GitHub repository with the connected app and secrets configured as described above.

Step 1: Update Your Project Configuration

Your sfdx-project.json should look something like this:

{
  "packageDirectories": [
    {
      "path": "force-app",
      "default": true,
      "package": "MyAppPackage",
      "versionName": "ver 1.0",
      "versionNumber": "1.0.0.NEXT"
    }
  ],
  "name": "my-salesforce-project",
  "namespace": "",
  "sfdcLoginUrl": "https://login.salesforce.com",
  "sourceApiVersion": "62.0"
}

The NEXT keyword in the version number tells Salesforce to auto-increment the build number each time you create a new version.

Step 2: Create a Shell Script for the Pipeline

Complex pipelines benefit from moving logic into shell scripts rather than putting everything inline in the YAML file. Create a scripts/ directory and add a file called ci-pipeline.sh:

#!/bin/bash
set -e
set -o pipefail

echo "=== Starting CI Pipeline ==="

# Step 1: Create scratch org
echo "--- Creating scratch org ---"
sf org create scratch \
  --definition-file config/project-scratch-def.json \
  --alias ci-scratch \
  --set-default \
  --duration-days 1 \
  --wait 10

echo "Scratch org created successfully."

# Step 2: Create a new package version
echo "--- Creating package version ---"
PACKAGE_CREATE_OUTPUT=$(sf package version create \
  --package "MyAppPackage" \
  --installation-key-bypass \
  --wait 30 \
  --code-coverage \
  --json)

PACKAGE_VERSION_ID=$(echo "$PACKAGE_CREATE_OUTPUT" | jq -r '.result.SubscriberPackageVersionId')

if [ -z "$PACKAGE_VERSION_ID" ] || [ "$PACKAGE_VERSION_ID" = "null" ]; then
  echo "ERROR: Failed to create package version"
  echo "$PACKAGE_CREATE_OUTPUT" | jq '.'
  exit 1
fi

echo "Package version created: $PACKAGE_VERSION_ID"

# Step 3: Install the package in the scratch org
echo "--- Installing package version ---"
sf package install \
  --package "$PACKAGE_VERSION_ID" \
  --target-org ci-scratch \
  --installation-key-bypass \
  --wait 15 \
  --publish-wait 10

echo "Package installed successfully."

# Step 4: Run Apex tests in the scratch org
echo "--- Running Apex tests ---"
sf apex run test \
  --target-org ci-scratch \
  --code-coverage \
  --result-format human \
  --test-level RunLocalTests \
  --wait 20

echo "All tests passed."

# Step 5: Extract and display code coverage
echo "--- Checking code coverage ---"
COVERAGE_OUTPUT=$(sf apex run test \
  --target-org ci-scratch \
  --code-coverage \
  --result-format json \
  --test-level RunLocalTests \
  --wait 20)

COVERAGE_PERCENT=$(echo "$COVERAGE_OUTPUT" | jq -r '.result.summary.orgWideCoverage' | tr -d '%')

echo "Org-wide code coverage: ${COVERAGE_PERCENT}%"

if [ "$COVERAGE_PERCENT" -lt 75 ]; then
  echo "ERROR: Code coverage is below 75%"
  exit 1
fi

echo "=== CI Pipeline Complete ==="

Make the script executable:

chmod +x scripts/ci-pipeline.sh

Step 3: Create the Full Workflow File

Now create the GitHub Actions workflow at .github/workflows/salesforce-ci.yml:

name: Salesforce CI - Package Validation

on:
  pull_request:
    branches:
      - main
    paths:
      - "force-app/**"
      - "config/**"
      - "sfdx-project.json"
      - "scripts/**"

  workflow_dispatch:

env:
  SF_ORG_ALIAS: ci-scratch
  DEVHUB_ALIAS: devhub

jobs:
  build-and-test:
    name: Build Package and Run Tests
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - name: Checkout source code
        uses: actions/checkout@v4

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

      - name: Install jq
        run: sudo apt-get install -y jq

      - name: Authenticate Dev Hub
        run: |
          echo "${{ secrets.SF_JWT_KEY }}" > server.key
          sf org login jwt \
            --client-id ${{ secrets.SF_CLIENT_ID }} \
            --jwt-key-file server.key \
            --username ${{ secrets.SF_USERNAME }} \
            --set-default-dev-hub \
            --alias ${{ env.DEVHUB_ALIAS }}
          rm server.key

      - name: Run CI Pipeline
        run: bash scripts/ci-pipeline.sh

      - name: Delete Scratch Org
        if: always()
        run: |
          sf org delete scratch \
            --target-org ${{ env.SF_ORG_ALIAS }} \
            --no-prompt || true

  static-analysis:
    name: Static Code Analysis
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source code
        uses: actions/checkout@v4

      - name: Install Salesforce CLI
        run: |
          npm install -g @salesforce/cli
          sf plugins install @salesforce/sfdx-scanner

      - name: Run Salesforce Code Analyzer
        run: |
          sf scanner run \
            --target "force-app" \
            --format csv \
            --outfile scanner-results.csv \
            --severity-threshold 2

      - name: Upload Scanner Results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: scanner-results
          path: scanner-results.csv

Step 4: Understanding the Complete Workflow

This workflow has two jobs that run in parallel:

build-and-test — This is the main job. It authenticates with the Dev Hub, then hands off to the shell script which creates a scratch org, builds a package version, installs it, and runs tests. The timeout-minutes: 30 ensures the job does not run forever if something hangs. The scratch org cleanup step uses || true at the end so that a failure to delete the org does not fail the entire pipeline.

static-analysis — This job runs Salesforce Code Analyzer independently. It does not need a scratch org or Dev Hub authentication because static analysis works on the source code directly. The --severity-threshold 2 flag causes the step to fail if any issues of severity 2 (high) or above are found. The results are uploaded as a build artifact so you can download and review them.

The workflow_dispatch trigger lets you run the pipeline manually from the GitHub Actions tab, which is useful for debugging.

Step 5: Test Your Pipeline

  1. Commit and push the workflow file and shell script to a new branch.
  2. Open a pull request against main.
  3. Watch the Actions tab in GitHub. You should see both jobs start running.
  4. If the pipeline fails, click into the failed step to see the logs. Common issues include authentication failures (check your secrets), scratch org limits (check your Dev Hub), and test failures (check your Apex code).

Step 6: Add Branch Protection

Once your pipeline is working, go to your repository settings and add a branch protection rule for main:

  • Require status checks to pass before merging.
  • Select both the “Build Package and Run Tests” and “Static Code Analysis” checks.
  • Require branches to be up to date before merging.

This ensures that no code can be merged to main without passing the full CI pipeline. Every pull request must have green checks before the merge button becomes available.


Section Notes

  • CI/CD stands for Continuous Integration and Continuous Deployment. CI automatically validates and tests your code on every push. CD automatically deploys validated code to a target environment.
  • A production-grade Salesforce pipeline should include source validation, Apex test execution, code coverage reporting, static analysis, scratch org provisioning, and environment cleanup.
  • Popular Salesforce CI/CD tools include GitHub Actions, Gearset, Copado, Jenkins, GitLab CI/CD, Azure DevOps, and Salesforce DevOps Center. GitHub Actions is a strong free option for teams already on GitHub.
  • YAML is the language used to define GitHub Actions workflows. It is indentation-based, uses key-value pairs and lists, and is strict about spaces (no tabs allowed).
  • Shell scripting fundamentals — variables, conditionals, exit codes, piping, and set -e — are essential for writing robust pipeline scripts.
  • The JWT Bearer Flow is the recommended way to authenticate the Salesforce CLI in a CI/CD environment. Store your private key and credentials as GitHub Secrets — never commit them to your repository.
  • Always use if: always() on cleanup steps (like deleting scratch orgs) to ensure they run even when earlier steps fail.
  • Running two parallel jobs — one for build and test, one for static analysis — speeds up your pipeline by doing both checks simultaneously.
  • Branch protection rules turn your CI pipeline into a gate that prevents untested code from reaching your main branch.
  • The combination of the Salesforce CLI, scratch orgs, unlocked packages, and GitHub Actions gives you a fully automated, repeatable deployment pipeline that eliminates manual deployment errors and gives your team confidence in every release.