GitHub Actions CI
Run Apex tests automatically on every push or pull request using the aer GitHub Action. The action installs the aer CLI, runs your test suite, and surfaces failures directly in your workflow logs—no Salesforce org deployment required.
Basic setup
Add a workflow file to your repository at .github/workflows/apex-tests.yml:
name: Apex Tests
on:
pull_request:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run aer tests
uses: octoberswimmer/aer-dist@v1.4.0
with:
source: force-app/main/default
env:
AER_LICENSE_KEY: ${{ secrets.AER_LICENSE_KEY }}The AER_LICENSE_KEY secret is required for CI usage. Add it to your repository secrets at Settings > Secrets and variables > Actions > New repository secret. Subscribe at octoberswimmer.com/tools/aer/subscribe to obtain a license key.
Action inputs
Configure the action with these inputs under the with key:
source
Path or paths to your Apex source directories. Pass a single directory:
with:
source: force-app/main/defaultOr multiple directories on separate lines:
with:
source: |
force-app/main/default
force-app-common/main/defaultSpace-separated paths also work:
with:
source: force-app/main/default force-app-common/main/defaultDefaults to . (current directory) if omitted.
flags
Additional arguments passed to aer test. Use this to specify schemas, packages, filters, or any other CLI flags:
with:
source: force-app/main/default
flags: --schema build/schema.gob --package packages/oslog.pkgSkip specific tests with the --skip flag:
with:
source: force-app/main/default
flags: --skip BrokenTest.testMethodRun the classes named by a test suite in your source with --suite:
with:
source: force-app/main/default
flags: --suite BillingFlags that apply to every run can also live in an .aerrc file checked into the repository. See Configuration.
version
Pin the action to a specific aer release:
with:
source: force-app/main/default
version: v0.0.31Defaults to latest, which resolves to the most recent published release. Pinning a version ensures consistent behavior across builds and prevents unexpected failures when new releases ship.
Example workflows
Filter tests by name
Run only tests that match a pattern:
- name: Run integration tests
uses: octoberswimmer/aer-dist@v1.4.0
with:
source: force-app/main/default
flags: --filter Integration
env:
AER_LICENSE_KEY: ${{ secrets.AER_LICENSE_KEY }}Code coverage
Write a coverage report with --coverage and upload it as a workflow artifact. The format is chosen from the file extension: .xml produces Cobertura and .json produces aer’s JSON format, which lists each class’s covered and uncovered lines.
- name: Run tests with coverage
uses: octoberswimmer/aer-dist@v1.4.0
with:
source: force-app/main/default
flags: --coverage coverage.xml
env:
AER_LICENSE_KEY: ${{ secrets.AER_LICENSE_KEY }}
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: apex-coverage
path: coverage.xmlTo record which test method covered which lines, the way sfapex does, add --json results.jsonl --coverage-per-test. Each test record then carries a perClassCoverage field listing the covered and uncovered lines of every class the test touched.
Matrix strategy for multiple source directories
Test several independent projects in parallel:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
project:
- force-app/main/default
- force-app-common/main/default
- force-app-api/main/default
steps:
- uses: actions/checkout@v4
- name: Run tests for ${{ matrix.project }}
uses: octoberswimmer/aer-dist@v1.4.0
with:
source: ${{ matrix.project }}
env:
AER_LICENSE_KEY: ${{ secrets.AER_LICENSE_KEY }}Choosing a runner
In this section a CPU is a logical processor, the unit the operating system counts and nproc prints. A physical core with hyper-threading provides two CPUs, and two CPUs on one physical core do less work than two CPUs on separate physical cores.
aer test runs tests on every CPU the operating system reports. Each CPU that runs tests keeps its own test database and its own copy of your object metadata, so more CPUs shorten the run and use more memory.
Doubling the number of CPUs roughly halves the time it takes to run the tests. Memory use also grows with the number of CPUs, but typically by less than the 4 GB of RAM per CPU that GitHub’s larger runners provide. The runner with 2 CPUs and 8 GB is the size where a test run is most likely to run out of memory when running tests for a large repo.
Runner sizes
| Runner | CPUs | RAM |
|---|---|---|
ubuntu-latest, private repository | 2 | 8 GB |
ubuntu-latest, public repository | 4 | 16 GB |
| Larger runners, sold as 2-core through 96-core (GitHub Team and GitHub Enterprise Cloud) | 2 to 96 | 4 GB per CPU |
GitHub charges for each runner size by the minute, at a rate close to proportional to its number of CPUs, so a larger runner that finishes the tests sooner costs about the same. See GitHub’s runner pricing.
If you cannot use a larger runner
When the runner runs out of memory, the operating system stops aer and the step fails without a test summary, usually with exit code 137. To run the tests with less memory on the same runner:
- Keep the test databases on disk for the whole run with
--spill-to-disk=always. By default,aermoves them to disk when memory runs short, but it is possible for memory pressure to grow quickly causing GitHub to kill the runner before the database is automatically spilled to disk. - Run the tests on one CPU by setting
GOMAXPROCS: 1in the step’senv. The run takes about twice as long.
- name: Run aer tests
uses: octoberswimmer/aer-dist@v1.4.0
with:
source: force-app/main/default
flags: --spill-to-disk=always
env:
AER_LICENSE_KEY: ${{ secrets.AER_LICENSE_KEY }}
GOMAXPROCS: 1Troubleshooting
License errors
If the workflow fails with a license error, verify that:
- The
AER_LICENSE_KEYsecret exists in your repository settings - The license key is valid and not expired
- The license is a CI license, not a developer license (developer licenses cannot be used in GitHub Actions)
- The secret name in your workflow matches exactly (case-sensitive)
Source directory not found
The action fails if the specified source path does not exist. Common causes:
- Forgetting to run
actions/checkout@v4before the aer action - Specifying the wrong path (check your repository structure)
- Using a relative path from the wrong working directory
Tests fail in CI but pass locally
Check that your CI environment includes all dependencies:
- Package files (
.pkg) must be present if you use--packageor--package-dir - Schema files must exist if you use
--schema - Any bootstrap databases must be generated or checked in before the test step
Run the same aer test command locally with identical flags to reproduce CI behavior.
Related documentation
- Getting Started Guide → – Configure schemas, packages, and data
- Test-Driven Development → – Use watch mode for fast local iteration
- aer test command → – Full CLI reference