
Tekunda Team

Andrew Hanna

Salesforce development has come a long way from the days of manually pushing changes through Change Sets. As teams scale and releases become more frequent, manual deployments quickly turn into bottlenecks.
That’s where CI/CD (Continuous Integration and Continuous Delivery) comes in.
A proper pipeline automates the process of validating, testing, and promoting changes across environments, from development all the way to production. While many teams use commercial DevOps platforms, it is still useful to understand how the underlying automation works.
In this guide, we will walk through how Salesforce CI/CD pipelines work today, how to build one using GitHub Actions, and which approaches work best depending on your team size, release cadence, and organisational complexity.
We will also look at where DIY pipelines start to struggle and why many teams eventually move to a dedicated Salesforce DevOps platform once their process matures.
New to the discipline? Our guide to mastering Salesforce DevOps covers the fundamentals this pipeline builds on.
Salesforce development is different from traditional application development.
Instead of shipping compiled applications, Salesforce teams deploy metadata and configuration such as:
Apex classes and triggers
Lightning Web Components
Flows and automation
Profiles and permission sets
Objects, fields, layouts, and schema
Without automation, deployments quickly become fragile.
Typical issues include:
Configuration drift between environments
Broken deployments due to missing dependencies
Manual deployment mistakes
Limited visibility into what actually changed
Poor traceability between work items and release content
CI/CD pipelines address these issues by:
validating changes automatically
running tests before deployment
keeping Git as the source of truth
promoting only approved changes
keeping environments consistent over time
This is still the most common model.
Developers make changes directly in sandboxes and move those changes through environments such as:
Developer Sandbox
↓
QA
↓
UAT / Staging
↓
Production
Metadata is retrieved from the org and committed to Git. CI/CD pipelines validate and promote that metadata across environments.
Many enterprise teams still operate this way, even when they start introducing unlocked packages later in the release flow.
Package-based development uses modular packages instead of promoting the entire org metadata tree as a single unit.
Each package represents a logical feature set or domain. This approach is common for:
ISVs
AppExchange partners
large enterprise teams with multiple parallel workstreams
The reality is that many teams start in an org-based model and gradually adopt packages for release versioning and cleaner promotion.
For very small teams, for example two or three contributors, too many environments can create more overhead than value. In that case a leaner process is usually better.
Developer Sandbox
↓
Retrieve to Git
↓
Pull Request validation
↓
Build unlocked package version
↓
Install in Staging / QA Sandbox
↓
Regression / signoff
↓
Install same package version in Production
This works well when:
the team is small and closely coordinated
release frequency is high
one shared staging or QA environment is enough for final validation
the team wants discipline without too much process overhead
The key point is that even a small team still needs a clean release process. It just should not be heavier than the team itself.
As teams grow, a dedicated QA environment becomes useful for validating integrated source changes before a release candidate is built.
Developer Sandbox
↓
Retrieve to Git
↓
Feature branch
↓
Pull Request validation
↓
Merge to develop
↓
Deploy source to QA
↓
Create release branch
↓
Build unlocked package version
↓
Install package in UAT
↓
Regression / business signoff
↓
Install same package version in Production
This model is stronger for larger teams because QA and UAT serve different purposes:
QA validates integrated source changes from multiple contributors
UAT validates the actual release artifact
Production receives the same package version that passed UAT
That gives better release traceability, cleaner rollback options, and fewer surprises in production.
ISVs often follow similar principles, but their workflows are more package-led from the beginning.
Scratch Org / Dev Org
↓
Source tracked to Git
↓
Pull Request validation
↓
Package version build
↓
Install in internal test org
↓
Install in staging / UAT
↓
Promote release artifact
The same ideas still apply: Git as the source of truth, clear promotion steps, and versioned release artifacts.
As the Salesforce org becomes larger and more domain-heavy, a single unlocked package can become too broad. At that point, modular unlocked packages usually make more sense.
Examples might include:
sales pipeline and opportunity management package
service operations package
customer onboarding package
core automation package
This is especially useful in large companies where multiple teams are working on different domains in parallel. As the team grows or the org becomes more complex, the DevOps process has to grow with it to preserve speed, quality, visibility, and traceability.
Before building the pipeline, decide how work will move through Git. The two most common strategies are GitFlow and trunk-based development.
main → production history
develop → integration branch
feature/* → work item branches
release/* → release candidate branches
hotfix/* → urgent production fixes
GitFlow works well when:
multiple people are working in parallel
QA and UAT need stable branches
releases happen weekly, fortnightly, or on a schedule
the team wants explicit release candidate branches
The downside is more branch management and more merge coordination.
main → always releasable
feature/* → short-lived branches merged quickly
This works well when:
the team is small
deployments are frequent
validation and testing are strong
the team wants fewer long-lived branches
The downside is that weak test discipline can destabilise the shared branch quickly.
For many org-based Salesforce teams, GitFlow is easier to adopt because it mirrors the familiar Dev → QA → UAT → Prod release model. Trunk-based development can work well for small, mature teams with strong automation.
The important thing is not that one model is always better. The important thing is to choose one model deliberately and make your environments, approvals, and pipeline logic consistent with it.
For most growing Salesforce teams, the recommended flow is:
Developer Sandbox
↓
Retrieve metadata to Git
↓
Feature branch
↓
Pull Request validation
↓
Merge to develop
↓
Deploy source to QA
↓
Create release branch
↓
Build unlocked package version
↓
Install package in UAT
↓
Regression testing and signoff
↓
Install same package version in Production
Why do we recommend this model?
source deploy to QA is flexible and fast for integration testing
the package version becomes a stable release artifact
UAT validates the exact artifact intended for production
production receives the same tested package version
This creates a process that is both practical and controlled. It also works well with source tracking during development while giving you better release traceability later in the flow.
| Category | Package.xml | Source Tracking + Unlocked Packages | Serpent Workflow |
|---|---|---|---|
| Primary model | Manifest-driven deployment | Git-driven development with package-based release promotion | Task-driven workflow built around Git, environments, and release visibility |
| Best for | Hotfixes, targeted deploys, legacy teams | Modern teams wanting versioned releases | Teams of any size wanting speed, visibility, and easier operations |
| Metadata selection | Manual | Source tracking plus Git selection | Guided, task-based, and dependency-aware selection |
| Dependency handling | Manual and error-prone | Better, but still requires discipline | Assisted by metadata analysis and workflow logic |
| Production traceability | Limited | Good when using package versions | Strong visibility across work items, releases, and orgs |
| Rollback clarity | Harder, especially with destructive changes | Better with versioned packages | Clearer through release artefacts, workflow history, and package promotion |
| Hotfix / back-promotion | Useful and often necessary | Still sometimes needs package.xml for production recovery | Supported as part of the release workflow and visibility model |
| Operational overhead | Low at first, high later | Moderate | Lower ongoing overhead because more of the workflow is native and visible |
| Collaboration | Mostly manual | Git-centric, developer-friendly | Developer, admin, tester, and release-manager friendly |
| When we recommend it | Specific edge cases and legacy processes | Recommended default for most modern teams | Recommended when you want the same DevOps principles with much less friction |
package.xml is the traditional manifest file listing the metadata types and members you want to retrieve or deploy.
Example:
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>AccountService</members>
<name>ApexClass</name>
</types>
<types>
<members>AccountTrigger</members>
<name>ApexTrigger</name>
</types>
<version>61.0</version>
</Package>
Pros:
explicit control over what gets deployed
works in all org types including production
useful for hotfixes and targeted deploys
Cons:
manual to maintain
easy to miss dependencies
does not automatically reflect what changed in the org
harder to manage as release scope grows
A lot of teams still use package.xml, and that makes sense historically. For a long time it was the most practical option, especially when source tracking was still immature.
Source tracking lets Salesforce CLI compare what changed in an org relative to local source. It is especially useful in scratch orgs and has become much more practical in modern sandbox-based workflows as the platform has matured.
This area improved a lot after the migration from the old SFDX CLI to the modern sf CLI, and it has continued improving over the last two years.
Typical commands include:
sf project retrieve start
sf project deploy start
Pros:
faster feedback during development
reduces manual manifest maintenance
better developer ergonomics
works well when combined with Git-driven promotion
Cons:
production does not support source tracking in the same way
teams still need a strategy for release versioning
partial feature selection can still be awkward without the right tooling
For most modern teams, we recommend using source tracking during development and unlocked packages for release promotion.
Why?
source tracking is a better day-to-day development experience than manual manifests
unlocked packages give you proper release versioning later in the flow
production traceability becomes much clearer through package versions
rollback and destructive-change handling are easier to reason about
That said, package.xml still matters in edge cases, so the real goal is not purity. The real goal is using the right tool for the right step.
Production is where source tracking limitations become important. You cannot treat production like a scratch org and ask Salesforce to tell you cleanly what changed there over time.
That means production deployments generally depend on:
package.xml manifests
source directories built from Git
unlocked package versions
This is one of the main reasons versioned package promotion is so valuable. It gives production a clearer release identity than source tracking alone can provide there.
We should be careful not to overstate intent here. The practical point is simple: production does not behave like a source-tracked dev org, so teams need another way to make release state explicit. Unlocked packages are one of the best ways to do that.
A common pain point with source tracking is this: your developer sandbox contains several unrelated changes, but you only want to move one feature forward.
The manual process usually looks like this:
retrieve everything from the sandbox
review the changed files in Git
select only the metadata relevant to the feature
commit just that feature branch scope
Example:
sf project retrieve start
git status
git add force-app/main/default/classes/PricingService.cls
git add force-app/main/default/triggers/OpportunityTrigger.trigger
git add force-app/main/default/flows/Discount_Approval.flow-meta.xml
git commit -m "Feature: pricing approval flow"
The challenge is that dependencies are not always obvious. A Flow may reference a field, a field may depend on a permission set, and a permission set change may be needed to make the feature usable.
This is where teams often either miss metadata or over-deploy extra metadata just to be safe.
With Serpent, this becomes easier because the workflow is built around work items and metadata selection. Serpent can help identify which components belong to a feature, and its Salesforce-trained AI can assist with logical metadata selection and dependency analysis based on the feature description itself.
Delta deployments push only the metadata that changed. They are useful when:
validating pull requests quickly
promoting small, isolated features
reducing validation time in very large orgs
Example using sfdx-git-delta:
sf sgd source delta \
--to HEAD \
--from origin/develop \
--output-dir delta \
--generate-delta
Then validate or deploy the delta:
sf project deploy validate --source-dir delta --test-level RunLocalTests
sf project deploy start --source-dir delta
Full branch deployments push the whole metadata tree or a full branch state. They are better when:
handling deletions or destructive changes
reverting commits
making sure an environment matches a known branch state
resetting drift in a shared environment
Example:
sf project deploy start --source-dir force-app
This is important because delta deployments are great when you are mostly adding or modifying metadata. They are less reliable when the branch intent includes deletions, revert commits, or broader consistency resets.
In practice, a strong process uses both:
delta deploys for speed
full deploys for certainty when the change type requires it
Serpent helps here as well by making the deployment flow less brittle. Instead of every team re-creating this logic in GitHub Actions by hand, the workflow can guide when targeted movement is appropriate and when a broader promotion is safer.
Even in org-based development, unlocked packages can improve release management significantly.
Instead of deploying a loose set of metadata directly into every higher environment, teams can package a logical set of changes into a versioned release artefact.
That means:
clear version numbers for releases
easier promotion from UAT to Production
better visibility into what is deployed where
cleaner dependency handling
more predictable rollback planning
For a larger org-based team, the release flow usually looks like this:
Developer Sandbox
↓
Retrieve to Git
↓
Pull Request validation
↓
Deploy source to QA
↓
Create release branch
↓
Build unlocked package version
↓
Install in UAT
↓
Regression and approval
↓
Install same package version in Production
For a very small team, it can be simpler:
Developer Sandbox
↓
Retrieve to Git
↓
Pull Request validation
↓
Build unlocked package version
↓
Install in Staging
↓
Promote same package version to Production
sf package create --name SalesApp --package-type Unlocked --path force-app
sf package version create --package SalesApp --wait 20 --installation-key-bypass
sf package install --package 04tXXXXXXXXXXXX --target-org myUATOrg --wait 20
sf package install --package 04tXXXXXXXXXXXX --target-org myProdOrg --wait 20
Unlocked packages do not magically solve every rollback problem, especially when data changes are involved. But compared with raw source deployment or package.xml-only promotion, they make rollback easier to understand because:
the release is a versioned artefact
you know exactly which package version introduced the change
you can compare what changed between versions more clearly
destructive changes are easier to track in the context of release history
This matters even more in production, where source tracking cannot give you the same visibility it can in development environments.
GitHub Actions is a good option for teams who want to understand the mechanics and build their own CI/CD foundation. It integrates directly with your repository, and workflows are defined in YAML files stored alongside the code.
feature/* → developer work item branches
develop → source-integrated branch deployed to QA
release/* → release candidate branches used for package version build
main → production history
hotfix/* → urgent production fixes
The example below shows a practical end-to-end setup using:
PR validation into develop
automatic source deploy to QA on merge to develop
package version build on release/*
package install to UAT
manual promotion of the same package version to Production
name: Salesforce End-to-End CI/CD
on:
pull_request:
branches:
- develop
push:
branches:
- develop
- 'release/*'
workflow_dispatch:
inputs:
package_version_id:
description: 'Unlocked package version id to promote to Production'
required: false
type: string
env:
PROJECT_DIR: force-app
DELTA_DIR: delta
QA_ALIAS: qa
UAT_ALIAS: uat
PROD_ALIAS: prod
PACKAGE_NAME: SalesApp
jobs:
pr-validate:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Salesforce CLI
run: npm install @salesforce/cli --global
- name: Install plugins
run: |
sf plugins install sfdx-git-delta
sf plugins install @salesforce/plugin-packaging
- name: Create JWT key file
run: echo "${{ secrets.SF_JWT_KEY_QA }}" > server.key
- name: Authenticate to QA validation org
run: |
sf org login jwt \
--client-id ${{ secrets.SF_CLIENT_ID_QA }} \
--jwt-key-file server.key \
--username ${{ secrets.SF_USERNAME_QA }} \
--instance-url ${{ secrets.SF_INSTANCE_URL_QA }} \
--alias $QA_ALIAS
- name: Generate delta package
run: |
sf sgd source delta \
--to HEAD \
--from origin/develop \
--output-dir $DELTA_DIR \
--generate-delta
- name: Show generated delta
run: |
echo "Delta contents:"
find $DELTA_DIR || true
- name: Run PMD scan
run: |
pmd -d $PROJECT_DIR -R category/apex/design.xml -f text || true
- name: Run ESLint
run: |
if [ -f package.json ]; then
npm ci
npm run lint || true
fi
- name: Validate delta deployment against QA
run: |
if [ -d "$DELTA_DIR" ]; then
sf project deploy validate \
--source-dir $DELTA_DIR \
--target-org $QA_ALIAS \
--test-level RunLocalTests
else
echo "No delta generated, validating full source instead."
sf project deploy validate \
--source-dir $PROJECT_DIR \
--target-org $QA_ALIAS \
--test-level RunLocalTests
fi
deploy-to-qa:
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Salesforce CLI
run: npm install @salesforce/cli --global
- name: Install plugins
run: |
sf plugins install sfdx-git-delta
- name: Create JWT key file
run: echo "${{ secrets.SF_JWT_KEY_QA }}" > server.key
- name: Authenticate to QA
run: |
sf org login jwt \
--client-id ${{ secrets.SF_CLIENT_ID_QA }} \
--jwt-key-file server.key \
--username ${{ secrets.SF_USERNAME_QA }} \
--instance-url ${{ secrets.SF_INSTANCE_URL_QA }} \
--alias $QA_ALIAS
- name: Generate delta for QA deploy
run: |
sf sgd source delta \
--to HEAD \
--from HEAD^ \
--output-dir $DELTA_DIR \
--generate-delta || true
- name: Deploy to QA
run: |
if [ -d "$DELTA_DIR" ] && [ "$(find $DELTA_DIR -type f | wc -l)" -gt 0 ]; then
sf project deploy start \
--source-dir $DELTA_DIR \
--target-org $QA_ALIAS \
--test-level RunLocalTests
else
echo "Falling back to full branch deploy"
sf project deploy start \
--source-dir $PROJECT_DIR \
--target-org $QA_ALIAS \
--test-level RunLocalTests
fi
build-package-and-install-uat:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/heads/release/')
runs-on: ubuntu-latest
outputs:
package_version_id: ${{ steps.capture_version.outputs.package_version_id }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Salesforce CLI
run: npm install @salesforce/cli --global
- name: Install packaging plugin
run: sf plugins install @salesforce/plugin-packaging
- name: Create Dev Hub JWT key file
run: echo "${{ secrets.SF_JWT_KEY_DEVHUB }}" > server.key
- name: Authenticate to Dev Hub
run: |
sf org login jwt \
--client-id ${{ secrets.SF_CLIENT_ID_DEVHUB }} \
--jwt-key-file server.key \
--username ${{ secrets.SF_USERNAME_DEVHUB }} \
--instance-url ${{ secrets.SF_INSTANCE_URL_DEVHUB }} \
--alias devhub
- name: Create package version
run: |
sf package version create \
--package $PACKAGE_NAME \
--target-dev-hub devhub \
--wait 30 \
--installation-key-bypass \
--code-coverage
- name: Capture latest package version id
id: capture_version
run: |
VERSION_ID=$(sf package version list --packages $PACKAGE_NAME --target-dev-hub devhub --json | jq -r '.result[0].SubscriberPackageVersionId')
echo "package_version_id=$VERSION_ID" >> $GITHUB_OUTPUT
echo "Latest package version id: $VERSION_ID"
- name: Create UAT JWT key file
run: echo "${{ secrets.SF_JWT_KEY_UAT }}" > server.key
- name: Authenticate to UAT
run: |
sf org login jwt \
--client-id ${{ secrets.SF_CLIENT_ID_UAT }} \
--jwt-key-file server.key \
--username ${{ secrets.SF_USERNAME_UAT }} \
--instance-url ${{ secrets.SF_INSTANCE_URL_UAT }} \
--alias $UAT_ALIAS
- name: Install package in UAT
run: |
sf package install \
--package ${{ steps.capture_version.outputs.package_version_id }} \
--target-org $UAT_ALIAS \
--wait 30 \
--publish-wait 10 \
--no-prompt
promote-to-production:
if: github.event_name == 'workflow_dispatch' && github.event.inputs.package_version_id != ''
runs-on: ubuntu-latest
environment: production
steps:
- name: Install Salesforce CLI
run: npm install @salesforce/cli --global
- name: Create Production JWT key file
run: echo "${{ secrets.SF_JWT_KEY_PROD }}" > server.key
- name: Authenticate to Production
run: |
sf org login jwt \
--client-id ${{ secrets.SF_CLIENT_ID_PROD }} \
--jwt-key-file server.key \
--username ${{ secrets.SF_USERNAME_PROD }} \
--instance-url ${{ secrets.SF_INSTANCE_URL_PROD }} \
--alias $PROD_ALIAS
- name: Install approved package in Production
run: |
sf package install \
--package ${{ github.event.inputs.package_version_id }} \
--target-org $PROD_ALIAS \
--wait 30 \
--publish-wait 10 \
--no-prompt
This is not the only valid GitHub Actions design, but it captures the recommended split between source-based validation in lower environments and package-based promotion into UAT and Production.
A CI/CD pipeline is only part of a working Salesforce DevOps process. What often determines success or failure is how teams handle the operational details around development, testing, promotion, and release coordination.
One challenge in Salesforce development is that sandboxes frequently accumulate multiple changes at once. A developer may be working on two or three features, but only one is ready for promotion.
If you simply retrieve all metadata and deploy it forward, you risk promoting unfinished work.
The safer pattern is:
Developer Sandbox
↓
Retrieve all metadata locally
↓
Select only the components belonging to the feature
↓
Commit those components to a feature branch
↓
Submit a pull request for validation
Example workflow:
sf project retrieve start
git checkout -b feature/pricing-update
git add force-app/main/default/classes/PricingService.cls
git add force-app/main/default/triggers/PricingTrigger.trigger
git add force-app/main/default/lwc/pricingWidget
git commit -m "Feature: pricing update"
This ensures the repository reflects a single logical feature rather than a mix of unrelated changes.
The difficulty is that Salesforce metadata is often interconnected. A Flow might depend on:
custom fields
Apex classes
permission sets
record types
Missing one of those dependencies can break deployment validation.
Many teams solve this through experience and manual checks. Platforms like Serpent help by analysing metadata relationships and identifying dependencies automatically so the correct set of components moves together.
Recent improvements in developer tooling have introduced AI assistance for dependency analysis.
In practice this means:
a feature description can be analysed
related metadata components can be detected
dependency graphs can be evaluated
the most likely metadata scope can be suggested
Serpent includes AI agents trained on Salesforce metadata patterns that helps determine the logical set of components required for a feature, reducing the risk of incomplete deployments.
This does not remove developer control. It simply makes the selection process faster and safer.
Another common issue in Salesforce environments is sandbox drift.
This happens when someone makes a change directly in a shared environment such as QA or UAT that never reaches the Git repository.
Over time the environment diverges from source control.
To detect drift, teams often perform periodic retrieval comparisons:
sf project retrieve start
git diff
If unexpected differences appear, they should be reconciled immediately by committing them to Git or removing them.
Allowing drift to accumulate makes later deployments unpredictable.
Each environment should have a clear purpose.
| Environment | Purpose |
|---|---|
| Developer Sandbox | Feature development and early testing |
| QA | Integrated testing across multiple features |
| UAT | Business validation of release candidates |
| Production | Live system |
Problems often arise when environments lose their purpose. If QA becomes another development sandbox, it stops functioning as a reliable integration checkpoint.
A robust CI/CD process includes automated tests at multiple levels.
unit tests for Apex
component tests for LWC
integration tests for cross-feature workflows
PR validation pipelines often run targeted tests for speed, while release pipelines run broader test suites.
Example PR validation:
sf project deploy validate \
--source-dir delta \
--test-level RunSpecifiedTests
Example release validation:
sf project deploy validate \
--source-dir force-app \
--test-level RunLocalTests
The goal is to catch issues early without slowing down developer feedback cycles unnecessarily.
One of the most underestimated DevOps challenges is simply knowing what is in a release.
Questions teams frequently ask include:
Which features are included in the next release?
Which components were deployed to QA yesterday?
Which package version is currently installed in production?
Without a structured workflow, answering those questions often requires searching across Git history, pipeline logs, and manual release notes.
Serpent addresses this by linking deployments to work items, making releases easier to understand across environments.
Developer productivity also matters.
Many developers prefer working inside VS Code with the Salesforce extensions and CLI tools.
DevOps platforms should integrate with that workflow rather than forcing developers into entirely separate interfaces.
Serpent includes a VS Code extension so developers can:
compare metadata
validate deployments
manage release tasks
deploy changes
This keeps DevOps operations close to the development environment.
The pipeline examples above assume your GitHub Actions runner can authenticate to Salesforce headlessly. The standard pattern is JWT-based auth backed by a Connected App. Here is the practical setup, end to end.
In Salesforce Setup, go to App Manager → New Connected App.
Enable OAuth settings. Set the callback URL to
http://localhost:1717/OauthRedirect.
Select the scopes Manage user data via APIs (api) and Perform requests at any time (refresh_token, offline_access).
Enable Use digital signatures and upload your certificate (generated below).
openssl genrsa -out server.key 2048
openssl req -new -x509 -days 1825 -key server.key -out server.crt
Upload server.crt to the Connected App. Store
server.key securely — it goes into GitHub Secrets, never into the
repo.
In your GitHub repository go to Settings → Secrets and variables → Actions and add:
SF_CLIENT_ID — Consumer Key from your Connected App.
SF_USERNAME — your Salesforce username, for example
[email protected].
SF_JWT_KEY — the contents of your server.key file.
SF_INSTANCE_URL — https://test.salesforce.com for
sandbox or https://login.salesforce.com for production.
Most CI/CD failures fall into a small set of repeat issues. The table below covers the ones we see most often.
| Error | Likely cause | Fix |
|---|---|---|
INVALID_LOGIN |
JWT key or client ID wrong | Verify secrets match exactly. No trailing newlines in the key. |
INSUFFICIENT_ACCESS |
Connected App permissions or activation | Check OAuth scopes and confirm the connected app is activated for the integration user. |
| Tests failing on deploy | Code coverage below 75 percent |
Run sf apex run test locally first to see which classes need
coverage.
|
| Metadata not deploying | SFDX project structure wrong |
Ensure sfdx-project.json has the correct
packageDirectories.
|
| Job times out mid-deploy | Long-running validation or full-org tests |
Use --test-level RunLocalTests on PR validation. Reserve
RunAllTestsInOrg for the production job.
|
Salesforce DevOps is not about one specific tool or one exact pipeline format.
It is about creating a reliable process that allows teams to deliver changes safely and consistently.
Small teams may start with simple pipelines and a single staging environment. Larger organisations require structured environments, versioned releases, and modular packages.
The key principles remain the same:
Git should be the source of truth
changes should be validated automatically
releases should be versioned and traceable
environments should remain consistent
Understanding how CI/CD pipelines work under the hood is valuable. But as Salesforce environments grow more complex, coordinating releases across people, environments, and packages becomes the real challenge.
That is where tools like Serpent help teams maintain speed while preserving control, visibility, and release confidence.
Commitment free!