The job is one install step, one test command, and one upload, with no secrets, no JWT, and no DevHub minutes. JUnit XML feeds your CI test panel, and 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, or anything else that runs a shell script.
- No Salesforce credentials.
GitHub Actions: a 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: a 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, so your existing merge gate can use it as is.
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 HEADThe JWT and scratch-org setup, for comparison
The old pattern:
# 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
The easiest migration is to 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.