@bearingpointsalesforce/cicd-builder
v0.9.3
Published
Reusable Salesforce CI/CD step functions and pipeline runner.
Readme
CI/CD Builder
A shared Node.js library that provides reusable Salesforce CI/CD pipeline steps as plain async functions, driven by JSON
configuration files. Replace complex bash scripts and other approaches that are not clear. Its dependency is node, which
is required for SF CLI as well. If you use the loadData step, the SFDMU plugin must also be installed
(sf plugins install sfdmu).
1. Quick Start
# Install dependencies
npm install
# Run a pipeline
node src/bin.js <pipeline.json> <scratchOrgAlias>
# Examples
node src/bin.js config/pipeline.validate.json myScratchOrg
node src/bin.js config/pipeline.scratch.json newDevOrgThe scratch org alias is required. If the org already exists, the createOrg step will be skipped. The dev hub is
always taken from the sf CLI default configuration (sf config set target-dev-hub=yourHub).
2. Pipeline Files
A pipeline file defines an ordered sequence of steps to execute. It is a JSON file placed in the config/ directory
alongside ciconfig.json.
2.1 Structure
{
"pipeline": "pipelineName",
"steps": [
{ "type": "stepType", "option": "value" },
{ "type": "anotherStep" }
]
}| Property | Required | Description |
|------------|----------|-------------------------------------------|
| pipeline | Yes | Name of the pipeline (used in log output) |
| steps | Yes | Ordered array of step objects to execute |
Each step must have a type property. Additional properties are step-specific options.
2.2 Available Step Types
| Step Type | Description |
|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| createOrg | Creates a new scratch org using the default dev hub. If an org with the given alias already exists, creation is skipped. |
| deleteOrg | Deletes the scratch org. |
| printOrgUrl | Prints the org login URL without opening a browser. |
| executeApex | Executes anonymous apex scripts for a named phase defined in ciconfig.json. |
| runTests | Runs apex tests in the org. By default runs in parallel with automatic synchronous rerun of failures. |
| installPackages | Installs packages defined in ciconfig.packageIds. |
| deploySource | Deploys source metadata to the org. |
| sortMetadata | Sorts metadata XML files (profiles, permission sets, muting permission sets, permission set groups) for consistent formatting. |
| assignPermSets | Assigns permission sets from ciconfig.permSets to the default org user. Already assigned permission sets are skipped. |
| assignPermSetGroups | Assigns permission set groups from ciconfig.permSetGroups to the default org user. Polls for PermissionSetGroup.Status = 'Updated' before assigning (up to 10 attempts, 7.5s interval). |
| assignProfile | Assigns a profile from ciconfig.profile to the default org user. Uses a temporary System Administrator user as a workaround because Salesforce does not allow users to change their own profile. |
| loadData | Loads data into the scratch org from CSV files using the SFDMU plugin. |
| displayLimits | Displays the org's API limits. |
2.2.1 createOrg
Creates a new scratch org using the default dev hub. If an org with the given alias already exists, creation is skipped.
{
"type": "createOrg",
"definitionFile": "config/project-scratch-def.json",
"ci": true
}| Option | Type | Default | Description |
|---------------------|---------|-----------------------------------|---------------------------------------------------------------------------------|
| definitionFile | string | config/project-scratch-def.json | Path to the scratch org definition file |
| durationDays | number | 30 | Number of days before the scratch org expires |
| wait | number | 60 | Minutes to wait for org creation to complete |
| ci | boolean | false | CI mode: merges features/settings into definition file, suppresses manual steps |
| validateOrg | boolean | false | Run apex verification script after creation, retry if corrupted |
| validationTimeout | number | 30 | Seconds to wait before running org validation |
2.2.2 deleteOrg
Deletes the scratch org.
{
"type": "deleteOrg"
}No options. Uses the scratch org alias from the CLI argument.
2.2.3 printOrgUrl
Prints the org login URL without opening a browser.
{
"type": "printOrgUrl"
}No options.
2.2.4 executeApex
Executes anonymous apex scripts for a named phase defined in ciconfig.json.
{
"type": "executeApex",
"phase": "setup"
}| Option | Type | Required | Description |
|---------|--------|----------|---------------------------------------------------------------------|
| phase | string | Yes | Key in ciconfig.apexPhases (e.g. setup, postDeploy, verify) |
2.2.5 runTests
Runs apex tests in the org. By default, tests run in parallel. Any failed test classes are automatically rerun
synchronously to catch flaky failures caused by parallel execution. Set parallel to false to run all tests
synchronously in a single run.
{
"type": "runTests",
"testLevel": "RunLocalTests",
"parallel": true
}| Option | Type | Default | Description |
|-------------|---------|-----------------|----------------------------------------------------------------------------------------------------------|
| testLevel | string | RunLocalTests | RunLocalTests or RunAllTestsInOrg |
| parallel | boolean | true | Run tests in parallel with automatic synchronous rerun of failures. Set to false for synchronous only. |
2.2.6 installPackages
Installs packages defined in ciconfig.packageIds.
{
"type": "installPackages"
}| Option | Type | Default | Description |
|----------------|--------|--------------|----------------------------|
| securityType | string | AdminsOnly | AdminsOnly or AllUsers |
2.2.7 deploySource
Deploys source metadata to the org.
{
"type": "deploySource",
"sourceDir": "force-app",
"ci": true
}| Option | Type | Default | Description |
|-------------|---------|---------|-----------------------------------------------------------------------|
| sourceDir | string | — | Source directory to deploy (e.g. force-app) |
| checkOnly | boolean | false | Validate only, do not deploy |
| ci | boolean | false | Append ciconfig.ciForceignore paths to .forceignore before deploy |
2.2.8 sortMetadata
Sorts metadata XML files (profiles, permission sets, muting permission sets, permission set groups) for consistent formatting.
{
"type": "sortMetadata",
"directory": "force-app/main/default"
}| Option | Type | Default | Description |
|-------------|--------|--------------------------|-------------------------|
| directory | string | force-app/main/default | Root metadata directory |
2.2.9 assignPermSets
Assigns permission sets from ciconfig.permSets to the default org user. Already assigned permission sets are skipped.
{
"type": "assignPermSets"
}No options. Reads from ciconfig.permSets.
2.2.10 assignPermSetGroups
Assigns permission set groups from ciconfig.permSetGroups to the default org user. Polls for
PermissionSetGroup.Status = 'Updated' before assigning (up to 10 attempts, 7.5s interval).
{
"type": "assignPermSetGroups"
}No options. Reads from ciconfig.permSetGroups.
2.2.11 assignProfile
Assigns a profile from ciconfig.profile to the default org user. Uses a temporary System Administrator user as a
workaround because Salesforce does not allow users to change their own profile.
{
"type": "assignProfile"
}No options. Reads from ciconfig.profile.
2.2.12 loadData
Loads data into the scratch org from CSV files using the SFDMU SF CLI plugin. The data
directory must contain an export.json configuration file and CSV files named after sObject API names (e.g.
Account.csv, Contact.csv). SFDMU handles relationship resolution, lookup matching, and bulk operations
automatically.
Requires the SFDMU plugin: sf plugins install sfdmu
{
"type": "loadData",
"path": "data"
}| Option | Type | Default | Description |
|--------|--------|---------|-------------------------------------------------------|
| path | string | data | Directory containing export.json and CSV data files |
2.2.13 displayLimits
Displays the org's API limits.
{
"type": "displayLimits"
}No options.
2.3 Example Pipelines
2.3.1 Validation pipeline — create scratch org, deploy, test, delete:
{
"pipeline": "validate",
"steps": [
{ "type": "createOrg", "definitionFile": "config/project-scratch-def.json", "durationDays": 1, "ci": true },
{ "type": "installPackages" },
{ "type": "deploySource", "sourceDir": "force-app", "ci": true },
{ "type": "runTests", "testLevel": "RunLocalTests" },
{ "type": "deleteOrg" }
]
}2.3.2 Scratch org setup pipeline — full setup for development:
{
"pipeline": "createScratch",
"steps": [
{
"type": "createOrg",
"definitionFile": "config/project-scratch-def.json",
"ci": true,
"validateOrg": true
},
{
"type": "installPackages"
},
{
"type": "deploySource",
"sourceDir": "force-app"
},
{
"type": "assignPermSets"
},
{
"type": "assignPermSetGroups"
},
{
"type": "assignProfile"
},
{
"type": "loadData"
}
]
}3. Configuration File (ciconfig.json)
The ciconfig.json file lives in the config/ directory alongside the pipeline files. It holds project-specific
settings consumed by the pipeline steps.
3.1 Full Example
{
"projectName": "my-project",
"packageIds": [
{ "id": "04t5p000000Gey6AAC", "name": "DLRS", "versionNumber": "x.y" },
{ "id": "04t1n000002NZ6gAAG" }
],
"apexPhases": {
"setup": ["scripts/apex/enableMarketingUser.apex"],
"postDeploy": ["scripts/apex/insertCustomSettings.apex"],
"verify": ["scripts/apex/verifyOrgFeatures.apex"]
},
"profile": "Custom_Sales",
"permSetGroups": ["Integration_Managers", "Standard_User_Access"],
"permSets": ["Custom_Edit_Permissions"],
"defaultUserFields": {
"Country": "Czechia",
"City": "Prague",
"TimeZoneSidKey": "Europe/Prague"
},
"ciForceignore": [
"Admin.profile",
"src/main/default/entitlementProcesses/basic_sla.entitlementProcess-meta.xml"
],
"postCreateManualSteps": {
"apexOrgVerification": "scripts/apex/verifyOrgFeatures.apex",
"continueInstruction": "Please continue by running 'npm run sfci:init:postcreate'.",
"features": [
{
"name": "ContactsToMultipleAccounts",
"manualStepDescription": "Enable 'Allow users to relate a contact to multiple accounts' in Setup > Account Settings."
}
],
"settings": {
"accountSettings": {
"enableRelateContactToMultipleAccounts": true
}
}
}
}3.2 Properties Overview
| Property | Type | Required | Description |
|-------------------------|------------------|---------------------------------|--------------------------------------------------------------------------------------------|
| projectName | string | Yes | A human-readable name for your Salesforce project, used in log output during org creation. |
| packageIds | array of objects | Yes (for installPackages) | List of managed/unlocked packages to install into the scratch org. |
| apexPhases | object | Yes (for executeApex) | Map of phase name to array of apex script file paths, executed by executeApex steps. |
| profile | string | Yes (for assignProfile) | Salesforce profile name to assign to the default scratch org user. |
| permSetGroups | array of strings | Yes (for assignPermSetGroups) | Permission set group developer names to assign to the default org user. |
| permSets | array of strings | Yes (for assignPermSets) | Permission set API names to assign to the default org user. |
| defaultUserFields | object | No | User field values to set on the scratch org admin user after creation. |
| ciForceignore | array of strings | No | Additional paths to append to .forceignore before deployment in CI mode. |
| postCreateManualSteps | object | No | Configuration for post-creation behaviour: CI auto-merge and local manual step display. |
3.3 Properties Reference
3.3.1 projectName
| | |
|--------------|-------------|
| Type | string |
| Required | Yes |
| Used by | createOrg |
A human-readable name for your Salesforce project. This is used to identify the project during org creation and will appear in log output. The pipeline will fail early if this is missing, so make sure to set it before running any pipeline.
3.3.2 packageIds
| | |
|--------------|----------------------------------|
| Type | array of objects |
| Required | Yes (for installPackages step) |
| Used by | installPackages |
The list of managed or unlocked packages that need to be installed into the scratch org before you can work with it. Think of this as your project's package dependencies — things like DLRS, CPQ, or any custom packages your org relies on.
Each entry must have an id (the package version ID starting with 04t). You can optionally add name and
versionNumber for clearer log output, but they don't affect installation.
| Attribute | Description | Example |
|-----------------|------------------------------------------------------------------|------------------------|
| id | Package version ID (required). Must start with 04t. | "04t5p000000Gey6AAC" |
| name | Human-readable package name (optional). Used in log output only. | "DLRS" |
| versionNumber | Package version number (optional). Used in log output only. | "x.y" |
{
"packageIds": [
{
"id": "04tXXXXXXXXXXXXXXX",
"name": "Package Name",
"versionNumber": "1.0"
},
{
"id": "04tYYYYYYYYYYYYYYY"
}
]
}3.3.3 apexPhases
| | |
|--------------|---------------------------------------------------|
| Type | object (map of phase name to array of file paths) |
| Required | Yes (for executeApex step) |
| Used by | executeApex |
Defines groups of anonymous apex scripts that should run at different points during the pipeline. Each phase is a named
collection of scripts — you choose when they run by placing executeApex steps in your pipeline and referencing the
phase name.
Common phases include:
- setup — scripts that need to run before anything is deployed (e.g. enabling features, setting up users)
- postDeploy — scripts that depend on your deployed metadata (e.g. inserting custom settings, test data)
- verify — scripts that validate the org is configured correctly after everything is done
You can define as many phases as you need with any names you want.
| Attribute | Description | Example |
|---------------|--------------------------------------------------------------------------------|------------------------------------------------|
| <phaseName> | Array of file paths to anonymous apex scripts. The key is any name you choose. | "setup": ["scripts/apex/enableFeature.apex"] |
{
"apexPhases": {
"setup": [
"scripts/apex/enableFeature.apex"
],
"postDeploy": [
"scripts/apex/insertData.apex",
"scripts/apex/configureOrg.apex"
],
"verify": [
"scripts/apex/verifyFeatures.apex"
]
}
}3.3.4 profile
| | |
|--------------|--------------------------------|
| Type | string |
| Required | Yes (for assignProfile step) |
| Used by | assignProfile |
The name of the Salesforce profile to assign to the default scratch org user. By default, scratch org users get the System Administrator profile. If your project requires developers to work under a different profile (e.g. a custom sales profile), specify it here.
Note: Salesforce doesn't allow users to change their own profile, so the tool creates a temporary admin user behind the scenes to perform the switch.
3.3.5 permSetGroups
| | |
|--------------|--------------------------------------|
| Type | array of strings |
| Required | Yes (for assignPermSetGroups step) |
| Used by | assignPermSetGroups |
List of permission set group developer names to assign to the default scratch org user. These are the API names you see in Setup, not the labels.
The tool automatically waits for each permission set group to reach Status = 'Updated' before assigning it, since
Salesforce needs time to calculate the combined permissions after a deployment.
3.3.6 permSets
| | |
|--------------|---------------------------------|
| Type | array of strings |
| Required | Yes (for assignPermSets step) |
| Used by | assignPermSets |
List of permission set names to assign to the default scratch org user. These are the API names of the permission sets.
The tool checks which permission sets are already assigned and skips them, so it's safe to re-run the pipeline without causing duplicate assignment errors.
3.3.7 defaultUserFields
| | |
|--------------|-------------|
| Type | object |
| Required | No |
| Used by | createOrg |
Salesforce user field values to set on the scratch org admin user right after creation. This prevents common errors like
FIELD_INTEGRITY_EXCEPTION on the Country field, which happens when the default user has no country set and certain
features expect one.
If this property is absent, no user fields are updated. Add any standard User field name as a key with the desired value.
| Attribute | Description | Example |
|------------------|-------------------------------------------------------------------------|------------------------|
| <fieldName> | Any standard Salesforce User field name as key, with the desired value. | "Country": "Czechia" |
| Country | Country of the scratch org user. | "Czechia" |
| City | City of the scratch org user. | "Prague" |
| TimeZoneSidKey | Timezone identifier for the scratch org user. | "Europe/Prague" |
{
"defaultUserFields": {
"Country": "Czechia",
"City": "Prague",
"TimeZoneSidKey": "Europe/Prague"
}
}3.3.8 ciForceignore
| | |
|--------------|----------------------------------|
| Type | array of strings |
| Required | No |
| Used by | deploySource (when ci: true) |
Additional paths to append to .forceignore before deployment when running in CI mode. This is useful for metadata that
exists in your repository but causes deployment failures in scratch orgs — for example, certain profiles or org-specific
configurations.
These paths are only added when the deploySource step has "ci": true. When developers run locally without the ci
flag, their .forceignore stays untouched.
3.3.9 postCreateManualSteps
| | |
|--------------|-------------|
| Type | object |
| Required | No |
| Used by | createOrg |
Configuration for what happens after a scratch org is created. This serves two purposes:
In CI mode (
ci: true): Thefeaturesnames andsettingsare automatically merged into the scratch org definition file before creation, so the org gets the right configuration without manual intervention.For local development (no
ciflag): The tool prints a coloured box with manual steps the developer needs to perform in the org's Setup UI, since some features can't be enabled through the definition file alone.
| Attribute | Description | Example |
|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------|
| apexOrgVerification | Path to an apex script that checks whether all required features were enabled correctly. Used when validateOrg: true — if the script detects errors, the org is deleted and re-created automatically. | "scripts/apex/verifyOrgFeatures.apex" |
| continueInstruction | A message shown to developers after org creation, typically telling them what command to run next. Only shown outside CI mode. | "Please continue by running 'npm run sfci:init:postcreate'." |
| features | List of features that need special handling. Each entry has a name (merged into the definition file in CI) and a manualStepDescription (shown to developers locally). | See sub-attributes below. |
| settings | Salesforce settings to merge into the scratch org definition file in CI mode. Uses the same structure as settings in the definition file itself. | {"accountSettings": {"enableRelateContactToMultipleAccounts": true}} |
Each entry in features has:
| Attribute | Description | Example |
|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|
| name | Salesforce feature name — automatically added to the scratch org definition file when running in CI mode. | "ContactsToMultipleAccounts" |
| manualStepDescription | Human-readable instructions for the developer on how to enable this feature manually in the org's Setup UI. Only shown when running locally (non-CI). | "Enable 'Allow users to relate a contact to multiple accounts' in Setup > Account Settings." |
4. CI Integration
Install the package in your project, then call the CLI in your pipeline. CI stays thin — one command per pipeline:
# GitHub Actions
- run: npm install @bearingpointsalesforce/cicd-builder
- run: npx sf-cicd-builder config/pipeline.validate.json ci-${{ github.run_id }}# GitLab CI
script:
- npm install @bearingpointsalesforce/cicd-builder
- npx sf-cicd-builder config/pipeline.validate.json ci-$CI_PIPELINE_ID# Azure DevOps
- script: npm install @bearingpointsalesforce/cicd-builder
- script: npx sf-cicd-builder config/pipeline.validate.json ci-$(Build.BuildId)5. Consuming as a Dependency
Add the package to your project:
npm install @bearingpointsalesforce/cicd-builderThen run pipelines from the command line:
# Run a full pipeline
npx sf-cicd-builder config/pipeline.validate.json myScratchOrg
# Different pipeline for scratch org setup
npx sf-cicd-builder config/pipeline.scratch.json newDevOrg