cirrus-for-sfdx
Version:
Cirrus CLI tool
475 lines (346 loc) • 15 kB
Markdown
# 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)
```bash
npm install -g cirrus-for-sfdx
```
### Using npx (no installation required)
```bash
npx cirrus-for-sfdx <command>
```
## Quick Start
1. Initialize a `cirrus.yaml` configuration file in your project:
```bash
cirrus init
```
2. Edit `cirrus.yaml` to define your scratch orgs, commands, and flows
3. Run commands or flows:
```bash
cirrus run <command_name>
cirrus flow <flow_name>
```
## Usage
After installation, you can use the `cirrus` command from anywhere in your terminal:
```bash
cirrus <command> [options]
```
### Available Commands
#### Global Commands
```bash
cirrus --help # Show help information
cirrus --version # Show version information
```
#### `cirrus init`
Initializes a new `cirrus.yaml` configuration file in the current directory.
```bash
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.
```bash
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:
```bash
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.
```bash
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:
```bash
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.
```bash
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:
```bash
cirrus flow setup
cirrus flow deploy-and-test
```
#### `cirrus package create`
Creates a new package version for Salesforce managed or unlocked packages.
```bash
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:
```bash
# 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.
```bash
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
# 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:
```yaml
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:
```yaml
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
```yaml
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:
```yaml
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:
```yaml
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:
```yaml
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:
```yaml
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
```yaml
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:
```bash
dart pub get
```
### Running Tests
```bash
dart test
```
### Building
```bash
dart compile exe bin/cirrus.dart -o bin/cirrus
```