renmy runtime was called Nimbus until October 2026. Same tool, same binary; the record of the rename is at https://renmy.net/blog/nimbus-is-now-renmy.
Short version. Most Salesforce CI pipelines spend their minutes provisioning an org so that Apex tests have somewhere to run. renmy runtime, a local Salesforce Apex runtime, executes Apex, SOQL, DML and Salesforce Flows on the CI runner itself against an embedded PostgreSQL, so the unit-test job needs no org, no JWT certificate and no scratch org pool, and it finishes in seconds with JUnit XML and Cobertura coverage as output. The org keeps the job only it can do:
renmy deployruns the local gates, validates the same bytes in the target org with a real check-only validation, then deploys. Running on a CI runner is part of the Team tier, which is free during the public beta.
Who this is for
Release managers, DevOps engineers and team leads who own a Salesforce pipeline: the YAML, the connected app, the Dev Hub allocation and the channel where people ask why the build is red again. No prior contact with a local runtime is assumed. For the whole toolchain in one read, start with renmy runtime explained.
What does a Salesforce CI pipeline spend its minutes on?
Whether it is hand-written around the Salesforce CLI or generated by a deployment suite, the pipeline most teams run has the same shape:
# 1. Authenticate with a connected app: consumer key and private key as CI secrets sf org login jwt --client-id "$SF_CONSUMER_KEY" --jwt-key-file server.key \ --username "$DEVHUB_USERNAME" --set-default-dev-hub # 2. Get an org: create one (minutes, counts against Dev Hub limits)... sf org create scratch --definition-file config/project-scratch-def.json \ --alias ci --duration-days 1 --wait 15 # ...or point at a shared CI sandbox and inherit whatever the last job left in it # 3. Deploy the source (minutes, grows with the metadata) sf project deploy start --target-org ci --wait 30 # 4. Run the tests and wait for the org's test queue sf apex run test --target-org ci --test-level RunLocalTests \ --code-coverage --result-format junit --output-dir test-results --wait 30 # 5. Tear down sf org delete scratch --target-org ci --no-prompt Steps 2, 3 and 4 each wait on Salesforce, and none of them is your code running. That is where the minutes come from. The flakiness comes from the same place: the Dev Hub has no scratch orgs left for today, creation fails and the job retries, the shared sandbox is mid-refresh or holds another branch's metadata, the test run queues behind someone else's, a UNABLE_TO_LOCK_ROW on shared data fails a test that passed last time. Every one of these failures is about the environment, not about the pull request. And when Salesforce has an incident, nothing gets tested.
What changes when Apex tests run on the CI runner
In every other ecosystem the tests run where the code is. A Java project's CI job runs mvn test on the runner: the JVM is a download, the database is a container or an in-memory one, and nothing outside the runner is involved. Apex never had that option. The compiler, the runtime, the database and the test runner all live in Salesforce's data centres, so the only way to run an Apex test has been to put the code in an org and ask the org to run it. The pipeline above exists to rent that org for a few minutes.
renmy runtime is the missing piece: a local runtime for Salesforce Apex. It is an interpreter for Apex classes, interfaces, enums, generics and triggers, for SOQL and DML, and for Salesforce Flows (autolaunched, record-triggered and screen), with an embedded PostgreSQL behind it and the platform's rules in front of it: governor limits enforced, Test.startTest/Test.stopTest, @testSetup, per-test transaction isolation, the Stub API, triggers and flows firing in order of execution. It is one binary for macOS, Linux and Windows, with no Docker, no JVM and no org. It is not a mock framework and not a transpiler to Java: the Apex runs as Apex, the SOQL runs as queries against real tables. How closely it matches the platform is a public record rather than a claim: renmy.net/parity.
For a CI job, four things follow.
No org for the unit-test job. The job that runs on every push needs no JWT certificate, no connected app, no Dev Hub and no scratch org pool. Nothing to create, expire or tear down, and ten pull requests can test at once without contending for allocations.
Minutes become seconds. The published benchmark runs four open-source Apex suites, 837 tests, both in a Salesforce org and locally: about 1 min 46 s for deploy and run in the org against 2.4 s locally, roughly 45× faster, with every test passing in both. Method and numbers: renmy.net/benchmarks.
Standard output. renmy test writes JUnit XML for your test reporter and coverage as HTML, JSON and Cobertura XML. Coverage is line and branch; the org reports only line coverage. renmy coverage diff gives the delta for a pull request. Details: renmy.net/coverage.
The test stage keeps working during a Salesforce incident. Deploys still wait, because they need the org. Tests do not.
A GitHub Actions job for Apex tests without an org
The job
name: Apex tests on: pull_request: push: branches: [main] jobs: apex-tests: runs-on: ubuntu-latest timeout-minutes: 10 env: # CI runs are part of the Team tier (free during the public beta). RENMY_LICENSE_KEY: ${{ secrets.RENMY_LICENSE_KEY }} steps: - uses: actions/checkout@v4 - name: Install renmy runtime run: curl -fsSL https://renmy.net/install.sh | sh - name: Run Apex tests with coverage run: renmy test "*" --coverage # The run writes JUnit XML and Cobertura XML. Point the test reporter # and coverage upload steps you already have at them: renmy.net/coverage What the job no longer contains
No sf org login jwt, no private key in the secrets store, no sf org create scratch, no sf project deploy start, no polling, no delete step, and no if: always() cleanup for the org a cancelled job left behind. The only secret is the licence key. timeout-minutes: 10 is generous: on a suite of several hundred tests the test step is measured in seconds.
Where the gates go
A coverage floor belongs on the deploy, where --min-coverage refuses a payload below it before the org sees it (next section). On the pull request, renmy coverage diff reports the delta, line and branch, so a reviewer sees what the change did rather than a single percentage.
GitLab CI/CD, Azure DevOps and plain shell get the same job as ready-to-copy templates on renmy.net/ci, and the step-by-step wiring guide is Apex CI without an org.
How do you deploy to Salesforce from CI with gates?
The test job proves the code. The deploy job is where the org comes back, and where the other half of the pipeline problem lives: a deploy that tested one set of bytes and shipped another, a validation nobody can prove matched the payload, a job that sits in Pending for an hour.
renmy deploy: one command, no skipped steps
renmy deploy is a gated one-shot deploy. It snapshots the selected source into a content-addressed bundle, so what gets tested is byte-for-byte what gets shipped. It runs renmy runtime validation and the configured local test and coverage gates against that bundle. It then validates the identical bytes in the target org with a real Salesforce check-only validation, the org's own compile and its own test run, and only then deploys. The deploy job still logs in to the target org with the Salesforce CLI as it does today; what changes is what has to be true before any metadata reaches it.
# Local gates, then a check-only validation of the same bytes in the org, then deploy renmy deploy --source-dir force-app --min-coverage 75 It is in the Free tier, because the safe default should not be the paid one. renmy.net/deploy has the full set of options: manifests, destructive changes, test levels, the explicit confirmation production deploys require.
renmy release: validate, approve, deploy, with a receipt
Teams that need a person between validation and production split the deploy in two. renmy release validate runs the gates and the org-side check-only validation and writes an immutable receipt: bundle digest, git revision, org fingerprint, gate results, the Salesforce validation job id and the quick-deploy expiry. The receipt and the bundle are portable CI artifacts. After approval, renmy release deploy quick-deploys the validated job within Salesforce's 10-day window, or re-sends the exact stored bundle. A changed, expired or wrong-org receipt is rejected, and there is no override flag.
# Job 1: validate. Writes the receipt under .nimbus/releases/ and keeps the bundle. renmy release validate # A person approves the release. # Job 2, on this runner or another one: deploy exactly what was validated. renmy release deploy Two-job templates for GitHub Actions, GitLab CI/CD, Azure DevOps and generic shell are on renmy.net/release; the Salesforce side of it is in Quick deploy in Salesforce, and the reasoning behind each field of the receipt is in What a release receipt should contain. renmy release is Pro, free during the public beta.
Stuck in Pending
Deploy jobs queue in the org, and sometimes they stay queued. renmy release status shows where a job sits, renmy release watch follows it, and renmy release requeue resubmits a job stuck in Pending. The Salesforce deploy queue explained covers why it happens; Fix a stuck Salesforce deployment is the runbook.
Pin the Salesforce CLI
A pipeline that installs @salesforce/cli fresh on every run gets a different CLI whenever Salesforce publishes one. renmy toolchain sf install --version … pins the exact version so every runner provably runs the configured toolchain, and renmy sf <anything> passes any Salesforce CLI command through unchanged, arguments, prompts and exit codes preserved. Guide: Pin the Salesforce CLI version in CI.
What still needs an org
The honest division: anything a person looks at and anything that talks to another system stays in a sandbox. Lightning pages, permission checks, integration tests against real endpoints and the final validation of the payload all belong there, and renmy deploy keeps that last one in the org on purpose. What moves to the runner is the part you run a dozen times per pull request: the Apex tests. The sandbox is still where you ship; the runner is where you find out whether to.
"I already have a CI sandbox" is the usual objection, and the answer is that you keep it. It stops being the thing every push waits for. The comparison in detail: renmy runtime vs scratch orgs.
What running renmy runtime in CI costs
Running on a CI runner is part of the Team tier, together with the CI runner licence, seats and the self-hosted assurance console. Team is free during the public beta; the list price is $49 per developer per month. The Free tier, for individual developers at $0 with no card, covers unlimited local test runs, gated deploys, CLI pass-through and IDE integration on one machine. The runner reads its licence key from RENMY_LICENSE_KEY. Pricing: renmy.net/#pricing.
Try it
curl -fsSL https://renmy.net/install.sh | sh # macOS, Linux irm https://renmy.net/install.ps1 | iex # Windows PowerShell cd your-sfdx-project renmy test "*" --coverage The quickstart takes you from install to a first green run. Issues, docs, examples and the released binaries are at github.com/renmy-solutions/renmy; the CI templates are on renmy.net/ci; questions go to the Slack.
Frequently asked questions
Does Salesforce CI without a scratch org still need a Dev Hub?
Not for the test job. renmy runtime runs Apex tests on the CI runner against an embedded PostgreSQL, so the job that runs on every push needs no Dev Hub, no connected app and no scratch org allocation. The deploy job needs the target org, as it always did, and renmy deploy validates the payload there with a real check-only validation before deploying.
Is the coverage renmy runtime reports the coverage Salesforce enforces?
renmy runtime measures line and branch coverage locally during the test run and lets you gate on it with --min-coverage before anything reaches the org. Salesforce computes its own line coverage during deployment and applies its own 75% rule; that check still runs inside the check-only validation step of renmy deploy. Branch coverage exists only locally, because the org does not report it.
Can I keep my current deployment tool and only move the test stage?
Yes. The test job is a single command, renmy test "*" --coverage, that writes JUnit XML and Cobertura XML; nothing about it requires renmy deploy. It is the natural place to start, because it is the stage that runs most often and waits longest. Gated deploys and release receipts can follow.
What does it cost to run renmy runtime in CI?
CI runs are part of the Team tier, which is free during the public beta; the list price is $49 per developer per month. The runner reads its licence key from the RENMY_LICENSE_KEY environment variable. The Free tier covers unlimited local test runs and gated deploys for an individual developer on one machine.
What happens to the pipeline when Salesforce has an incident?
The test job keeps running, because nothing in it talks to Salesforce. Deploys wait, because renmy deploy validates the payload in the target org before shipping it. Pull requests keep getting tested and reviewed, and deployments resume when the org does.