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 every other language: install a binary, run the test command, upload the result. Here's the working setup for GitHub Actions and GitLab.

Short version

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

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 — 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. 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:

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

Comparing to the JWT/scratch-org setup

The old pattern, for contrast:

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

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.