Skip to content

Repository files navigation

Cirrus CLI

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.

Installation

Using npm (recommended)

npm install -g cirrus-for-sfdx

Using npx (no installation required)

npx cirrus-for-sfdx <command>

Quick Start

  1. Initialize a cirrus.yaml configuration file in your project:

    cirrus init
  2. Edit cirrus.yaml to define your scratch orgs, commands, and flows

  3. Run commands or flows:

    cirrus run <command_name>
    cirrus flow <flow_name>

Usage

After installation, you can use the cirrus command from anywhere in your terminal:

cirrus <command> [options]

Available Commands

Global Commands

cirrus --help    # Show help information
cirrus --version # Show version information

cirrus init

Initializes a new cirrus.yaml configuration file in the current directory.

cirrus init

This creates a cirrus.yaml file with a schema reference and commented examples to help you get started.

cirrus org create

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

cirrus run

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

Passing arguments through

Anything after -- is appended to the command line:

cirrus run e2e -- --project=chromium --workers=1

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

cirrus flow

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

cirrus package create

Creates 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:

  1. Reading your sfdx-project.json file
  2. Automatically incrementing the version number based on the version type
  3. Updating the sfdx-project.json with the new version
  4. Running sf package version create with 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 it
    • major: 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.json is then left as it was found, unless --version-name is 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 30

cirrus package get_latest

Retrieves 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)

Exit status

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.

Configuration (cirrus.yaml)

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: test

That 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: 1

Absent, 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.

Moving from cirrus.toml

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

Names

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.

Keys and paths

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.

The default org

defaultOrg names the org cirrus org create creates when it is given none:

defaultOrg: dev

orgs:
  dev:
    definitionFile: config/dev-scratch-def.json

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

Scratch org definitions

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 holding cirrus.yaml
  • duration: how many days the org lives, 1-30. Salesforce's own default applies without it
  • alias: the alias the org is created under. Defaults to the name it is keyed by
  • namespace: set false for an org that stands in for a subscriber's, which does not carry the package's namespace
  • wait: 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-org

With defaultOrg set, cirrus org create needs no arguments at all.

Commands

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 start

Commands 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 two

A sequence of commands is a flow, and anything genuinely needing a shell belongs in a script the command calls.

Prerequisites

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.

Flows

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: deploy

Prerequisites 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 takes setDefault (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-report

Platform Support

Cirrus CLI supports the following platforms:

  • Linux (x64)
  • macOS (x64, arm64)
  • Windows (x64)

Development

This CLI is built with Dart and distributed as platform-specific binaries through npm.

Setting Up for Development

  1. Install Dart SDK (if not already installed)
  2. Clone the repository
  3. Install dependencies:
    dart pub get

Running Tests

dart test

Building

dart compile exe bin/cirrus.dart -o bin/cirrus

Releases

Packages

Contributors

Languages