One install step, one test command, one upload. No secrets, no JWT, no DevHub minutes. JUnit XML powers your CI test panel; Cobertura XML feeds Codecov, SonarQube, or GitHub's coverage status check.
What you'll need
- An SFDX project in version control.
- A CI provider — GitHub Actions, GitLab CI, CircleCI, Azure Pipelines, Bitbucket Pipelines, Jenkins. Anything that runs a shell script.
- No Salesforce credentials. That's the point.
GitHub Actions — minimal working workflow
# .github/workflows/apex-tests.yml
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 -sSL https://testnimbus.dev/install.sh | bash
- name: Run tests
run: |
nimbus test "*" \
--junit junit.xml \
--coverage cobertura.xml \
--profile ci
- name: Publish test results
if: always()
uses: dorny/test-reporter@v1
with:
name: Apex tests
path: junit.xml
reporter: java-junit
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: cobertura.xmlThe --profile ci flag activates CI-specific overrides in your nimbus.properties — typically stricter governor limit enforcement, parallel workers, and no browser auto-open. See configuration.
GitLab CI — minimal working pipeline
# .gitlab-ci.yml
stages: [test]
apex-tests:
stage: test
image: ubuntu:24.04
before_script:
- apt-get update -qq && apt-get install -y curl
- curl -sSL https://testnimbus.dev/install.sh | bash
- export PATH="$HOME/.nimbus/bin:$PATH"
script:
- nimbus test "*" --junit junit.xml --coverage cobertura.xml --profile ci
artifacts:
when: always
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: cobertura.xml
coverage: '/Total\s+\|\s+(\d+\.\d+)/'What you get out
junit.xml— standard JUnit format. GitHub's test reporter, GitLab's test summary, Azure DevOps test results, Jenkins JUnit plugin all read it.cobertura.xml— per-file, per-line coverage. Codecov, SonarQube, and GitHub's PR coverage diff all consume Cobertura natively.- Exit code — non-zero if any test fails or coverage drops below your configured threshold. Your merge gate just works.
Pre-commit hooks (optional, recommended)
Tests at 100 ms apiece means tests at pre-commit are realistic. Run only the tests affected by the change set:
# .git/hooks/pre-commit
#!/usr/bin/env bash
set -e
nimbus test --changed --since HEADComparing to the JWT/scratch-org setup
The old pattern, for contrast:
# What you used to need
steps:
- name: Install Salesforce CLI
run: npm install --global @salesforce/cli
- name: Decrypt server.key
run: openssl enc -aes-256-cbc -d -in server.key.enc -out server.key -k $JWT_KEY_PASSPHRASE
- name: Authenticate to DevHub
run: sf org login jwt --client-id $CONSUMER_KEY --jwt-key-file server.key --username $DEVHUB_USERNAME --alias devhub --set-default-dev-hub
- name: Create scratch org
run: sf org create scratch -f config/project-scratch-def.json -d -y 1 -a ci-scratch
- name: Push source
run: sf project deploy start -o ci-scratch
- name: Run tests
run: sf apex run test --target-org ci-scratch --code-coverage --result-format junit --output-dir test-results --wait 20
- name: Delete scratch org
if: always()
run: sf org delete scratch -o ci-scratch -pEvery line is a potential failure: cert rotation, DevHub limit, deploy timeout, scratch org pool exhaustion, expired connected app. The Nimbus version is two steps and has no secrets to rotate.
Adopt incrementally
Easiest migration: add the Nimbus job alongside your existing CI. Watch which one finds bugs first and which one breaks for unrelated reasons. Promote Nimbus to the required check; demote the scratch-org workflow to a nightly platform-fidelity gate.