The job is one install step, one test command, and one upload. The only secret is your licence key: no JWT, no connected app, 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.
- A licence key for CI runs, from the portal, stored as a secret named
NIMBUS_LICENSE_KEY. The free tier does not run tests in CI; pricing lists the plans that do.
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 renmy
run: curl -sSL https://renmy.net/install.sh | bash
- name: Run tests
env:
NIMBUS_LICENSE_KEY: ${{ secrets.NIMBUS_LICENSE_KEY }}
run: |
renmy test "*" \
--results-xml junit.xml \
--coverage-report 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
# NIMBUS_LICENSE_KEY: a masked CI/CD variable in the project settings
before_script:
- apt-get update -qq && apt-get install -y curl
- curl -sSL https://renmy.net/install.sh | bash
script:
- renmy test "*" --results-xml junit.xml --coverage-report cobertura.xml --profile ci
artifacts:
when: always
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: cobertura.xml
coverage: '/Lines:\s+\d+\s*\/\s*\d+\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, 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 what changed since the last run's impact map (with no map yet, the whole suite runs):
# .git/hooks/pre-commit
#!/usr/bin/env bash
set -e
renmy test --impactedThe 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 renmy runtime workflow is two steps and needs no Salesforce credentials at all.
Adopt incrementally
The easiest migration is to add the renmy runtime job alongside your existing CI. Watch which one finds bugs first and which one breaks for unrelated reasons. Promote renmy runtime to the required check; demote the scratch-org workflow to a nightly platform-fidelity gate.