Your pipeline.
No org required.
Your Java team has Codecov badges that just work. Your Python team has pytest publishing JUnit XML to every PR. Apex developers deserve the same - and now they can have it, without a connected Salesforce org anywhere in the pipeline.
For a working GitHub Actions + GitLab walkthrough, see Apex CI without a connected org.
What org-based CI actually costs you
A Salesforce CI pipeline that uses an org isn't just slow - it's fragile by design. It requires a JWT certificate, a connected DevHub, an available scratch org from a finite pool, and a network connection. Any one of those can fail, and when they do, it's never obvious why.
Every CI run burns a scratch org slot. Every scratch org expires. When the pool exhausts during a release crunch - exactly when you need CI most - you're blocked. Meanwhile, your team is pushing to main and hoping tests pass in production.
Org-based CI pipeline
# What you're replacing
steps:
- name: Authenticate to DevHub
# Requires: JWT cert in secrets, connected app
run: sf org login jwt --client-id $CLIENT_ID ...
- name: Create scratch org
# Burns a slot from your finite pool
# Fails silently if pool is exhausted
run: sf org create scratch --definition-file ...
- name: Deploy source
# 4-8 minutes, every run
run: sf project deploy start ...
- name: Run tests
# Another 3-5 minutes
run: sf apex run test ...
- name: Delete scratch org
# If the above crashed, this doesn't run
run: sf org delete scratch ...Nimbus CI pipeline
# What you get instead
steps:
- uses: actions/checkout@v4
- name: Install Nimbus
run: curl -fsSL https://install.testnimbus.dev | sh
- name: Run tests
run: nimbus test
--coverage --coverage-report coverage.xml
--results-xml results.xml
env:
NIMBUS_LICENSE_KEY: ${{ secrets.NIMBUS_LICENSE_KEY }}
NIMBUS_PROFILE: ciWhat Nimbus produces
Nimbus outputs standard CI formats - the same ones your Java, Python, and Go pipelines already produce. Drop them into GitHub Actions, SonarQube, Codecov, or Jenkins without any custom integration work.
Test results for CI dashboards
PR annotations, test trend tracking, failure history. Works with GitHub Actions, GitLab CI, Jenkins, CircleCI.
--results-xml results.xmlCoverage for SonarQube & Codecov
Industry-standard coverage format. Plug straight into SonarQube, Codecov, GitHub Actions coverage gates.
--coverage-report coverage.xmlVisual coverage report
Browse coverage line-by-line in your browser. Share with the team as a CI artifact.
--coverage-report coverage.htmlCustom tooling & dashboards
Machine-readable coverage data. Build your own dashboards or integrate with internal tooling.
--coverage-report coverage.jsonGitHub Actions
The minimal setup: install Nimbus, run tests, done. No service containers, no external dependencies. Nimbus starts its own embedded PostgreSQL and tears it down when the job finishes.
For teams: add coverage and JUnit XML output to get PR annotations, coverage trend tracking, and SonarQube/Codecov integration in the same job.
For faster CI on large projects: use a Postgres service container with DATABASE_URL - removes the embedded DB startup time and lets you pre-warm the schema.
name: Apex Tests
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Nimbus
run: curl -fsSL https://install.testnimbus.dev | sh
- name: Run tests
run: |
nimbus test \
--coverage --coverage-report coverage.xml \
--results-xml results.xml
env:
NIMBUS_LICENSE_KEY: ${{ secrets.NIMBUS_LICENSE_KEY }}
NIMBUS_PROFILE: ci
- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: results.xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}With external Postgres (faster for large projects)
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: nimbus_ci
POSTGRES_USER: nimbus
POSTGRES_PASSWORD: nimbus
ports: ['5432:5432']
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Install Nimbus
run: curl -fsSL https://install.testnimbus.dev | sh
- name: Run tests
run: nimbus test --coverage --coverage-report coverage.xml
env:
DATABASE_URL: postgres://nimbus:nimbus@localhost:5432/nimbus_ci
NIMBUS_LICENSE_KEY: ${{ secrets.NIMBUS_LICENSE_KEY }}
NIMBUS_PROFILE: ciGitLab CI
GitLab CI supports JUnit XML natively via artifacts:reports:junit - test results appear directly in merge request widgets without any plugin.
Cobertura XML integrates with GitLab's built-in coverage visualization, showing diff coverage directly on merge request diffs.
apex-tests:
image: ubuntu:22.04
script:
- curl -fsSL https://install.testnimbus.dev | sh
- nimbus test
--coverage --coverage-report coverage.xml
--results-xml results.xml
variables:
NIMBUS_LICENSE_KEY: $NIMBUS_LICENSE_KEY
NIMBUS_PROFILE: ci
artifacts:
when: always
reports:
junit: results.xml
coverage_report:
coverage_format: cobertura
path: coverage.xmlSonarQube & SonarCloud
SonarQube and SonarCloud consume Cobertura XML for coverage and can parse JUnit XML for test result history. Point both at Nimbus output and your Apex project gets the same quality gate treatment as the rest of your codebase.
Configure sonar.apex.coverage.reportPaths in your sonar-project.properties. The Cobertura report maps line coverage to each class file so SonarQube can display coverage inline.
# sonar-project.properties
sonar.projectKey=my-salesforce-project
sonar.sources=force-app/main/default/classes
sonar.tests=force-app/main/default/classes
sonar.test.inclusions=**/*Test.cls
# Point at Nimbus Cobertura output
sonar.apex.coverage.reportPaths=coverage.xml
# JUnit results for test history
sonar.junit.reportPaths=results.xml# In your CI pipeline
- name: Run tests
run: nimbus test
--coverage --coverage-report coverage.xml
--results-xml results.xml
env:
NIMBUS_PROFILE: ci
- name: SonarQube scan
uses: SonarSource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}The CI profile pattern
Nimbus supports Quarkus-style configuration profiles. Commit a nimbus.properties file to your repo and define CI-specific overrides with the %ci. prefix. Set NIMBUS_PROFILE=ci in your pipeline and the overrides activate automatically.
This means your CI environment gets strict governor limit enforcement, no browser-opening, no tracing output, and an external database - all defined once in source control, not scattered across pipeline environment variables.
Full configuration reference →# nimbus.properties - commit to your repo
# Defaults: relaxed for local development
nimbus.governor.mode=warn
nimbus.trace.enabled=true
nimbus.devui.open-browser=true
# CI overrides - activated by NIMBUS_PROFILE=ci
%ci.nimbus.governor.mode=strict
%ci.nimbus.test.parallel=4
%ci.nimbus.trace.enabled=false
%ci.nimbus.devui.open-browser=false
# Use DATABASE_URL from environment in CI
%ci.nimbus.db.provider=external
%ci.nimbus.db.url=${DATABASE_URL}# Activate in CI - one environment variable
NIMBUS_PROFILE=ci nimbus test
# Or pass the flag explicitly
nimbus test --profile ciParallel sharding
Split your test suite across multiple CI jobs with --shard N/M. Job N runs only its assigned slice of test classes. All jobs run concurrently in a matrix, cutting wall-clock time proportionally.
Classes are distributed using modulo assignment - not contiguous slicing - so uneven class durations balance naturally across shards. All methods within a class stay together, so @testSetup always runs correctly.
When combined with --results-xml, each shard's JUnit XML includes a shard="N/M" attribute so CI aggregators can merge results across shards.
name: Apex Tests (sharded)
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- name: Install Nimbus
run: curl -fsSL https://install.testnimbus.dev | sh
- name: Run tests (shard ${{ matrix.shard }}/4)
run: |
nimbus test \
--shard ${{ matrix.shard }}/4 \
--results-xml results-${{ matrix.shard }}.xml
env:
NIMBUS_LICENSE_KEY: ${{ secrets.NIMBUS_LICENSE_KEY }}
NIMBUS_PROFILE: ci
- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: results-${{ matrix.shard }}.xmlCaching for faster runs
Nimbus caches parsed ASTs in .nimbus/cache/. On a project with hundreds of Apex classes, this can reduce startup time from several seconds to under a second on subsequent runs.
Cache the .nimbus/cache/ directory between CI runs, keyed on your Apex source files. The binary download and embedded PostgreSQL runtime can also be cached to shave additional seconds.
- name: Cache Nimbus
uses: actions/cache@v4
with:
path: |
~/.nimbus
.nimbus/cache
key: nimbus-${{ hashFiles('force-app/**/*.cls', 'force-app/**/*.trigger') }}
restore-keys: nimbus-
- name: Cache binary
uses: actions/cache@v4
with:
path: /usr/local/bin/nimbus
key: nimbus-bin-${{ runner.os }}-v1Apex CI that just works.
No org. No JWT. No scratch org pool. JUnit XML and Cobertura coverage plug into the same tools your other languages already use.