Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
aba65d8
feat(go): run --wait cancels on interrupt, --timeout, --report, --sho…
achoimet Sep 25, 2026
2d69069
feat(go): -t json|yaml on every listing and a global --jq filter
achoimet Sep 25, 2026
826bd59
feat(go): apply, delete and import experiment templates
achoimet Sep 25, 2026
dafba14
feat: a GitHub Action that installs the CLI; CI docs
achoimet Sep 25, 2026
b4dbd01
feat(go): environments and their variables
achoimet Sep 25, 2026
cc39fba
feat(go): diff and apply --dry-run for experiments, schedules, servic…
achoimet Sep 25, 2026
32a3ef7
feat(go): teams, their members and environments
achoimet Sep 25, 2026
dcb8205
feat(go): invite users
achoimet Sep 25, 2026
c8ec89d
feat(go): access tokens
achoimet Sep 25, 2026
9ea1bbd
feat(go): export, apply and diff a team's project
achoimet Sep 25, 2026
a4494d1
feat(go): --profile on every command; completion from the platform
achoimet Sep 25, 2026
22d0772
feat(go): webhook, Slack and preflight integrations
achoimet Sep 25, 2026
57ada04
feat(go): reports
achoimet Sep 25, 2026
5bf0f13
feat(go): audit log
achoimet Sep 25, 2026
5638f39
feat(go): kill switch
achoimet Sep 25, 2026
922498e
feat(go): targets, their attributes, and actions
achoimet Sep 25, 2026
5037066
feat(go): execution watch, experiment init; experiment delete restored
achoimet Sep 25, 2026
a00d805
build: go-difflib is a direct dependency
achoimet Sep 25, 2026
7903123
feat(go): hubs
achoimet Sep 25, 2026
3961a22
Merge branch 'spike/go-cli' into feat/go-cli-ergonomics
achoimet Sep 25, 2026
ea48778
docs: everyday use, GitOps and CI features
achoimet Sep 25, 2026
799cc2c
feat(go): property definitions and associations
achoimet Sep 25, 2026
9b26850
fix(go): say why the platform refused to delete a team
achoimet Sep 25, 2026
50257e2
docs: the new command groups in the README
achoimet Sep 25, 2026
22d9dfa
Merge branch 'spike/go-cli' into feat/go-cli-ergonomics
achoimet Sep 25, 2026
0b6dd8f
Merge remote-tracking branch 'origin/spike/go-cli' into feat/go-api-p…
achoimet Sep 25, 2026
70e1e1d
docs: the new command groups ship in v6.0.0
achoimet Sep 25, 2026
c157563
Merge remote-tracking branch 'origin/main' into feat/go-cli-ergonomics
achoimet Sep 25, 2026
b9a3a92
Merge remote-tracking branch 'origin/feat/go-cli-ergonomics' into fea…
achoimet Sep 25, 2026
b48bc81
feat(go): -t json|yaml and --jq on the new listings; complete team ke…
achoimet Sep 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,24 @@ jobs:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
command: monitor

# The action downloads from the release, so it can only be checked once one exists.
verify-action:
if: startsWith(github.ref, 'refs/tags/v')
needs: release
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- id: setup
uses: ./
with:
version: ${{ github.ref_name }}
- shell: bash
run: |
steadybit --version
test "$(steadybit --version)" = "${GITHUB_REF_NAME#v}"
test "${{ steps.setup.outputs.version }}" = "${GITHUB_REF_NAME#v}"
49 changes: 48 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,54 @@
before.
- Writing a new experiment's key back into a YAML file no longer rewrites the file: the key
is added at the top and comments, anchors and formatting are kept.
- Shell completion: `steadybit completion bash|zsh|fish|powershell`.
- Shell completion: `steadybit completion bash|zsh|fish|powershell`, which completes
experiment keys, team keys and the ids of templates, schedules, services and profiles
from the platform.
- **Interrupting `experiment run --wait` now cancels the run it started**, before exiting
with 130 (Ctrl-C) or 143 (SIGTERM, as CI runners send when a job is cancelled), so an
aborted pipeline no longer leaves an attack running. `--keep-running-on-interrupt` keeps
the previous behaviour.
- `experiment run --wait` gains `--timeout` (cancel and fail a run that takes too long),
`--report` (a JUnit report with a test case per step, or JSON) and `--show-steps`. In
GitHub Actions a summary of every run is added to the job summary.
- `diff` for experiments, schedules, services and service profiles shows how files differ
from the platform, and exits with 2 when they do; `apply --dry-run` reports what an
apply would change.
- `export --team X -d dir`, `apply -d dir` and `diff -d dir` keep a team's experiments,
schedules, services and custom service profiles in Git as one project.
- Every listing prints the platform's items with `-t json` or `-t yaml`, and `--jq` filters
the JSON any command prints, without jq installed.
- `execution watch` follows a run live; `experiment init` creates an experiment from a
template by asking for its placeholders.
- `--profile <name>` uses a configured profile for one command.
- A GitHub Action, `uses: steadybit/cli@v6`, installs the CLI on a runner.
- `template apply` and `template delete` manage experiment templates as files in Git, and
`template import` imports templates from a connected hub.
- `environment` commands to `list`, `get`, `apply` and `delete` environments, and
`environment variable` to get, merge or replace their variables.
- `team` commands to `list`, `get`, `apply` and `delete` teams, `team member` to list, add,
remove or replace their members, and `team environment` to do the same with the
environments they may use.
- `user invite` invites users by email, optionally into a team.
- `access-token` commands to `list`, `create`, `recreate` and `delete` API access tokens. A
new token is printed once; `-t json` prints only its id and value for a script to read.
- `integration webhook|slack|preflight|preflight-action` commands to `list`, `get`, `apply`
and `delete` integrations. The platform masks secrets when reading them, so `apply`
refuses a file still holding the mask instead of removing the secret.
- `report` commands print the platform's reports as JSON or YAML: `users`, `teams`,
`environments`, `experiments-executed`, `experiments-created`, `services-distribution`,
`services-by-category` and `services-average`. Teams are filtered by key and environments
by name.
- `audit-log` shows who changed what and when, as a table or with `-t json|yaml`.
- `killswitch status`, `activate` and `deactivate`. Activating asks for confirmation unless
`--yes` is given, as it stops every running experiment of the tenant.
- `target query` lists the targets of an environment, by type and query, with the
attributes asked for as columns; `target attribute keys|values` lists what can be
queried. `action list` and `action get` show the actions experiments can use.
- `hub` commands to `list`, `get`, `apply`, `delete` and `resync` the hubs templates are
imported from.
- `property definition` and `property association` commands to `list`, `get`, `apply` and
`delete` the properties experiments and services carry.
- Errors about a missing required flag are worded differently (`required flag(s) "key" not
set`); they still exit with 1.

Expand Down
116 changes: 116 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,122 @@ steadybit service-profile list --origin custom
steadybit service-profile apply -f profile.yml
```

### Environments, teams and more as files in Git

Experiment templates, environments, teams, hubs, integrations and property definitions and
associations are managed the same way, with `list`, `get`, `apply` and `delete`:

```bash
steadybit environment get -i <environment-id> -f environment.yml
steadybit team apply -f ./teams -R
steadybit integration webhook apply -f webhook.yml
steadybit template apply -f ./templates -R
```

Their parts have commands of their own:

```bash
steadybit environment variable set -i <environment-id> region=eu
steadybit team member add -k ADM --email jane@example.com --role OWNER
steadybit team environment add -k ADM --environment Global
```

### Tenant administration

```bash
steadybit access-token create --name ci --type TEAM --team ADM --expires-at 2026-12-31
steadybit user invite --email jane@example.com --team ADM
steadybit killswitch status
steadybit audit-log --from 2026-09-01 -t json
steadybit report experiments-executed --group-by STATE --rollup MONTHLY
```

Commands that cannot be undone, such as `killswitch activate`, `access-token delete` or
`team member set`, ask for confirmation on a terminal; `--yes` skips the question.

### Targets and actions

```bash
steadybit target query -e Global --target-type com.steadybit.extension_container.container --attribute k8s.namespace
steadybit target attribute values -e Global --target-type com.steadybit.extension_container.container -k k8s.namespace
steadybit action list --kind ATTACK
```

## Everyday use

```bash
steadybit experiment init # create an experiment from a template, answering its placeholders
steadybit execution watch -k ADM-1 # follow the latest run of an experiment live
steadybit experiment get -k ADM-1 --profile prod # use another configured profile for one command
```

Shell completion (`steadybit completion --help`) completes experiment keys, team keys and
the ids of templates, schedules, services and service profiles from the platform.

## GitOps

Keep a team's experiments, schedules, services and custom service profiles in Git:

```bash
steadybit export --team ADM -d ./chaos # write them as files
steadybit diff -d ./chaos # what differs from the platform; exits with 2 if anything does
steadybit apply -d ./chaos --dry-run # what an apply would create or update
steadybit apply -d ./chaos # profiles, services, experiments, then schedules
```

Each kind also has its own `diff`, and its `apply` a `--dry-run`, e.g.
`steadybit experiment diff -f ./experiments -R`. Fields the platform fills in with defaults
are not reported as differences.

## In CI

`experiment run --wait` fails the job when a run fails, and a few options make it fit
pipelines:

| Option | Does |
| ----------------------------- | ----------------------------------------------------------------------- |
| `--report steadybit.xml` | A JUnit report, one test case per step; `.json` for JSON |
| `--timeout 30m` | Cancels the run and fails when it has not ended in time |
| `--show-steps` | Prints each step's state as it changes |
| `--keep-running-on-interrupt` | Leaves the run going when the job is cancelled; by default it is stopped |

In GitHub Actions a summary of every run is added to the job summary.

### GitHub Actions

```yaml
- uses: steadybit/cli@v6
- run: steadybit experiment run -f ./experiments -R --yes --report steadybit.xml
env:
STEADYBIT_TOKEN: ${{ secrets.STEADYBIT_TOKEN }}
- uses: mikepenz/action-junit-report@v5
if: always()
with:
report_paths: steadybit.xml
```

### GitLab CI

```yaml
chaos:
image:
name: steadybit/cli:6
entrypoint: ['']
script:
- steadybit experiment run -f ./experiments -R --yes --report steadybit.xml
artifacts:
when: always
reports:
junit: steadybit.xml
```

Every listing prints the platform's items with `-t json` or `-t yaml`, and `--jq` filters
whatever JSON a command prints, without jq installed:

```bash
steadybit service list --team ADM --jq '.[] | "\(.id) \(.name)"'
```

## Container Image

You can also use the cli via our container image:
Expand Down
59 changes: 59 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2026 Steadybit GmbH

name: Set up the Steadybit CLI
description: Installs the steadybit CLI on the runner, so later steps can run experiments.
author: Steadybit GmbH
branding:
icon: activity
color: green

inputs:
version:
description: The version to install, like 5.0.0, or latest.
default: latest

outputs:
version:
description: The installed version.
value: ${{ steps.install.outputs.version }}

runs:
using: composite
steps:
- id: install
shell: bash
env:
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
case "${RUNNER_OS}" in
Linux) os=linux ;;
macOS) os=darwin ;;
Windows) os=windows ;;
*) echo "::error::Unsupported runner OS ${RUNNER_OS}"; exit 1 ;;
esac
case "${RUNNER_ARCH}" in
X64) arch=amd64 ;;
ARM64) arch=arm64 ;;
*) echo "::error::Unsupported runner architecture ${RUNNER_ARCH}"; exit 1 ;;
esac
ext=tar.gz
[ "$os" = windows ] && ext=zip
archive="steadybit_${os}_${arch}.${ext}"
if [ "$VERSION" = latest ]; then
base="https://github.com/steadybit/cli/releases/latest/download"
else
base="https://github.com/steadybit/cli/releases/download/v${VERSION#v}"
fi
dir="${RUNNER_TOOL_CACHE:-$RUNNER_TEMP}/steadybit-cli/${VERSION}/${arch}"
mkdir -p "$dir"
cd "$dir"
curl -fsSL --retry 3 -o "$archive" "$base/$archive"
curl -fsSL --retry 3 -o checksums.txt "$base/checksums.txt"
# Only the archive's own line; checksums.txt lists every platform.
grep " ${archive}\$" checksums.txt > "${archive}.sha256"
if command -v sha256sum >/dev/null; then sha256sum -c "${archive}.sha256"; else shasum -a 256 -c "${archive}.sha256"; fi
if [ "$ext" = zip ]; then unzip -oq "$archive" steadybit.exe; else tar -xzf "$archive" steadybit; fi
echo "$dir" >> "$GITHUB_PATH"
echo "version=$("$dir/steadybit" --version)" >> "$GITHUB_OUTPUT"
2 changes: 2 additions & 0 deletions cmd/steadybit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"os"

"github.com/steadybit/cli/internal/cli"
// Imported for its signal handling, which every command needs from the start.
_ "github.com/steadybit/cli/internal/interrupt"
)

func main() {
Expand Down
10 changes: 7 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,34 @@ go 1.26.2
tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen

require (
github.com/itchyny/gojq v0.12.19
github.com/mattn/go-runewidth v0.0.30
github.com/oapi-codegen/runtime v1.7.0
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.9
github.com/stretchr/testify v1.12.1
go.yaml.in/yaml/v3 v3.0.5
golang.org/x/term v0.46.0
)

require (
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
github.com/getkin/kin-openapi v0.142.0 // indirect
github.com/go-openapi/jsonpointer v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.26.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-runewidth v0.0.30 // indirect
github.com/itchyny/timefmt-go v0.1.8 // indirect
github.com/oapi-codegen/oapi-codegen/v2 v2.8.0 // indirect
github.com/oasdiff/yaml v0.1.1 // indirect
github.com/oasdiff/yaml3 v0.0.14 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/speakeasy-api/jsonpath v0.6.3 // indirect
github.com/speakeasy-api/openapi v1.24.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
golang.org/x/mod v0.38.0 // indirect
golang.org/x/sync v0.22.0 // indirect
Expand Down
16 changes: 12 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvF
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -40,15 +42,19 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/itchyny/gojq v0.12.19 h1:ttXA0XCLEMoaLOz5lSeFOZ6u6Q3QxmG46vfgI4O0DEs=
github.com/itchyny/gojq v0.12.19/go.mod h1:5galtVPDywX8SPSOrqjGxkBeDhSxEW1gSxoy7tn1iZY=
github.com/itchyny/timefmt-go v0.1.8 h1:1YEo1JvfXeAHKdjelbYr/uCuhkybaHCeTkH8Bo791OI=
github.com/itchyny/timefmt-go v0.1.8/go.mod h1:5E46Q+zj7vbTgWY8o5YkMeYb4I6GeWLFnetPy5oBrAI=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand Down Expand Up @@ -85,6 +91,8 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
Expand Down
Loading
Loading