---
url: /salesforce-devops-starter-kit/getting-started.md
description: >-
Set up a first Salesforce CI/CD pipeline with the kit - from choosing a
strategy and configuring JWT authentication to pull request validation and
deployment.
---
# Getting Started
This guide takes you from an empty repository to a working Salesforce CI/CD pipeline using the building blocks of this kit.
## 1. Decide your strategy
Before you wire up a single workflow, settle **which** org landscape and branching model the pipeline runs on. That decision determines how many environments you need, which branches are protected, and which workflows you switch on - and it is expensive to reverse once a team is working in it.
If you already have a branching model that works, skip ahead. If you do not, or you are not sure it still fits:
➡️ **[Find your blueprint](https://svierk.github.io/salesforce-devops-starter-kit/strategy.md)** - a short guided questionnaire - a handful of questions - leading to one of five blueprints. Each blueprint page lists the exact workflows, GitHub Environments and branch protection rules to configure, so you arrive at step 4 below knowing what to copy.
::: tip Not a commitment
Every blueprint documents its migration path to the next one. Start with the simplest model that fits and move when the signals say so - [Strategy & Blueprints](https://svierk.github.io/salesforce-devops-starter-kit/strategy.md) covers both.
:::
## 2. Prerequisites
* A Salesforce DX project in a GitHub repository (a `sfdx-project.json` at the root).
* A **Dev Hub** enabled org (for scratch org flows) and/or the sandbox/production orgs you deploy to.
* A dedicated CI integration user.
## 3. Set up authentication
Follow [authentication.md](https://svierk.github.io/salesforce-devops-starter-kit/authentication.md) to authenticate your org with the JWT flow and store the credentials as GitHub Actions secrets (`SFDX_CONSUMER_KEY`, `SFDX_JWT_SECRET_KEY` and `SFDX_USERNAME`). This is the only manual setup step.
If your blueprint uses more than one target org, create one **GitHub Environment per org** and store these three secrets in each - the deployment workflow resolves them per environment, so one caller serves every stage. Pull request validation can use the same mechanism through its own `environment` input; scratch org CI keeps using the repository secrets of your Dev Hub.
## 4. Choose a starting point
You have two ways to adopt the kit:
### Option 1 - Use a reusable workflow (fastest)
Copy one of the [examples](https://github.com/svierk/salesforce-devops-starter-kit/blob/main/examples) into your project's `.github/workflows/` directory. For pull request validation:
```yaml
name: PR Validation
on:
pull_request:
branches: [main]
# A reusable workflow can never get more permissions than the caller grants -
# pr-validation needs security-events: write to upload its SARIF report.
permissions:
contents: read
security-events: write
jobs:
validate:
uses: svierk/salesforce-devops-starter-kit/.github/workflows/pr-validation.yml@v1.5.0
with:
source-dir: force-app
secrets:
SFDX_CONSUMER_KEY: ${{ secrets.SFDX_CONSUMER_KEY }}
SFDX_JWT_SECRET_KEY: ${{ secrets.SFDX_JWT_SECRET_KEY }}
SFDX_USERNAME: ${{ secrets.SFDX_USERNAME }}
```
Commit it, open a pull request, and watch the validation run.
### Option 2 - Compose your own pipeline
If you need more control, wire the building blocks together yourself:
```yaml
name: CI
on:
pull_request:
branches: [main]
# Least-privilege token: this pipeline only reads the repository. Add
# security-events: write if you upload the code analysis results as SARIF.
permissions:
contents: read
jobs:
validate:
name: Validate
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
fetch-depth: 0
persist-credentials: false # don't leave the GITHUB_TOKEN in .git/config for later steps
- name: Install SF CLI
uses: svierk/sfdx-cli-setup@v1.2.0
- name: Salesforce Org Login
uses: svierk/sfdx-login@v1.4.3
with:
client-id: ${{ secrets.SFDX_CONSUMER_KEY }}
jwt-secret-key: ${{ secrets.SFDX_JWT_SECRET_KEY }}
username: ${{ secrets.SFDX_USERNAME }}
alias: ci
- name: Code Review
uses: svierk/sfdx-code-review@v1.1.1
with:
workspace: force-app
severity-threshold: 3
- name: Validate Deployment
uses: svierk/sfdx-deploy@v1.3.0
with:
delta: true
delta-from: origin/${{ github.base_ref }}
target-org: ci
test-level: RunLocalTests
dry-run: true
```
## 5. Recommended next steps
* Add the **scratch org CI** workflow to run tests in a clean org on every change.
* Add the **create scratch org** workflow so team members can provision ready-to-use orgs on demand (self-service, no local CLI needed).
* Add a **deployment** workflow gated by a GitHub Environment for production.
* Grant the deployed permission sets with **sfdx-assign-permissions** and automate post-deploy steps (data migrations, cache clearing) with **sfdx-apex-execute** - both are wired into the reusable workflows as optional inputs.
* Keep the version pins current with **Dependabot** and review the hardening checklist (see [security & versioning](https://github.com/svierk/salesforce-devops-starter-kit/blob/main/README.md#-security--versioning)).
* Revisit your [branching strategy](https://svierk.github.io/salesforce-devops-starter-kit/strategy.md) once a second team or a formal approval gate appears - each blueprint documents the signals and the migration path.
## Building block reference
Every action documents its full inputs and outputs in its own repository - see the [building blocks catalog](https://github.com/svierk/salesforce-devops-starter-kit/blob/main/README.md#-building-blocks).
---
---
url: /salesforce-devops-starter-kit/authentication.md
description: >-
Authenticate CI/CD pipelines to Salesforce orgs with the JWT bearer flow or an
SFDX Auth URL, and store the credentials as GitHub Actions secrets.
---
# Authentication
CI/CD pipelines need a non-interactive way to authenticate to your Salesforce orgs. The [sfdx-login](https://github.com/svierk/sfdx-login) building block supports two options. Pick **one** per org and store the resulting credential as a GitHub Actions secret.
> Always use a dedicated **integration user** for CI/CD, never a personal account, and grant it only the permissions it needs.
**The reusable workflows and examples in this kit use the JWT flow (Option A)**, since it does not depend on a refresh token that can expire or be revoked - the closest match to a production-ready setup. The SFDX Auth URL (Option B) is offered as a quicker alternative for local experiments and throwaway scratch orgs.
## Option A - JWT bearer flow (recommended)
Best for production and long-lived sandbox connections, as it does not rely on a refresh token.
1. Create a self-signed certificate with OpenSSL:
```bash
openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -keyout server.key -out server.crt
```
2. Create a **connected app** in the org, enable OAuth, upload `server.crt`, and enable the JWT flow. Pre-authorize the integration user via a permission set or profile. See the Salesforce guide [Authorize an Org Using the JWT Flow](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth_jwt_flow.htm).
3. Store these three values as repository secrets:
* `SFDX_CONSUMER_KEY` - the connected app's consumer key (client ID)
* `SFDX_JWT_SECRET_KEY` - the **contents** of `server.key`
* `SFDX_USERNAME` - the integration user's username
4. Use them in a workflow:
```yaml
- name: Salesforce Org Login
uses: svierk/sfdx-login@v1.4.3
with:
client-id: ${{ secrets.SFDX_CONSUMER_KEY }}
jwt-secret-key: ${{ secrets.SFDX_JWT_SECRET_KEY }}
username: ${{ secrets.SFDX_USERNAME }}
```
## Option B - SFDX Auth URL (quickest)
Best for getting started and for throwaway scratch org / sandbox flows. Note that the reusable workflows in this kit expect the JWT secrets above; to use an Auth URL instead, swap the login step's inputs in your own composed pipeline (see the [getting started guide](https://svierk.github.io/salesforce-devops-starter-kit/getting-started.md)).
1. Authorize the org once on your machine:
```bash
sf org login web --alias my-org
```
2. Display the authorization URL and write it to a file:
```bash
sf org display --target-org my-org --verbose --json > authFile.json
```
The URL is the `sfdxAuthUrl` property inside `result`.
3. Storing raw JSON in GitHub secrets is known to cause issues, so Base64-encode the file contents:
```bash
cat authFile.json | base64
```
4. Save the resulting Base64 string as a repository secret, e.g. `SFDX_AUTH_URL`.
5. Use it in a workflow:
```yaml
- name: Salesforce Org Login
uses: svierk/sfdx-login@v1.4.3
with:
sfdx-url: ${{ secrets.SFDX_AUTH_URL }}
alias: my-org
```
## Storing secrets
Add secrets under **Settings → Secrets and variables → Actions** in your repository (or organization). For environment-specific credentials (e.g. separate sandbox and production orgs), use [GitHub Environments](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment) with required reviewers for production deployments. The [deployment workflow](https://github.com/svierk/salesforce-devops-starter-kit/blob/main/.github/workflows/deployment.yml) accepts an `environment` input for exactly this - set it to your environment name (see [examples/deployment.yml](https://github.com/svierk/salesforce-devops-starter-kit/blob/main/examples/deployment.yml)) and store that environment's credentials as environment-scoped secrets.
## References
* [Using secrets in GitHub Actions](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions)
* [How To Use GitHub Actions, OAuth and SFDX-CLI for Continuous Integration](https://salesforcedevops.net/index.php/2022/04/05/how-to-use-github-actions-oauth-and-sfdx-cli-for-continuous-integration/)
---
---
url: /salesforce-devops-starter-kit/strategy.md
description: >-
Five questions about orgs, teams, team setup, maturity and release style route
a Salesforce team to one of five DevOps blueprints.
---
# Find your blueprint
Answer a handful of questions about your org landscape, your team and how you release. You get one of five blueprints - a concrete target picture with the workflows, environments and branch protection rules of this kit to switch on. The **?** next to each question explains why it matters.
::: tip Not a commitment
A blueprint is a starting point, not an identity. Every blueprint page ends with a **migration path** and the signals that tell you it is time to move. If two answers feel equally true, take the simpler one - adding a long-lived branch later is far cheaper than unwinding one.
:::
## The five blueprints
| Blueprint | Fits when | Branch model |
| ------------------------------------------------------------------------------------ | -------------------------------------------------- | ------------------------------------------------------------------------------------ |
| [**B1** Simple Org Development](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b1-simple-org-development.md) | One team, one production org | GitHub Flow: `main` + short-lived `feature/*` |
| [**B2** Multi-Team with Environment Branches](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md) | Several teams, one production org, approval gates | One branch per org - stories promoted one by one, as a bundle, or as a release train |
| [**B3** Modular Trunk](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b3-modular-trunk.md) | Several teams on one trunk, maturity level 4 | Trunk-based with feature flags; modules or packages as artifacts |
| [**B4** Multi-Org / Regional Rollout](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b4-multi-org-regional.md) | Several production orgs by region or business unit | Trunk with a shared core, or a B2 chain per org |
| [**B5** Low-Code First](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b5-low-code-admin-first.md) | Admin-heavy team, Git knowledge is the bottleneck | Two org-bound branches, tool-driven promotion |
Every blueprint page opens with an **In short** box, so you can compare them in a minute. [Kit Mapping](https://svierk.github.io/salesforce-devops-starter-kit/strategy/mapping.md) puts their configuration side by side; [Background & Trade-Offs](https://svierk.github.io/salesforce-devops-starter-kit/strategy/background.md) holds the reasoning - sandbox licences, org vs. package development, branching models, release styles and maturity signals.
## How the routing works
The questionnaire always asks the same five questions in this order, so every path through the tree has the same length. Where an answer cannot change the blueprint, the path runs through a small dot - the answer still tailors the result.
1. **How many production orgs do you run?** One · Several
2. **How many teams or parallel workstreams?** One · Several
3. **How is the team set up?** Mostly developers · Balanced, cross-functional · Mostly admins and low-code builders
4. **How mature is your DevOps practice?** Level 1 - starting out · Level 2-3 - established · Level 4 - advanced
5. **How do you ship to production?** Story by story · Story by story to UAT, bundled into production · Whole releases on planned dates
```mermaid
flowchart TD
QO{"How many production orgs?"} -->|One| QT{"How many teams or parallel workstreams?"}
QO -->|Several| P2M(( ))
QT -->|One| QS{"How is the team set up?"}
QT -->|Several| P3N(( ))
P2M --> P3M(( ))
QS -->|Developers / balanced| P4B1(( ))
QS -->|Low-code| P4B5(( ))
P3N --> QM{"How mature is your DevOps practice?"}
P3M --> P4M(( ))
P4B1 --> P5B1(( ))
P4B5 --> P5B5(( ))
QM -->|Level 1-3| QR{"How do you ship to production?"}
QM -->|Level 4| P5B3(( ))
P4M --> P5M(( ))
P5B1 --> B1["B1 Simple Org Development"]
P5B5 --> B5["B5 Low-Code First"]
QR -->|Story by story| B2S["B2 Environment Branches · Story Promotion"]
QR -->|Bundled from UAT| B2B["B2 Environment Branches · Release Bundle"]
QR -->|Whole releases| B2T["B2 Environment Branches · Release Train"]
P5B3 --> B3["B3 Modular Trunk"]
P5M --> B4["B4 Multi-Org / Regional"]
classDef bp fill:#dde4f9,stroke:#1d4ed8,color:#0b1a4a,font-weight:bold;
classDef pass fill:#9aa0a6,stroke:#9aa0a6;
class B1,B2S,B2B,B2T,B3,B4,B5 bp;
class P2M,P3N,P3M,P4B1,P4B5,P4M,P5B1,P5B5,P5B3,P5M pass;
```
| Your answers (questions 1-5) | Blueprint |
| ------------------------------------------------------- | ------------------------------------------------------------ |
| Several orgs · any · any · level 1-3 · any | [B4](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b4-multi-org-regional.md), per-org environment chains |
| Several orgs · any · any · level 4 · any | [B4](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b4-multi-org-regional.md), trunk with a shared core |
| One org · one team · developers or balanced · any · any | [B1](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b1-simple-org-development.md) |
| One org · one team · admins · any · any | [B5](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b5-low-code-admin-first.md) |
| One org · several · any · level 1-3 · story by story | [B2](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md), story promotion |
| One org · several · any · level 1-3 · bundled | [B2](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md), release bundle |
| One org · several · any · level 1-3 · whole releases | [B2](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md), release train |
| One org · several · any · level 4 · any | [B3](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b3-modular-trunk.md) |
At level 1, B2 starts with two stages. Several teams of admins keep their blueprint, but let tooling do the promoting.
The routing is a heuristic, not a measurement: there is no neutral, Salesforce-specific data that ties a branching model to delivery performance. The blueprints are for teams building on their own production orgs - if you distribute an app through AppExchange, you need managed 2GP packages and a namespace, and none of them is your model.
---
---
url: /salesforce-devops-starter-kit/strategy/b1-simple-org-development.md
description: >-
One team on one production org - GitHub Flow with short-lived feature
branches, PR validation and a gated production deployment.
---
# B1 - Simple Org Development
The default starting point. One team, one production org, one branch that is always deployable.
::: tip In short
For **one team on one production org**. Work on short-lived `feature/*` branches off `main`, let every pull request pass the validation check, and deploy `main` to production behind a GitHub Environment with required reviewers. **Watch out:** once merges get held back to protect production, you have outgrown it - see the migration path.
:::
## At a glance
| | |
| ----------------- | ----------------------------------------------------------------------------- |
| **Team size** | 1-6 developers and admins |
| **Environments** | 1 developer sandbox + production |
| **Branch model** | GitHub Flow - `main` plus short-lived `feature/*` |
| **Cadence** | Continuous, deploy on merge |
| **Maturity** | Level 1-2 |
| **Prerequisites** | A Dev Hub only if you add scratch org CI; otherwise a single integration user |
## Target picture
```mermaid
flowchart LR
F1["feature/story-123"]
F2["feature/story-456"]
MAIN["main"]
DEV["Developer Sandbox validation target"]
PROD["Production"]
F1 -->|"pull request pr-validation"| MAIN
F2 -->|"pull request pr-validation"| MAIN
F1 -.->|"check-only deploy"| DEV
F2 -.->|"check-only deploy"| DEV
MAIN -->|"merge → deployment"| PROD
classDef org fill:#dde4f9,stroke:#1d4ed8,color:#0b1a4a;
classDef branch fill:#ebe4fa,stroke:#5b21d6,color:#24104f;
class DEV,PROD org;
class MAIN,F1,F2 branch;
```
## Org & sandbox landscape
* **One Developer sandbox** as the validation target. Pull requests deploy into it check-only, so it never actually changes - which means it does not need refreshing on a schedule and one sandbox serves the whole team.
* **Production** as the only deployment target.
* Individual developers may have their own Developer sandboxes or scratch orgs for building. That is a local preference, not part of the pipeline.
No Full or Partial Copy sandbox is required, so this blueprint runs inside a standard Enterprise Edition entitlement with no add-on.
## Branch model
* `main` is always deployable and is the only long-lived branch.
* `feature/*` branches are short-lived - hours to a few days. Anything longer means the change should have been split or feature-flagged.
* Merge by pull request only, squash or merge commit as you prefer.
* No `develop`, no release branches. If you feel you need one, read [B2](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md) before adding it.
## Promotion path & roles
| Role | Does |
| ----------------- | ---------------------------------------------------------------------------------- |
| Developer / admin | Builds in a sandbox or scratch org, commits to `feature/*`, opens the pull request |
| Reviewer | Reviews the diff, checks that the validation run is green |
| Whoever merges | Merging to `main` deploys to production - so the merge *is* the release decision |
Declarative changes need an explicit owner: someone has to retrieve them from the sandbox into a feature branch. In a team this size that is usually the person who built them, assisted by the CLI or an IDE - name them, or the change will reach production by hand.
## Strengths
* The least ceremony of any model here. Nothing to explain to a new joiner beyond "branch, PR, merge".
* No long-lived branch pairs to keep in sync, so essentially no XML merge conflicts on profiles or flows.
* Fast lead time: commit to production in one hop.
## Trade-offs & anti-patterns
* **Nothing sits between merge and production.** Your safety has to come from the validation gate and from a protected production environment, not from a staging branch. Configure both.
* **No natural place to freeze scope.** If you need to hold a set of changes back for a coordinated launch, use feature flags or simply do not merge - do not invent a release branch as a workaround.
* **Long-lived feature branches defeat the model.** A branch open for three weeks accumulates exactly the conflicts this model is designed to avoid.
* **Hotfixes have no special path**, and do not need one: a hotfix is just a short feature branch.
## Kit mapping
**Pull request validation** - [`pr-validation.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/pr-validation.md) on every pull request into `main`, as a required status check. Delta validation is on by default, so only changed components are checked:
```yaml
name: PR Validation
on:
pull_request:
branches: [main]
permissions:
contents: read
security-events: write
jobs:
validate:
uses: svierk/salesforce-devops-starter-kit/.github/workflows/pr-validation.yml@v1.5.0
with:
source-dir: force-app
test-level: RunLocalTests
secrets:
SFDX_CONSUMER_KEY: ${{ secrets.SFDX_CONSUMER_KEY }}
SFDX_JWT_SECRET_KEY: ${{ secrets.SFDX_JWT_SECRET_KEY }}
SFDX_USERNAME: ${{ secrets.SFDX_USERNAME }}
```
**Production deployment** - [`deployment.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/deployment.md) on push to `main`, gated by a GitHub Environment so a human still approves the release:
```yaml
name: Deploy to Production
on:
push:
branches: [main]
permissions:
contents: read
jobs:
deploy:
uses: svierk/salesforce-devops-starter-kit/.github/workflows/deployment.yml@v1.5.0
with:
source-dir: force-app
target-org-alias: production
test-level: RunLocalTests
environment: production # required reviewers + environment-scoped secrets
secrets:
SFDX_CONSUMER_KEY: ${{ secrets.SFDX_CONSUMER_KEY }}
SFDX_JWT_SECRET_KEY: ${{ secrets.SFDX_JWT_SECRET_KEY }}
SFDX_USERNAME: ${{ secrets.SFDX_USERNAME }}
```
**Branch protection on `main`:**
* Require a pull request before merging, with at least one approval
* Require the PR Validation check to pass
* Require branches to be up to date before merging
* Block force pushes and deletions
**Optional additions:**
* [`scratch-org-ci.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/scratch-org-ci.md) on feature branches for a clean-org test run - the cheapest way to catch "works in my sandbox" problems.
* [`create-scratch-org.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/create-scratch-org.md) so anyone can provision a ready-to-use org from the Actions tab without a local CLI.
## Migration path
**To [B2](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md)** when a second stream appears and you start holding merges back to avoid shipping someone else's half-finished work, or when a stakeholder needs a stage to sign off in before production.
**To [B3](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b3-modular-trunk.md)** directly, skipping B2, if your metadata is already modular - or the team is disciplined enough to keep one trunk releasable with feature flags - and you would rather invest in modules than in stages. This is the better move if the pressure is "teams block each other" rather than "we need an approval gate".
**Signals to watch:** rising change fail rate, pull requests waiting on each other, or the phrase "let's not merge that yet" becoming routine.
---
---
url: /salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md
description: >-
Several teams sharing one production org - environment branches with story
promotion, release bundles or a release train, and approval gates per stage.
---
# B2 - Multi-Team with Environment Branches
Several streams, one long-lived Git branch per org, and an explicit release decision. The classic enterprise Salesforce setup - and the one that costs the most to operate, so adopt it only for the stages your approval process actually requires.
The environment branches are the same in every variant. What differs is **what travels between them**, and that follows from [how you release](https://svierk.github.io/salesforce-devops-starter-kit/strategy/background.md#release-styles):
* **[Release train](#release-train)** - the whole state of a branch moves up the chain, stage by stage, on planned release dates. It is the default model of the open-source [sfdx-hardis](https://sfdx-hardis.cloudity.com/salesforce-devops-deploy-major-branches/).
* **[Story promotion](#story-promotion)** - every user story travels on its own, into each stage through a short-lived promotion branch, and reaches production when it is ready. [Gearset Pipelines](https://docs.gearset.com/en/articles/6115722-the-gearset-pipelines-branching-model) implements it almost exactly; Copado and DevOps Center follow the same principle with different mechanics. Here it runs on plain GitHub and the kit's workflows.
* **[Release bundle](#release-bundle)** - stories travel one by one up to UAT, where they are signed off individually; on the release date, a release branch carries the signed-off stories as one bundle through pre-production into production. DevOps Center builds the same idea in as a "bundling stage".
::: tip In short
For **several teams sharing one production org** with approval gates. One long-lived branch per org - `integration`, `uat`, `preprod`, `main` - and changes promoted story by story, as a bundle on release dates, or as a whole release train. Switch on PR validation on every environment branch and one deployment per branch, each with its own GitHub Environment. **Watch out:** merge overhead and drift grow with every stage - keep only the stages your approvals need.
:::
## At a glance
| | |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------- |
| **Team size** | 5-25 developers and admins across several streams |
| **Environments** | Integration + UAT + pre-production + production |
| **Branch model** | Four environment branches: `integration`, `uat`, `preprod`, `main` |
| **Release style** | Story promotion: story by story · Release bundle: per story to UAT, bundled into production · Release train: release dates |
| **Maturity** | Level 2-3 - at level 1, start with two stages (see below) |
| **Prerequisites** | Sandbox budget - see the licence warning below |
## Three ways to promote
| | Story promotion | Release bundle | Release train |
| -------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------- |
| What moves | One user story, separately into each stage | One story at a time up to UAT; from there, the signed-off stories as one bundle | The whole state of a branch |
| Feature branches start at | `main` - the production state | `main` - the production state | `integration` |
| Stage-to-stage merges | Never - environment branches are not merged into each other | Never - up to UAT per story, from there one release branch per date | Yes, strictly up the chain |
| Deployment between stages | Delta - only what the story brought | Delta - the story, then the bundle | Full |
| A story that is not ready | Holds only itself | Stays out of the bundle and waits for the next date | Holds the train, or has to be backed out |
| Where conflicts are solved | In the story's promotion branch, once per stage | In promotion branches up to UAT, then once more in the release branch | In the target branch, once per hop |
| Urgent fixes | A story like any other, promoted only as far as it needs | A story promoted straight on, or a small bundle of its own | Separate hotfix path with a hard constraint |
| Fits | Continuous delivery of independent stories, with approval gates | Sign-off per story, production on release dates | Planned release dates, scope frozen per release |
If stories should reach production as soon as they are signed off, run story promotion. If stories are signed off one by one but production gets them on dates, run the release bundle. If whole releases are planned and tested as one, run the train. Switching later does not touch the orgs or the environment branches - only how changes travel between them.
## Target picture
### Release train
```mermaid
flowchart LR
subgraph BUILD["BUILD stream"]
F1["feature/story-123"]
F2["feature/story-456"]
end
subgraph RUN["RUN stream"]
HF["hotfix/prod-issue"]
end
INT["integration Integration Sandbox"]
UAT["uat UAT Sandbox (Partial/Full)"]
PP["preprod PreProd Sandbox (Full)"]
MAIN["main Production"]
F1 -->|"PR + delta deploy"| INT
F2 -->|"PR + delta deploy"| INT
INT -->|"promote (full deploy)"| UAT
UAT -->|"promote (full deploy)"| PP
PP -->|"promote (full deploy)"| MAIN
HF -->|"PR"| PP
MAIN -.->|"retrofit branch"| INT
classDef org fill:#dde4f9,stroke:#1d4ed8,color:#0b1a4a;
classDef branch fill:#ebe4fa,stroke:#5b21d6,color:#24104f;
class INT,UAT,PP,MAIN org;
class F1,F2,HF branch;
```
### Story promotion
```mermaid
flowchart LR
F["feature/story-123 branched from main"]
P1["promotion/ story-123-integration"]
P2["promotion/ story-123-uat"]
P3["promotion/ story-123-preprod"]
INT["integration Integration Sandbox"]
UAT["uat UAT Sandbox (Partial/Full)"]
PP["preprod PreProd Sandbox (Full)"]
MAIN["main Production"]
F -->|"merge"| P1
F -->|"merge"| P2
F -->|"merge"| P3
P1 -->|"PR + delta deploy"| INT
P2 -->|"PR + delta deploy"| UAT
P3 -->|"PR + delta deploy"| PP
F -->|"PR + delta deploy"| MAIN
classDef org fill:#dde4f9,stroke:#1d4ed8,color:#0b1a4a;
classDef branch fill:#ebe4fa,stroke:#5b21d6,color:#24104f;
class INT,UAT,PP,MAIN org;
class F,P1,P2,P3 branch;
```
### Release bundle
```mermaid
flowchart LR
F1["feature/story-123"]
F2["feature/story-456"]
INT["integration Integration Sandbox"]
UAT["uat UAT Sandbox (Partial/Full)"]
REL["release/2026-11 branched from main"]
PP["preprod PreProd Sandbox (Full)"]
MAIN["main Production"]
F1 -->|"promotion branches"| INT
F1 -->|"promotion branches"| UAT
F2 -->|"promotion branches"| INT
F2 -->|"promotion branches"| UAT
F1 -->|"signed off: merge"| REL
F2 -->|"signed off: merge"| REL
REL -->|"PR + delta deploy"| PP
REL -->|"PR + delta deploy"| MAIN
classDef org fill:#dde4f9,stroke:#1d4ed8,color:#0b1a4a;
classDef branch fill:#ebe4fa,stroke:#5b21d6,color:#24104f;
class INT,UAT,PP,MAIN org;
class F1,F2,REL branch;
```
## Org & sandbox landscape
One sandbox per environment branch:
| Branch | Org | Sandbox type | Why |
| ------------- | -------------- | -------------------- | ------------------------------------------------------ |
| `integration` | Integration | Developer Pro | Where streams collide first; metadata only is enough |
| `uat` | UAT | Partial Copy or Full | Business sign-off needs realistic data |
| `preprod` | Pre-production | Full | Last rehearsal - as close to production as you can get |
| `main` | Production | - | - |
Plus Developer sandboxes or scratch orgs per developer for building.
::: tip Starting out? Begin with two stages
At maturity level 1, four long-lived branches collapse under their own weight. Start with `uat` and `main` only - the pull request into `uat` is where streams collide, and the UAT sign-off is your gate. Add `integration` once CI runs on every pull request, and `preprod` only when a release rehearsal on production-like data is genuinely required.
:::
::: warning This is the expensive blueprint
On Enterprise Edition, Full and Developer Pro sandboxes are a paid add-on. Before committing to four stages, price the entitlement - and ask whether UAT on a Partial Copy would be enough, which is often the case. A stage nobody signs off in is pure overhead. See [Background](https://svierk.github.io/salesforce-devops-starter-kit/strategy/background.md#sandbox-types).
:::
Remember the refresh intervals: a Full sandbox can only be refreshed every 29 days, so `preprod` drifts from production between refreshes. Schedule the refresh, and treat "it worked in preprod" as evidence, not proof.
## Branch model
**Environment branches** are long-lived, protected, and each bound to one org: `integration`, `uat`, `preprod`, `main`. Each one describes what its org should contain.
**Short-lived branches** carry the work and are deleted once merged. Which ones you use depends on the variant.
### Release train
`feature/*` and `hotfix/*` are the short-lived branches. Make `integration` the repository's default branch - it is where feature work targets.
**Merge direction is strictly one way**, up the chain. Deployment mode differs by hop:
* **Feature → environment** (`feature/*` → `integration`): **delta** deployment. Only what the branch changed.
* **Environment → environment** (`integration` → `uat` → …): **full** deployment. The promoted state is the whole branch, and a delta here would silently skip anything a previous hop missed.
Two streams share the chain:
* **BUILD** - normal features, entering at `integration` and traversing all four stages.
* **RUN** - urgent fixes, entering at `preprod` and reaching `main` in two hops.
::: danger The RUN stream has a hard constraint
While a release is being validated in `preprod`, the RUN stream cannot deploy to production without shipping that unvalidated release with it. Either your pre-production validation window is short, or your hotfix path is blocked for its duration. Decide which, in advance, and write it down.
:::
After a hotfix reaches `main`, **retrofit** it back into the BUILD stream: branch from `main`, open a pull request into `integration`. Do this immediately - a retrofit deferred by a week is a merge conflict on a flow you no longer remember. Where retrofits are routine, a Salesforce-aware merge driver is worth the setup.
### Story promotion
`feature/*` branches start from **`main`**, so a story carries the production state plus its own change - nothing that is still in flight in a lower stage. Keep `main` as the default branch.
Environment branches are **never merged into each other**. A story reaches each stage on its own, through a **promotion branch**: a short-lived branch cut from the target environment branch, holding exactly one story for exactly one stage. (Some tools use the term differently - sfdx-hardis, for example, calls a cherry-picked partial release a promotion branch.)
For each stage:
1. Create a promotion branch from the target environment branch, e.g. `promotion/story-123-uat` from `uat`.
2. Merge the feature branch into it. Resolve conflicts **here**, so the feature branch stays clean for the next stage.
3. Open a pull request into the environment branch. Once merged, the stage deploys the delta - exactly what the merge brought in.
4. Delete the promotion branch.
```bash
git switch -c promotion/story-123-uat origin/uat
git merge --no-ff origin/feature/story-123 # resolve conflicts on this branch
git push -u origin promotion/story-123-uat
gh pr create --base uat --title "Promote story-123 to uat"
```
To production, the feature branch itself goes into `main` - through a promotion branch only if it conflicts. An urgent fix is simply a story that is promoted only as far as it has to go, typically `uat` and then `main`, without waiting for anything else.
**Sync after every release.** Each stage holds a different set of stories, so the environment branches drift apart by design. After each production deployment, merge `main` back into every environment branch through a pull request. It keeps each stage at "production plus the stories in flight", and it stops the same conflict from coming back at every stage. Do not replace it with story-by-story back-merges: those carry a story's own commits, but not the merge commits on `main`, which then resurface later as unexplained differences.
### Release bundle
Up to `uat`, stories travel exactly as in [story promotion](#story-promotion-1): feature branches from `main`, one promotion branch per story and stage. On the release date the bundle takes over:
1. Cut `release/` from `main`.
2. Merge every story that passed its UAT sign-off into it. Conflicts between the stories of this release are solved here, once.
3. Open a pull request into `preprod`. Once the rehearsal passes, open the next one from the same release branch into `main`.
4. Delete the release branch, and sync `main` back into every environment branch as in story promotion.
What reaches `preprod` is byte for byte what reaches `main` - the gap story promotion leaves between test stage and production closes at the last two stages. A story that misses its sign-off simply stays out of the bundle and waits for the next date.
## Promotion path & roles
| Role | Release train | Story promotion |
| --------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
| Developer | Builds in a dev sandbox, commits to `feature/*`, opens a PR into `integration` | Builds, then promotes the story into `integration` and the test stages |
| Tech lead | Reviews PRs into `integration`, keeps the branch green | Reviews the story's first promotion and its PR into `main` |
| Release manager | Owns the stage-to-stage promotions, decides what enters the train, runs the approval gates | Decides which stories enter `preprod` and `main`, runs the approval gates |
| Business owner | Signs off in UAT - the gate that justifies the stage existing | Signs off each story in UAT |
The **release bundle** follows the story promotion roles up to `uat`; from there, the release manager owns the release branch and decides which signed-off stories go into it.
Grant merge rights on `uat`, `preprod` and `main` to the release manager role only. `integration` stays open to the team - that is the point of having it.
## Strengths
* Clear stage gates with a named owner per gate.
* Maps onto approval and segregation-of-duties requirements without inventing process.
* **Release train:** conflicts surface in `integration`, early and cheaply, rather than during a release. BUILD and RUN separate cleanly, so an urgent fix does not have to wait for the train.
* **Story promotion:** stories move independently - one that fails UAT holds nobody else back. `main` only ever receives stories, never the leftovers of a lower stage. Urgent fixes need no special path.
* **Release bundle:** sign-off stays per story, yet production receives exactly the bundle that was rehearsed in `preprod`. A story that is not ready never blocks the date.
## Trade-offs & anti-patterns
* **Four long-lived branches is real, permanent overhead.** Every promotion is a merge, and XML metadata merges badly. This is the cost you are paying for the gates - make sure you are getting them.
* **Do not degrade into branch-per-org as backup.** If promotions stop being reviewed pull requests and become "sync the branch to what is in the org", you have lost the model's only real benefit. See the [warning in Dimensions](https://svierk.github.io/salesforce-devops-starter-kit/strategy/background.md#branching-models).
* **Do not add a fifth stage.** If something needs another environment, it usually needs feature flags instead.
* **Drift is the silent failure mode.** Orgs receive changes outside the pipeline; each one makes the next promotion less predictable.
* **Release train: do not test large features directly in `preprod`.** It blocks the RUN stream and skips the UAT gate.
* **Story promotion: what you tested is not byte for byte what you ship.** Each stage resolves its own conflicts, so `uat` and `main` can differ in more than the stories they hold. The sync after every release keeps that gap small; skipping it makes it grow.
* **Story promotion: dependent stories need a rule.** If story B builds on story A, promoting B alone breaks the stage. Once a dependency is known, combine the two into one feature branch and promote them together - "promote A first" fails as soon as A is held back.
* **Story promotion and release bundle: deltas hide gaps.** Every stage only ever receives deltas, and a delta can pass where a full deployment of the same branch would fail - a deleted picklist value that is still referenced, for example. Run a full check-only deployment of every environment branch into its org regularly - weekly is enough. The [weekly full validation](#kit-mapping) under Kit mapping does exactly that.
* **Release bundle: stories are merged twice.** Once per stage up to `uat`, then again into the release branch - conflicts can need solving at both points. Small, independent stories keep that cheap.
* **Story promotion: do not promote by hand at scale.** Four promotion branches per story is toil. Let [`promote.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/promote.md) do it - see Kit mapping.
## Kit mapping
**Validation on every environment branch** - [`pr-validation.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/pr-validation.md) as a required check for pull requests into all four, with delta validation against the org behind the target branch. In story promotion, these are the pull requests from the promotion branches - each one validates the story merged with that stage's state, in that stage's org:
```yaml
name: PR Validation
on:
pull_request:
branches: [integration, uat, preprod, main]
permissions:
contents: read
security-events: write
jobs:
validate:
uses: svierk/salesforce-devops-starter-kit/.github/workflows/pr-validation.yml@v1.5.0
with:
source-dir: force-app
test-level: RunLocalTests
delta: true
severity-threshold: '3'
# integration-validation | uat-validation | preprod-validation |
# main-validation - the org behind the target branch, see below
environment: ${{ format('{0}-validation', github.base_ref) }}
secrets: inherit
```
**One deployment per environment branch** - [`deployment.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/deployment.md), triggered on push, with a GitHub Environment per org. The environment carries both the approval rules and the org's credentials, so the same reusable workflow serves all four stages:
```yaml
name: Deployment
on:
push:
branches: [integration, uat, preprod, main]
permissions:
contents: read
jobs:
deploy:
uses: svierk/salesforce-devops-starter-kit/.github/workflows/deployment.yml@v1.5.0
with:
source-dir: force-app
target-org-alias: ${{ github.ref_name }}
test-level: RunLocalTests
# Release train: stage-to-stage promotions deploy the full branch state
delta: false
# integration | uat | preprod | production - each with its own secrets
# and, for preprod and production, required reviewers
environment: ${{ github.ref_name == 'main' && 'production' || github.ref_name }}
secrets: inherit
```
For **story promotion** and the **release bundle**, deploy the delta of each merge instead - the story or the bundle, plus any conflict resolution from its promotion or release branch. `github.event.before` is the state of the environment branch before the push:
```yaml
delta: true
delta-from: ${{ github.event.before }}
```
::: tip One secret set per org
Create four GitHub Environments and store `SFDX_CONSUMER_KEY`, `SFDX_JWT_SECRET_KEY` and `SFDX_USERNAME` as environment secrets in each. `secrets: inherit` then resolves the right org automatically, and adding a stage never means editing the caller. Add required reviewers to `preprod` and `production` for the approval gate.
Validation needs a twin per branch - `integration-validation`, `uat-validation`, `preprod-validation`, `main-validation` - with the same three secrets but **no** required reviewers and no branch restrictions. A protected environment would hold every pull request until someone approves it, or reject it outright.
:::
**Promotions as a form in the Actions tab** - [`promote.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/promote.md) runs the steps above: it cuts the promotion branch, merges the story and opens the pull request into the stage. The same workflow cuts a release bundle and opens the sync pull requests after a release - the [example caller](https://svierk.github.io/salesforce-devops-starter-kit/workflows/promote.md#example-usage) shows all three. A conflict stops the run, and its summary lists the files and the commands to finish the promotion locally.
```yaml
jobs:
promote:
uses: svierk/salesforce-devops-starter-kit/.github/workflows/promote.yml@v1.5.0
with:
target: uat
sources: feature/story-123
secrets:
PROMOTION_APP_ID: ${{ secrets.PROMOTION_APP_ID }}
PROMOTION_APP_PRIVATE_KEY: ${{ secrets.PROMOTION_APP_PRIVATE_KEY }}
```
Install a GitHub App with contents and pull request write access and pass its credentials: a pull request opened with the default `GITHUB_TOKEN` does **not** trigger other workflows, so the PR validation would never run on it.
**Weekly full validation** (story promotion and release bundle) - [`deployment.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/deployment.md) as a check-only, full deployment of every environment branch into its org. The `ref` input checks out each branch, so one scheduled caller covers all of them:
```yaml
name: Weekly Full Validation
on:
schedule:
- cron: '0 3 * * 1'
workflow_dispatch:
permissions:
contents: read
jobs:
validate:
strategy:
fail-fast: false
matrix:
branch: [integration, uat, preprod, main]
uses: svierk/salesforce-devops-starter-kit/.github/workflows/deployment.yml@v1.5.0
with:
ref: ${{ matrix.branch }}
target-org-alias: ${{ matrix.branch }}
dry-run: true
delta: false
environment: ${{ format('{0}-validation', matrix.branch) }}
secrets: inherit
```
**Branch protection on all four environment branches:**
* Require a pull request before merging - no direct pushes, including for the release manager
* Require the PR Validation check to pass
* Require branches to be up to date before merging
* Restrict who can merge into `uat`, `preprod` and `main` to the release manager role
* Block force pushes and deletions
* Release train: merge environment-to-environment pull requests with a merge commit - never squash, and do not delete the source branch. The next promotion's scope is computed from that history.
* Default branch: `integration` for the release train, `main` for story promotion and the release bundle
**Optional:** [`scratch-org-ci.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/scratch-org-ci.md) on `feature/*` pushes as fast CI before the first promotion - it creates a clean org, deploys, tests and deletes it, so a story is tested in isolation before it reaches shared `integration`.
## Migration path
**Between the variants** - in either direction, at any time. The orgs and the environment branches stay; only the way changes travel between them changes. Teams often start on the train, move to the release bundle once stories are signed off individually, and to story promotion once they are small and independent enough to ship one by one.
**To [B3](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b3-modular-trunk.md)** - the usual destination. Cut the first module out of the happy soup - as source, or as an org-dependent unlocked package if it is tightly coupled - and as modules and feature flags take over, collapse the stages: `uat` usually goes first, then `preprod`.
**Signals you are ready:** deployment frequency, lead time and change fail rate improving over several quarters, measured against your own baseline, and metadata that has proven separable in practice - not just on a whiteboard.
**Signals you over-built:** a stage where nothing is ever rejected, a UAT sign-off that is a formality, or a promotion chain that takes longer than the development it gates. Remove the stage. Moving back toward [B1](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b1-simple-org-development.md) is a legitimate outcome.
**If compliance boundaries or data residency appear** → [B4](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b4-multi-org-regional.md), which can run this same model once per production org.
---
---
url: /salesforce-devops-starter-kit/strategy/b3-modular-trunk.md
description: >-
Several mature teams on one trunk - feature flags, source modules or unlocked
packages, and a validation gate plus scratch org CI that genuinely block.
---
# B3 - Modular Trunk
Several teams on one trunk: `main` is always releasable, work merges daily behind feature flags, and the metadata is cut into modules with explicit dependencies. Fewest long-lived branches of any blueprint here - and the highest bar to entry, set by discipline rather than tooling.
Packaging is a means, not the goal. The goal is modular metadata with explicit dependencies and releasable artifacts. Source modules get you most of the way and are the better default; unlocked packages earn their extra build and promotion cost for stable, shared building blocks.
::: tip In short
For **several teams mature enough to share one trunk**. `main` is always releasable, work merges daily behind feature flags, and the metadata is cut into modules - packages only where a version boundary pays. Switch on a validation check that genuinely blocks, and scratch org CI. **Watch out:** trunk-based without a real gate is just pushing to production.
:::
## At a glance
| | |
| ----------------- | --------------------------------------------------------------------------------------------------- |
| **Team size** | 8-40+ across several product-aligned teams |
| **Environments** | Scratch orgs or developer sandboxes for development and CI + one staging sandbox + production |
| **Branch model** | Trunk-based: `main` plus short-lived feature branches, feature-flagged |
| **Release style** | Any - continuously, or a tagged state of `main` on a release date |
| **Maturity** | Level 4 - modular metadata helps, but is not the entry ticket |
| **Prerequisites** | A validation gate that genuinely blocks, a feature-flag convention, and a home for org-level config |
## What you ship
The trunk stays the same; what differs is the artifact you release from it. Pick per module, not once for the whole blueprint - the type of a module can change later.
| Artifact | What it is | Fits | Watch out for |
| ---------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| **One source tree** | The whole source, deployed as a delta from `main` | A mature team whose metadata is not modular yet | Feature flags carry everything; no partial releases |
| **Source modules** (default) | Package directories in `sfdx-project.json`, deployed module by module | Most teams with separable metadata | Dependencies are a convention you enforce in review, not something the platform checks |
| **Unlocked packages** | Versioned 2GP packages, built, promoted and installed org by org | Stable building blocks shared across teams or orgs - the shared core in [B4](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b4-multi-org-regional.md) | Build and promotion time, 75 % code coverage per package, metadata types that cannot be packaged |
[Org-dependent unlocked packages](https://developer.salesforce.com/docs/platform/sfdx-dev/guide/sfdx-dev-unlocked-pkg-org-dependent.html) are Salesforce's bridge out of a tightly coupled org: they defer dependency validation to install time. They cannot depend on other packages, do not calculate code coverage, and are developed in source-tracked sandboxes rather than scratch orgs - use them for the first cut, not as the end state.
The open-source package toolchain [sfp](https://docs.flxbl.io/libraries/flxbl-framework/techniques/development-practices/choosing-a-package-type) reached the same conclusion: default to source packages, and choose unlocked packages when a specific requirement calls for them.
## Target picture
```mermaid
flowchart LR
subgraph FEAT["Short-lived branches"]
F1["feature/billing-42"]
F2["feature/service-17"]
end
SO["Scratch Org created and destroyed per run"]
MAIN["main trunk, always releasable"]
REL["Release tagged trunk state or package versions"]
STG["Staging Sandbox Partial Copy"]
PROD["Production"]
F1 -->|"PR"| MAIN
F2 -->|"PR"| MAIN
F1 -.->|"create → deploy → test → delete"| SO
F2 -.->|"create → deploy → test → delete"| SO
MAIN -->|"tag or build + promote"| REL
REL -->|"deploy / install"| STG
REL -->|"deploy / install"| PROD
classDef org fill:#dde4f9,stroke:#1d4ed8,color:#0b1a4a;
classDef branch fill:#ebe4fa,stroke:#5b21d6,color:#24104f;
classDef pkg fill:#d1f2fc,stroke:#0093c4,color:#05303f;
class SO,STG,PROD org;
class MAIN,F1,F2 branch;
class REL pkg;
```
## Org & sandbox landscape
* **Scratch orgs** carry development and CI. They are created from a definition file, source-tracked, and deleted at the end of every run - so capacity, not entitlement, is the constraint. Work on org-dependent packages, or on metadata that needs real org data, happens in **developer sandboxes** instead.
* **One staging sandbox** (Partial Copy is usually enough) to deploy or install a release into against realistic data before production.
* **Production**, receiving module deployments or package installs - never edits that bypass the trunk.
This blueprint is much lighter on sandbox entitlement than [B2](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md) - it trades sandbox licences for Dev Hub scratch org capacity and for the engineering effort of modularisation.
## Branch model
* `main` is the trunk and is always releasable.
* Feature branches live hours to a day or two. If a branch cannot merge that fast, the work goes behind a **feature flag** and merges unfinished instead.
* No integration branch, no environment branches. A release is a tagged state of `main` or a promoted package version, not a merge.
* Releasing on dates? Tag `main` on the date - or cut a short-lived release branch from it when a release needs stabilising. Fixes land on `main` first and are cherry-picked into the release branch, never the other way round.
**Feature flags carry the load here.** The common Salesforce implementation is a custom permission per flag (for example prefixed `FF_`), switched on by assigning a permission set - go-live and rollback are an assignment, not a deployment. The simplest version is still valid: do not deploy a component until it is needed. The limit is configuration metadata: a new field or a permission change cannot hide behind a flag, so schema changes stay additive until the feature is live.
## Promotion path & roles
| Role | Does |
| ----------------------- | ----------------------------------------------------------------------------------------------- |
| Module team | Owns one or more modules end to end, including their dependencies and - for packages - versions |
| Developer | Works in a scratch org, merges to trunk daily, hides incomplete work behind a flag |
| Release owner | Tags a release or promotes a package version, and decides where it goes |
| Platform / DevOps owner | Owns the Dev Hub, the module dependency graph and the CI configuration |
The dependency graph is the thing that needs an owner. Modules that depend on each other must be deployed or installed in order, and nobody notices when that order goes wrong until a release fails in production.
## Strengths
* Teams release independently. No shared release train, so no cross-team blocking.
* Inspectable units of change - you can say exactly which module version is where.
* Almost no merge overhead: one long-lived branch means essentially no XML conflicts between stages.
* Scratch orgs are clean every run, so "works in my sandbox" disappears as a failure class.
## Trade-offs & anti-patterns
* **Cutting out the first module is the real cost.** A mature happy soup does not factor cleanly, and the effort lands on senior people. Budget it as a project, not a sprint task.
* **Something always stays at org level.** Some metadata cannot be packaged at all - check the [Metadata Coverage Report](https://developer.salesforce.com/docs/metadata-coverage) per release - and some belongs to the org rather than to any module. Plan a residual org module from the start.
* **Packages cost build and promotion time.** Every unlocked package version has to be built, needs 75 % code coverage to be promoted, and is installed separately. Use packages where the version boundary pays for itself, not by default.
* **Trunk-based without a real gate is just pushing to production.** The validation check must actually block, and someone must care when trunk goes red.
* **Feature flags accumulate.** Removing a flag is part of finishing a feature. Without that discipline, the flag framework becomes its own legacy system.
## Kit mapping
**Validation as the gate** - [`pr-validation.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/pr-validation.md) on every pull request into `main`, as a required check. With source modules or one source tree, this is the gate that keeps trunk releasable.
**Scratch org CI for modules** - [`scratch-org-ci.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/scratch-org-ci.md) on every pull request. It creates a clean org, installs dependency packages, deploys, runs tests and always deletes the org again - which proves a module builds on its declared dependencies alone:
```yaml
name: Scratch Org CI
on:
pull_request:
branches: [main]
permissions:
contents: read
jobs:
ci:
uses: svierk/salesforce-devops-starter-kit/.github/workflows/scratch-org-ci.yml@v1.5.0
with:
source-dir: force-app
definition-file: config/project-scratch-def.json
duration-days: '1'
test-level: RunLocalTests
# Dependency packages this one builds on, in install order
packages: '04t...'
secrets:
SFDX_CONSUMER_KEY: ${{ secrets.SFDX_CONSUMER_KEY }}
SFDX_JWT_SECRET_KEY: ${{ secrets.SFDX_JWT_SECRET_KEY }}
SFDX_USERNAME: ${{ secrets.SFDX_USERNAME }}
```
**Releasing source** - [`deployment.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/deployment.md) on push to `main` (continuous) or on a release tag (release dates), with `delta: true` and `delta-from` set to the previous release, behind a `production` GitHub Environment exactly as in [B1](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b1-simple-org-development.md).
**Installing package versions** - the [`sfdx-package-installation`](https://github.com/svierk/sfdx-package-installation) building block, in a job gated by the same environment:
```yaml
- name: Install package version
uses: svierk/sfdx-package-installation@v1.3.0
with:
packages: '04t...'
target-org: production
```
::: warning Package version creation is not yet a building block
The kit does not ship an action for `sf package version create` / `promote` yet. Until it does, that step is a plain `sf` CLI call in your own workflow - the CLI is already installed by [`sfdx-cli-setup`](https://github.com/svierk/sfdx-cli-setup) and authenticated by [`sfdx-login`](https://github.com/svierk/sfdx-login), so it is a few lines rather than a gap in the pipeline.
:::
**Developer self-service** - [`create-scratch-org.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/create-scratch-org.md) matters more here than anywhere else. Developers need orgs constantly, and a `workflow_dispatch` caller means they get one with packages installed, source deployed, data imported and a password generated, without a local CLI.
**Branch protection on `main`:**
* Require a pull request, with review from the owning module team
* Require both the scratch org CI and the validation checks to pass
* Require branches to be up to date before merging
* Block force pushes and deletions
## Migration path
**From here, the changes are usually organisational rather than structural.** More modules, clearer ownership boundaries, tighter dependency management. Moving a module from source to an unlocked package - or back - is a decision per module, not a change of blueprint.
**To [B4](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b4-multi-org-regional.md)** if a second production org appears. B3 makes that transition easy - modules, and packages in particular, are already the unit of distribution that a multi-org rollout needs.
**Signals you over-built:** modules or packages that always release together (merge them), a dependency graph nobody can draw, or more time spent on package plumbing than on the features inside. Fewer, larger modules is a valid correction.
---
---
url: /salesforce-devops-starter-kit/strategy/b4-multi-org-regional.md
description: >-
Several production orgs by region or business unit - a shared core as pinned
package versions, or one environment chain per org.
---
# B4 - Multi-Org / Regional Rollout
Several production orgs - by region, business unit or brand - with a shared core that is maintained once while local differences stay local.
Two shapes work. The default below is **a trunk with a packaged core**: one repository, the core released as package versions, each org adopting them on its own schedule. The alternative is **[per-org environment chains](#per-org-environment-chains)**: one repository per org, each running [B2](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md), with the shared metadata in a repository of its own.
::: tip In short
For **several production orgs** - by region, business unit or brand. Maintain the shared core once and keep local differences local: either one trunk with the core as modules or packages, or one repository per org running [B2](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md) plus a shared-metadata repository. **Watch out:** orgs drift apart by default - review local exceptions as strictly as the core.
:::
## At a glance
| | |
| ----------------- | -------------------------------------------------------------------------------------------------- |
| **Team size** | 10-50+, usually a core team plus local teams |
| **Environments** | One sandbox chain per production org, plus shared scratch orgs |
| **Branch model** | Trunk plus one short release branch per org rollout - or a B2 chain per org |
| **Cadence** | Core releases continuously; each org adopts on its own schedule |
| **Maturity** | Level 3-4 for the trunk shape; per-org chains work from level 2-3 |
| **Prerequisites** | A packaged core, or a defined way to share common metadata; sandbox entitlement per production org |
## Target picture
```mermaid
flowchart TD
MAIN["main shared core"]
CORE["Core package version 04t…"]
subgraph EU["Region EU"]
REU["release/eu"]
SEU["EU Sandbox"]
PEU["EU Production"]
end
subgraph NA["Region NA"]
RNA["release/na"]
SNA["NA Sandbox"]
PNA["NA Production"]
end
MAIN --> CORE
CORE --> REU
CORE --> RNA
REU --> SEU --> PEU
RNA --> SNA --> PNA
classDef org fill:#dde4f9,stroke:#1d4ed8,color:#0b1a4a;
classDef branch fill:#ebe4fa,stroke:#5b21d6,color:#24104f;
classDef pkg fill:#d1f2fc,stroke:#0093c4,color:#05303f;
class SEU,PEU,SNA,PNA org;
class MAIN,REU,RNA branch;
class CORE pkg;
```
## Org & sandbox landscape
* **One production org per region or business unit.** That is the definition of this blueprint.
* **One sandbox chain per production org** - at minimum a UAT-grade sandbox each, because local data and local configuration are exactly what you are validating.
* **Shared scratch orgs** for core development.
The sandbox cost scales with the number of orgs, and so does everything else: secrets, environments, deployment runs, on-call. Confirm that multi-org is a business requirement (data residency, compliance, genuinely divergent processes) and not the residue of an acquisition nobody has consolidated. See [Background](https://svierk.github.io/salesforce-devops-starter-kit/strategy/background.md#single-org-vs-multi-org).
**Not every multi-org landscape needs this blueprint.** If your orgs share little or nothing - separate businesses that happen to run Salesforce - there is no core to share: treat each org as its own landscape and run the [questionnaire](https://svierk.github.io/salesforce-devops-starter-kit/strategy.md) once per org. And if you are consolidating orgs, run B4 as a time-boxed bridge toward [B2](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md) or [B3](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b3-modular-trunk.md), not as the destination.
## Repository topology
Two workable shapes:
| | Monorepo (recommended) | Multi-repo |
| ---------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Layout | One repo: `packages/core` + `orgs/` overlays | One repo per org, plus a shared repo for common metadata - consumed as a package or deployed alongside each org's source |
| Core change reaches all orgs | Atomically, in one pull request | Via a version bump per repo |
| Local team autonomy | Lower - shared CI, shared review | Higher - own repo, own cadence |
| Fits | Strong shared core, central platform team | Autonomous orgs with their own teams; the home of per-org environment chains |
Start with the monorepo if one platform team owns the core. Splitting later is mechanical; merging back is not. Choose multi-repo from the start when each org already has its own team and its own release rhythm.
Structure the source so that "shared" and "local" are unambiguous:
```
packages/
core/ # the shared package - source of truth for common metadata
orgs/
eu/ # EU-only metadata and overrides
na/ # NA-only metadata and overrides
config/
scratch-def-core.json
```
## Branch model
* `main` is the trunk for the core, exactly as in [B3](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b3-modular-trunk.md).
* `release/` branches are **short-lived** - created for a rollout, merged or deleted after it lands. They exist to pin the core version an org is adopting and to carry rollout-specific fixes.
* Org-local metadata under `orgs//` changes on `main` too. It is scoped by path, not by branch.
**The anti-pattern to avoid:** a permanent long-lived branch per production org *inside one repository*. That is [branch-per-org](https://svierk.github.io/salesforce-devops-starter-kit/strategy/background.md#branching-models) at organisational scale, and it guarantees that the regions diverge until the shared core is a fiction. Version pinning through packages is what keeps orgs independent - not branches.
### Per-org environment chains
When each org has its own team, its own sandbox chain and its own approval process, run [B2](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md) **once per org, in that org's own repository** - with whichever of its three variants fits how that org releases. The orgs stay independent the way separate repositories are independent, not through branches.
What remains shared is the common metadata. Keep it in its own repository with its own review, and decide once how it reaches the orgs: as a package version each org pins, or as source deployed together with each org's metadata. Either way, a change to shared metadata is a change to every org - validate it against each of them before it lands.
## Promotion path & roles
| Role | Does |
| ------------------------ | ------------------------------------------------------------------ |
| Core / platform team | Owns `packages/core`, its versions and the rollout calendar |
| Regional lead | Owns `orgs/`, decides when their org adopts a core version |
| Regional release manager | Runs the rollout for one org, owns its production approval |
The key contract: **the core team decides what is released; regional teams decide when they adopt it.** Write that down, because the failure mode is the core team being held responsible for a region's timeline.
Set a maximum version lag - "no org may be more than two core versions behind". Without it, the oldest org silently becomes unsupportable.
## Strengths
* Regions get autonomy over their release timing without forking the codebase.
* The shared core is built and tested once, then installed many times.
* Compliance and data-residency boundaries are respected by construction.
* Adding a region is a configuration change, not an architectural one.
## Trade-offs & anti-patterns
* **Everything multiplies.** Pipelines, secrets, environments, test matrices, incidents. The per-org cost is roughly linear - budget for it.
* **Divergence is the default outcome.** Every local exception is reasonable in isolation and corrosive in aggregate. Review what goes into `orgs//` as strictly as you review the core.
* **Cross-org reporting is somebody's problem.** This blueprint does not solve it; it makes it harder. Data platform work usually follows.
* **A change failing in region three of five** leaves you partially rolled out with no rollback. Decide in advance whether you pause the rollout or fix forward.
* **Size each org's chain to its own approval process.** Full four-stage chains per org work when each org has a team and a repository of its own. Run centrally by one team, five of them are unmanageable - then keep each chain to one sandbox plus production.
## Kit mapping
The callers below are for the trunk shape. **With per-org environment chains**, each org's repository uses the [B2 callers](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md#kit-mapping) unchanged - the multi-org part is the shared metadata, not the pipeline.
**Fan out with a matrix.** One caller deploys to every org, with a GitHub Environment per org carrying that org's credentials and approval rules:
```yaml
name: Regional Rollout
on:
workflow_dispatch:
inputs:
region:
description: Region to roll out to (or "all")
type: choice
options: [all, eu, na, apac]
permissions:
contents: read
jobs:
rollout:
strategy:
# Never cancel the remaining orgs because one failed - a partial rollout
# needs a deliberate decision, not an automatic abort.
fail-fast: false
# Roll out one org at a time so a failure can be caught before it spreads.
max-parallel: 1
matrix:
# "all" fans out to every org; a single region rolls out on its own
region: ${{ fromJSON(inputs.region == 'all' && '["eu","na","apac"]' || format('["{0}"]', inputs.region)) }}
uses: svierk/salesforce-devops-starter-kit/.github/workflows/deployment.yml@v1.5.0
with:
source-dir: orgs/${{ matrix.region }}
target-org-alias: ${{ matrix.region }}-production
test-level: RunLocalTests
environment: ${{ matrix.region }}-production # own secrets + own reviewers
secrets: inherit
```
::: tip One GitHub Environment per production org
Store each org's `SFDX_CONSUMER_KEY`, `SFDX_JWT_SECRET_KEY` and `SFDX_USERNAME` as environment secrets and add that region's release manager as a required reviewer. `secrets: inherit` then resolves the right org per matrix leg, and no credential is ever visible to another region's job.
:::
**Validate against every affected org.** A core change touches all of them, and a check-only delta deployment into each org's sandbox catches the region whose local configuration conflicts with the core change - the failure this blueprint exists to survive. Run [`pr-validation.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/pr-validation.md) as a matrix with `environment: ${{ format('{0}-validation', matrix.region) }}` - one validation environment per region, holding that region's sandbox credentials, without required reviewers.
**Distribute the core with** [`sfdx-package-installation`](https://github.com/svierk/sfdx-package-installation), pinning an explicit version per org rather than "latest". The pin is what makes independent adoption real.
**Path-filter the CI.** A change under `orgs/eu/` should not trigger validation for NA and APAC. Use `paths:` on the caller workflows - without it, every pull request runs the full matrix and the feedback loop becomes unusable.
**Branch protection on `main`:** as [B3](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b3-modular-trunk.md), plus [`CODEOWNERS`](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners) mapping `packages/core/` to the core team and each `orgs//` to its regional team. In a monorepo, CODEOWNERS is what makes ownership boundaries real.
## Migration path
**This is usually a terminal state** - you arrive here because of business structure, and you leave only if that structure changes.
**Toward consolidation:** if the regions' processes converge, the honest move is to merge orgs and drop back to [B3](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b3-modular-trunk.md). Watch for it - "we keep the orgs separate because we always have" is worth challenging every couple of years.
**Signals of trouble:** an org more than two core versions behind, `orgs//` growing faster than `packages/core/`, or rollouts that routinely stop halfway. Each means the shared core is losing to local divergence.
---
---
url: /salesforce-devops-starter-kit/strategy/b5-low-code-admin-first.md
description: >-
Admin-heavy teams where Git knowledge is the bottleneck - two org-bound
branches, tool-driven promotion and a validation gate tuned to declarative
work.
---
# B5 - Low-Code First
Most changes are built by clicking, by people who do not use Git. The strategy's job here is to make version control real without making it the admins' problem.
::: tip In short
For **admin-heavy teams** where Git knowledge is the bottleneck. Two org-bound branches, `uat` and `main`, with tooling such as DevOps Center doing the promoting and a static-analysis gate tuned to declarative work. Admins never have to touch Git. **Watch out:** the bus factor - name a pipeline owner and a deputy.
:::
## At a glance
| | |
| ----------------- | ------------------------------------------------------------------------ |
| **Team size** | 2-10, mostly admins and low-code builders |
| **Environments** | Shared dev sandbox + UAT + production |
| **Branch model** | Simplified environment branches, driven by tooling rather than by hand |
| **Cadence** | Mixed - regular small releases plus ad-hoc changes |
| **Maturity** | Level 1-2 |
| **Prerequisites** | One person who owns the pipeline; the rest of the team never touches Git |
## Target picture
```mermaid
flowchart LR
subgraph BUILD["Built by clicking"]
ADMIN["Admin / low-code builder"]
DEVSB["Dev Sandbox"]
end
WI["work item branch created by tooling"]
UAT["uat UAT Sandbox"]
MAIN["main Production"]
ADMIN -->|"builds in"| DEVSB
DEVSB -->|"retrieve tool-assisted"| WI
WI -->|"pull request"| UAT
UAT -->|"promote"| MAIN
classDef org fill:#dde4f9,stroke:#1d4ed8,color:#0b1a4a;
classDef branch fill:#ebe4fa,stroke:#5b21d6,color:#24104f;
classDef human fill:#fff3e0,stroke:#e65100,color:#3d2400;
class DEVSB,UAT,MAIN org;
class WI branch;
class ADMIN human;
```
## Org & sandbox landscape
* **One shared Developer sandbox** where admins build. Shared, because coordinating one org is easier for this team than owning one each.
* **One UAT sandbox** (Partial Copy if the budget allows - business users testing against empty objects will not find the bugs that matter).
* **Production.**
Three environments is the ceiling here, not a starting point. Every additional stage is one more place where someone has to understand what a merge is.
## Branch model
Two long-lived branches, `uat` and `main`, each bound to an org. Work-item branches are created **by tooling**, not by hand:
* Salesforce **DevOps Center** lists the changes in the dev sandbox; the admin ticks the ones that belong to a work item, and DevOps Center creates the branch and promotes through a UI. This is the on-ramp it was built for, and the default here.
* For a route that stays closer to Git, [sfdx-hardis](https://sfdx-hardis.cloudity.com/salesforce-ci-cd-publish-task/) guides the same steps from VS Code: it lists the changes, commits the selected ones and opens the pull request.
Either way, the admin picks changes from a list - nobody types metadata names or runs a command line. Both rely on source tracking, so the dev sandbox has to be a Developer or Developer Pro sandbox with source tracking enabled. A form in the Actions tab cannot do this job: it cannot list what changed in an org, so the admin would have to name every component by hand.
::: warning Two stages, deliberately
This is an environment-branch model, which [Background](https://svierk.github.io/salesforce-devops-starter-kit/strategy/background.md#branching-models) warns about - and the warning holds. It is defensible here only because there are **two** stages, not four, and because tooling performs the promotions. If you find yourself adding a third org-bound branch, the model has outgrown its justification: move to [B1](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b1-simple-org-development.md) if the team has learned Git, or to [B2](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md) if you genuinely need the gates.
:::
## Promotion path & roles
| Role | Does |
| ------------------------ | ----------------------------------------------------------------------------------------- |
| Admin / low-code builder | Builds in the shared dev sandbox, requests a retrieve, tests in UAT. Never touches Git |
| Pipeline owner | Owns the workflows, reviews every pull request, resolves the conflicts. The critical role |
| Business owner | Signs off in UAT |
**The pipeline owner is the single point of failure.** One person understanding the repository is the defining risk of this blueprint. Mitigate it: document the promotion procedure as a runbook, make sure a second person can run it, and treat "the pipeline owner is on holiday" as a scenario to rehearse rather than discover.
## Working agreements
Process carries more weight here than tooling. Three agreements that decide whether this blueprint works:
1. **Nothing is built directly in production.** No exceptions - a change made in production is a change the pipeline will overwrite.
2. **The shared dev sandbox is claimed before it is used.** A message in a channel is enough. Two admins editing the same flow in one org produces a conflict nobody can resolve from the XML.
3. **Retrieve promptly.** A change built three weeks ago and never retrieved is lost the next time the sandbox is refreshed.
## Strengths
* Admins keep working the way they already work.
* Real version control and an audit trail, without retraining the team.
* Only two stages, so the merge overhead is modest.
* A genuine step up from change sets, with a low adoption cost.
## Trade-offs & anti-patterns
* **The bus factor is one.** See above. This is the trade-off you are accepting.
* **Retrieval is the weak link.** If someone forgets, the change exists only in a sandbox. Automate the reminder, or run a scheduled diff against the org.
* **Flow and profile conflicts still land on the pipeline owner.** Prefer permission sets over profiles from day one - it removes the single worst conflict source.
* **DevOps Center has real limits:** no rollback, org-based development rather than scratch orgs and packages, its own work items rather than your issue tracker, and it copes poorly with merges made outside the tool. Its quality gates (tests, coverage, Code Analyzer) now cover part of what the kit's validation does - but only for changes that go through DevOps Center. Treat it as an on-ramp, not the destination.
* **Do not add stages to buy confidence.** Confidence comes from the validation gate and the UAT sign-off, not from a third sandbox.
## Kit mapping
**Validation, tuned down.** [`pr-validation.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/pr-validation.md) still runs on every pull request, but the static analysis threshold is relaxed - declarative metadata trips analyzer rules written for Apex, and a gate that blocks constantly for reasons nobody in the team can act on will be switched off within a month:
```yaml
name: PR Validation
on:
pull_request:
branches: [uat, main]
permissions:
contents: read
security-events: write
jobs:
validate:
uses: svierk/salesforce-devops-starter-kit/.github/workflows/pr-validation.yml@v1.5.0
with:
source-dir: force-app
delta: true
run-code-review: true
severity-threshold: '2' # only high-severity findings block
test-level: RunLocalTests
secrets: inherit
```
Start at `severity-threshold: '2'` and tighten it as the team's Apex share grows.
`secrets: inherit` hands the validation the repository secrets - store the UAT sandbox credentials there. The environment secrets below only reach the deployments.
::: warning One deployment engine per stage
DevOps Center deploys when it promotes a work item, and it merges into `uat` and `main` as it does so. If DevOps Center is your promotion tool, do **not** add the deployment caller below as well - every promotion would deploy twice, the second time outside DevOps Center's view. Let DevOps Center own deployment, keep the kit for validation and self-service orgs, and check that your branch protection rules still allow DevOps Center's merges.
:::
**Deployment per stage** - only on the Git route, where the work-item pull requests come from VS Code tooling such as sfdx-hardis: [`deployment.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/deployment.md) on push to `uat` and `main`, with a GitHub Environment each:
```yaml
name: Deployment
on:
push:
branches: [uat, main]
permissions:
contents: read
jobs:
deploy:
uses: svierk/salesforce-devops-starter-kit/.github/workflows/deployment.yml@v1.5.0
with:
source-dir: force-app
target-org-alias: ${{ github.ref_name }}
test-level: RunLocalTests
environment: ${{ github.ref_name == 'main' && 'production' || 'uat' }}
secrets: inherit
```
Add required reviewers to the `production` environment. That approval is the release decision, and it is the one moment a human must be in the loop.
**Self-service org provisioning** - [`create-scratch-org.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/create-scratch-org.md) as a `workflow_dispatch` caller gives an admin a fully configured org from a form in the Actions tab, with data imported and a password generated. No CLI, no Dev Hub knowledge. This is the highest-value optional piece for this audience.
**Branch protection on `uat` and `main`:**
* Require a pull request before merging
* Require the PR Validation check to pass
* Require review from the pipeline owner
* Block force pushes and deletions
* Keep `uat` as the default branch, so tooling targets it by default
## Migration path
**To [B1](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b1-simple-org-development.md)** once enough of the team is comfortable with pull requests. Collapsing `uat` into a deployment gate on `main` removes a whole branch and its merge overhead - this is a simplification, and the most common next step.
**To [B2](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md)** only if a second stream and a formal approval process appear together. Do not go there for the environments alone.
**Signals you are ready to move:** admins opening their own pull requests, the pipeline owner no longer being the only one who resolves conflicts, and a growing share of pro-code work.
**Signals of strain:** retrieves being forgotten, the shared dev sandbox being contended, or the pipeline owner as a permanent queue. The first two are process problems; the third means it is time for a second pipeline owner, not a new branching model.
---
---
url: /salesforce-devops-starter-kit/strategy/mapping.md
description: >-
Which reusable workflows, input settings, branch protection rules and GitHub
Environments each of the five blueprints switches on.
---
# Kit Mapping
The bridge between the strategy layer and the pipeline. Once you have picked a blueprint, this page tells you which parts of the kit to switch on - and what to configure in GitHub around them.
Each blueprint page carries the full, copy-pasteable callers. This page is the overview you compare against.
## Reusable workflows per blueprint
| Workflow | [B1](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b1-simple-org-development.md) | [B2](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md) | [B3](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b3-modular-trunk.md) | [B4](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b4-multi-org-regional.md) | [B5](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b5-low-code-admin-first.md) |
| --------------------------------------------------------- | :--------------------------------: | :-----------------------------------------: | :--------------------------: | :----------------------------: | :------------------------------: |
| [`pr-validation.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/pr-validation.md) | required | required, on all 4 env branches | required | required, one job per org | required, relaxed threshold |
| [`deployment.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/deployment.md) | on `main` | per env branch | from `main` or a release tag | per-org matrix | on `uat` and `main` |
| [`scratch-org-ci.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/scratch-org-ci.md) | optional | optional, on `feature/*` | **gate for modules** | core CI | - |
| [`create-scratch-org.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/create-scratch-org.md) | optional | optional | recommended | recommended | recommended |
| [`promote.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/promote.md) | - | story promotion and release bundle | - | B2 chain per org | - |
## Key input settings
| Setting | B1 | B2 | B3 | B4 | B5 |
| --------------------------- | :-------------: | :------------------------------------: | :----------------------------: | :-------------: | :-----------------: |
| `delta` on validation | `true` | `true` | `true` | `true` | `true` |
| `delta` on deployment | `false` | train `false`, story and bundle `true` | `true` (from the last release) | `false` | `false` |
| `test-level` | `RunLocalTests` | `RunLocalTests` | `RunLocalTests` | `RunLocalTests` | `RunLocalTests` |
| `severity-threshold` | `3` | `3` | `3` | `3` | `2` |
| `environment` on deployment | `production` | one per stage | `production` | one per org | `uat`, `production` |
Keep `RunLocalTests` as the test level for production. `RunRelevantTests`, introduced as a beta in Spring '26, selects tests from the changed code - but Salesforce itself advises against using it in production pipelines yet. Try it in validation runs first.
::: tip Why delta on validation but mostly not on deployment
Validating a pull request only needs to check what the branch changed - that is the fast feedback loop. A promotion between long-lived branches deploys the branch's whole state, because a delta there would silently skip anything an earlier hop missed.
[B2's story promotion and release bundle](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md#three-ways-to-promote) are the exception: environment branches are never merged into each other, so each push carries exactly what one pull request merged - usually one story. Deploy it as a delta with `delta-from: ${{ github.event.before }}`. Otherwise, `delta-from` is for the case where you deliberately want to deploy the difference since a release tag.
:::
## Building blocks
All blueprints use [`get-node-version`](https://github.com/svierk/get-node-version), [`sfdx-cli-setup`](https://github.com/svierk/sfdx-cli-setup) and [`sfdx-login`](https://github.com/svierk/sfdx-login) - the reusable workflows call them for you. The blocks below matter when you compose your own pipeline:
| Block | Where it earns its place |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| [`sfdx-deploy`](https://github.com/svierk/sfdx-deploy) | Everywhere. `delta` + `dry-run` is the validation gate; `delta-from` targets a release tag |
| [`sfdx-run-tests`](https://github.com/svierk/sfdx-run-tests) | Everywhere. Apex, LWC (Jest) and Flow tests with coverage |
| [`sfdx-code-review`](https://github.com/svierk/sfdx-code-review) | Everywhere. Relax `severity-threshold` in B5, keep it strict in B3/B4 |
| [`sfdx-create-scratch-org`](https://github.com/svierk/sfdx-create-scratch-org) / [`sfdx-delete-scratch-org`](https://github.com/svierk/sfdx-delete-scratch-org) | B3 and B4, where scratch orgs are the development environment |
| [`sfdx-package-installation`](https://github.com/svierk/sfdx-package-installation) | B3 (dependencies) and B4 (distributing the core, pinned per org) |
| [`sfdx-data-import`](https://github.com/svierk/sfdx-data-import) | Wherever a fresh org needs to be usable - strongest with `create-scratch-org.yml` |
| [`sfdx-assign-permissions`](https://github.com/svierk/sfdx-assign-permissions) | Wherever a fresh org needs to be usable - `create-scratch-org.yml` grants permission sets before the data import |
| [`sfdx-apex-execute`](https://github.com/svierk/sfdx-apex-execute) | B2 and B4, where each promotion may need data migrations - `apex-file-path` on `deployment.yml` runs them after a real (non-dry-run) deploy |
::: warning Not covered yet
Package **version creation and promotion** (`sf package version create` / `promote`) has no building block. B3 and B4 need it, so that step is currently a direct `sf` CLI call in your own workflow - the CLI is already installed and authenticated by the time your step runs.
:::
## Branch protection
Configure these under **Settings → Branches** for every long-lived branch in your blueprint.
| Rule | B1 | B2 | B3 | B4 | B5 |
| ------------------------------ | :--------: | :-------------------------------------------: | :-------------------------: | :----------------: | :----------------: |
| Protected branches | `main` | `integration`, `uat`, `preprod`, `main` | `main` | `main` | `uat`, `main` |
| Default branch | `main` | `integration` (train), `main` (story, bundle) | `main` | `main` | `uat` |
| Require a pull request | yes | yes | yes | yes | yes |
| Required approvals | 1 | 1 | 1 (owning team) | 1 (CODEOWNERS) | 1 (pipeline owner) |
| Required status checks | validation | validation | validation + scratch org CI | validation per org | validation |
| Require up-to-date branches | yes | yes | yes | yes | yes |
| Restrict who can merge | - | release manager on `uat`/`preprod`/`main` | - | CODEOWNERS | pipeline owner |
| Block force pushes & deletions | yes | yes | yes | yes | yes |
## GitHub Environments
[`deployment.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/deployment.md) takes an `environment` input, and that one input carries two things at once: **environment-scoped secrets** (so each org has its own credentials, and no job ever sees another org's) and **protection rules** (required reviewers, wait timers, branch restrictions).
This is what lets one reusable workflow serve every stage and every org. Create one environment per target org, store `SFDX_CONSUMER_KEY`, `SFDX_JWT_SECRET_KEY` and `SFDX_USERNAME` in each, and use `secrets: inherit` in the caller.
[`pr-validation.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/pr-validation.md) takes an `environment` input too, so validation can run against the org behind each target branch. Give it its own environment per org - for example `uat-validation` - with the same credentials but **no** required reviewers or branch restrictions, or every pull request waits for an approval. [`scratch-org-ci.yml`](https://svierk.github.io/salesforce-devops-starter-kit/workflows/scratch-org-ci.md) reads the **repository secrets** of your Dev Hub.
| Blueprint | Environments to create |
| --------- | -------------------------------------------------------------------------------- |
| B1 | `production` |
| B2 | `integration`, `uat`, `preprod`, `production` |
| B3 | `staging`, `production` |
| B4 | one per production org, e.g. `eu-production`, `na-production`, `apac-production` |
| B5 | `uat`, `production` |
Add **required reviewers** to every environment that fronts a production org. That approval is your release gate, and in a segregation-of-duties context it is the control an auditor will ask about.
See [Authentication](https://svierk.github.io/salesforce-devops-starter-kit/authentication.md) for setting up the JWT flow and producing these secrets.
## What no blueprint gives you
Two gaps are the same in all five, because the platform does not solve them:
* **Backup and recovery.** Salesforce has no rollback. Every blueprint needs an explicit backup solution with a stated RPO and RTO. The pipeline is not one.
* **Drift detection.** Changes made directly in an org are invisible to the pipeline until they cause a deployment failure. A scheduled diff between the org and the branch is the cheap mitigation, and it belongs in every blueprint.
---
---
url: /salesforce-devops-starter-kit/strategy/background.md
description: >-
The reasoning behind the blueprints - sandbox types and licences, single- vs.
multi-org, org vs. package development, branching models, release styles and
maturity levels.
---
# Background & Trade-Offs
The reasoning behind the [questionnaire](https://svierk.github.io/salesforce-devops-starter-kit/strategy.md) and the blueprints. Read this if you want to argue with a blueprint rather than adopt it.
## Why Salesforce is different
The choice of org landscape and branching model is harder on Salesforce than elsewhere. Sandboxes are licensed, metered and slow to refresh; there is no true rollback; declarative changes arrive from people who may never touch Git; and XML metadata merges badly. Copying a branching model from a web project rarely survives contact with those constraints - the sections below are the trade-offs the blueprints are built on.
## Sandbox types
Your branching model can never be richer than your sandbox budget. Each long-lived branch in an environment-style model needs an org behind it, and those orgs are licensed, sized and rate-limited.
| Type | Storage (data / file) | Contains | Refresh interval |
| ------------- | ------------------------------------------------------------------------------------------------ | ---------------------- | ---------------- |
| Developer | 200 MB / 200 MB | Metadata only | 1 day |
| Developer Pro | 1 GB / 1 GB | Metadata only | 1 day |
| Partial Copy | 5 GB data, sampled via a template (up to 10,000 records per object) / file storage as production | Metadata + sample data | 5 days |
| Full | Same as production | Metadata + all data | 29 days |
The **refresh interval** is the constraint people underestimate. A Full sandbox used as pre-production can only be brought back in line with production once every 29 days - so it drifts, and the drift is invisible until a deployment fails there and not in production. A failed release rehearsal in a Full sandbox cannot be repeated from a clean state within the same month, either.
Two platform features soften this. **Quick Clone** creates a new sandbox from a template sandbox instead of from production, which is much faster for short-lived test environments. **Data Mask & Seed** masks personal data in sandboxes and seeds them with realistic test data - an alternative to the template sampling of a Partial Copy.
::: warning Licences are edition-dependent
Sandbox entitlements ship with the edition, and the gap between editions is large. Enterprise Edition includes 25 Developer sandboxes and one Partial Copy but **not** Full or Developer Pro - those are a [paid add-on](https://www.salesforce.com/editions-pricing/platform/environments/), priced as a share of your net spend (at list price roughly 5 % for Developer Pro, 20 % for Partial Copy and 30 % for Full). Performance and Unlimited Edition include a Full sandbox and several Developer Pro sandboxes as standard.
Any blueprint below that asks for a Full-copy UAT or pre-production stage is therefore a **budget decision** on Enterprise Edition, not just a technical one. Entitlements change; confirm yours against [Salesforce Help](https://help.salesforce.com/s/articleView?id=platform.deploy_sandboxes_parent.htm\&type=5) before you plan around them.
:::
Scratch orgs sidestep the entitlement question for development and CI - they are ephemeral, source-tracked and created from a definition file. They cannot replace a data-realistic UAT stage, because they start empty. Their limits come from the Dev Hub: Enterprise Edition allows 40 active scratch orgs and 80 new ones per day, Unlimited and Performance 100 and 200 ([Salesforce DX Developer Guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_scratch_orgs_editions_and_allocations.htm)). A scratch org lives 1 to 30 days, 7 by default.
## Single-org vs. multi-org
| Single-org fits | Multi-org fits |
| -------------------------------------------- | --------------------------------------------- |
| Standardised processes across the business | Genuinely divergent processes per unit |
| Unified reporting and a single customer view | Data residency or compliance boundaries |
| Small to mid-size organisations | Regional or BU autonomy, independent roadmaps |
| Maximum reuse of metadata and automation | Avoiding hard org limits |
The cost of multi-org is data silos, harder cross-org reporting, and a pipeline that fans out. The cost of single-org is that every business unit shares one release cadence and one set of limits.
The established framing comes from operating-model theory - business process **integration** on one axis, **standardisation** on the other. High integration and high standardisation argue for as few orgs as possible; high autonomy on both argues for multi-org; standardised-but-independent units are the case for multi-org plus a shared managed or unlocked package carrying the common core. That last pattern is exactly [B4](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b4-multi-org-regional.md).
## Org development vs. package development
| | Org Development Model | Package Development Model (2GP) |
| ---------------- | ------------------------------------------------ | --------------------------------------------------- |
| Unit of change | Metadata in one "happy soup" | Versioned unlocked packages |
| Source of truth | Version control | Version control |
| Develops against | Sandboxes | Scratch orgs with source tracking |
| Release coupling | One release train | Packages release independently |
| Fits | Single team, legacy orgs, declarative-heavy work | Several teams, modular metadata, decoupled roadmaps |
Between the two sits **modular source**: package directories in `sfdx-project.json`, deployed as source rather than built as package versions. It gives you most of the structure of package development without its build and promotion cost, and it is the default artifact in [B3](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b3-modular-trunk.md#what-you-ship).
**When to switch:** growing org complexity, several teams or admins stepping on each other, and a wish to decouple a project from the big release train.
**What blocks the switch:**
* Not every metadata type is packageable. The [Metadata Coverage Report](https://developer.salesforce.com/docs/metadata-coverage) is the binding source - check it before committing, and re-check per release, because it moves.
* Dependencies. Org-dependent unlocked packages (`--org-dependent`) move dependency validation from build time to install time, which resolves "field does not exist" build failures against a loosely coupled org - at the price of losing "build once, install anywhere".
* Existing metadata rarely factors cleanly. Extracting the first package from a mature happy soup is the real cost, not the pipeline change.
## Branching models
| Model | Shape | Fits | Watch out for |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| **GitHub Flow / feature branch** | `main` + short-lived `feature/*` | Small teams, Git newcomers, [B1](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b1-simple-org-development.md) | Nothing between a merge and production - production gating has to be a deployment gate |
| **Git Flow** | `main` + `develop` + release and hotfix branches | Structured sprint releases, larger teams | Two permanent branches to keep in sync; heavy for most Salesforce teams |
| **Environment branches - release train** | One long-lived branch per org; the whole state moves stage to stage | Planned release dates with approval gates, [B2](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md#release-train) | Permanent merge overhead - see the warning below |
| **Environment branches - story promotion** | One long-lived branch per org; each story is merged into each stage through a short-lived promotion branch | Independent stories with approval gates, [B2](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md#story-promotion); the model of Gearset Pipelines, and in principle of Copado and DevOps Center | Stages drift apart by design - sync them after every release |
| **Environment branches - release bundle** | Story promotion up to UAT; from there, a release branch carries the signed-off stories as one bundle | Sign-off per story, production on release dates, [B2](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md#release-bundle); DevOps Center's "bundling stage" | Stories are merged twice - per stage and into the release branch |
| **Trunk-based** | One branch + feature flags, modules or packages optional | High maturity, [B3](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b3-modular-trunk.md) | Requires real discipline and a quality gate that actually blocks |
::: warning Branch-per-org as an org mirror is an anti-pattern
Mapping one long-lived Git branch to each Salesforce org is the most common Salesforce branching model, and most teams adopt it without examining why. The model itself is workable. What fails is the version where promotions stop being reviewed pull requests and the branches become a copy of whatever is in each org - Git as a metadata backup rather than as a review mechanism.
What keeps it healthy is automation and discipline: every promotion a reviewed pull request, conflicts resolved in a defined place, and a regular sync so the branches cannot drift apart. DevOps platforms (Copado, Gearset Pipelines and similar) provide that out of the box; with GitHub Actions and this kit you provide it yourself - [B2](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md) shows all three variants.
If you land on an environment-style model - [B2](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md) or [B5](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b5-low-code-admin-first.md) - adopt it deliberately and keep the number of stages to the minimum that your approval process actually requires.
:::
## Release styles
How you release is a separate choice from how you branch, and it narrows the branching options more than people expect. The [questionnaire](https://svierk.github.io/salesforce-devops-starter-kit/strategy.md) asks it last.
| Release style | A release is | Fits |
| --------------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Story by story** | One change, shipped as soon as it is signed off | [B1](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b1-simple-org-development.md), [B2 story promotion](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md#story-promotion), [B3](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b3-modular-trunk.md), [B5](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b5-low-code-admin-first.md) |
| **Bundled into production** | Stories signed off one by one, shipped together on a date | [B2 release bundle](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md#release-bundle), [B3](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b3-modular-trunk.md) with a tag per release date |
| **Whole releases** | A planned, frozen scope tested as one | [B2 release train](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b2-multi-team-environment-branches.md#release-train), [B4](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b4-multi-org-regional.md) rollouts per org |
Package versions are an artifact, not a release style - they can ship in any of the three ways. Whether to package is a question of modularity, covered in [B3](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b3-modular-trunk.md#what-you-ship).
Release dates do not require release branches. In [B1](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b1-simple-org-development.md), merging stays continuous and only the production deployment waits for the date - run it from a release tag or a manually triggered workflow, behind a GitHub Environment with required reviewers.
## Salesforce-specific pitfalls
These apply to every blueprint, and each one shapes at least one recommendation in this section.
* **There is no true rollback.** You can deploy a destructive change, but you cannot undo a deployment. Every blueprint needs a backup and recovery answer with a stated RPO/RTO - the pipeline does not provide one.
* **XML metadata merges badly.** Profiles, permission sets and flows are the usual casualties. Mitigate by keeping permissions in permission sets rather than profiles, by keeping long-lived branches few, and by using a Salesforce-aware merge driver where merges between long-lived branches are routine - the open-source [sf-git-merge-driver](https://github.com/scolladon/sf-git-merge-driver) merges profiles, permission sets, labels and value sets by key rather than line by line. Check its coverage for flows and layouts before relying on it there.
* **Declarative change arrives outside Git.** Someone will build a flow in a sandbox and expect it to reach production. The strategy has to name who retrieves it and when, or it will be deployed by hand.
* **Apex test runs are slow.** Long validation times push people to skip the gate. Mitigate with delta deployments (only changed metadata) and by scoping test levels to what the change can actually affect.
* **Feature flags decouple deployment from release.** Custom permissions plus custom metadata types are the common implementation. The simplest variant is still the best one for small teams: do not deploy a feature until it is needed.
## Maturity signals
The [DORA metrics](https://dora.dev/guides/dora-metrics/) are the standard vocabulary for Salesforce DevOps maturity, and the annual [State of Salesforce DevOps](https://gearset.com/blog/state-of-salesforce-devops-report/) reports track them for this ecosystem specifically:
| Metric | Question it answers |
| ------------------------------- | ------------------------------------------------------------ |
| Deployment frequency | How often do you ship to production? |
| Change lead time | How long from commit to production? |
| Change fail rate | What share of deployments causes a problem? |
| Failed deployment recovery time | How fast do you recover from one? |
| Deployment rework rate | What share of deployments are unplanned fixes for incidents? |
Condensed into the four levels the [questionnaire](https://svierk.github.io/salesforce-devops-starter-kit/strategy.md) asks about:
1. **Starting out** - manual or lightly scripted deployments, no metrics, testing late.
2. **Automated** - CI on every pull request, scripted deployments, static analysis blocking.
3. **Measured** - delta deployments, tracked DORA metrics, deliberate environment strategy.
4. **Optimised** - trunk-based, feature-flagged, delivery metrics tracked and improving against your own baseline.
Use the metrics as **migration triggers**, not as a scoreboard. DORA itself no longer publishes industry performance tiers and warns against fixed targets and comparisons between teams - compare each team with its own baseline. Each blueprint page states which signals mean you are ready for the next one.
Two practices matter at every level and in every blueprint, independent of the branching model: **backups** with a stated recovery point and recovery time, because Salesforce has no rollback, and **observability** - knowing within hours, not weeks, that a deployment broke something. In the available survey data, a consistent release process, a consolidated toolchain, backups and observability track with delivery performance more clearly than any branching model does.
## Salesforce DevOps Center
Salesforce's free DevOps Center manages Git branches on the admin's behalf: a work item becomes a feature branch, and promotion happens through a UI rather than through Git. Since 2026 it is a native platform capability, enabled in Setup instead of installed as a managed package, and its pipeline can enforce quality gates - Apex and Flow tests, code coverage and Code Analyzer findings - before a promotion. That makes it a genuine successor to change sets and a reasonable on-ramp for [B5](https://svierk.github.io/salesforce-devops-starter-kit/strategy/b5-low-code-admin-first.md).
Its limits matter for the target architecture, though: it is built around org-based development rather than scratch orgs and packages, there is no rollback, its work items stand in for your issue tracker rather than connecting to it, and it copes poorly with merges made outside the tool. GitLab and Azure DevOps support and a Jira integration are announced but not yet available. The product is moving quickly - check [what is new in DevOps Center](https://admin.salesforce.com/blog/2026/the-future-of-salesforce-devops-is-here-whats-new-for-admins) before you rule it in or out. Treat it as an entry point, not a destination.
---
---
url: /salesforce-devops-starter-kit/workflows/pr-validation.md
description: >-
Reusable workflow that validates a pull request without changing the target
org.
---
# PR Validation
Reusable workflow that validates a pull request without changing the target org: it runs static code analysis and a check-only (dry-run) delta deployment including Apex tests. Call it from your own repository via `uses:`.
> Reusable workflow - call it from your own repository with `uses:`. [View source](https://github.com/svierk/salesforce-devops-starter-kit/blob/main/.github/workflows/pr-validation.yml)
## Inputs
| Input | Description | Type | Required | Default |
| --- | --- | --- | --- | --- |
| `source-dir` | Source directory to analyze and validate. | string | no | `force-app` |
| `target-org-alias` | Alias used for the authenticated validation org. | string | no | `ci` |
| `test-level` | Apex test level for the validation deployment. Ignored when apex-tests is set. | string | no | `RunLocalTests` |
| `apex-tests` | Comma-separated or multi-line list of Apex test classes to run during the validation deployment. A selection always runs with RunSpecifiedTests. Leave empty to run the test-level. | string | no | - |
| `delta` | Validate only the components changed in the pull request. | boolean | no | `true` |
| `run-code-review` | Run the code review (Salesforce Code Analyzer). | boolean | no | `true` |
| `severity-threshold` | Fail if a code analyzer violation with this severity or higher is found. | string | no | `3` |
| `environment` | Optional GitHub Environment to validate in - its environment-scoped secrets select the org to validate against, e.g. one environment per target branch. Use an environment without required reviewers or branch restrictions, or every pull request waits for an approval or is rejected. Leave empty to use the repository secrets. | string | no | - |
## Secrets
| Secret | Description | Required |
| --- | --- | --- |
| `SFDX_CONSUMER_KEY` | Consumer key (client ID) of the connected app used for the JWT flow. | yes |
| `SFDX_JWT_SECRET_KEY` | Contents of the private server key (server.key) used for the JWT flow. | yes |
| `SFDX_USERNAME` | Username of the integration user to authenticate as. | yes |
## Example usage
Copy this caller into your project's `.github/workflows/` directory ([source](https://github.com/svierk/salesforce-devops-starter-kit/blob/main/examples/pr-validation.yml)):
```yaml
# Example: call the reusable PR validation workflow.
# Copy this file into your project's .github/workflows/ directory.
name: PR Validation
# Keep this on `pull_request`. Never switch it to `pull_request_target`: that
# event runs with the base repository's secrets, so a pull request from a fork
# could execute its own code against your Salesforce org.
on:
pull_request:
branches: [main]
# Least-privilege token: read the repository, plus write the code analysis
# results back as a SARIF report. A reusable workflow can never get more
# permissions than the caller grants here, so security-events: write has to be
# granted in this file as well.
permissions:
contents: read
security-events: write
jobs:
validate:
uses: svierk/salesforce-devops-starter-kit/.github/workflows/pr-validation.yml@v1.5.0
with:
source-dir: force-app
test-level: RunLocalTests
# Run only a selection of Apex test classes instead (implies RunSpecifiedTests):
# apex-tests: AccountServiceTest,LeadServiceTest
delta: true
run-code-review: true
severity-threshold: '3'
secrets:
SFDX_CONSUMER_KEY: ${{ secrets.SFDX_CONSUMER_KEY }}
SFDX_JWT_SECRET_KEY: ${{ secrets.SFDX_JWT_SECRET_KEY }}
SFDX_USERNAME: ${{ secrets.SFDX_USERNAME }}
```
---
---
url: /salesforce-devops-starter-kit/workflows/create-scratch-org.md
description: >-
Reusable workflow that provisions a ready-to-use scratch org as a self-service
task.
---
# Create Scratch Org
Reusable workflow that provisions a ready-to-use scratch org as a self-service task: it authenticates to the Dev Hub, creates a scratch org, optionally installs dependency packages, deploys the source, assigns permissions, imports sample data, runs anonymous Apex setup scripts, and generates a login password so the org can be handed over to a project member. Designed to be called from a `workflow_dispatch` caller (see examples/create-scratch-org.yml) so that even non-technical team members can request a fresh org straight from the GitHub Actions tab.
> Reusable workflow - call it from your own repository with `uses:`. [View source](https://github.com/svierk/salesforce-devops-starter-kit/blob/main/.github/workflows/create-scratch-org.yml)
## Inputs
| Input | Description | Type | Required | Default |
| --- | --- | --- | --- | --- |
| `org-name` | Human-readable name for the scratch org. | string | no | - |
| `description` | Description stored on the scratch org. | string | no | - |
| `admin-email` | Email address of the scratch org admin user. | string | no | - |
| `duration-days` | Number of days before the scratch org expires (1-30). | string | no | `7` |
| `source-dir` | Source directory to deploy to the scratch org. | string | no | `force-app` |
| `definition-file` | Path to the scratch org definition file. | string | no | `config/project-scratch-def.json` |
| `deploy-metadata` | Deploy the project source after the org is created. | boolean | no | `true` |
| `install-packages` | Install dependency packages before deploying. | boolean | no | `false` |
| `packages` | Comma-separated or multi-line list of dependency packages (aliases or IDs) to install, e.g. "04t...,04t...". | string | no | - |
| `permission-sets` | Comma-separated or multi-line list of permission sets to assign to the scratch org admin user after the deployment. Leave empty to skip. | string | no | - |
| `permission-set-groups` | Comma-separated or multi-line list of permission set groups to assign to the scratch org admin user. Leave empty to skip. | string | no | - |
| `permission-set-licenses` | Comma-separated or multi-line list of permission set licenses to assign to the scratch org admin user. Leave empty to skip. | string | no | - |
| `import-data` | Import sample data after the deployment. | boolean | no | `false` |
| `csv-file-path` | Path to a CSV file imported via the Bulk API 2.0. Leave empty to skip. | string | no | - |
| `csv-object-type` | sObject type of the records in the CSV file (required when csv-file-path is set). | string | no | - |
| `json-file-path` | Comma-separated or multi-line list of sObject Tree JSON file(s) to import, in order. Leave empty to skip. | string | no | - |
| `apex-file-path` | Comma-separated anonymous Apex file(s) or glob(s) to execute after the data import, e.g. "./scripts/apex/setup/\*.apex". Leave empty to skip. | string | no | - |
## Secrets
| Secret | Description | Required |
| --- | --- | --- |
| `SFDX_CONSUMER_KEY` | Consumer key (client ID) of the Dev Hub connected app used for the JWT flow. | yes |
| `SFDX_JWT_SECRET_KEY` | Contents of the private server key (server.key) used for the JWT flow. | yes |
| `SFDX_USERNAME` | Username of the Dev Hub integration user to authenticate as. | yes |
## Example usage
Copy this caller into your project's `.github/workflows/` directory ([source](https://github.com/svierk/salesforce-devops-starter-kit/blob/main/examples/create-scratch-org.yml)):
```yaml
# Example: self-service scratch org creation.
# Copy this file into your project's .github/workflows/ directory. Team members
# then run it on demand from the Actions tab ("Run workflow") and fill in the
# form below - no local Salesforce CLI setup required.
name: Create Scratch Org
on:
workflow_dispatch:
inputs:
org-name:
description: Scratch org name
type: string
description:
description: Scratch org description
type: string
admin-email:
description: Admin user email
type: string
duration-days:
description: Lifespan in days (1-30)
type: string
default: '7'
install-packages:
description: Install dependency packages?
type: boolean
default: false
import-data:
description: Import sample data?
type: boolean
default: true
# Least-privilege token: the called workflow only reads the repository and talks
# to Salesforce. A reusable workflow can never get more permissions than the
# caller grants here.
permissions:
contents: read
jobs:
create-scratch-org:
uses: svierk/salesforce-devops-starter-kit/.github/workflows/create-scratch-org.yml@v1.5.0
with:
org-name: ${{ inputs.org-name }}
description: ${{ inputs.description }}
admin-email: ${{ inputs.admin-email }}
duration-days: ${{ inputs.duration-days }}
source-dir: force-app
deploy-metadata: true
install-packages: ${{ inputs.install-packages }}
# Dependency packages to install when "Install dependency packages?" is checked:
# packages: '04t...,04t...'
# Permissions granted to the admin user after the deployment, so the
# handed-over org (and the data import below) can use the deployed metadata:
# permission-sets: 'My_App_Admin,My_App_User'
# permission-set-groups: 'Sales_Manager'
import-data: ${{ inputs.import-data }}
# Sample data imported when "Import sample data?" is checked (CSV via Bulk API 2.0,
# JSON via sObject Tree). Point these at files that exist in your repository:
csv-file-path: ./data/accounts.csv
csv-object-type: Account
json-file-path: ./data/accounts.json,./data/contacts.json
# Anonymous Apex setup scripts executed last, in alphabetical order:
# apex-file-path: ./scripts/apex/setup/*.apex
secrets:
SFDX_CONSUMER_KEY: ${{ secrets.SFDX_CONSUMER_KEY }}
SFDX_JWT_SECRET_KEY: ${{ secrets.SFDX_JWT_SECRET_KEY }}
SFDX_USERNAME: ${{ secrets.SFDX_USERNAME }}
```
---
---
url: /salesforce-devops-starter-kit/workflows/scratch-org-ci.md
description: Reusable workflow that runs a full scratch org lifecycle.
---
# Scratch Org CI
Reusable workflow that runs a full scratch org lifecycle: it authenticates to the Dev Hub, creates a scratch org, optionally installs dependency packages, deploys the source, optionally assigns permissions and runs anonymous Apex setup scripts, runs Apex tests, and always deletes the scratch org again.
> Reusable workflow - call it from your own repository with `uses:`. [View source](https://github.com/svierk/salesforce-devops-starter-kit/blob/main/.github/workflows/scratch-org-ci.yml)
## Inputs
| Input | Description | Type | Required | Default |
| --- | --- | --- | --- | --- |
| `source-dir` | Source directory to deploy to the scratch org. | string | no | `force-app` |
| `definition-file` | Path to the scratch org definition file. | string | no | `config/project-scratch-def.json` |
| `duration-days` | Number of days before the scratch org expires. | string | no | `1` |
| `test-level` | Apex test level for the test run. Ignored when apex-tests is set. | string | no | `RunLocalTests` |
| `apex-tests` | Comma-separated or multi-line list of Apex tests to run as "Class" or "Class.method". A selection always runs with RunSpecifiedTests. Leave empty to run the test-level. | string | no | - |
| `packages` | Comma-separated or multi-line list of dependency packages (aliases or IDs) to install, e.g. "04t...,04t...". Leave empty to skip. | string | no | - |
| `permission-sets` | Comma-separated or multi-line list of permission sets to assign to the scratch org admin user before the tests run. Leave empty to skip. | string | no | - |
| `permission-set-groups` | Comma-separated or multi-line list of permission set groups to assign to the scratch org admin user. Leave empty to skip. | string | no | - |
| `permission-set-licenses` | Comma-separated or multi-line list of permission set licenses to assign to the scratch org admin user. Leave empty to skip. | string | no | - |
| `apex-file-path` | Comma-separated anonymous Apex file(s) or glob(s) to execute before the tests, e.g. "./scripts/apex/setup/\*.apex". Leave empty to skip. | string | no | - |
## Secrets
| Secret | Description | Required |
| --- | --- | --- |
| `SFDX_CONSUMER_KEY` | Consumer key (client ID) of the Dev Hub connected app used for the JWT flow. | yes |
| `SFDX_JWT_SECRET_KEY` | Contents of the private server key (server.key) used for the JWT flow. | yes |
| `SFDX_USERNAME` | Username of the Dev Hub integration user to authenticate as. | yes |
## Example usage
Copy this caller into your project's `.github/workflows/` directory ([source](https://github.com/svierk/salesforce-devops-starter-kit/blob/main/examples/scratch-org-ci.yml)):
```yaml
# Example: call the reusable scratch org CI workflow.
# Copy this file into your project's .github/workflows/ directory.
name: Scratch Org CI
on:
push:
branches: [main]
pull_request:
branches: [main]
# Least-privilege token: the called workflow only reads the repository and talks
# to Salesforce. A reusable workflow can never get more permissions than the
# caller grants here.
permissions:
contents: read
jobs:
scratch-org-ci:
uses: svierk/salesforce-devops-starter-kit/.github/workflows/scratch-org-ci.yml@v1.5.0
with:
source-dir: force-app
definition-file: config/project-scratch-def.json
duration-days: '1'
test-level: RunLocalTests
# Run only a selection of Apex tests instead (implies RunSpecifiedTests):
# apex-tests: AccountServiceTest,LeadServiceTest.testConversion
# Optional dependency packages to install before deploying:
# packages: '04t...,04t...'
# Optional permissions and setup scripts applied before the tests run:
# permission-sets: 'My_App_Admin'
# apex-file-path: ./scripts/apex/setup/*.apex
secrets:
SFDX_CONSUMER_KEY: ${{ secrets.SFDX_CONSUMER_KEY }}
SFDX_JWT_SECRET_KEY: ${{ secrets.SFDX_JWT_SECRET_KEY }}
SFDX_USERNAME: ${{ secrets.SFDX_USERNAME }}
```
---
---
url: /salesforce-devops-starter-kit/workflows/deployment.md
description: >-
Reusable workflow that deploys Salesforce metadata to a target org (sandbox or
production).
---
# Deployment
Reusable workflow that deploys Salesforce metadata to a target org (sandbox or production). Supports a dry-run (validation only) mode, delta deployments and anonymous Apex post-deploy scripts.
> Reusable workflow - call it from your own repository with `uses:`. [View source](https://github.com/svierk/salesforce-devops-starter-kit/blob/main/.github/workflows/deployment.yml)
## Inputs
| Input | Description | Type | Required | Default |
| --- | --- | --- | --- | --- |
| `source-dir` | Source directory to deploy. | string | no | `force-app` |
| `target-org-alias` | Alias used for the authenticated target org. | string | no | `target-org` |
| `test-level` | Apex test level for the deployment. Ignored when apex-tests is set. | string | no | `RunLocalTests` |
| `apex-tests` | Comma-separated or multi-line list of Apex test classes to run during the deployment. A selection always runs with RunSpecifiedTests. Leave empty to run the test-level. | string | no | - |
| `dry-run` | Validate the deployment without saving the components (check-only). | boolean | no | `false` |
| `delta` | Deploy only the components changed since delta-from. | boolean | no | `false` |
| `delta-from` | Git ref to compare from when delta is enabled, e.g. a previous release tag. | string | no | - |
| `apex-file-path` | Comma-separated anonymous Apex file(s) or glob(s) to execute after a successful deployment, e.g. "./scripts/apex/post-deploy/\*.apex". Skipped on a dry-run. Leave empty to skip. | string | no | - |
| `environment` | Optional GitHub Environment to run in - enables protection rules (required reviewers) and environment-scoped secrets. Leave empty to run without one. | string | no | - |
| `ref` | Optional branch, tag or commit SHA to check out and deploy, e.g. an environment branch for a scheduled full validation, or a release tag. Leave empty to deploy the ref that triggered the workflow. | string | no | - |
## Secrets
| Secret | Description | Required |
| --- | --- | --- |
| `SFDX_CONSUMER_KEY` | Consumer key (client ID) of the connected app used for the JWT flow. | yes |
| `SFDX_JWT_SECRET_KEY` | Contents of the private server key (server.key) used for the JWT flow. | yes |
| `SFDX_USERNAME` | Username of the integration user to authenticate as. | yes |
## Example usage
Copy this caller into your project's `.github/workflows/` directory ([source](https://github.com/svierk/salesforce-devops-starter-kit/blob/main/examples/deployment.yml)):
```yaml
# Example: call the reusable deployment workflow.
# Copy this file into your project's .github/workflows/ directory.
name: Deployment
on:
push:
branches: [main]
workflow_dispatch:
# Least-privilege token: the called workflow only reads the repository and talks
# to Salesforce. A reusable workflow can never get more permissions than the
# caller grants here.
permissions:
contents: read
jobs:
deploy:
uses: svierk/salesforce-devops-starter-kit/.github/workflows/deployment.yml@v1.5.0
with:
source-dir: force-app
target-org-alias: production
test-level: RunLocalTests
# Run only a selection of Apex test classes instead (implies RunSpecifiedTests):
# apex-tests: AccountServiceTest,LeadServiceTest
dry-run: false
# Gate this deployment behind a GitHub Environment (required reviewers,
# environment-scoped secrets). Create it under Settings → Environments and
# store the production credentials there. Remove to deploy without one.
environment: production
# For an incremental deployment of changed components only:
# delta: true
# delta-from:
# Anonymous Apex post-deploy scripts (data migrations, cache clearing, ...),
# executed in alphabetical order after a successful, non-dry-run deployment:
# apex-file-path: ./scripts/apex/post-deploy/*.apex
secrets:
SFDX_CONSUMER_KEY: ${{ secrets.SFDX_CONSUMER_KEY }}
SFDX_JWT_SECRET_KEY: ${{ secrets.SFDX_JWT_SECRET_KEY }}
SFDX_USERNAME: ${{ secrets.SFDX_USERNAME }}
```
---
---
url: /salesforce-devops-starter-kit/workflows/promote.md
description: >-
Reusable workflow that prepares a promotion between long-lived branches as a
pull request.
---
# Promote
Reusable workflow that prepares a promotion between long-lived branches as a pull request: it cuts a branch from a base, merges one or more source branches into it and opens a pull request into the target. One mechanism covers story promotion, release bundles and the sync of environment branches after a release. Merge conflicts are never resolved automatically - the run fails and its summary lists the files and the commands to finish the promotion locally.
> Reusable workflow - call it from your own repository with `uses:`. [View source](https://github.com/svierk/salesforce-devops-starter-kit/blob/main/.github/workflows/promote.yml)
## Inputs
| Input | Description | Type | Required | Default |
| --- | --- | --- | --- | --- |
| `target` | Branch the pull request goes into, e.g. "uat". | string | yes | - |
| `sources` | Comma-separated or multi-line list of branches to merge, in order, e.g. "feature/story-123". Leave empty to open the pull request from the base branch itself, e.g. a release branch into main. | string | no | - |
| `base` | Branch to cut the promotion branch from. Leave empty to use the target - right for story promotion and the sync after a release. | string | no | - |
| `branch` | Name of the branch to create, e.g. "release/2026-11". Leave empty for "promotion/\-\" with one source, or "promotion/\-\" with several. | string | no | - |
| `title` | Title of the pull request. Leave empty for "Promote \ to \". | string | no | - |
## Secrets
| Secret | Description | Required |
| --- | --- | --- |
| `PROMOTION_APP_ID` | ID of a GitHub App with contents and pull request write access. Pull requests opened with the default GITHUB_TOKEN do not trigger other workflows, so without an app the pull request validation never starts on the promotion pull request. | no |
| `PROMOTION_APP_PRIVATE_KEY` | Private key of that GitHub App. | no |
## Outputs
| Output | Description |
| --- | --- |
| `pull-request` | URL of the opened pull request; empty when there was nothing to promote. |
| `branch` | Name of the branch the pull request was opened from. |
## Example usage
Copy this caller into your project's `.github/workflows/` directory ([source](https://github.com/svierk/salesforce-devops-starter-kit/blob/main/examples/promote.yml)):
```yaml
# Example: call the reusable promote workflow.
# Copy this file into your project's .github/workflows/ directory.
#
# Adds a "Run workflow" form to the Actions tab that prepares a promotion as a
# pull request - for environment-branch models with story promotion or release
# bundles (see the B2 blueprint):
#
# - story promotion: target uat, sources feature/story-123
# - release bundle: target preprod, sources = the signed-off stories,
# base main, branch release/2026-11
# then: target main, base release/2026-11, no sources
# - sync after release: target uat, sources main
name: Promote
on:
workflow_dispatch:
inputs:
target:
description: Environment branch to promote into
type: choice
options: [integration, uat, preprod, main]
sources:
description: Branches to merge, comma-separated (empty = open a pull request from base)
type: string
base:
description: Branch to start from (empty = the target)
type: string
branch:
description: Name of the branch to create (empty = promotion/-)
type: string
# The called workflow pushes the promotion branch and opens the pull request. A
# reusable workflow can never get more permissions than the caller grants here.
permissions:
contents: write
pull-requests: write
jobs:
promote:
uses: svierk/salesforce-devops-starter-kit/.github/workflows/promote.yml@v1.5.0
with:
target: ${{ inputs.target }}
sources: ${{ inputs.sources }}
base: ${{ inputs.base }}
branch: ${{ inputs.branch }}
# A GitHub App with contents and pull request write access, installed on this
# repository. Without it the pull request is opened with the GITHUB_TOKEN and
# the PR validation never starts on it.
secrets:
PROMOTION_APP_ID: ${{ secrets.PROMOTION_APP_ID }}
PROMOTION_APP_PRIVATE_KEY: ${{ secrets.PROMOTION_APP_PRIVATE_KEY }}
# To open the sync pull requests automatically after every production
# deployment, add a second workflow triggered on push to main:
#
# on:
# push:
# branches: [main]
# jobs:
# sync:
# strategy:
# fail-fast: false
# matrix:
# target: [integration, uat, preprod]
# uses: svierk/salesforce-devops-starter-kit/.github/workflows/promote.yml@v1.5.0
# with:
# target: ${{ matrix.target }}
# sources: main
# secrets: inherit
```
---
---
url: /salesforce-devops-starter-kit/blocks/get-node-version.md
description: Resolve and set up the Node.js version from package.json.
---
# 🕵🏻 Get Node Version
This repository implements a very simple GitHub composite action that pulls the Node.js version from the *package.json* file of the project and ensures that it is used for the current pipeline execution. The action relies on the Node.js version specified within the [engines](https://docs.npmjs.com/cli/v10/configuring-npm/package-json#engines) section of the *package.json*, e.g:
```json
{
"engines": {
"node": "24.x.x",
},
}
```
## Usage
### Why?
What you will often notice in Node.js projects is that you quickly get to the point where the correct version relevant to the project is defined and needs to be maintained in different places, e.g. different pipeline / workflow definitions. Ideally, however, you would like to maintain the Node.js version only in one place and thus at the same time ensure that all developers use the same version for their local development, which is also used by the CI/CD automations of the project. Exactly this is what this action enables you to do!
### How?
In a GitHub workflow, the use of the action after the initial checkout step would look like this:
```yaml
name: build
on:
push:
branches: [main]
pull_request:
branches: [main]
# Least-privilege token: this workflow only reads the repository.
permissions:
contents: read
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
persist-credentials: false # the GITHUB_TOKEN is not needed after the checkout - don't leave it in .git/config
- name: Select Node Version
uses: svierk/get-node-version@v1.6.0
```
Every `uses:` reference above is pinned to an exact release tag rather than a floating major tag, so adopting a new version is always an explicit, reviewable change of that pin.
### Inputs
| Name | Required | Default | Description |
| ----------------- | -------- | ------- | -------------------------------------------------------------------------- |
| `node-version` | no | – | Explicit Node.js version spec (e.g. `24.x`) that takes precedence over the *package.json* lookup. An empty value falls back to `engines.node`. |
| `path` | no | `./` | Path where the *package.json* file can be found. |
| `package-manager` | no | `auto` | Package manager used for dependency caching. Supported: `auto`, `npm`, `yarn`, `pnpm`. See [Package manager and dependency cache](#package-manager-and-dependency-cache). |
| `step-summary` | no | `true` | Write a result section to the GitHub Actions [job summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary). Set to `false` to avoid collisions with a custom workflow summary. |
### Outputs
| Name | Description |
| -------------- | ------------------------------------------------------------------------------------ |
| `node-version` | The Node.js version spec resolved from the `node-version` input or the `engines.node` field. |
| `source` | Where the version spec was resolved from: `node-version input` or `package.json`. |
| `package-manager` | The package manager used for the dependency cache, either set explicitly or detected with `auto`. Falls back to `npm` if nothing is detected. |
The action fails with a clear error if no `node-version` input is provided and the *package.json* or its `engines.node` field is missing, so a misconfiguration surfaces immediately instead of silently falling back to an undefined version.
### Package manager and dependency cache
By default (`package-manager: auto`), the action detects the package manager of the project and enables the matching [dependency cache](https://github.com/actions/setup-node#caching-global-packages-data) of `actions/setup-node`:
1. The [`packageManager`](https://nodejs.org/api/packages.html#packagemanager) field in the *package.json*, e.g. `"packageManager": "yarn@4.18.1"`.
2. Otherwise the lock file: *package-lock.json* or *npm-shrinkwrap.json* → `npm`, *yarn.lock* → `yarn`, *pnpm-lock.yaml* → `pnpm`.
3. If no lock file is found, Node.js is set up without a dependency cache and the action only emits a notice. This keeps projects that do not commit a lock file working.
For `yarn` and `pnpm`, the action runs `corepack enable` before setting up Node.js, so the version pinned in the `packageManager` field is used and `pnpm` does not have to be installed separately. A `pnpm` installed beforehand, e.g. via [`pnpm/action-setup`](https://github.com/pnpm/action-setup), keeps working as well. The resolved value is available as the `package-manager` output for subsequent steps:
```yaml
- name: Select Node Version
id: node
uses: svierk/get-node-version@v1.6.0
- name: Install dependencies
run: ${{ steps.node.outputs.package-manager }} install
```
Setting `package-manager` explicitly to `npm`, `yarn` or `pnpm` skips the detection. In this case the matching lock file is required and the action fails if it is missing.
### Overriding the version via repository variable
The *package.json* remains the recommended single source of truth, since it is the only place local developer tooling picks the version up from as well. However, sometimes you temporarily need to pin the pipeline to a fixed, known-good Node.js version - ideally without touching the workflow file at all. For this, wire the `node-version` input to a [repository variable](https://docs.github.com/en/actions/learn-github-actions/variables) (organization and environment variables work the same way):
```yaml
- name: Select Node Version
uses: svierk/get-node-version@v1.6.0
with:
node-version: ${{ vars.NODE_VERSION_OVERRIDE }}
```
This wiring is safe to keep in place permanently: as long as the variable is unset or blank, the expression evaluates to an empty string and the action falls back to the `engines.node` field as usual. Only when the variable is populated does it take precedence - so a temporary override becomes a pure configuration change, and removing the variable's value restores the default behaviour. The job summary and the `source` output always show which of the two sources was used.
Note that when an explicit version is provided this way, a *package.json* is no longer required for the version lookup itself. With `package-manager: auto`, the package manager is then detected from the lock file alone.
## References
The idea for the creation of this action and some further details are described in the following Medium post: [How to enforce a specific node version within a project and the associated CI/CD automations](https://medium.com/gitconnected/how-to-enforce-a-specific-node-version-within-a-project-and-the-associated-ci-cd-automations-aca9697fb9e)
## Releases
Latest release notes can be found on the [release page](https://github.com/svierk/get-node-version/releases).
## License
The scripts and documentation in this project are released under the [MIT License](https://github.com/svierk/get-node-version/blob/main/LICENSE).
***
➡️ **Full source & releases:** [svierk/get-node-version](https://github.com/svierk/get-node-version)
---
---
url: /salesforce-devops-starter-kit/blocks/sfdx-cli-setup.md
description: Install the Salesforce CLI and related plugins.
---
# ⚙️ SFDX CLI Setup
This repository implements a simple GitHub composite action for installing the Salesforce CLI and related plugins. The CLI installation is done via [npm](https://www.npmjs.com/package/@salesforce/cli).
## Usage
In a GitHub workflow, the use of the action after the initial checkout step and selecting the Node.js version to be used could look like this:
```yaml
jobs:
validation:
name: Validation
runs-on: ubuntu-latest
permissions:
contents: read # least-privilege GITHUB_TOKEN - this job only reads the repository
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
persist-credentials: false # the GITHUB_TOKEN is not needed after the checkout - don't leave it in .git/config
- name: Select Node Version
uses: svierk/get-node-version@v1.5.1
- name: Install Dependencies
run: npm ci
- name: Install SF CLI
uses: svierk/sfdx-cli-setup@v1.2.0
with:
version: 2.32.8
plugins: 'sfdx-git-delta,@salesforce/plugin-packaging'
```
Two optional parameters for the sfdx-cli-setup action can be used to set a specific CLI version if needed, in this example 2.32.8, as well as the CLI plugins to be installed, in this case sfdx-git-delta and @salesforce/plugin-packaging.
The Node.js version in this example workflow is selected by using the action [get-node-version](https://github.com/svierk/get-node-version) that automatically pulls the version to be used from the *package.json* file of the SFDX project.
### List input (comma-separated or multi-line)
The `plugins` input accepts a comma-separated string as well as a multi-line block, and the two notations can be mixed freely within one step. A comma-separated string keeps short lists on a single line, a block stays readable once a list grows:
```yaml
- name: Install SF CLI
uses: svierk/sfdx-cli-setup@v1.2.0
with:
plugins: | # multi-line block
sfdx-git-delta
@salesforce/plugin-packaging
code-analyzer@5.0.0
```
Surrounding whitespace and blank lines are ignored, so `'sfdx-git-delta, @salesforce/plugin-packaging'` is read exactly like the tightly packed `'sfdx-git-delta,@salesforce/plugin-packaging'`. Each entry is passed to `sf plugins install` as is, so a version (`code-analyzer@5.0.0`) or a git URL works as well.
The JSON array notation used up to v1.1.x, e.g. `"['sfdx-git-delta','@salesforce/plugin-packaging']"`, is still supported, so existing workflows keep working unchanged. A malformed input, such as a space-separated list or an unclosed array, fails the step with an explicit error message instead of installing something unintended.
## Inputs
| Name | Required | Default | Description |
| --------- | -------- | ------- | -------------------------------------------------------------------------------------------------- |
| `plugins` | no | | SF CLI plugins to install, comma-separated or multi-line, e.g. `@salesforce/plugin-packaging,sfdx-git-delta`. The JSON array notation `"['@salesforce/plugin-packaging','sfdx-git-delta']"` is still supported. |
| `version` | no | | SF CLI version to install, e.g. `2.32.8`. Installs the latest version if omitted. |
| `step-summary` | no | `true` | Write a result section to the GitHub Actions [job summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary). Set to `false` to avoid collisions with a custom workflow summary. |
## Overriding the version via repository variable
Installing the latest SF CLI version is the recommended default, since Salesforce ships fixes and features at a high cadence. However, sometimes you temporarily need to pin the pipeline to a fixed, known-good CLI version - ideally without touching the workflow file at all. For this, wire the `version` input to a [repository variable](https://docs.github.com/en/actions/learn-github-actions/variables) (organization and environment variables work the same way):
```yaml
- name: Install SF CLI
uses: svierk/sfdx-cli-setup@v1.2.0
with:
version: ${{ vars.SF_CLI_VERSION_OVERRIDE }}
```
This wiring is safe to keep in place permanently: as long as the variable is unset or blank, the expression evaluates to an empty string and the action installs the latest version as usual. Only when the variable is populated does it take precedence - so a temporary pin becomes a pure configuration change, and removing the variable's value restores the default behaviour. The job summary always shows the CLI version that was actually installed.
## Security & versioning
Every `uses:` reference in the snippets above is **pinned to an exact release version**, e.g. `svierk/sfdx-cli-setup@v1.2.0`. Do the same in your own pipelines:
* **Never reference a mutable ref** such as `@main` or `@v1`. It runs whatever code sits on that branch/tag at run time - with access to your org credentials - so a compromised or rewritten ref would run unnoticed.
* **Good - pin to an exact release tag** (`@v1.2.0`). Readable, concrete, and bumped through reviewed pull requests.
* **Strictest - pin to a full-length commit SHA** (`@a1b2c3d…`) with the version as a trailing comment. A SHA can never be re-pointed by the publisher; the cost is readability.
* **Enable [Dependabot](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot) for `github-actions`** in the repository that consumes this action, so those pins are bumped for you instead of silently ageing.
This action itself needs **no secrets and no `GITHUB_TOKEN` permissions** - it only installs the Salesforce CLI and its plugins. Grant the calling job the least privilege it needs (`contents: read` is enough for the example above) and use `persist-credentials: false` on the checkout so the token is not written to `.git/config` where the CLI or a plugin could pick it up.
If you pass credentials to *other* steps of the same workflow, reference them in shell steps as **environment variables** (`"$SFDX_AUTH_URL"`), never by interpolating `${{ secrets.… }}` into the script itself - that would allow command injection and can leak values into the log. The steps of this action follow that rule for all of their inputs.
## Releases
Latest release notes can be found on the [release page](https://github.com/svierk/sfdx-cli-setup/releases).
## License
The scripts and documentation in this project are released under the [MIT License](https://github.com/svierk/sfdx-cli-setup/blob/main/LICENSE).
***
➡️ **Full source & releases:** [svierk/sfdx-cli-setup](https://github.com/svierk/sfdx-cli-setup)
---
---
url: /salesforce-devops-starter-kit/blocks/sfdx-login.md
description: Authenticate to any org via SFDX Auth URL or the JWT flow.
---
# 🔐 SFDX Login
This repository implements a simple GitHub composite action that allows logging into any Salesforce org from CI/CD automations based on either a Salesforce DX (SFDX) authorization URL or using a JSON web token (JWT). Logging into an org authorizes the CLI to run other commands that connect to that org, such as deploying or retrieving a project. You can log into different types of orgs, such as sandboxes, Dev Hubs, Env Hubs, production orgs, and scratch orgs.
## Usage
### Log in to a Salesforce org using a Salesforce DX authorization URL
To be able to log in with an SFDX Auth URL, you must first generate it. The easiest option to achieve this is to redirect the output of the following command for an already authorized org to a JSON file like:
```bash
sf org display --target-org my-org --verbose --json > authFile.json
```
The resulting JSON file contains the URL in the "sfdxAuthUrl" property of the "result" object. Since we need the *authFile.json* contents for the login action, but saving raw JSON inputs in GitHub secrets is known to cause problems, we perform an additional step and encode the contents as a Base64 string to avoid headaches like:
```bash
cat authFile.json | base64
```
We then only have to store the Base64 string received in a GitHub action secret, e.g. SFDX_AUTH_URL, and can reference it whenever we are using the action in one of our workflows. A complete guide to secrets can be found here: [Using secrets in GitHub Actions](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions)
In a GitHub workflow, the use of the action after the initial checkout step and the installation of the SF CLI could then look like this:
```yaml
# Least-privilege token: this job only reads the repository.
permissions:
contents: read
jobs:
validation:
name: Validation
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
persist-credentials: false # don't leave the GITHUB_TOKEN in .git/config for later steps
- name: Install SF CLI
uses: svierk/sfdx-cli-setup@v1.1.3
- name: Salesforce Org Login
uses: svierk/sfdx-login@v1.4.2
with:
sfdx-url: ${{ secrets.SFDX_AUTH_URL }}
alias: awesome-org
```
The SF CLI in this example workflow is installed via the action [sfdx-cli-setup](https://github.com/svierk/sfdx-cli-setup).
### Log in to a Salesforce org using a JSON web token (JWT)
The JWT login flow requires a custom connected app to be created as well as a digital certificate, also called a digital signature, to sign the JWT request. You can create a self-signed certificate using OpenSSL. How to achieve this is already well documented:
* [Authorize an Org Using the JWT Flow](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth_jwt_flow.htm) | Salesforce DX Developer Guide
* [How To Use GitHub Actions, OAuth and SFDX-CLI for Continuous Integration](https://salesforcedevops.net/index.php/2022/04/05/how-to-use-github-actions-oauth-and-sfdx-cli-for-continuous-integration/) | Blog Post
The following three parameters must be passed to the login action:
1. **client-id** | OAuth client ID (consumer key) of the custom connected app
2. **jwt-secret-key** | Contents of the server.key file containing the private key
3. **username** | Username of the user logging in
In a GitHub workflow, the use of the action after the initial checkout step and the installation of the SF CLI could then look like this:
```yaml
# Least-privilege token: this job only reads the repository.
permissions:
contents: read
jobs:
validation:
name: Validation
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
persist-credentials: false # don't leave the GITHUB_TOKEN in .git/config for later steps
- name: Install SF CLI
uses: svierk/sfdx-cli-setup@v1.1.3
- name: Salesforce Org Login
uses: svierk/sfdx-login@v1.4.2
with:
client-id: ${{ secrets.SFDX_CONSUMER_KEY }}
jwt-secret-key: ${{ secrets.SFDX_JWT_SECRET_KEY }}
username: ${{ vars.SFDX_USERNAME }}
```
The SF CLI in this example workflow is installed via the action [sfdx-cli-setup](https://github.com/svierk/sfdx-cli-setup).
> **Note:** The `username` is passed as a [configuration variable](https://docs.github.com/en/actions/learn-github-actions/variables) (`vars.*`) rather than a secret. A Salesforce username is not a credential on its own — the actual secrets are the app's consumer key and the private key. GitHub automatically redacts any value that comes from `secrets.*` as `***` in **all** logs and in the job summary, so storing the username as a secret would cause the action output (and the SF CLI's own `Successfully authorized …` line) to display `***` instead of the real username. If you intentionally want the username hidden, keep it in `secrets.*` and the `***` masking is expected behaviour.
## Inputs
Provide **either** `sfdx-url` (SFDX Auth URL flow) **or** the `client-id` / `jwt-secret-key` / `username` trio (JWT flow).
| Name | Required | Default | Description |
| --------------------- | -------- | ------- | -------------------------------------------------------------------------------------------- |
| `sfdx-url` | no | | Base64 encoded SFDX authorization URL (uses the `force://` protocol) for the SFDX Auth URL flow. |
| `client-id` | no | | OAuth client ID (consumer key) of the connected app for the JWT login flow. |
| `jwt-secret-key` | no | | Contents of the `server.key` private key file for the JWT login flow. |
| `username` | no | | Username of the user logging in for the JWT login flow. |
| `set-default-dev-hub` | no | `false` | Set the authenticated org as the default Dev Hub. |
| `set-default` | no | `true` | Set the authenticated org as the default that all org-related commands run against. |
| `alias` | no | | Alias for the org. |
| `step-summary` | no | `true` | Write a result section to the GitHub Actions [job summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary). Set to `false` to avoid collisions with a custom workflow summary. |
## Outputs
After a successful login the action exposes details of the authenticated org so that follow-up steps can reference them:
| Name | Description |
| -------------- | -------------------------------------- |
| `username` | Username of the authenticated org. |
| `org-id` | ID of the authenticated org. |
| `instance-url` | Instance URL of the authenticated org. |
```yaml
- name: Salesforce Org Login
id: login
uses: svierk/sfdx-login@v1.4.2
with:
sfdx-url: ${{ secrets.SFDX_AUTH_URL }}
alias: awesome-org
# Outputs are consumed as environment variables instead of being interpolated
# into the script itself - "${{ ... }}" would be pasted into the shell verbatim.
- name: Show authenticated org
env:
ORG_USERNAME: ${{ steps.login.outputs.username }}
ORG_INSTANCE_URL: ${{ steps.login.outputs.instance-url }}
run: echo "Logged in as $ORG_USERNAME ($ORG_INSTANCE_URL)"
```
## Security
Credentials are passed to the underlying CLI commands via environment variables instead of being interpolated into the shell script, which avoids leaking them into the rendered command and prevents command injection.
The temporary `authFile.json` file created during the SFDX Auth URL flow is removed automatically when the step finishes, even if the login fails. For the JWT flow, the private key is written to a `server.key` file in the runner's temporary directory (`$RUNNER_TEMP`) with `600` permissions and is intentionally kept for the duration of the job. This is required because the Salesforce CLI stores the key's path in the auth record and re-reads it on every JWT token refresh — for example when a later step runs `sf org create scratch`, deploys metadata, or deletes the org. Removing the key right after login would cause those steps to fail with `ENOENT: no such file or directory ... server.key`. The key never persists beyond the job: GitHub-hosted runners are destroyed afterward, and `$RUNNER_TEMP` is cleared between jobs on self-hosted runners.
### Versioning
Every `uses:` reference in the examples above is **pinned to an exact release tag**, e.g. `svierk/sfdx-login@v1.4.2`. Do the same in your own pipelines:
* **Never reference a mutable ref** such as `@main` or `@v1`. It runs whatever code sits on that branch/tag at run time — with access to your org credentials — so a compromised or re-pointed ref would run unnoticed.
* **Good — pin to an exact release tag** (`@v1.4.2`). Readable, concrete, and bumped through reviewed pull requests.
* **Strictest — pin to a full-length commit SHA** with the version as a trailing comment. A SHA can never be re-pointed by the publisher; the cost is readability. Worth it for actions from publishers you don't control.
* **Enable [Dependabot](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot) for `github-actions`** in the repository that hosts your workflows so those pins are bumped for you instead of silently ageing.
### Recommendations for the calling workflow
* **Least-privilege `GITHUB_TOKEN`** — declare a `permissions:` block granting only what the job needs (`contents: read` in most cases).
* **`persist-credentials: false` on checkout** — the token is not written to `.git/config`, so later steps (SF CLI, third-party actions) cannot reuse it.
* **Reference secrets and step outputs as environment variables** (`"$ORG_USERNAME"`) in `run:` scripts, never by interpolating `${{ ... }}` into the script itself — that would allow command injection and can leak values into the log.
* **Validate pull requests with `pull_request`, never `pull_request_target`** — the latter runs with the base repository's secrets, which would let a fork execute its own code against your org.
## References
The two authorisation options supported by this GitHub composite action can be found in the Salesforce CLI Command Reference here:
* [org login sfdx-url](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference_org_commands_unified.htm#cli_reference_org_login_sfdx-url_unified)
* [org login jwt](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference_org_commands_unified.htm#cli_reference_org_login_jwt_unified)
## Releases
Latest release notes can be found on the [release page](https://github.com/svierk/sfdx-login/releases).
## License
The scripts and documentation in this project are released under the [MIT License](https://github.com/svierk/sfdx-login/blob/main/LICENSE).
***
➡️ **Full source & releases:** [svierk/sfdx-login](https://github.com/svierk/sfdx-login)
---
---
url: /salesforce-devops-starter-kit/blocks/sfdx-create-scratch-org.md
description: Create a scratch org and output its username and org ID.
---
# 🌩️ SFDX Create Scratch Org
This repository implements a simple GitHub composite action for creating Salesforce scratch orgs.
## Usage
In a GitHub workflow, the use of the action after installing the SF CLI and authorizing the respective DevHub Org could look like this:
```yaml
# Least-privilege token: the workflow only reads the repository and talks to
# Salesforce via the CLI, it never writes back to GitHub.
permissions:
contents: read
jobs:
create-scratch-org:
name: Create Scratch Org
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
fetch-depth: 0
persist-credentials: false # the GITHUB_TOKEN is not needed after the checkout - don't leave it in .git/config
- name: Select Node Version
uses: svierk/get-node-version@v1.5.1
- name: Install SF CLI
uses: svierk/sfdx-cli-setup@v1.1.3
- name: Authorize DevHub
uses: svierk/sfdx-login@v1.4.2
with:
client-id: ${{ secrets.SFDX_CONSUMER_KEY }}
jwt-secret-key: ${{ secrets.SFDX_JWT_SECRET_KEY }}
username: ${{ secrets.SFDX_USERNAME }}
set-default: true
set-default-dev-hub: true
alias: devhub
- name: Create Scratch Org
id: scratch
uses: svierk/sfdx-create-scratch-org@v1.3.1
with:
alias: scratch-${{ github.run_id }}
name: ${{ inputs.name }}
description: ${{ inputs.description }}
admin-email: ${{ inputs.email }}
set-default: true
definition-file: config/project-scratch-def.json
target-dev-hub: devhub
duration-days: ${{ inputs.lifespan }}
wait: 10
generate-password: true # hands the login password over via the job summary
- name: Package Installation
if: ${{ inputs.install-packages }}
uses: svierk/sfdx-package-installation@v1.2.1
with:
packages: ${{ inputs.packages }}
wait: 30
publish-wait: 20
- name: Deploy Metadata
if: ${{ inputs.deploy-metadata }}
env:
TARGET_ORG: ${{ steps.scratch.outputs.username }} # pass values through the environment, never interpolate them into the script
run: sf project deploy start --target-org "$TARGET_ORG"
```
A few conventions the example follows and that are worth keeping in your own workflows:
* **Pin every `uses:` to an exact release tag** (`@v1.3.1`), not to a branch like `@main` and not to a floating major like `@v1`. An update then becomes an explicit, reviewable change - ideally raised automatically by [Dependabot](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot).
* **Declare `permissions:`** at workflow or job level. Creating a scratch org needs nothing but `contents: read`.
* **Keep secrets in `secrets`/`env`.** Hand them to an action via its inputs, and pass anything dynamic into a `run:` script through `env:` instead of interpolating `${{ ... }}` directly into the shell command.
The following actions were also used in the examples on this page:
* [Get Node Version](https://github.com/svierk/get-node-version) | Pulls Node.js version to be used from the *package.json* of the project
* [SFDX CLI Setup](https://github.com/svierk/sfdx-cli-setup) | Installs the Salesforce CLI and related plugins
* [SFDX Login](https://github.com/svierk/sfdx-login) | Handles Salesforce login using a JSON web token (JWT)
* [SFDX Package Installation](https://github.com/svierk/sfdx-package-installation) | Handles Salesforce package installation on target org
* [SFDX Delete Scratch Org](https://github.com/svierk/sfdx-delete-scratch-org) | Deletes a scratch org again - the natural counterpart of this action
Of course, the create scratch org action can be used flexibly and the respective approach can vary.
## Inputs
| Name | Required | Default | Description |
| ----------------- | -------- | ------- | --------------------------------------------------------------------------------------------- |
| `target-dev-hub` | yes | | Username or alias of the Dev Hub org. |
| `alias` | no | | Alias for the scratch org. |
| `set-default` | no | | Set the scratch org as your default org. |
| `definition-file` | no | | Path to a scratch org definition file (blueprint for the scratch org). |
| `edition` | no | | Salesforce edition, e.g. `developer`, `enterprise`. Overrides the definition file. |
| `duration-days` | no | | Number of days before the org expires (1–30). |
| `wait` | no | | Number of minutes to wait for the scratch org to be ready. |
| `api-version` | no | | Override the api version used for api requests. |
| `client-id` | no | | Consumer key of the Dev Hub connected app. |
| `username` | no | | Username of the scratch org admin user. Omit to auto-generate a unique username. |
| `description` | no | | Description of the scratch org in the Dev Hub. |
| `name` | no | | Name of the org, e.g. `Acme Company`. |
| `release` | no | | Release of the scratch org relative to the Dev Hub release. |
| `admin-email` | no | | Email address applied to the org's admin user. |
| `source-org` | no | | 15-character ID of the org whose shape the new scratch org is based on. |
| `generate-password` | no | `false` | Generate a password for the scratch org admin user. The password is masked in the job log, but exposed in clear text via the job summary and the `password` output - anyone with access to the workflow run can read the summary. |
| `step-summary` | no | `true` | Write a result section to the GitHub Actions [job summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary). Set to `false` to avoid collisions with a custom workflow summary. |
## Outputs
The action exposes the details of the newly created scratch org so that follow-up steps (deploy, test, delete) can target it without guessing the generated username:
| Name | Description |
| ---------- | ------------------------------------------------------------------------ |
| `username` | Username of the created scratch org. |
| `org-id` | ID of the created scratch org. |
| `password` | Password of the scratch org admin user. Only set when `generate-password` is `true`. Registered as a secret, so it stays masked in the log of any follow-up step. |
```yaml
- name: Create Scratch Org
id: scratch
uses: svierk/sfdx-create-scratch-org@v1.3.1
with:
target-dev-hub: devhub
definition-file: config/project-scratch-def.json
duration-days: 1
- name: Deploy Metadata
env:
TARGET_ORG: ${{ steps.scratch.outputs.username }}
run: sf project deploy start --target-org "$TARGET_ORG"
- name: Delete Scratch Org
if: always() # release the org even when the deployment above failed
uses: svierk/sfdx-delete-scratch-org@v1.1.2
with:
target-org: ${{ steps.scratch.outputs.username }}
```
Pairing the `username` output with [SFDX Delete Scratch Org](https://github.com/svierk/sfdx-delete-scratch-org) and `if: always()` is the recommended cleanup pattern for CI runs: the org is released again even if a previous step failed, and no step has to guess the generated username.
## References
The scratch org creation option supported by this GitHub composite action can be found in the Salesforce CLI Command Reference here:
* [org create scratch](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference_org_commands_unified.htm#cli_reference_org_create_scratch_unified)
More details can be found in the related Salesforce DX Developer Guide: [Create Scratch Orgs](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_scratch_orgs_create.htm)
## Releases
Latest release notes can be found on the [release page](https://github.com/svierk/sfdx-create-scratch-org/releases).
## License
The scripts and documentation in this project are released under the [MIT License](https://github.com/svierk/sfdx-create-scratch-org/blob/main/LICENSE).
***
➡️ **Full source & releases:** [svierk/sfdx-create-scratch-org](https://github.com/svierk/sfdx-create-scratch-org)
---
---
url: /salesforce-devops-starter-kit/blocks/sfdx-delete-scratch-org.md
description: Delete a scratch org to clean up after a pipeline run.
---
# 🗑️ SFDX Delete Scratch Org
This repository implements a simple GitHub composite action for deleting a Salesforce scratch org. It is the natural counterpart to [sfdx-create-scratch-org](https://github.com/svierk/sfdx-create-scratch-org) and is typically used as a cleanup step so that scratch orgs created during a CI run are released again, even if previous steps fail.
## Usage
The action pairs nicely with the `username` output of the create action and an `if: always()` condition to guarantee cleanup:
```yaml
# Least-privilege token: the workflow only reads the repository and talks to
# Salesforce via the CLI, it never writes back to GitHub.
permissions:
contents: read
jobs:
scratch-org-ci:
name: Scratch Org CI
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
persist-credentials: false # the GITHUB_TOKEN is not needed after the checkout - don't leave it in .git/config
- name: Install SF CLI
uses: svierk/sfdx-cli-setup@v1.1.3
- name: Authorize DevHub
uses: svierk/sfdx-login@v1.4.2
with:
sfdx-url: ${{ secrets.SFDX_AUTH_URL }}
set-default-dev-hub: true
alias: devhub
- name: Create Scratch Org
id: scratch
uses: svierk/sfdx-create-scratch-org@v1.3.2
with:
target-dev-hub: devhub
definition-file: config/project-scratch-def.json
duration-days: 1
- name: Deploy Metadata
uses: svierk/sfdx-deploy@v1.2.2
with:
source-dir: force-app
target-org: ${{ steps.scratch.outputs.username }}
destructive-changes: false # a fresh scratch org has nothing to delete
# ... run tests, etc. ...
- name: Delete Scratch Org
if: always() # release the org even when a previous step failed
uses: svierk/sfdx-delete-scratch-org@v1.1.2
with:
target-org: ${{ steps.scratch.outputs.username }}
```
A few conventions the example follows and that are worth keeping in your own workflows:
* **Pin every `uses:` to an exact release tag** (`@v1.1.2`), not to a branch like `@main` and not to a floating major like `@v1`. An update then becomes an explicit, reviewable change - ideally raised automatically by [Dependabot](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot).
* **Declare `permissions:`** at workflow or job level. Creating and deleting a scratch org needs nothing but `contents: read`.
* **Keep secrets in `secrets`/`env`.** Hand them to an action via its inputs. Wherever you do fall back to a `run:` script, pass dynamic values in through `env:` instead of interpolating `${{ ... }}` directly into the shell command.
The following actions were also used in the example on this page:
* [SFDX CLI Setup](https://github.com/svierk/sfdx-cli-setup) | Installs the Salesforce CLI and related plugins
* [SFDX Login](https://github.com/svierk/sfdx-login) | Handles Salesforce login using a SFDX auth URL or a JSON web token (JWT)
* [SFDX Create Scratch Org](https://github.com/svierk/sfdx-create-scratch-org) | Creates the scratch org that this action deletes again - the natural counterpart of this action
* [SFDX Deploy](https://github.com/svierk/sfdx-deploy) | Deploys Salesforce metadata to a target org
## Inputs
| Name | Required | Default | Description |
| ------------ | -------- | ------- | ---------------------------------------------------------------------------- |
| `target-org` | no | | Username or alias of the scratch org to delete. Not required if the default org is set. |
| `step-summary` | no | `true` | Write a result section to the GitHub Actions [job summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary). Set to `false` to avoid collisions with a custom workflow summary. |
The action always runs with `--no-prompt`, so it never blocks the pipeline waiting for confirmation.
On success the action logs a confirmation with the deleted org and, unless `step-summary` is set to `false`, adds an entry to the [GitHub Step Summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary), so the cleanup result is visible directly in the run overview.
## References
The deletion option supported by this GitHub composite action can be found in the Salesforce CLI Command Reference here:
* [org delete scratch](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference_org_commands_unified.htm#cli_reference_org_delete_scratch_unified)
## Releases
Latest release notes can be found on the [release page](https://github.com/svierk/sfdx-delete-scratch-org/releases).
## License
The scripts and documentation in this project are released under the [MIT License](https://github.com/svierk/sfdx-delete-scratch-org/blob/main/LICENSE).
***
➡️ **Full source & releases:** [svierk/sfdx-delete-scratch-org](https://github.com/svierk/sfdx-delete-scratch-org)
---
---
url: /salesforce-devops-starter-kit/blocks/sfdx-deploy.md
description: Deploy metadata, with delta and validation-only (dry-run) modes.
---
# 🚀 SFDX Deploy
This repository implements a simple GitHub composite action for deploying Salesforce metadata to a target org. It supports source directories, manifest (`package.xml`) and metadata component selectors, configurable Apex test levels, validation-only (`--dry-run`) deployments, and optional **delta deployments** (only the components that changed between two git refs) via the [sfdx-git-delta](https://github.com/scolladon/sfdx-git-delta) plugin - which makes it equally suitable for pull request validation and for the actual deployment to higher environments.
## Usage
### Validate a deployment on pull requests (dry-run)
After installing the SF CLI and authorizing the relevant org, a check-only validation could look like this:
> Check out with `fetch-depth: 0` so metadata deletions can be detected (see [Deleting metadata](#deleting-metadata-destructive-changes)). Set `destructive-changes: false` if you don't need this - then a shallow checkout is fine.
```yaml
# Validate pull requests with `pull_request`, never `pull_request_target` - the
# latter would run fork code with this repository's org credentials.
on:
pull_request:
branches: [main]
# Least-privilege token: this pipeline only reads the repository.
permissions:
contents: read
jobs:
validation:
name: Validation
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
fetch-depth: 0
persist-credentials: false # don't leave the GITHUB_TOKEN in .git/config for later steps
- name: Install SF CLI
uses: svierk/sfdx-cli-setup@v1.1.2
- name: Salesforce Org Login
uses: svierk/sfdx-login@v1.4.2
with:
sfdx-url: ${{ secrets.SFDX_AUTH_URL }}
alias: target-org
- name: Validate Deployment
uses: svierk/sfdx-deploy@v1.3.0
with:
source-dir: force-app
target-org: target-org
test-level: RunLocalTests
dry-run: true
```
### Deploy metadata to an org
```yaml
- name: Checkout
uses: actions/checkout@v7.0.1
with:
fetch-depth: 0 # required so metadata deletions can be detected
persist-credentials: false
- name: Deploy Metadata
uses: svierk/sfdx-deploy@v1.3.0
with:
source-dir: force-app
target-org: target-org
test-level: RunLocalTests
```
### List inputs (comma-separated or multi-line)
The list inputs - `source-dir`, `metadata` and `tests` - accept a comma-separated string as well as a multi-line block, and the two notations can be mixed freely within one step. A comma-separated string keeps short lists on a single line, a block stays readable once a list grows:
```yaml
- name: Deploy Metadata
uses: svierk/sfdx-deploy@v1.3.0
with:
source-dir: 'force-app,my-other-app' # comma-separated
tests: | # multi-line block
AccountServiceTest
OpportunityServiceTest
LeadConversionTest
target-org: target-org
test-level: RunSpecifiedTests
```
Surrounding whitespace and blank lines are ignored, so `'force-app, my-other-app'` is read exactly like the tightly packed `'force-app,my-other-app'`. Whitespace inside a value is kept, e.g. for a layout like `Layout:Account-Account Layout`.
### Delta deployment (only changed components)
Delta mode uses [sfdx-git-delta](https://github.com/scolladon/sfdx-git-delta) to deploy only the components that changed between two git refs. This requires the full git history, so check out with `fetch-depth: 0`. The generated delta manifest takes precedence over `source-dir`, `manifest` and `metadata`, and component deletions are applied as post-destructive changes (see [Deleting metadata](#deleting-metadata-destructive-changes)). If no deployable changes are detected, the step succeeds without deploying.
```yaml
- name: Checkout
uses: actions/checkout@v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Deploy Changed Components
uses: svierk/sfdx-deploy@v1.3.0
with:
delta: true
delta-from: origin/${{ github.base_ref }}
delta-to: HEAD
target-org: target-org
test-level: RunLocalTests
```
### Deleting metadata (destructive changes)
A plain Salesforce source deploy is **additive** - it never deletes a component from the org just because its file was removed from the repo. This action improves on that: it detects metadata you deleted in the repository (via [sfdx-git-delta](https://github.com/scolladon/sfdx-git-delta)) and applies those deletions to the target org **after** the deployment, for both delta *and* normal full deployments. This behaviour is **on by default** (`destructive-changes: true`); set it to `false` for an additive-only deploy.
```yaml
- name: Checkout
uses: actions/checkout@v7.0.1
with:
fetch-depth: 0 # required so deletions can be detected
persist-credentials: false
- name: Deploy Metadata
uses: svierk/sfdx-deploy@v1.3.0
with:
source-dir: force-app
target-org: target-org
test-level: RunLocalTests
# destructive-changes: true # default - components deleted in the repo are deleted on the org
```
How it works:
* Deletions are derived from the git range `delta-from`..`delta-to`. In a full deployment `delta-from` defaults to the previous commit (`HEAD~1`) and `delta-to` to `HEAD`; set `delta-from` explicitly (e.g. a base branch or release tag) to compare against a different baseline.
* The full git history is required (`fetch-depth: 0`). On a shallow clone deletions cannot be detected and the action emits a warning instead of deleting.
* Everything runs as a **single, atomic deploy** (one test run): when deletions exist the action deploys via a generated `package.xml` plus the derived `destructiveChanges.xml`, since Salesforce requires post-destructive changes to be paired with a manifest.
* With `dry-run: true` the deletions are validated (check-only) but not applied.
## Inputs
| Name | Required | Default | Description |
| ------------------ | -------- | ------- | --------------------------------------------------------------------------------------------- |
| `source-dir` | no | | Source directories to deploy, comma-separated or multi-line. |
| `manifest` | no | | Path to a manifest (`package.xml`) file specifying the components to deploy. |
| `metadata` | no | | Metadata component names, comma-separated or multi-line, e.g. `ApexClass,CustomObject:Account`. |
| `destructive-changes` | no | `true` | Auto-detect metadata deleted in the repo (via sfdx-git-delta) and delete it on the org **after** the deploy. Works for full and delta deployments. Needs `fetch-depth: 0`. Set `false` for additive-only. |
| `delta` | no | `false` | Deploy only components changed between two git refs (via sfdx-git-delta). Needs `fetch-depth: 0`. |
| `delta-from` | no | | Git ref to compare from when detecting changes/deletions, e.g. `origin/main`. Required in delta mode; defaults to `HEAD~1` for destructive changes on full deployments. |
| `delta-to` | no | `HEAD` | Git ref to compare to when detecting changes/deletions. |
| `target-org` | no | | Username or alias of the target org. Not required if the default org is set. |
| `test-level` | no | | `NoTestRun`, `RunSpecifiedTests`, `RunLocalTests` or `RunAllTestsInOrg`. |
| `tests` | no | | Apex tests for the `RunSpecifiedTests` level, comma-separated or multi-line. |
| `dry-run` | no | `false` | Validate the deployment without saving components (check-only deployment). |
| `ignore-conflicts` | no | `false` | Deploy local files even if they overwrite changes in the org. |
| `ignore-warnings` | no | `false` | Allow the deployment to complete successfully despite warnings. |
| `wait` | no | `33` | Number of minutes to wait for the deployment to complete. |
| `api-version` | no | | Override the api version used for api requests, for example `59.0`. |
| `step-summary` | no | `true` | Write a result section to the GitHub Actions [job summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary). Set to `false` to avoid collisions with a custom workflow summary. |
> When the `tests` input is provided, the CLI runs the specified tests and the `test-level` input is ignored.
> If none of `source-dir`, `manifest` or `metadata` is provided (and delta mode is off), the CLI deploys the package directories defined in your `sfdx-project.json` (typically `force-app`).
## Outputs
| Name | Description |
| ----------- | --------------------------------------------------- |
| `deploy-id` | ID of the deployment job. |
| `status` | Final status of the deployment (e.g. `Succeeded`). |
The step fails if the deployment fails, while still printing the deployment result and setting the outputs.
The action prints a concise, human-readable table of the affected components (created, changed and deleted) to the log and folds a detailed, human-readable breakdown of the deployment result - including component errors, test failures and code coverage warnings - into a collapsible "Deployment details" group (or "Validation details" for dry-runs). When `step-summary` is enabled, the job summary additionally lists the deployed, modified and deleted metadata - including deletions applied via destructive changes - plus a failures table when the deployment did not succeed.
## Security & versioning
Every `uses:` reference in the snippets above is **pinned to an exact release version**, e.g. `svierk/sfdx-deploy@v1.3.0`. Do the same in your own pipelines:
* **Never reference a mutable ref** such as `@main` or `@v1`. It runs whatever code sits on that branch/tag at run time - with access to your org credentials - so a compromised or rewritten ref would run unnoticed.
* **Good - pin to an exact release tag** (`@v1.3.0`). Readable, concrete, and bumped through reviewed pull requests.
* **Strictest - pin to a full-length commit SHA** (`@a1b2c3d…`) with the version as a trailing comment. A SHA can never be re-pointed by the publisher; the cost is readability. Worth it for actions from publishers you don't control.
* **Enable [Dependabot](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot) for `github-actions`** so those pins are bumped for you instead of silently ageing. Note that Dependabot only scans `.github/workflows` and a root `action.yml` - copy-paste snippets in a README are not covered and have to be updated by hand.
This applies to **all** actions your workflows reference - this action as well as `actions/*` and any other third-party action. Latest versions at the time of writing: [`actions/checkout@v7.0.1`](https://github.com/actions/checkout/releases), [`svierk/sfdx-cli-setup@v1.1.2`](https://github.com/svierk/sfdx-cli-setup/releases), [`svierk/sfdx-login@v1.4.2`](https://github.com/svierk/sfdx-login/releases), [`svierk/sfdx-deploy@v1.3.0`](https://github.com/svierk/sfdx-deploy/releases).
Beyond pinning, a few rules are worth copying into the workflow that calls this action:
* **Least-privilege `GITHUB_TOKEN`** - declare a `permissions:` block granting only what the job needs. This action itself needs no token at all, so `contents: read` is enough for the deploy job.
* **`persist-credentials: false` on checkout** - the token is not written to `.git/config`, so later steps (SF CLI, third-party actions) cannot reuse it. The delta and destructive-change detection reads the local git history only and works fine without persisted credentials.
* **Secrets travel as secrets** - pass them straight into an action input (`sfdx-url: ${{ secrets.SFDX_AUTH_URL }}`) and reference them in your own shell steps as **environment variables** (`"$TARGET_ORG"`), never by interpolating `${{ ... }}` into the script itself - that would allow command injection and can leak values into the log. Every input of this action is handed to the shell through an `env:` mapping for exactly that reason.
* **Validate pull requests with `pull_request`, never `pull_request_target`** - the latter runs with the base repository's secrets, which would let a fork execute its own code against your org.
* **Gate production behind a [GitHub Environment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment)** - required reviewers and environment-scoped secrets keep a non-dry-run deploy from being triggered accidentally.
## References
The deployment option supported by this GitHub composite action can be found in the Salesforce CLI Command Reference here:
* [project deploy start](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference_project_commands_unified.htm#cli_reference_project_deploy_start_unified)
## Releases
Latest release notes can be found on the [release page](https://github.com/svierk/sfdx-deploy/releases).
## License
The scripts and documentation in this project are released under the [MIT License](https://github.com/svierk/sfdx-deploy/blob/main/LICENSE).
***
➡️ **Full source & releases:** [svierk/sfdx-deploy](https://github.com/svierk/sfdx-deploy)
---
---
url: /salesforce-devops-starter-kit/blocks/sfdx-run-tests.md
description: Run Apex, LWC (Jest) and Flow tests with coverage reporting.
---
# ✅ SFDX Run Tests
This repository implements a unified GitHub composite action to run Salesforce **Apex**, **LWC (Jest)** and **Flow** tests from a single step. Each test type can be toggled on or off, coverage is highlighted in the CLI logs and the GitHub step summary, and the coverage reports are written to the paths expected by quality tools like **SonarQube/SonarCloud** or **Codecov**.
Apex tests are enabled by default (they are mandatory in Salesforce); LWC and Flow tests are opt-in.
## Usage
### Run all test types and report to Sonar
This streamlines the otherwise separate, manual test steps into one action and produces the coverage reports a subsequent SonarCloud scan consumes:
```yaml
# Least-privilege token: this workflow only reads the repository.
permissions:
contents: read
jobs:
validation:
name: Validation
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
fetch-depth: 0 # required for the Sonar analysis
persist-credentials: false # the GITHUB_TOKEN is not needed after the checkout - don't leave it in .git/config
- name: Select Node Version
uses: svierk/get-node-version@v1.5.2
- name: Install Dependencies
run: npm ci --ignore-scripts
- name: Install SF CLI
uses: svierk/sfdx-cli-setup@v1.2.0
- name: Salesforce Org Login
uses: svierk/sfdx-login@v1.4.3
with:
client-id: ${{ secrets.SFDX_CONSUMER_KEY }}
jwt-secret-key: ${{ secrets.SFDX_JWT_SECRET_KEY }}
username: ${{ vars.SFDX_USERNAME }}
- name: Deploy Metadata
run: sf project deploy start
- name: Run Tests
uses: svierk/sfdx-run-tests@v1.2.0
with:
apex: true
lwc: true
flow: false
- name: SonarCloud Scan
uses: SonarSource/sonarqube-scan-action@v8.2.1
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
```
> LWC tests run locally with Jest, so make sure the Node.js dependencies are installed first (e.g. `npm install`). Apex and Flow tests run against the authenticated org, so log in (and deploy the metadata) beforehand. When Flow tests are enabled, the required `@salesforce/plugin-flow` CLI plugin is installed automatically if it is not already present.
In the workflow logs, each test run reports a human-readable result - one line per test plus a run summary - folded into a collapsible section, with a concise pass/fail line that stays visible. Test failures are always printed unfolded (including message and stack trace), so they can be inspected without expanding anything.
### Apex tests only (default)
```yaml
- name: Run Tests
uses: svierk/sfdx-run-tests@v1.2.0
with:
target-org: ci
test-level: RunLocalTests
```
### Run specific tests (RunSpecifiedTests)
To run only part of the tests, e.g. a fast smoke test on every push, select them via one of the selection inputs. They map 1:1 to the selection flags of the Salesforce CLI:
| Test type | Input | CLI flag | Values |
| --------- | ------------------ | --------------- | ---------------------------------------------- |
| Apex | `apex-class-names` | `--class-names` | Test classes, all methods of each class run |
| Apex | `apex-suite-names` | `--suite-names` | Test suites |
| Apex | `apex-tests` | `--tests` | `Class` or `Class.method` (`ns.Class.method` for namespaced tests) |
| Flow | `flow-class-names` | `--class-names` | Flow API names, all flow tests of each flow run |
| Flow | `flow-tests` | `--tests` | Individual flow tests as `FlowName.TestName` |
All selection inputs accept a comma-separated string as well as a multi-line block, and the two notations can be mixed freely. A comma-separated string keeps short lists on a single line, a block stays readable once a list grows:
```yaml
- name: Run Tests
uses: svierk/sfdx-run-tests@v1.2.0
with:
flow: true
apex-tests: | # multi-line block
AccountServiceTest
OpportunityServiceTest.testClosedWon
LeadConversionTest
flow-class-names: 'Create_Case,Update_Account' # comma-separated
```
Surrounding whitespace and blank lines are ignored, so `'Create_Case, Update_Account'` is read exactly like `'Create_Case,Update_Account'`.
How the selection interacts with `test-level`:
* A test type with a selection **always runs with `RunSpecifiedTests`**, so `test-level` does not need to be changed. This also allows mixing, e.g. a selection of Apex tests while the Flow tests keep running with `RunLocalTests`.
* Setting `test-level: RunSpecifiedTests` without a selection for an enabled test type fails the step with a clear error before any test runs.
* The CLI accepts only one kind of selection per test type, so set either `apex-class-names`, `apex-suite-names` or `apex-tests` (and either `flow-class-names` or `flow-tests`). Combining them fails the step up front as well.
> With a selection, coverage only reflects the selected tests. Keep that in mind if a subsequent quality gate (e.g. Sonar) enforces a minimum coverage.
## Inputs
| Name | Required | Default | Description |
| ------------------ | -------- | --------------------------- | --------------------------------------------------------------------------------------------- |
| `apex` | no | `true` | Run Apex tests. |
| `lwc` | no | `false` | Run LWC (Jest) unit tests. |
| `flow` | no | `false` | Run Flow tests. |
| `target-org` | no | | Username or alias of the target org (Apex and Flow). Not required if the default org is set. |
| `test-level` | no | `RunLocalTests` | Test level for Apex and Flow: `RunLocalTests`, `RunAllTestsInOrg` or `RunSpecifiedTests`. A test type with a selection always runs with `RunSpecifiedTests` (see [Run specific tests](#run-specific-tests-runspecifiedtests)). |
| `apex-class-names` | no | | Apex test classes to run, comma-separated or multi-line. |
| `apex-suite-names` | no | | Apex test suites to run, comma-separated or multi-line. |
| `apex-tests` | no | | Apex tests to run as `Class` or `Class.method`, comma-separated or multi-line. |
| `flow-class-names` | no | | Flows (API names) whose flow tests should run, comma-separated or multi-line. |
| `flow-tests` | no | | Individual flow tests to run as `FlowName.TestName`, comma-separated or multi-line. |
| `code-coverage` | no | `true` | Collect code coverage for Apex and Flow tests. |
| `apex-output-dir` | no | `./tests/apex` | Directory for Apex result and coverage files (contains `test-result-codecoverage.json`). |
| `flow-output-dir` | no | `./tests/flow` | Directory for Flow result and coverage files. |
| `lwc-test-command` | no | `npx sfdx-lwc-jest --coverage` | Command used to run the LWC Jest tests with coverage (must write an lcov report). Works on any SFDX project without a specific package.json script; override it to use your own, e.g. `npm run test:unit:coverage`. |
| `lwc-lcov-path` | no | `./coverage/lcov.info` | Path to the lcov report produced by the LWC tests, used to report overall coverage. |
| `wait` | no | `33` | Number of minutes to wait for the Apex and Flow test runs to complete. |
| `api-version` | no | | Override the api version used for Apex and Flow test api requests, e.g. `59.0`. |
| `step-summary` | no | `true` | Write a result section to the GitHub Actions [job summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary). Set to `false` to avoid collisions with a custom workflow summary. |
## Outputs
| Name | Description |
| --------------- | ------------------------------------------------------- |
| `apex-outcome` | Outcome of the Apex test run (`Passed`/`Failed`). |
| `apex-coverage` | Overall Apex code coverage. |
| `lwc-outcome` | Outcome of the LWC test run (`Passed`/`Failed`). |
| `lwc-coverage` | Overall LWC line coverage. |
| `flow-outcome` | Outcome of the Flow test run (`Passed`/`Failed`). |
| `flow-coverage` | Overall Flow code coverage. |
Outputs for a test type that was not run are empty. The step fails if any enabled test type fails, while still writing the summary and outputs. An invalid test selection (see above) fails the step before any test runs.
## Coverage reporting (Sonar / Codecov)
The action writes coverage reports to standard locations so downstream quality gates can pick them up without extra steps:
| Test type | Report | Matching Sonar property |
| --------- | ------ | ----------------------- |
| Apex | `/test-result-codecoverage.json` | `sonar.apex.coverage.reportPath` |
| LWC | `` (`./coverage/lcov.info`) | `sonar.javascript.lcov.reportPaths` |
Example `sonar-project.properties` snippet:
```properties
sonar.javascript.lcov.reportPaths=./coverage/lcov.info
sonar.apex.coverage.reportPath=./tests/apex/test-result-codecoverage.json
```
> The scan step above only needs `SONAR_TOKEN` (plus `SONAR_HOST_URL` for a self-hosted SonarQube Server). A `GITHUB_TOKEN` is **not** required: pull request decoration is done by the SonarQube Cloud GitHub App, so handing the repository token to a third-party action would widen its access for no benefit.
## 🔐 Security & versioning
Every `uses:` reference in the snippets above is **pinned to an exact release version**, e.g. `svierk/sfdx-run-tests@v1.2.0`. Do the same in your own pipelines:
* **Never reference a mutable ref** such as `@main` or `@v1`. It runs whatever code sits behind that branch/tag at run time - with access to your org credentials - so a compromised or rewritten ref would run unnoticed.
* **Good - pin to an exact release tag** (`@v1.2.0`). Readable, concrete, and bumped through reviewed pull requests. This is what this repository itself uses.
* **Strictest - pin to a full-length commit SHA** (`@a1b2c3d…`) with the version as a trailing comment. A SHA can never be re-pointed by the publisher; the cost is readability.
* **Enable [Dependabot](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot) for `github-actions`** in the repository that holds your pipelines, so those pins are bumped for you instead of silently ageing.
Beyond pinning, the same rules apply to workflows using this action:
* **Least-privilege `GITHUB_TOKEN`** - declare a `permissions:` block granting only what the workflow needs (`contents: read` for a validation pipeline).
* **`persist-credentials: false` on checkout** - the token is not written to `.git/config`, so later steps (SF CLI, third-party actions) cannot reuse it.
* **Secrets travel as secrets** - pass them into an action input or expose them as **environment variables** in shell steps (`"$SONAR_TOKEN"`), never by interpolating `${{ secrets.* }}` into the script itself - that would allow command injection and can leak values into the log. This action follows the same rule internally: every input is handed to the composite script via `env:` and read as a shell variable.
* **Validate pull requests with `pull_request`, never `pull_request_target`** - the latter runs with the base repository's secrets, which would let a fork execute its own code against your org.
## References
* [apex run test](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference_apex_commands_unified.htm#cli_reference_apex_run_test_unified) | Salesforce CLI Command Reference
* [flow run test](https://developer.salesforce.com/docs/platform/salesforce-cli-reference/guide/cli_reference_flow_run_test.html) | Salesforce CLI Command Reference
* [flow get test](https://developer.salesforce.com/docs/platform/salesforce-cli-reference/guide/cli_reference_flow_get_test.html) | Salesforce CLI Command Reference
* [sfdx-lwc-jest](https://github.com/salesforce/sfdx-lwc-jest) | Run Jest against LWC components
## Releases
Latest release notes can be found on the [release page](https://github.com/svierk/sfdx-run-tests/releases).
## License
The scripts and documentation in this project are released under the [MIT License](https://github.com/svierk/sfdx-run-tests/blob/main/LICENSE).
***
➡️ **Full source & releases:** [svierk/sfdx-run-tests](https://github.com/svierk/sfdx-run-tests)
---
---
url: /salesforce-devops-starter-kit/blocks/sfdx-code-review.md
description: 'Static quality gate: Code Analyzer, Prettier and ESLint.'
---
# 🔎 SFDX Code Review
This repository implements a unified GitHub composite action for the **static code review** of Salesforce projects. It bundles the checks that are otherwise scattered across separate CI steps into a single, configurable quality gate:
* 🛡️ **Salesforce Code Analyzer** — static analysis for Apex, LWC, Aura and more (PMD, ESLint, RetireJS, Flow, …), including [SLDS 2 checks](#slds-2-checks)
* 🎨 **Prettier** — code formatting check
* 📏 **ESLint** — JavaScript/LWC code quality, with a native report for SonarQube/SonarCloud
The Salesforce Code Analyzer is enabled by default; Prettier and ESLint are opt-in.
> **Why all three?** The Salesforce Code Analyzer does *not* check formatting (Prettier is not a static-analysis engine), and while it bundles an ESLint engine, it emits its own unified format rather than the native `eslint-report.json` that Sonar's `sonar.eslint.reportPaths` consumes. Running ESLint and Prettier here keeps everything in one gate and produces Sonar-ready reports.
## Usage
### Full code review (analyzer + formatting + linting)
```yaml
jobs:
code-review:
name: Code Review
runs-on: ubuntu-latest
permissions:
contents: read # least-privilege GITHUB_TOKEN - this job only reads the repository
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
fetch-depth: 0 # full history, required for the Sonar analysis
persist-credentials: false # the GITHUB_TOKEN is not needed after the checkout - don't leave it in .git/config
- name: Select Node Version
uses: svierk/get-node-version@v1.5.1
- name: Install Dependencies
run: npm ci --ignore-scripts
- name: Install SF CLI
uses: svierk/sfdx-cli-setup@v1.1.3
- name: Code Review
uses: svierk/sfdx-code-review@v1.1.1
with:
code-analyzer: true
prettier: true
eslint: true
workspace: force-app
severity-threshold: 3
- name: SonarCloud Scan
uses: SonarSource/sonarqube-scan-action@v8.2.1
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # SONAR_HOST_URL is additionally required for a self-hosted SonarQube Server
```
> Prettier and ESLint run locally via Node, so install the project dependencies first (e.g. `npm ci`) and make sure the project provides the matching config (`.prettierrc`, `eslint.config.js`/`.eslintrc`).
> **Applying your ESLint config to the Code Analyzer engine.** By default the Code Analyzer's bundled ESLint engine uses its own base rules and does *not* apply your workspace `eslint.config.js` — it logs a hint when it finds one (`ESLint configuration file 'eslint.config.js' was found but not applied`). This is expected: the separate ESLint check above (`eslint: true`) is what applies your project config and produces the Sonar report. If you also want your config applied *inside* the Code Analyzer's ESLint engine, this is an engine setting (no CLI flag exists), so pass a `code-analyzer.yml` via the `config-file` input:
>
> ```yaml
> engines:
> eslint:
> # either point at the config explicitly …
> eslint_config_file: eslint.config.js
> # … or let the engine discover it automatically
> auto_discover_eslint_config: true
> ```
### List inputs (comma-separated or multi-line)
The list inputs - `workspace`, `rule-selector` and `output-file` - accept a comma-separated string as well as a multi-line block, and the two notations can be mixed freely within one step. A comma-separated string keeps short lists on a single line, a block stays readable once a list grows:
```yaml
- name: Code Review
uses: svierk/sfdx-code-review@v1.1.1
with:
rule-selector: 'Security,Performance' # comma-separated
workspace: | # multi-line block
force-app
my-other-app
output-file: |
results.html
results.sarif
```
Surrounding whitespace and blank lines are ignored, so `'Security, Performance'` is read exactly like the tightly packed `'Security,Performance'`. Whitespace inside a value is kept, e.g. for a path like `force-app/main/my folder`.
### Uploading Code Analyzer results to GitHub code scanning
When producing a SARIF file, let the analyzer report findings without failing the job immediately, then upload the SARIF. Uploading to code scanning requires the job to additionally grant `security-events: write`:
```yaml
permissions:
contents: read
security-events: write # required to upload the SARIF file to GitHub code scanning
steps:
- name: Code Review
continue-on-error: true
uses: svierk/sfdx-code-review@v1.1.1
with:
workspace: force-app
output-file: results.sarif
severity-threshold: 3
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v4.37.7
with:
sarif_file: results.sarif
```
The Code Analyzer only writes a SARIF run for each engine with violations. The action completes every `.sarif` output file with an empty run for each engine that ran without violations, so the upload also succeeds when no violations are found, and code scanning closes the alerts of engines that no longer report any.
### SLDS 2 checks
Since version 5.4.0, the ESLint engine of the Code Analyzer bundles the rules of the [SLDS Linter](https://www.npmjs.com/package/@salesforce-ux/slds-linter). The action therefore checks the styles and markup of Lightning Web Components and Aura components for SLDS 2 without an extra step. The rules carry the tags `SLDS` and `Recommended`, so the default `rule-selector` already runs them on `.css` and `.html`/`.cmp` files; `rule-selector: eslint:SLDS` runs them on their own.
| Finding | Rule | Default severity |
| ---------------------------------------------------------- | ------------------------------ | ---------------- |
| Retired SLDS 1 design tokens (`--lwc-*`) | `lwc-token-to-slds-hook` | Moderate (3) |
| Deprecated SLDS classes | `no-deprecated-classes-slds2` | Moderate (3) |
| Styling hooks without a fallback value | `no-slds-var-without-fallback` | Moderate (3) |
| Retired SLDS class names, e.g. `slds-icon--small` | `enforce-bem-usage` | Low (4) |
| Hard-coded values that have a matching SLDS 2 styling hook | `no-hardcoded-values-slds2` | Low (4) |
With `severity-threshold: 3`, retired tokens fail the step, while hard-coded values are only reported - whether a value should become a styling hook is often a judgement call, e.g. for layout sizes. To fail on the retired class names as well, raise their severity in a `code-analyzer.yml` passed via `config-file`:
```yaml
rules:
eslint:
"@salesforce-ux/slds/enforce-bem-usage":
severity: Moderate
```
> **SLDS checks next to the separate ESLint check.** With `eslint: true`, JavaScript is already linted with your project config, and the engine's JavaScript and LWC base rules would only report the same files a second time. Instead of turning the engine off (`disable_engine: true`), which also drops the SLDS rules, keep it on with only its SLDS base config:
>
> ```yaml
> engines:
> eslint:
> disable_javascript_base_config: true
> disable_lwc_base_config: true
> disable_typescript_base_config: true
> disable_react_base_config: true
> ```
A `plugin-version` older than 5.4.0 has no SLDS rules.
## Inputs
| Name | Required | Default | Description |
| -------------------- | -------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
| `code-analyzer` | no | `true` | Run the Salesforce Code Analyzer. |
| `prettier` | no | `false` | Run a Prettier formatting check. |
| `eslint` | no | `false` | Run ESLint and produce a report (e.g. for Sonar). |
| `workspace` | no | | Files/folders for the Code Analyzer, comma-separated or multi-line. Defaults to the current directory. |
| `rule-selector` | no | `Recommended` | Code Analyzer rule selectors, comma-separated or multi-line, e.g. `Security,Performance`. |
| `config-file` | no | | Path to a Code Analyzer configuration file (`code-analyzer.yml`). |
| `output-file` | no | | Code Analyzer output files, comma-separated or multi-line; format derived from the extension (`.sarif`, `.html`, `.json`, …). |
| `severity-threshold` | no | | Fail if the Code Analyzer finds a violation with this severity or higher (`1`–`5` / `Critical`–`Info`). |
| `install-plugin` | no | `true` | Install the Code Analyzer plugin before running. Set to `false` if installed via sfdx-cli-setup. |
| `plugin-version` | no | | Specific version of the `code-analyzer` plugin to install, e.g. `5.0.0`. |
| `prettier-command` | no | `npx prettier --check .` | Command used to check formatting. Override to use your own, e.g. `npm run prettier:verify`. |
| `eslint-command` | no | `npx eslint . --format json --output-file eslint-report.json` | Command used to run ESLint and write its report. Override to use your own, e.g. `npm run lint:sonar`. |
| `eslint-report-path` | no | `./eslint-report.json` | Path to the ESLint JSON report, used to report error/warning counts. |
| `step-summary` | no | `true` | Write a result section to the GitHub Actions [job summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary). Set to `false` to avoid collisions with a custom workflow summary. |
## Outputs
| Name | Description |
| ----------------------- | ------------------------------------------------------- |
| `code-analyzer-outcome` | Outcome of the Code Analyzer run (`Passed`/`Failed`). |
| `prettier-outcome` | Outcome of the Prettier check (`Passed`/`Failed`). |
| `eslint-outcome` | Outcome of the ESLint run (`Passed`/`Failed`). |
| `eslint-errors` | Number of ESLint errors. |
| `eslint-warnings` | Number of ESLint warnings. |
Outputs for a check that was not run are empty. The step fails if any enabled check fails, while still writing the summary and outputs.
## Sonar integration
ESLint writes its native JSON report so Sonar can pick it up directly:
```properties
sonar.eslint.reportPaths=./eslint-report.json
```
The Salesforce Code Analyzer can additionally emit a SARIF/JSON file via `output-file` for GitHub code scanning or other tools.
The scan step itself only needs `SONAR_TOKEN` as an environment variable (plus `SONAR_HOST_URL` for a self-hosted SonarQube Server). A `GITHUB_TOKEN` is **not** required: pull request decoration is performed by the SonarQube Cloud GitHub App, not by the scanner, so passing the token would hand a credential to a third-party action for no gain.
## Security & versioning
Every `uses:` reference in the snippets above is **pinned to an exact release version**, e.g. `svierk/sfdx-code-review@v1.1.1`. Do the same in your own pipelines:
* **Never reference a mutable ref** such as `@main`, `@master` or `@v1`. It runs whatever code sits on that branch/tag at run time — with access to your repository and org credentials — so a compromised or rewritten ref would run unnoticed.
* **Good — pin to an exact release tag** (`@v1.0.0`). Readable, concrete, and bumped through reviewed pull requests.
* **Strictest — pin to a full-length commit SHA** (`@a1b2c3d…`) with the version as a trailing comment. A SHA can never be re-pointed by the publisher; the cost is readability.
* **Enable [Dependabot](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot) for `github-actions`** in the repository that consumes this action, so those pins are bumped for you instead of silently ageing. Note that Dependabot only scans `.github/workflows` — copy-paste snippets in documentation have to be updated by hand.
This action itself needs **no secrets and no `GITHUB_TOKEN` permissions** — it only runs the Salesforce Code Analyzer, Prettier and ESLint against the checked-out sources. Grant the calling job the least privilege it needs (`contents: read` is enough, plus `security-events: write` when uploading SARIF) and use `persist-credentials: false` on the checkout so the token is not written to `.git/config`, where the CLI, a plugin or an ESLint config could pick it up.
If you pass credentials to *other* steps of the same workflow, reference them as **environment variables** (`env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}`, then `"$SONAR_TOKEN"` in the script), never by interpolating `${{ secrets.… }}` into a `run:` script itself — that would allow command injection and can leak values into the log. The steps of this action follow that rule for all of their inputs: every input is passed through `env:` and referenced as a shell variable, so a value such as `eslint-command` or `workspace` can never be expanded into the script body.
## References
* [Salesforce Code Analyzer](https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview) | Documentation
* [code-analyzer run](https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/references/code-analyzer-command-reference) | Command Reference
* [Prettier](https://prettier.io/docs/en/cli.html) | CLI
* [ESLint](https://eslint.org/docs/latest/use/command-line-interface) | CLI
* [SLDS Linter](https://www.npmjs.com/package/@salesforce-ux/slds-linter) | Rules of the SLDS 2 checks
## Releases
Latest release notes can be found on the [release page](https://github.com/svierk/sfdx-code-review/releases).
## License
The scripts and documentation in this project are released under the [MIT License](https://github.com/svierk/sfdx-code-review/blob/main/LICENSE).
***
➡️ **Full source & releases:** [svierk/sfdx-code-review](https://github.com/svierk/sfdx-code-review)
---
---
url: /salesforce-devops-starter-kit/blocks/sfdx-package-installation.md
description: Install managed or unlocked packages on a target org.
---
# 📦 SFDX Package Installation
This repository implements a simple GitHub composite action for installing packages on a specific Salesforce target org.
## Usage
After installing the SF CLI and authorizing the relevant org in your GitHub workflow, packages can be installed using this action as follows:
```yaml
jobs:
validation:
name: Validation
runs-on: ubuntu-latest
permissions:
contents: read # least privilege - the installation itself needs no write access
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
persist-credentials: false # the GITHUB_TOKEN is not needed after the checkout - don't leave it in .git/config
- name: Select Node Version
uses: svierk/get-node-version@v1.5.2
- name: Install Dependencies
run: npm ci
- name: Install SF CLI
uses: svierk/sfdx-cli-setup@v1.2.0
- name: Salesforce Org Login
uses: svierk/sfdx-login@v1.4.3
with:
sfdx-url: ${{ secrets.SFDX_AUTH_URL }}
- name: Package Installation
uses: svierk/sfdx-package-installation@v1.3.0
with:
packages: '04t6S000001UjutQAC,04t3y000000X0OaAAK'
wait: 30
publish-wait: 20
```
Every action above is pinned to an exact release tag instead of a moving reference like `@main`, so a workflow run is reproducible and an update is always an explicit, reviewable change.
### List input (comma-separated or multi-line)
The `packages` input accepts a comma-separated string as well as a multi-line block, and the two notations can be mixed freely within one step. A comma-separated string keeps short lists on a single line, a block stays readable once a list grows:
```yaml
- name: Package Installation
uses: svierk/sfdx-package-installation@v1.3.0
with:
packages: | # multi-line block
04t6S000001UjutQAC
04t3y000000X0OaAAK
MyPackage@1.2.0-1
```
Surrounding whitespace and blank lines are ignored, so `'04t6S000001UjutQAC, 04t3y000000X0OaAAK'` is read exactly like the tightly packed `'04t6S000001UjutQAC,04t3y000000X0OaAAK'`. Packages are installed one after another in the given order, so list dependencies before the packages that need them.
The JSON array notation used up to v1.2.x, e.g. `"['04t6S000001UjutQAC','04t3y000000X0OaAAK']"`, is still supported, so existing workflows keep working unchanged. A malformed input, such as an unclosed array, fails the step with an explicit error message instead of installing something unintended.
If the packages you want to install are key-protected, you can simply append the required installation key for each package with an underscore, e.g. `04t6S000001UjutQAC_INSTALLATIONKEY`. Since an installation key is a credential, keep it in a [GitHub Actions secret](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions) rather than in the workflow file:
```yaml
- name: Package Installation
uses: svierk/sfdx-package-installation@v1.3.0
with:
packages: |
04t6S000001UjutQAC_${{ secrets.SFDX_PACKAGE_KEY }}
04t3y000000X0OaAAK
```
The action masks every installation key in the job log and never prints it, so keys stay hidden even if they are passed as a literal value. Since commas and line breaks separate the list entries, an installation key must not contain either of them.
The following actions were also used in the example workflow to create the prerequisites for the package installation:
* [Get Node Version](https://github.com/svierk/get-node-version) | Pulls Node.js version to be used from the *package.json* of the project
* [SFDX CLI Setup](https://github.com/svierk/sfdx-cli-setup) | Installs the Salesforce CLI and related plugins
* [SFDX Login](https://github.com/svierk/sfdx-login) | Handles Salesforce login using a Salesforce DX authorization URL
Of course, the package installation action can be used flexibly and the respective approach can vary.
## Inputs
| Name | Required | Default | Description |
| -------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------- |
| `packages` | yes | | Packages to install as a comma-separated or multi-line list of aliases or IDs, e.g. `04t...,04t...`. Append `_` to a package for key-protected installs. The JSON array notation `"['04t...','04t...']"` is still supported. |
| `target-org` | no | | Username or alias of the target org. Not required if the default org is set. |
| `api-version` | no | | Override the api version used for api requests, e.g. `59.0`. |
| `wait` | no | `0` | Number of minutes to wait for installation status. |
| `publish-wait` | no | `0` | Maximum minutes to wait for the Subscriber Package Version ID to become available before canceling. |
| `step-summary` | no | `true` | Write a result section to the GitHub Actions [job summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary). Set to `false` to avoid collisions with a custom workflow summary. |
## Releases
Latest release notes can be found on the [release page](https://github.com/svierk/sfdx-package-installation/releases).
## License
The scripts and documentation in this project are released under the [MIT License](https://github.com/svierk/sfdx-package-installation/blob/main/LICENSE).
***
➡️ **Full source & releases:** [svierk/sfdx-package-installation](https://github.com/svierk/sfdx-package-installation)
---
---
url: /salesforce-devops-starter-kit/blocks/sfdx-data-import.md
description: Import records via Bulk API 2.0 (CSV) or sObject Tree (JSON).
---
# 💾 SFDX Data Import
This repository implements a simple GitHub composite action for uploading records to any kind of Salesforce org from within CI/CD automations based on CSV (Bulk API 2.0) or JSON (sObject Tree) format.
## Usage
After installing the SF CLI and authorizing the relevant org in your GitHub workflow, data can be imported using this action as follows:
```yaml
jobs:
validation:
name: Validation
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
persist-credentials: false
- name: Select Node Version
uses: svierk/get-node-version@v1.5.2
- name: Install Dependencies
run: npm ci
- name: Install SF CLI
uses: svierk/sfdx-cli-setup@v1.2.0
- name: Salesforce Org Login
uses: svierk/sfdx-login@v1.4.3
with:
sfdx-url: ${{ secrets.SFDX_AUTH_URL }}
alias: awesome-org
- name: CSV Data Import
uses: svierk/sfdx-data-import@v1.4.0
with:
file-path: './data/accounts.csv'
object-type: 'Account'
external-id: 'Id'
target-org: awesome-org
- name: JSON Data Import
uses: svierk/sfdx-data-import@v1.4.0
with:
file-path: './data/accounts.json,./data/contacts.json'
api-version: '59.0'
```
The following actions were also used in the example workflow to create the prerequisites for the data import:
* [Get Node Version](https://github.com/svierk/get-node-version) | Pulls Node.js version to be used from the *package.json* of the project
* [SFDX CLI Setup](https://github.com/svierk/sfdx-cli-setup) | Installs the Salesforce CLI and related plugins
* [SFDX Login](https://github.com/svierk/sfdx-login) | Handles Salesforce login using a Salesforce DX authorization URL
The file format is detected automatically from the file extension:
* `.csv` files are uploaded via Bulk API 2.0 and require the `object-type` input (the `external-id` defaults to `Id`).
* `.json` files are imported via the sObject Tree API and may be passed as an in-order list of files (see below).
Any other extension, a mix of extensions, more than one CSV file, or a CSV upload without `object-type` fails the step with a clear error instead of silently doing nothing.
Of course, the data import action can be used flexibly and the respective approach can vary.
### List input (comma-separated or multi-line)
The `file-path` input accepts a comma-separated string as well as a multi-line block, and the two notations can be mixed freely within one step. A comma-separated string keeps short lists on a single line, a block stays readable once a list grows:
```yaml
- name: JSON Data Import
uses: svierk/sfdx-data-import@v1.4.0
with:
file-path: | # multi-line block
./data/accounts.json
./data/contacts.json
./data/opportunities.json
```
The files are imported in the given order, so list parent records before the child records that reference them. Surrounding whitespace and blank lines are ignored, so `'./data/accounts.json, ./data/contacts.json'` is read exactly like the tightly packed `'./data/accounts.json,./data/contacts.json'`. Whitespace inside a path is kept, e.g. for `./my data/accounts.json`.
## Inputs
| Name | Required | Default | Description |
| ------------- | -------- | ------- | -------------------------------------------------------------------------------------------------- |
| `file-path` | yes | | Path to the CSV or JSON file(s) to upload. CSV supports a single file; JSON supports an in-order list, comma-separated or multi-line. |
| `object-type` | no | | API name of the Salesforce object. Required for CSV (Bulk API 2.0) uploads. |
| `external-id` | no | `Id` | Name of the external ID field (or `Id`). Used for CSV (Bulk API 2.0) uploads. |
| `target-org` | no | | Username or alias of the target org. Not required if the default org is set. |
| `api-version` | no | | Override the api version used for api requests, e.g. `59.0`. |
| `step-summary` | no | `true` | Write a result section to the GitHub Actions [job summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary). Set to `false` to avoid collisions with a custom workflow summary. |
## Security & versioning
Every `uses:` reference in the snippet above is **pinned to an exact release tag**, e.g. `svierk/sfdx-data-import@v1.4.0`. Do the same in your own pipelines:
* **Never reference a mutable ref** such as `@main` or `@v1`. It runs whatever code sits on that branch/tag at run time - with access to your org credentials - so a compromised or rewritten ref would run unnoticed.
* **Good - pin to an exact release tag** (`@v1.4.0`). Readable, concrete, and bumped through reviewed pull requests.
* **Strictest - pin to a full-length commit SHA** (`@a1b2c3d…`) with the version as a trailing comment. A SHA can never be re-pointed by the publisher; the cost is readability. Worth it for actions from publishers you don't control.
* **Enable [Dependabot](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot) for `github-actions`** in the repository that hosts the workflow, so those pins are bumped for you instead of silently ageing.
This applies to **all** actions your workflow references - this action as well as `actions/*` and any other third-party action.
Beyond pinning, a few rules are worth following in the workflow that calls this action:
* **Secrets travel as secrets** - hand the SFDX Auth URL straight into the `sfdx-url` input of [sfdx-login](https://github.com/svierk/sfdx-login) as shown above. Never interpolate a secret into a `run:` script via `${{ ... }}` - that allows command injection and can leak the value into the log. This action follows the same rule internally: every input is exposed to the shell as an **environment variable** and referenced as `"$FILE_PATH"`, `"$TARGET_ORG"` and so on.
* **Least-privilege `GITHUB_TOKEN`** - declare a `permissions:` block granting only what the job needs. A data import needs no write access at all, so `contents: read` is enough.
* **`persist-credentials: false` on checkout** - the token is not written to `.git/config`, so later steps (SF CLI, third-party actions) cannot reuse it.
* **Validate pull requests with `pull_request`, never `pull_request_target`** - the latter runs with the base repository's secrets, which would let a fork import its own data into your org.
## References
The data import options supported by this GitHub composite action can be found in the Salesforce CLI Command Reference here:
* [data import tree](https://developer.salesforce.com/docs/platform/salesforce-cli-reference/guide/cli_reference_data_import_tree.html) (JSON format)
* [data upsert bulk](https://developer.salesforce.com/docs/platform/salesforce-cli-reference/guide/cli_reference_data_upsert_bulk.html) (CSV format)
## Releases
Latest release notes can be found on the [release page](https://github.com/svierk/sfdx-data-import/releases).
## License
The scripts and documentation in this project are released under the [MIT License](https://github.com/svierk/sfdx-data-import/blob/main/LICENSE).
***
➡️ **Full source & releases:** [svierk/sfdx-data-import](https://github.com/svierk/sfdx-data-import)
---
---
url: /salesforce-devops-starter-kit/blocks/sfdx-assign-permissions.md
description: Assign permission sets, groups and licenses to org users.
---
# 🛡️ SFDX Assign Permissions
This repository implements a simple GitHub composite action for assigning permission sets, permission set groups and permission set licenses to Salesforce org users from within CI/CD automations. It is the missing step right after a deployment: a freshly created scratch org contains the metadata, but the admin user has none of the permissions the deployed permission sets are supposed to grant.
## Usage
After installing the SF CLI, authorizing the relevant org and deploying the metadata, the permissions can be assigned as follows:
```yaml
# Least-privilege token: the workflow only reads the repository and talks to
# Salesforce via the CLI, it never writes back to GitHub.
permissions:
contents: read
jobs:
scratch-org-ci:
name: Scratch Org CI
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
persist-credentials: false # the GITHUB_TOKEN is not needed after the checkout - don't leave it in .git/config
- name: Install SF CLI
uses: svierk/sfdx-cli-setup@v1.1.3
- name: Authorize DevHub
uses: svierk/sfdx-login@v1.4.3
with:
sfdx-url: ${{ secrets.SFDX_AUTH_URL }}
set-default-dev-hub: true
alias: devhub
- name: Create Scratch Org
id: scratch
uses: svierk/sfdx-create-scratch-org@v1.3.2
with:
target-dev-hub: devhub
definition-file: config/project-scratch-def.json
duration-days: 1
- name: Deploy Metadata
uses: svierk/sfdx-deploy@v1.2.2
with:
source-dir: force-app
target-org: ${{ steps.scratch.outputs.username }}
destructive-changes: false # a fresh scratch org has nothing to delete
- name: Assign Permissions
uses: svierk/sfdx-assign-permissions@v1.0.0
with:
permission-sets: 'DreamHouse,CloudHouse'
permission-set-groups: 'Sales_Manager'
target-org: ${{ steps.scratch.outputs.username }}
# ... run tests, etc. ...
- name: Delete Scratch Org
if: always() # release the org even when a previous step failed
uses: svierk/sfdx-delete-scratch-org@v1.1.3
with:
target-org: ${{ steps.scratch.outputs.username }}
```
All four list inputs - `permission-sets`, `permission-set-groups`, `permission-set-licenses` and `on-behalf-of` - accept a comma-separated string as well as a multi-line block, and the two notations can be mixed freely within one step. A comma-separated string keeps short lists on a single line, a block stays readable once a list grows:
```yaml
- name: Assign Permissions
uses: svierk/sfdx-assign-permissions@v1.0.0
with:
permission-set-licenses: 'SalesforceCPQ' # a single value is simply a one-element list
permission-set-groups: 'Sales_Manager,Service_Agent' # comma-separated
permission-sets: | # multi-line block
DreamHouse
CloudHouse
mypkg__Package_Admin
on-behalf-of: 'integration-user@example.com,test-user@example.com'
target-org: awesome-org
```
Surrounding whitespace and blank lines are ignored, so `'DreamHouse, CloudHouse'` is read exactly like the tightly packed `'DreamHouse,CloudHouse'`. Every permission in the example above is assigned to every user in `on-behalf-of`.
The following actions were also used in the example on this page:
* [SFDX CLI Setup](https://github.com/svierk/sfdx-cli-setup) | Installs the Salesforce CLI and related plugins
* [SFDX Login](https://github.com/svierk/sfdx-login) | Handles Salesforce login using a SFDX auth URL or a JSON web token (JWT)
* [SFDX Create Scratch Org](https://github.com/svierk/sfdx-create-scratch-org) | Creates the scratch org the permissions are assigned in
* [SFDX Deploy](https://github.com/svierk/sfdx-deploy) | Deploys the metadata that contains the permission sets and groups
* [SFDX Delete Scratch Org](https://github.com/svierk/sfdx-delete-scratch-org) | Releases the scratch org again at the end of the run
## Inputs
| Name | Required | Default | Description |
| ---- | -------- | ------- | ----------- |
| `permission-sets` | no | | Permission sets to assign, comma-separated or multi-line. Packaged permission sets can be referenced as `namespace__PermissionSet`. |
| `permission-set-groups` | no | | Permission set groups to assign, comma-separated or multi-line, referenced by their API name (`DeveloperName`). |
| `permission-set-licenses` | no | | Permission set licenses to assign, comma-separated or multi-line, referenced by their API name (`DeveloperName`), e.g. `SalesforceCPQ`. |
| `on-behalf-of` | no | | Org users that receive the permissions, comma-separated or multi-line. Defaults to the user the target org is authorized as - the admin user in a typical scratch org run. |
| `target-org` | no | | Username or alias of the target org. Not required if the default org is set. |
| `api-version` | no | | Override the api version used for api requests, e.g. `59.0`. |
| `wait` | no | `5` | Minutes to wait for permission set groups to finish recalculating before they are assigned. Set to `0` to assign them immediately. |
| `ignore-existing` | no | `true` | Treat "already assigned" as success, so re-runs stay green. Set to `false` to let duplicate assignments fail the step. |
| `step-summary` | no | `true` | Write a result section to the GitHub Actions [job summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary). Set to `false` to avoid collisions with a custom workflow summary. |
At least one of `permission-sets`, `permission-set-groups` or `permission-set-licenses` must be provided - an invocation that would assign nothing fails the step instead of silently passing.
## Assignment order
The three types are assigned in a fixed order, because they build on each other:
1. **Permission set licenses** - a permission set can only grant what the user is licensed for, so the licenses are assigned first.
2. **Permission sets** - assigned via `sf org assign permset`.
3. **Permission set groups** - assigned last, after the groups have finished recalculating.
## Permission set groups
The Salesforce CLI has no command for permission set groups. `sf org assign permset` resolves every name against the `PermissionSet` object and fails with *"Permission set … not found in target org"* when handed the API name of a group. This action therefore resolves groups itself: it looks up the `PermissionSetGroup` record via SOQL and creates the `PermissionSetAssignment` record through `sf data create record`.
Two things follow from that, and both are handled by the action:
* **Recalculation.** A group that was just deployed is recalculated asynchronously and only grants its permissions once its `Status` is `Updated`. Assigning it while it is still `Updating` or `Outdated` can leave the user without the very permissions the pipeline just deployed. The action polls the status for up to `wait` minutes, aborts early with a clear error if the recalculation ends in `Failed`, and - if the group is still not ready when the wait elapses - emits a warning and assigns it anyway rather than failing the pipeline on the timeout alone.
* **User resolution.** Because the assignment record is created directly, the assignee has to be resolved to a `User` record. Values passed to `on-behalf-of` are treated the same way the CLI treats them: a value matching a CLI alias is resolved to its username, anything else is assumed to be a username already. Do not use the `Alias` field of the `User` object. Without `on-behalf-of`, the user the target org is authorized as is used.
## Re-runs and idempotency
Assigning a permission a user already has makes Salesforce return a `duplicate value found` error. The CLI swallows this for permission set licenses, but not for permission sets - which makes any re-run against a long-lived sandbox or dev org fail on the second attempt.
With the default `ignore-existing: true` the action closes that gap: existing permission set group assignments are detected up front and skipped, and duplicate errors reported by `sf org assign permset` are downgraded to a notice. Every other failure - a permission set that does not exist in the org, a missing license, an authorization problem - still fails the step. Set `ignore-existing: false` to get the raw CLI behaviour back.
## Security & versioning
Every `uses:` reference in the snippets above is **pinned to an exact release tag**, e.g. `svierk/sfdx-assign-permissions@v1.0.0`. Do the same in your own pipelines:
* **Never reference a mutable ref** such as `@main` or `@v1`. It runs whatever code sits on that branch/tag at run time - with access to your org credentials - so a compromised or rewritten ref would run unnoticed.
* **Good - pin to an exact release tag** (`@v1.0.0`). Readable, concrete, and bumped through reviewed pull requests.
* **Strictest - pin to a full-length commit SHA** (`@a1b2c3d…`) with the version as a trailing comment. A SHA can never be re-pointed by the publisher; the cost is readability. Worth it for actions from publishers you don't control.
* **Enable [Dependabot](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot) for `github-actions`** in the repository that hosts the workflow, so those pins are bumped for you instead of silently ageing.
Beyond pinning, a few rules are worth following in the workflow that calls this action:
* **Secrets travel as secrets** - hand the SFDX Auth URL straight into the `sfdx-url` input of [sfdx-login](https://github.com/svierk/sfdx-login). Never interpolate a secret into a `run:` script via `${{ ... }}` - that allows command injection and can leak the value into the log. This action follows the same rule internally: every input is exposed to the shell as an **environment variable** and referenced as `"$PERMISSION_SETS"`, `"$TARGET_ORG"` and so on. Values that end up in a SOQL query are escaped before they are interpolated.
* **Least-privilege `GITHUB_TOKEN`** - declare a `permissions:` block granting only what the job needs. Assigning permissions needs no write access at all, so `contents: read` is enough.
* **`persist-credentials: false` on checkout** - the token is not written to `.git/config`, so later steps (SF CLI, third-party actions) cannot reuse it.
* **Validate pull requests with `pull_request`, never `pull_request_target`** - the latter runs with the base repository's secrets, which would let a fork grant itself permissions in your org.
Resolving the default assignee reads the org details, whose payload contains a live access token. The token is registered as a masked secret before anything is written to the log.
## References
The assignment options supported by this GitHub composite action can be found in the Salesforce CLI Command Reference here:
* [org assign permset](https://developer.salesforce.com/docs/platform/salesforce-cli-reference/guide/cli_reference_org_assign_permset.html)
* [org assign permsetlicense](https://developer.salesforce.com/docs/platform/salesforce-cli-reference/guide/cli_reference_org_assign_permsetlicense.html)
* [data create record](https://developer.salesforce.com/docs/platform/salesforce-cli-reference/guide/cli_reference_data_create_record.html) (used for permission set groups)
* [data query](https://developer.salesforce.com/docs/platform/salesforce-cli-reference/guide/cli_reference_data_query.html)
Background on the objects involved:
* [PermissionSetAssignment](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_permissionsetassignment.htm)
* [PermissionSetGroup](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_permissionsetgroup.htm)
## Releases
Latest release notes can be found on the [release page](https://github.com/svierk/sfdx-assign-permissions/releases).
## License
The scripts and documentation in this project are released under the [MIT License](https://github.com/svierk/sfdx-assign-permissions/blob/main/LICENSE).
***
➡️ **Full source & releases:** [svierk/sfdx-assign-permissions](https://github.com/svierk/sfdx-assign-permissions)
---
---
url: /salesforce-devops-starter-kit/blocks/sfdx-apex-execute.md
description: Run anonymous Apex for post-deploy scripts and data migrations.
---
# 📜 SFDX Apex Execute
This repository implements a GitHub composite action for executing **anonymous Apex** against any kind of Salesforce org from within CI/CD automations - the piece almost every pipeline needs at some point: **post-deploy scripts**, **data migrations**, **cache invalidation**, feature-flag toggles or one-off fixes.
Scripts are executed **in the given order**, the `System.debug` output is surfaced directly in the workflow log, and a failing script fails the step with its compile problem or runtime exception instead of a silent green build.
## Usage
### Run post-deploy scripts after a deployment
```yaml
# Least-privilege token: this pipeline only reads the repository.
permissions:
contents: read
jobs:
deployment:
name: Deployment
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
persist-credentials: false
- name: Install SF CLI
uses: svierk/sfdx-cli-setup@v1.1.2
- name: Salesforce Org Login
uses: svierk/sfdx-login@v1.4.2
with:
client-id: ${{ secrets.SFDX_CONSUMER_KEY }}
jwt-secret-key: ${{ secrets.SFDX_JWT_SECRET_KEY }}
username: ${{ secrets.SFDX_USERNAME }}
alias: awesome-org
- name: Deploy Metadata
uses: svierk/sfdx-deploy@v1.2.2
with:
source-dir: force-app
target-org: awesome-org
- name: Post-Deploy Scripts
uses: svierk/sfdx-apex-execute@v1.0.0
with:
file-path: './scripts/apex/post-deploy/*.apex'
target-org: awesome-org
```
The glob is expanded and executed in alphabetical order, so a folder of numbered scripts (`01-seed-settings.apex`, `02-migrate-accounts.apex`, ...) runs exactly in the order you named them.
### Run a single script or an inline snippet
```yaml
- name: Data Migration
uses: svierk/sfdx-apex-execute@v1.0.0
with:
file-path: './scripts/apex/migrate-accounts.apex'
target-org: awesome-org
- name: Invalidate Platform Cache
uses: svierk/sfdx-apex-execute@v1.0.0
with:
apex-code: |
Cache.Org.getPartition('local.Config').removeAll();
System.debug('Org cache partition cleared');
target-org: awesome-org
```
### Seed a scratch org and keep the debug logs
```yaml
- name: Seed Scratch Org
id: seed
uses: svierk/sfdx-apex-execute@v1.0.0
with:
file-path: './scripts/apex/setup/*.apex,./scripts/apex/seed-data.apex'
target-org: ${{ steps.scratch-org.outputs.username }}
fail-fast: false
log-output: full
output-dir: ./logs/apex
- name: Upload Apex Logs
if: always()
uses: actions/upload-artifact@v7.0.1
with:
name: apex-logs
path: ./logs/apex
```
With `fail-fast: false` every script is executed even if an earlier one failed, and the step fails afterwards with the complete picture - useful for seeding, where you want to see all problems in one run rather than one per pipeline attempt.
The following actions were also used in the example workflows to create the prerequisites for the Apex execution:
* [SFDX CLI Setup](https://github.com/svierk/sfdx-cli-setup) | Installs the Salesforce CLI and related plugins
* [SFDX Login](https://github.com/svierk/sfdx-login) | Handles Salesforce login via SFDX Auth URL or the JWT flow
* [SFDX Deploy](https://github.com/svierk/sfdx-deploy) | Deploys Salesforce metadata to a target org
Of course, the Apex execute action can be used flexibly and the respective approach can vary.
## Inputs
| Name | Required | Default | Description |
| -------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `file-path` | no\* | | Comma-separated list of files containing the anonymous Apex to execute, in the given order. Each entry may also be a glob pattern (e.g. `./scripts/apex/*.apex`), expanded and executed in alphabetical order. An entry matching no file fails the step. |
| `apex-code` | no\* | | Anonymous Apex to execute inline. If `file-path` is set as well, the files are executed first and the inline snippet last. |
| `target-org` | no | | Username or alias of the target org. Not required if the default org is set. |
| `api-version` | no | | Override the api version used for api requests, e.g. `59.0`. |
| `fail-fast` | no | `true` | Stop at the first failing script. Set to `false` to execute all scripts and fail the step afterwards. |
| `log-output` | no | `debug` | How much of the debug log to print for a **successful** execution: `debug` (only the `System.debug` output), `full` (the complete log in a collapsible group) or `none`. A failing execution always prints the error and the complete log. |
| `output-dir` | no | | Directory to write the debug log of each execution to, e.g. `./logs/apex` to upload them as a build artifact. Disabled when empty. |
| `step-summary` | no | `true` | Write a result section to the GitHub Actions [job summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary). Set to `false` to avoid collisions with a custom workflow summary. |
\* At least one of `file-path` and `apex-code` is required - the step fails if neither is provided.
## Outputs
| Name | Description |
| --------------- | ------------------------------------------------------------------ |
| `success` | `true` when every executed script succeeded, otherwise `false`. |
| `scripts-run` | Number of scripts that were executed. |
| `failed-script` | The first script that failed, empty if all succeeded. |
## Writing scripts for CI
Anonymous Apex in a pipeline runs unattended, so a few habits are worth adopting:
* **Make the scripts idempotent.** A deployment can be re-run, a workflow can be retried. Guard the changes (`if (![SELECT COUNT() FROM Setting__c] > 0) { ... }`) instead of assuming a clean org.
* **Mind the governor limits.** Anonymous Apex runs synchronously and is bound by the normal limits - 10.000 DML rows, 50.000 query rows. For larger volumes, kick off a Batch Apex job from the script, or use [sfdx-data-import](https://github.com/svierk/sfdx-data-import) for pure record loads.
* **Log what happened.** `System.debug` output is printed straight into the workflow log with the default `log-output: debug`, which is what makes a post-deploy script auditable after the fact.
* **Fail loudly.** Throw an exception (or `System.assert`) when a precondition is not met. The step then fails with the exception message and the full debug log, instead of reporting a green deployment on a half-migrated org.
* **Keep secrets out of the scripts.** If a script needs a credential, read it from a protected Custom Setting or Named Credential in the org - never interpolate a GitHub secret into the Apex source.
## Security & versioning
Every `uses:` reference in the snippets above is **pinned to an exact release tag**, e.g. `svierk/sfdx-apex-execute@v1.0.0`. Do the same in your own pipelines:
* **Never reference a mutable ref** such as `@main` or `@v1`. It runs whatever code sits behind that branch/tag at run time - with access to your org credentials - so a compromised or rewritten ref would run unnoticed.
* **Good - pin to an exact release tag** (`@v1.0.0`). Readable, concrete, and bumped through reviewed pull requests.
* **Strictest - pin to a full-length commit SHA** (`@a1b2c3d…`) with the version as a trailing comment. A SHA can never be re-pointed by the publisher; the cost is readability. Worth it for actions from publishers you don't control.
* **Enable [Dependabot](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot) for `github-actions`** in the repository that holds your pipelines, so those pins are bumped for you instead of silently ageing.
This applies to **all** actions your workflow references - this action as well as `actions/*` and any other third-party action.
Anonymous Apex runs arbitrary code against a real org, so the rules around the calling workflow matter even more than usual:
* **Validate pull requests with `pull_request`, never `pull_request_target`** - the latter runs with the base repository's secrets, which would let a fork execute *its own* Apex against your org. This is the single most important rule for this action.
* **Gate production behind a [GitHub Environment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment)** with required reviewers, so a script that touches production data is approved by a human first.
* **Secrets travel as secrets** - hand credentials into the inputs of [sfdx-login](https://github.com/svierk/sfdx-login) as shown above. Never interpolate a secret into a `run:` script via `${{ ... }}` - that allows command injection and can leak the value into the log. This action follows the same rule internally: every input is exposed to the shell as an **environment variable** and referenced as `"$FILE_PATH"`, `"$TARGET_ORG"` and so on, and the `apex-code` input is written to a file rather than interpolated into a command line.
* **Least-privilege `GITHUB_TOKEN`** - declare a `permissions:` block granting only what the job needs. Executing Apex needs no write access at all, so `contents: read` is enough.
* **`persist-credentials: false` on checkout** - the token is not written to `.git/config`, so later steps (SF CLI, third-party actions) cannot reuse it.
## References
The Apex execution used by this GitHub composite action can be found in the Salesforce CLI Command Reference here:
* [apex run](https://developer.salesforce.com/docs/platform/salesforce-cli-reference/guide/cli_reference_apex_run.html) | Execute anonymous Apex code from the command line or a local file
* [Anonymous Blocks](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_anonymous_block.htm) | Apex Developer Guide
## Releases
Latest release notes can be found on the [release page](https://github.com/svierk/sfdx-apex-execute/releases).
## License
The scripts and documentation in this project are released under the [MIT License](https://github.com/svierk/sfdx-apex-execute/blob/main/LICENSE).
***
➡️ **Full source & releases:** [svierk/sfdx-apex-execute](https://github.com/svierk/sfdx-apex-execute)