How-to

Apex CI without a connected org

The Salesforce CI handshake (JWT cert, connected app, DevHub, scratch org pool) was never the part of CI that found bugs; it was the part that broke at 2am. With Nimbus, Apex CI looks like CI for any other language: install a binary, run the test command, upload the result. Below is the working setup for GitHub Actions and GitLab.

Short version

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

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

The --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

yaml
# .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:

bash
# .git/hooks/pre-commit
#!/usr/bin/env bash
set -e
nimbus test --changed --since HEAD

The JWT and scratch-org setup, for comparison

The old pattern:

yaml
# 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 -p

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