A lean command-line interface tool for Salesforce development automation.
The Cirrus CLI streamlines repetitive Salesforce tasks by allowing you to automate flows of commonly run commands. Whether you're initializing new scratch orgs, deploying code, installing dependencies, or importing data, Cirrus lets you automate these processes with simple, reusable configurations.
It is intended to be a lightweight alternative to CumulusCI. Unlike CumulusCI, Cirrus is distributed as a standalone binary—so no Python or pip required—so you can get started instantly without extra dependencies.
Cirrus aims to provide a straightforward experience that is powerful enough for most use cases.
npm install -g cirrus-for-sfdxnpx cirrus-for-sfdx <command>-
Initialize a
cirrus.yamlconfiguration file in your project:cirrus init
-
Edit
cirrus.yamlto define your scratch orgs, commands, and flows -
Run commands or flows:
cirrus run <command_name> cirrus flow <flow_name>
After installation, you can use the cirrus command from anywhere in your terminal:
cirrus <command> [options]cirrus --help # Show help information
cirrus --version # Show version informationInitializes a new cirrus.yaml configuration file in the current directory.
cirrus initThis creates a cirrus.yaml file with a schema reference and commented examples to help you get started.
Creates a Salesforce scratch org from the definitions in your cirrus.yaml file.
cirrus org create <org_name>The org to create is named as an argument. Leave it out and cirrus creates the one named by the
root defaultOrg, so a project with one never names an org at all.
Options:
--set-default/--no-set-default: Set the created org as the CLI's default. On by default
The alias the org is created under comes from the org definition's alias, and defaults to the
name it is keyed by.
Examples:
cirrus org create dev
cirrus org create # the org named by `defaultOrg`
cirrus org create ci --no-set-defaultRuns a command defined under commands: in your cirrus.yaml file.
cirrus run <command_name>Every subcommand of run comes out of your config file, and none is built in - so no command you
name can collide with one of cirrus's own.
Anything after -- is appended to the command line:
cirrus run e2e -- --project=chromium --workers=1The arguments reach the command that was named and nothing else - a prerequisite dragged in behind
it runs as written. This is what lets one e2e command serve a pull-request build that runs one
browser and a merge build that runs them all.
Executes predefined flows from your cirrus.yaml file.
cirrus flow <flow_name>Flows allow you to orchestrate multiple commands and actions in sequence. Each step in a flow is executed one after another, and the flow stops if any step fails.
A flow takes no arguments of its own: there is no one step for them to belong to. Pass them to the
command that wants them with cirrus run <command> -- ....
Example:
cirrus flow setup
cirrus flow deploy-and-testCreates a new package version for Salesforce managed or unlocked packages.
cirrus package create -p <package_name> [options]This command automates the package versioning process by:
- Reading your
sfdx-project.jsonfile - Automatically incrementing the version number based on the version type
- Updating the
sfdx-project.jsonwith the new version - Running
sf package version createwith your specified options
Options:
-p, --package(required): The name of the package to release, as defined in the sfdx-project.json file-t, --version-type: Which part of the version number to increment (default:minor). Every type leaves the build number as.NEXT, which is Salesforce choosing itmajor: Increments X.0.0 (for breaking changes)minor: Increments 0.X.0 (for new features)patch: Increments 0.0.X (for bug fixes)
--no-bump: Leave the version number alone, for a project that lets Salesforce choose the build number.sfdx-project.jsonis then left as it was found, unless--version-nameis given - that is a label, and setting it still writes the file--promote: Whether to promote the package version after creation (default: false)-a, --version-name: The name/label for the new version-c, --code-coverage: Calculate and store code coverage percentage-f, --definition-file: Path to a definition file with required features and org preferences-k, --installation-key: Installation key for key-protected packages-x, --installation-key-bypass: Bypass the installation key requirement-v, --target-dev-hub: Username or alias of the Dev Hub org-w, --wait: Number of minutes to wait for package version creation--async-validation: Return immediately without waiting for validation--skip-validation: Skip validation during creation (can't promote unvalidated versions)--verbose: Display verbose command output
Please be aware that at least one of --installation-key or --installation-key-bypass must be provided.
Examples:
# Create a minor version update
cirrus package create -p MyPackage
# Create a major version with a specific name
cirrus package create -p MyPackage -t major -a "Summer 2024 Release"
# Create a patch version with code coverage
cirrus package create -p MyPackage -t patch -c
# Create version with installation key and wait 30 minutes
cirrus package create -p MyPackage -k MySecretKey123 -w 30Retrieves information about the latest package version for a 2GP package.
cirrus package get_latest -p <package_name> [options]Options:
-p, --package(required): The name of the package to get the version for. It must either be a package Id (starts with 0Ho), or the alias of the package Id as defined in the sfdx-project.json.-j, --sfdx-project-json-path: Path to the sfdx-project.json file (default: current directory)
The status is the only part of a run a build server can read, so it says which kind of failure it was:
| Status | Means |
|---|---|
0 |
Everything cirrus was asked to do, it did |
| n | A command cirrus ran exited n. Its answer, passed through unchanged |
2 |
Cirrus could not do what was asked: the config did not load, the command does not exist, the arguments name nothing |
141 |
Something stopped reading cirrus's output - cirrus run x | head. What every tool exits with on a closed pipe |
A script can therefore tell "your tests failed" from "your cirrus.yaml is wrong", and can pass a
command's own status on to whatever reads it next.
cirrus.yaml describes three things: the scratch orgs a project creates, the commands it
runs, and the flows that sequence them. Each is a mapping keyed by the name you refer to it by.
# yaml-language-server: $schema=https://cesarparra.github.io/cirrus/schema/v1/cirrus.schema.json
defaultOrg: dev
orgs:
dev:
definitionFile: config/project-scratch-def.json
duration: 30
commands:
deploy: sf project deploy start
test:
description: Run every local test with coverage.
run: sf apex test run --test-level RunLocalTests --code-coverage --wait 20
flows:
setup:
description: A fresh scratch org with the project deployed into it.
steps:
- createScratch: dev
setDefault: true
- command: deploy
- command: testThat first line is worth keeping. It points editors at cirrus's JSON Schema, which gives completion
and validation as you type - VS Code needs the YAML extension, JetBrains IDEs read it as is.
cirrus init writes it for you.
The URL carries the schema's major version. A v1 config keeps being validated against v1 once a
v2 exists, and schemaVersion at the root lets cirrus itself - not only your editor - say that a
file needs a newer cirrus:
schemaVersion: 1Absent, cirrus reads the file as the version it knows. Present and higher, it says which cirrus the file wants instead of failing on the first key this one does not recognise.
Cirrus read TOML up to 0.2.x. Running 0.3 in a project that still has a cirrus.toml tells you so.
The shape changed with the format:
| 0.2.x (TOML) | 0.3 (YAML) |
|---|---|
[[orgs]] with a name = "dev" field |
orgs: keyed by dev: |
[commands] deploy = "sf ..." |
commands: deploy: sf ... |
[flow.setup] |
flows: keyed by setup: |
{ type = "create_scratch", org = "dev" } |
- createScratch: dev |
{ type = "command", name = "deploy" } |
- command: deploy |
set-default = true |
setDefault: true |
Orgs, commands and flows are keyed by a name you type on the command line, so a name is letters,
digits, - and _, starting with a letter or a digit. Anything else is a config error, reported
with the key that has it.
A key cirrus does not read is an error, not a shrug. durationDays reads exactly like
duration to whoever wrote it, and silently getting the default instead is the failure a config
file cannot afford. The message names the key, what it is on, and the keys that section does take.
Paths are relative to the directory holding cirrus.yaml, not to wherever you happened to run
cirrus from.
${{ }} is reserved and refused today. Cirrus does not interpolate anything yet, and a later
release that pipes one step's output into the next will need a syntax - one that cannot be
introduced without breaking every config already using those characters. A $VARIABLE or
${BRACED} still reaches the program as written and is unaffected.
defaultOrg names the org cirrus org create creates when it is given none:
defaultOrg: dev
orgs:
dev:
definitionFile: config/dev-scratch-def.jsonIt is one key at the root rather than a flag on each org, so two orgs cannot both claim to be the default - there is nowhere to write it twice.
Each org is keyed by the name you pass to cirrus org create, and takes:
definitionFile(required): path to the Salesforce scratch org definition JSON file, relative to the directory holdingcirrus.yamlduration: how many days the org lives, 1-30. Salesforce's own default applies without italias: the alias the org is created under. Defaults to the name it is keyed bynamespace: setfalsefor an org that stands in for a subscriber's, which does not carry the package's namespacewait: minutes to wait for the org to be created
defaultOrg: dev
orgs:
dev:
definitionFile: config/dev-scratch-def.json
duration: 7
ci:
definitionFile: config/project-scratch-def.json
duration: 1
# Created as `scratch-org` rather than `ci`, which is what the pipeline expects to find.
alias: scratch-orgWith defaultOrg set, cirrus org create needs no arguments at all.
A command is the command line to run, keyed by the name cirrus run takes. Give it a mapping
instead when there is more to say:
commands:
status: sf org list
deploy:
description: Deploy the source to the default org.
run: sf project deploy startCommands are not run through a shell. &&, |, > and $VARIABLES are passed to the program
as arguments rather than interpreted, so:
commands:
# Runs `echo` with the arguments `one && echo two`. Probably not what you wanted.
chained: echo one && echo twoA sequence of commands is a flow, and anything genuinely needing a shell belongs in a script the command calls.
dependsOn says what has to have happened before a command can run. Cirrus works out the order and
runs each prerequisite once, however many times it is named:
commands:
tw: npx tailwindcss -i input.css -o output.css
compile: tsc -b
build:
description: Every deployable artifact.
dependsOn: [tw, compile]
lint:
run: eslint .
dependsOn: [build]
test:
run: vitest run
dependsOn: [build]
check:
description: Am I done?
dependsOn: [lint, test]cirrus run check runs tw, compile, lint, test - and build happens once, though both
lint and test name it. A command with only dependsOn and no run, like build
and check above, is a name for its prerequisites and runs nothing itself.
What cirrus promises is that a prerequisite has completed before the command that names it
starts, and that it runs once. It does not promise that two prerequisites of the same command run
one after the other - tw and compile are independent, and a later cirrus may run them at the
same time.
Prerequisites are checked when the config is read, so a name that matches no command, or a chain that comes back round to where it started, is reported before anything runs.
A flow is a list of steps, run in order, stopping at the first one that fails. It can take prerequisites of its own, which run before the first step:
flows:
release:
dependsOn: [build]
steps:
- command: deployPrerequisites and steps are different things. A prerequisite says what must already have happened, so it runs once. A step is an order you wrote down, so naming the same command twice runs it twice.
Each step names its kind with its first key:
createScratch: <org>creates one of the orgs defined above, and takessetDefault(true unless you say otherwise)command: <name>runs one of the commands defined above
flows:
setup:
description: Create a scratch org and deploy into it.
steps:
- createScratch: dev
setDefault: true
- command: deploy
release:
description: Everything that has to pass before a release.
steps:
- command: compile
- command: test
- command: coverage-reportCirrus CLI supports the following platforms:
- Linux (x64)
- macOS (x64, arm64)
- Windows (x64)
This CLI is built with Dart and distributed as platform-specific binaries through npm.
- Install Dart SDK (if not already installed)
- Clone the repository
- Install dependencies:
dart pub get
dart testdart compile exe bin/cirrus.dart -o bin/cirrus