Andrew Hanna

Andrew Hanna

Een Salesforce CI/CD pipeline bouwen met GitHub Actions

Een Salesforce CI/CD pipeline bouwen met GitHub Actions

Salesforce development is ver verwijderd van handmatige Change Sets. Zodra teams groeien en vaker releasen, worden handmatige deployments snel een bottleneck.

Daarom is CI/CD (Continuous Integration and Continuous Delivery) belangrijk. Een goede pipeline valideert, test en promoot wijzigingen automatisch van development naar production.

In deze gids kijken we hoe Salesforce CI/CD werkt, hoe je een pipeline bouwt met GitHub Actions, en wanneer DIY automation genoeg is of juist te kwetsbaar wordt.

Nieuw in het vakgebied? Onze gids over Salesforce DevOps onder de knie krijgen behandelt de basis waarop deze pipeline voortbouwt.

Waarom CI/CD belangrijk is voor Salesforce

Salesforce teams deployen geen gecompileerde app, maar metadata en configuratie zoals:

  • Apex classes en triggers

  • Lightning Web Components

  • Flows en automation

  • Profiles, permission sets, objects, fields en layouts

Zonder automation ontstaan snel drift tussen omgevingen, ontbrekende dependencies, handmatige fouten, weinig zicht op wat er echt is gewijzigd en slechte traceability tussen work items en releasecontent.

CI/CD pipelines lossen dit op door:

  • wijzigingen automatisch te valideren

  • tests te draaien vóór deployment

  • Git als source of truth te gebruiken

  • alleen goedgekeurde wijzigingen te promoten

  • omgevingen consistent te houden over tijd

Wat je nodig hebt voordat je begint

Deze gids gaat uit van een repository en een set orgs die al op automatisering zijn ingericht. Ontbreekt er een, dicht dat gat dan eerst: een pipeline eroverheen automatiseert alleen het gat.

  • Metadata al onder versiebeheer, in source format. Alles wat je wil deployen staat onder force-app en heeft een commit-historie. De pipeline promoot commits, geen orgs, dus wat alleen in een sandbox bestaat is voor haar onzichtbaar.

  • Salesforce CLI v2 (sf) op de runner. Elk commando in deze gids is de v2-vorm, in de workflow geinstalleerd met npm install @salesforce/cli --global. De oude sfdx CLI is deprecated.

  • Een org per fase, met een duidelijke eigenaar. Minimaal een sandbox om tegen te valideren en de org waarnaar je releaset. De branching strategy verderop koppelt branches een op een aan die orgs.

  • Een Connected App en een integration user in elke doelorg, met digital signatures aan en de user vooraf geautoriseerd. De stappen staan verderop bij Connected App en JWT authentication.

Salesforce development modellen

1. Org-based development

Dit blijft het meest gebruikte model. Developers werken in sandboxes, metadata wordt naar Git gehaald en CI/CD promoot die metadata door de omgevingen.

Developer Sandbox
      ↓
QA
      ↓
UAT / Staging
      ↓
Production

Veel enterprise teams werken nog steeds zo, ook wanneer ze verderop in de releaseflow unlocked packages beginnen te introduceren.

2. Package-based development

Package-based development gebruikt modulaire packages in plaats van de volledige org metadata als één geheel te promoten. Dit past goed bij ISVs, AppExchange partners en enterprise teams met meerdere workstreams.

In de praktijk starten veel teams org-based en groeien ze geleidelijk naar packages voor release versioning en een schonere promotion.

Aanbevolen patronen per teamgrootte

Kleine teams

Bij hele kleine teams, bijvoorbeeld twee of drie contributors, kunnen te veel omgevingen meer overhead dan waarde opleveren. Dan is een lichter proces meestal beter.

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

Dit werkt wanneer het team klein is en nauw samenwerkt, releases vaak plaatsvinden, één gedeelde staging of QA omgeving genoeg is voor de laatste validatie, en het team discipline wil zonder te veel procesoverhead.

De kern is dat ook een klein team een schoon releaseproces nodig heeft. Het moet alleen niet zwaarder zijn dan het team zelf.

Groeiende teams

Naarmate teams groeien, wordt een aparte QA omgeving nuttig om geïntegreerde source changes te valideren voordat er een release candidate wordt gebouwd.

Qa Devops

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

Dit model is sterker voor grotere teams omdat QA en UAT een ander doel dienen:

  • QA valideert geïntegreerde source changes van meerdere contributors

  • UAT valideert het daadwerkelijke release artifact

  • Production ontvangt dezelfde package version die UAT doorstond

Dat geeft betere release traceability, schonere rollback-opties en minder verrassingen in production.

ISVs en modular packages

ISVs volgen vaak dezelfde principes, maar hun workflows zijn vanaf het begin meer package-led.

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

Dezelfde ideeën blijven gelden: Git als source of truth, duidelijke promotion-stappen en versioned release artifacts.

Naarmate de Salesforce org groter en meer domeingedreven wordt, kan een enkel unlocked package te breed worden. Dan zijn modulaire unlocked packages meestal logischer, bijvoorbeeld voor sales, service operations, onboarding en core automation. Dit is vooral nuttig bij grote bedrijven waar meerdere teams parallel aan verschillende domeinen werken: groeit het team of de org, dan moet het DevOps-proces meegroeien om snelheid, kwaliteit, zichtbaarheid en traceability te behouden.

Branching strategy

Kies eerst hoe werk door Git beweegt. De meest voorkomende opties zijn GitFlow en trunk-based development.

GitFlow

main      → production history
develop   → integration branch
feature/* → work item branches
release/* → release candidate branches
hotfix/*  → urgent production fixes

GitFlow past goed bij teams met QA, UAT, scheduled releases en expliciete release candidate branches. Het nadeel is meer branch management en meer merge-coordinatie.

Trunk-Based Development

main       → always releasable
feature/*  → short-lived branches merged quickly

Dit werkt goed wanneer het team klein is, deployments frequent zijn, validatie en tests sterk staan, en het team liever geen langlevende branches beheert. Het nadeel is dat zwakke testdiscipline de gedeelde branch snel kan destabiliseren.

Welk model raden we aan?

Voor veel org-based Salesforce teams is GitFlow makkelijker te adopteren omdat het het vertrouwde Dev → QA → UAT → Prod releasemodel weerspiegelt. Trunk-based development kan goed werken voor kleine, mature teams met sterke automation.

Het ene model is niet per se beter dan het andere. Belangrijker is dat je bewust een model kiest en je omgevingen, approvals en pipeline-logica ermee in lijn brengt.

Aanbevolen pipeline architectuur

Voor de meeste groeiende Salesforce teams raden we dit model aan:

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

Waarom raden we dit model aan?

  • source deploy naar QA is flexibel en snel voor integratietests

  • de package version wordt een stabiel release artifact

  • UAT valideert het exacte artifact dat voor production bedoeld is

  • production ontvangt dezelfde geteste package version

Dit levert een proces op dat zowel praktisch als beheerst is. Het werkt ook goed met source tracking tijdens development, terwijl je verderop in de flow betere release traceability krijgt.

package.xml vs source tracking vs Serpent workflow

Categorie Package.xml Source tracking + unlocked packages Serpent workflow
Model Manifest-driven deployment Git-driven development met package promotion Task-driven workflow rond Git, omgevingen en releases
Best voor Hotfixes en legacy teams Moderne teams met versioned releases Teams die snelheid, zichtbaarheid en minder friction willen
Metadata selectie Handmatig Source tracking plus Git-selectie Begeleide, task-based en dependency-aware selectie
Dependencies Handmatig Beter, maar discipline nodig Ondersteund door metadata analyse en workflow logica
Productie traceability Beperkt Goed bij gebruik van package versions Sterke zichtbaarheid over work items, releases en orgs
Rollback duidelijkheid Moeilijker, vooral bij destructive changes Beter met versioned packages Duidelijker via release artifacts, workflow-historie en package promotion
Hotfix / back-promotion Nuttig en vaak noodzakelijk Soms nog steeds package.xml nodig voor productieherstel Ondersteund als onderdeel van de release workflow en het zichtbaarheidsmodel
Operationele overhead Laag in het begin, hoog later Gemiddeld Lagere doorlopende overhead omdat meer van de workflow native en zichtbaar is
Samenwerking Grotendeels handmatig Git-centric en developer-friendly Geschikt voor developers, admins, testers en release managers
Wanneer we het aanraden Specifieke edge cases en legacy processen Aanbevolen standaard voor de meeste moderne teams Aanbevolen wanneer je dezelfde DevOps-principes wilt met veel minder friction

package.xml en source tracking

package.xml is het traditionele manifest voor metadata die je wilt ophalen of deployen.

<?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>

Voordelen:

  • expliciete controle over wat er wordt gedeployed

  • werkt in alle orgtypes, inclusief production

  • nuttig voor hotfixes en gerichte deploys

Nadelen:

  • handmatig te onderhouden

  • dependencies worden makkelijk gemist

  • weerspiegelt niet automatisch wat er in de org is gewijzigd

  • moeilijker te beheren naarmate de releasescope groeit

Veel teams gebruiken package.xml nog steeds, en dat is historisch verklaarbaar: lange tijd was het de meest praktische optie, zeker toen source tracking nog onvolgroeid was.

Source tracking laat Salesforce CLI vergelijken wat in een org is gewijzigd ten opzichte van lokale source. Het is vooral nuttig in scratch orgs en is de afgelopen jaren, sinds de migratie van de oude SFDX CLI naar de moderne sf CLI, flink praktischer geworden in sandbox-gebaseerde workflows.

sf project retrieve start
sf project deploy start

Voordelen:

  • snellere feedback tijdens development

  • minder handmatig manifestonderhoud

  • betere developer ergonomics

  • werkt goed in combinatie met Git-gedreven promotion

Nadelen:

  • production ondersteunt source tracking niet op dezelfde manier

  • teams hebben nog steeds een strategie voor release versioning nodig

  • gedeeltelijke feature-selectie kan onhandig blijven zonder de juiste tooling

Voor de meeste moderne teams raden we source tracking aan tijdens development, met unlocked packages voor release promotion. Source tracking is dagelijks een prettigere developer-ervaring dan handmatige manifesten, unlocked packages geven je verderop in de flow echte release versioning, productie traceability wordt veel duidelijker via package versions, en rollback en destructive changes zijn makkelijker te doorgronden.

package.xml blijft wel relevant in edge cases. Het echte doel is niet zuiverheid, maar het juiste gereedschap voor de juiste stap.

En production dan?

Bij production worden de beperkingen van source tracking belangrijk. Je kunt production niet als scratch org behandelen en Salesforce netjes laten vertellen wat er over tijd is gewijzigd.

Dat betekent dat production deployments meestal steunen op:

  • package.xml manifests

  • source directories opgebouwd vanuit Git

  • unlocked package versions

Dit is een van de belangrijkste redenen waarom versioned package promotion zo waardevol is: het geeft production een duidelijkere release-identiteit dan source tracking alleen daar kan bieden.

De praktische kern is simpel: production gedraagt zich niet als een source-tracked dev org, dus teams hebben een andere manier nodig om de releasestatus expliciet te maken. Unlocked packages zijn een van de beste manieren om dat te doen.

Feature-level changes veilig selecteren

Een veelvoorkomend pijnpunt met source tracking: je developer sandbox bevat meerdere losse wijzigingen, maar je wilt maar één feature verder brengen.

Het handmatige proces ziet er meestal zo uit:

  1. alles ophalen uit de sandbox

  2. de gewijzigde bestanden in Git bekijken

  3. alleen de metadata selecteren die bij de feature hoort

  4. alleen die featurescope committen

Voorbeeld:

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"

De uitdaging is dat dependencies niet altijd voor de hand liggen. Een Flow kan naar een veld verwijzen, een veld kan afhankelijk zijn van een permission set, en een wijziging in die permission set kan nodig zijn om de feature bruikbaar te maken.

Hier missen teams vaak metadata, of deployen ze juist te veel metadata om het zekere voor het onzekere te nemen.

Met Serpent wordt dit makkelijker omdat work items, metadata selection en dependency analysis in dezelfde workflow zitten.

Delta deployments vs full branch deployments

Wanneer delta deployments zin hebben

Delta deployments pushen alleen de gewijzigde metadata. Ze zijn nuttig voor:

  • snel valideren van pull requests

  • promoten van kleine, geïsoleerde features

  • kortere validatietijd in zeer grote orgs

Voorbeeld met sfdx-git-delta:

sf sgd source delta \
  --to HEAD \
  --from origin/develop \
  --output-dir delta \
  --generate-delta

Valideer of deploy daarna de delta:

sf project deploy validate --source-dir delta --test-level RunLocalTests
sf project deploy start --source-dir delta

Open-source CLI suites verpakken diezelfde commando's achter een begeleid menu, een redelijk middenweg tussen ruwe scripts en een platform. Zie hoe Serpent zich verhoudt tot sfdx-hardis als je dat pad overweegt.

Wanneer full deployments meer zin hebben

Full branch deployments pushen de hele metadata tree of een volledige branchstaat. Ze zijn beter bij:

  • het verwerken van deletions of destructive changes

  • het terugdraaien van commits

  • ervoor zorgen dat een omgeving exact een bekende branchstaat volgt

  • het resetten van drift in een gedeelde omgeving

  • het promoten van profiles of permission sets, waar het hele bestand in één stuk moet landen in plaats van als berekende diff

sf project deploy start --source-dir force-app

Delta deployments zijn geweldig zolang je vooral metadata toevoegt of wijzigt. Ze zijn minder betrouwbaar zodra de branch deletions, revert commits of bredere consistency resets bevat. Hetzelfde geldt voor types die nog in grote gedeelde bestanden leven, vooral profiles en permission sets, die niet schoon diffen en door een delta deels gemist of overschreven kunnen worden.

In de praktijk gebruikt een sterk proces allebei:

  • delta deploys voor snelheid

  • full deploys voor zekerheid wanneer het wijzigingstype dat vereist

Beide vormen duwen code omhoog. De andere richting telt net zo hard: na een hotfix rechtstreeks op productie lopen je lagere omgevingen achter totdat je back-promotion in de pipeline inricht, de stap die de meeste handgemaakte workflows overslaan.

Serpent helpt hier ook door de deploymentflow minder brittle te maken. In plaats van dat elk team deze logica handmatig in GitHub Actions naboetseert, kan de workflow aangeven wanneer gerichte beweging volstaat en wanneer een bredere promotion veiliger is.

Unlocked packages voor release versioning

Ook in org-based development kunnen unlocked packages het releasebeheer flink verbeteren. In plaats van een losse set metadata rechtstreeks in elke hogere omgeving te deployen, verpakken teams een logische set wijzigingen in een versioned release artifact.

Dat betekent:

  • duidelijke versienummers voor releases

  • eenvoudigere promotion van UAT naar Production

  • beter zicht op wat waar is gedeployed

  • schonere dependency-afhandeling

  • beter voorspelbare rollback planning

Aanbevolen unlocked package flow

Voor een groter org-based team ziet de releaseflow er meestal zo uit:

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

Voor een heel klein team kan het simpeler:

Developer Sandbox
      ↓
Retrieve to Git
      ↓
Pull Request validation
      ↓
Build unlocked package version
      ↓
Install in Staging
      ↓
Promote same package version to Production

Voorbeeldcommando's

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

Waarom rollback makkelijker te doorgronden is

Unlocked packages lossen niet elk rollbackprobleem magisch op, zeker niet zodra datawijzigingen erbij komen. Maar vergeleken met rauwe source deployment of alleen package.xml-promotion maken ze rollback makkelijker te begrijpen, omdat:

  • de release een versioned artifact is

  • je precies weet welke package version de wijziging introduceerde

  • je duidelijker kunt vergelijken wat er tussen versions is gewijzigd

  • destructive changes makkelijker te volgen zijn binnen de releasehistorie

Dit weegt nog zwaarder in production, waar source tracking je niet dezelfde zichtbaarheid geeft als in development-omgevingen.

GitHub Actions pipeline bouwen

GitHub Actions is geschikt voor teams die de mechanics willen begrijpen en hun eigen CI/CD foundation willen bouwen. Het integreert direct met je repository, en workflows staan als YAML-bestanden naast de code.

Voorgestelde branch-naar-omgeving mapping

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

Volledig end-to-end GitHub Actions pipeline voorbeeld

Het voorbeeld hieronder laat een praktische end-to-end opzet zien met:

  • PR-validatie richting develop

  • automatische source deploy naar QA bij merge naar develop

  • package version build op release/*

  • package install naar UAT

  • handmatige promotion van dezelfde package version naar 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

Dit is niet het enige geldige GitHub Actions ontwerp, maar het toont de aanbevolen scheiding tussen source-based validatie in lagere omgevingen en package-based promotion naar UAT en Production.

Aanvullende pipelinepraktijken en operationele richtlijnen

Een CI/CD pipeline is maar een deel van een werkend Salesforce DevOps proces. Wat vaak het verschil maakt tussen slagen en falen is hoe teams omgaan met de operationele details rond development, testen, promotion en releasecoördinatie.

Feature-level wijzigingen veilig beheren

Een uitdaging in Salesforce development is dat sandboxes vaak meerdere wijzigingen tegelijk opstapelen. Een developer werkt misschien aan twee of drie features, maar slechts één is klaar voor promotion.

Als je simpelweg alle metadata ophaalt en doorzet, riskeer je onaf werk te promoten.

Het veiligere patroon 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

Voorbeeldworkflow:

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"

Zo weerspiegelt de repository één logische feature in plaats van een mix van niet gerelateerde wijzigingen.

Het lastige is dat Salesforce metadata vaak onderling verbonden is. Een Flow kan afhankelijk zijn van:

  • custom fields

  • Apex classes

  • permission sets

  • record types

Het missen van zo'n dependency kan de deploymentvalidatie breken.

Veel teams lossen dit op via ervaring en handmatige checks. Platforms zoals Serpent helpen door metadatarelaties te analyseren en dependencies automatisch te herkennen, zodat de juiste set componenten samen meebeweegt.

AI-ondersteunde metadataselectie

Recente verbeteringen in developer tooling hebben AI-ondersteuning voor dependency-analyse geintroduceerd.

In de praktijk betekent dit:

  • een featurebeschrijving kan worden geanalyseerd

  • gerelateerde metadatacomponenten kunnen worden gedetecteerd

  • dependency graphs kunnen worden geëvalueerd

  • de meest waarschijnlijke metadatascope kan worden voorgesteld

Serpent bevat AI-agents die zijn getraind op Salesforce metadatapatronen en die helpen de logische set componenten voor een feature te bepalen, wat het risico op onvolledige deployments verkleint.

Dit haalt de controle niet bij de developer weg. Het maakt het selectieproces alleen sneller en veiliger.

Sandbox drift beheersen

Een ander veelvoorkomend probleem in Salesforce omgevingen is sandbox drift.

Dit gebeurt wanneer iemand een wijziging rechtstreeks doorvoert in een gedeelde omgeving zoals QA of UAT, zonder dat die ooit in de Git repository terechtkomt.

Na verloop van tijd raakt de omgeving los van source control.

Om drift te detecteren, doen teams vaak periodieke retrieve-vergelijkingen:

sf project retrieve start
git diff

Verschijnen er onverwachte verschillen, dan moeten die meteen worden opgelost door ze naar Git te committen of te verwijderen.

Laat je drift opstapelen, dan worden latere deployments onvoorspelbaar.

Omgevingsstrategie

Elke omgeving moet een duidelijk doel hebben.

Omgeving Doel
Developer Sandbox Feature development en vroege tests
QA Geïntegreerde tests over meerdere features
UAT Business validatie van release candidates
Production Live systeem

Problemen ontstaan vaak wanneer omgevingen hun doel verliezen. Wordt QA zelf weer een development sandbox, dan werkt het niet langer als betrouwbaar integratiepunt.

Teststrategie

Een robuust CI/CD proces bevat automated tests op meerdere niveaus.

  • unit tests voor Apex

  • component tests voor LWC

  • integratietests voor cross-feature workflows

PR-validatiepipelines draaien vaak gerichte tests voor snelheid, terwijl releasepipelines bredere testsuites draaien.

Voorbeeld PR-validatie:

sf project deploy validate \
  --source-dir delta \
  --test-level RunSpecifiedTests

Voorbeeld releasevalidatie:

sf project deploy validate \
  --source-dir force-app \
  --test-level RunLocalTests

Het doel is problemen vroeg te vangen zonder de developer-feedbackcyclus onnodig te vertragen. Welke suite op welke gate draait, en waar UI-coverage past, is een onderwerp op zich: hoe je een Salesforce test automation pipeline in CI bouwt loopt de lagen in volgorde langs.

Releasezichtbaarheid

Een van de meest onderschatte DevOps-uitdagingen is simpelweg weten wat er in een release zit.

Vragen die teams vaak stellen:

  • Welke features zitten in de volgende release?

  • Welke componenten zijn gisteren naar QA gedeployed?

  • Welke package version staat momenteel in production?

Zonder gestructureerde workflow vraagt het beantwoorden van die vragen vaak zoeken door Git-historie, pipelinelogs en handmatige release notes.

Serpent pakt dit aan door deployments te koppelen aan work items, waardoor releases makkelijker te begrijpen zijn over omgevingen heen.

Developer experience

Ook developerproductiviteit telt.

Veel developers werken het liefst in VS Code met de Salesforce-extensies en CLI-tools.

DevOps platforms moeten zich aanpassen aan die workflow, in plaats van developers naar volledig aparte interfaces te dwingen.

Serpent heeft een VS Code extension waarmee developers:

  • metadata kunnen vergelijken

  • deployments kunnen valideren

  • releasetaken kunnen beheren

  • wijzigingen kunnen deployen

Dit houdt DevOps-operaties dicht bij de development-omgeving.

Connected App en JWT authentication

De pipelinevoorbeelden hierboven gaan ervan uit dat je GitHub Actions runner headless kan authenticeren bij Salesforce. Het standaardpatroon is JWT-based auth via een Connected App. Hier is de praktische opzet, end to end.

1. Connected App aanmaken in Salesforce

  1. Ga in Salesforce Setup naar App Manager → New Connected App.

  2. Zet OAuth aan en gebruik als callback URL http://localhost:1717/OauthRedirect.

  3. Selecteer de scopes Manage user data via APIs (api) en Perform requests at any time (refresh_token, offline_access).

  4. Zet Use digital signatures aan en upload je certificaat (hieronder gegenereerd).

2. Het certificaat genereren

openssl genrsa -out server.key 2048
openssl req -new -x509 -days 1825 -key server.key -out server.crt

Upload server.crt naar de Connected App. Bewaar server.key veilig - die gaat in GitHub Secrets, nooit in de repo.

3. GitHub Secrets toevoegen

Ga in je GitHub repository naar Settings → Secrets and variables → Actions en voeg toe:

De workflow hierboven authenticeert bij vier losse orgs - QA, de Dev Hub, UAT en Production - dus heeft elk secret een kopie per omgeving nodig, met het achtervoegsel _QA, _DEVHUB, _UAT of _PROD (bijvoorbeeld SF_CLIENT_ID_QA en SF_CLIENT_ID_DEVHUB). Herhaal de vier secrets hieronder voor elk achtervoegsel, wijzend naar de Connected App en org van die specifieke omgeving:

  • SF_CLIENT_ID_<ENV> - de Consumer Key van de Connected App van die omgeving.

  • SF_USERNAME_<ENV> - de Salesforce gebruikersnaam van die omgeving, bijvoorbeeld [email protected].

  • SF_JWT_KEY_<ENV> - de inhoud van het server.key bestand van die omgeving.

  • SF_INSTANCE_URL_<ENV> - https://test.salesforce.com voor sandboxes (QA, UAT, Dev Hub) of https://login.salesforce.com voor production.

Veelvoorkomende fouten

De meeste CI/CD-fouten vallen in een kleine set terugkerende problemen. De tabel hieronder behandelt de fouten die we het vaakst zien.

Error Waarschijnlijke oorzaak Fix
INVALID_LOGIN JWT key of client ID klopt niet Controleer secrets en trailing newlines.
INSUFFICIENT_ACCESS Connected App permissies Controleer scopes en activatie voor de integration user.
Tests failing on deploy Code coverage onder 75 procent Draai sf apex run test lokaal en verhoog coverage.
Metadata not deploying SFDX-projectstructuur klopt niet Controleer of sfdx-project.json de juiste packageDirectories heeft.
Job loopt vast tijdens deploy Langlopende validatie of full-org tests Gebruik --test-level RunLocalTests bij PR-validatie. Reserveer RunAllTestsInOrg voor de productiejob.

FAQ

Hoe authenticeert GitHub Actions bij een Salesforce-org?

Via een Connected App met de JWT-flow. Je maakt de app aan in App Manager, uploadt een certificaat en bewaart de private key plus de consumer key in GitHub Secrets, waarna de workflow daarmee inlogt voor elke deploystap.

Waarom faalt mijn pipeline met INVALID_LOGIN?

De JWT-key of de client ID komt niet overeen met wat de Connected App verwacht. Controleer je secrets teken voor teken en let op een trailing newline aan het eind van de private key.

Waarom falen mijn tests tijdens de deploy?

Meestal ligt de code coverage onder 75 procent. Draai sf apex run test lokaal om te zien welke klassen dekking missen, en gebruik bij PR-validatie RunLocalTests zodat de job snel blijft. Welke suite op welke gate hoort, leggen we uit in de gids over test automation in CI.

Kies je delta deployments of full deployments?

Delta deployments pushen alleen wat er is gewijzigd en zijn ideaal voor het valideren van pull requests en kleine features in grote orgs. Full deployments zijn veiliger bij verwijderingen, reverts en als een omgeving exact een branchstaat moet volgen.

Heb ik sfdx-git-delta nodig?

Nee, maar het is de moeite waard zodra deploys traag worden. Combineer het met een full deploy voor verwijderingen, zodat destructive changes worden afgehandeld en niet stilletjes overgeslagen.

Moet ik sfdx of sf gebruiken voor een Salesforce GitHub Actions-pijplijn?

Gebruik sf, de moderne Salesforce CLI, want de oude sfdx CLI die hij verving is deprecated en hoort niet de basis van een nieuwe pijplijn te zijn. Een bestaande pijplijn overzetten is vooral een kwestie van commandonamen: sfdx force:source:deploy wordt sf project deploy start, sfdx force:apex:test:run wordt sf apex run test, en plugins installeer je opnieuw via sf plugins install.

Waarom valideren bij de pull request en bij merge opnieuw deployen?

De pull request-job bewijst dat de branch compileert en zijn tests haalt tegen de doelorg, zonder er iets naartoe te schrijven. De merge-job wijzigt de org wel, en draait tegen de merge-commit, niet tegen de boom die de pull request valideerde.

Conclusie

Salesforce DevOps draait niet om één tool of één perfecte pipeline. Het gaat om een betrouwbaar proces waarin Git de source of truth is, wijzigingen automatisch worden gevalideerd, releases versioned zijn en omgevingen consistent blijven.

Kleine teams beginnen vaak met eenvoudige pipelines en één staging omgeving. Grotere organisaties hebben gestructureerde omgevingen, versioned releases en modulaire packages nodig.

GitHub Actions helpt teams de onderliggende automation te begrijpen. Naarmate Salesforce omgevingen complexer worden, helpen tools zoals Serpent om snelheid te behouden zonder controle, zichtbaarheid en release confidence te verliezen.

Gerelateerde artikelen

Benieuwd naar sneller releasen voordat je instapt? Laten we praten

Vrijblijvend.