CI/CD Integration

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

yaml
# 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 ...
Total time: 12–20 minutes per run. Fails when the org pool is exhausted, when the JWT cert expires, or when the network is slow.

Nimbus CI pipeline

yaml
# 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: ci
Total time: 30–90 seconds. No org. No JWT. No scratch org pool. No network dependency beyond installing the binary.

What 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.

JUnit XMLPro

Test results for CI dashboards

PR annotations, test trend tracking, failure history. Works with GitHub Actions, GitLab CI, Jenkins, CircleCI.

--results-xml results.xml
Cobertura XMLPro

Coverage for SonarQube & Codecov

Industry-standard coverage format. Plug straight into SonarQube, Codecov, GitHub Actions coverage gates.

--coverage-report coverage.xml
HTMLPro

Visual coverage report

Browse coverage line-by-line in your browser. Share with the team as a CI artifact.

--coverage-report coverage.html
JSONFree

Custom tooling & dashboards

Machine-readable coverage data. Build your own dashboards or integrate with internal tooling.

--coverage-report coverage.json

GitHub 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.

yaml
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)

yaml
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: ci
When to use this: Projects with large schemas or many test classes. The service container starts in parallel with checkout and binary install, so there's no net startup penalty. Embedded PostgreSQL is fine for most projects.

GitLab 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.

yaml
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.xml

SonarQube & 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.

properties
# 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
yaml
# 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 →
properties
# 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}
bash
# Activate in CI - one environment variable
NIMBUS_PROFILE=ci nimbus test

# Or pass the flag explicitly
nimbus test --profile ci

Parallel 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.

yaml
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 }}.xml

Caching 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.

yaml
- 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 }}-v1

Apex 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.