Command
What it does
Command runs an arbitrary program or shell command from a monochange workflow.
This is the escape hatch step that lets you combine monochange’s structured state with the rest of your toolchain.
Why use it
Use Command when you need to:
- run project-specific tooling that monochange does not own
- upload artifacts
- call deployment, chat, or notification tools
- bridge monochange release context into custom scripts
- compose outputs from earlier steps into external automation
The important design rule is this:
prefer a built-in step whenever monochange already has a first-class semantic for the work.
Use Command for what is truly custom.
Core fields
command— the command to run in normal modewhen— optional boolean condition controlling whether the step runsdry_run_command— optional replacement command used only when the command runs with--dry-runshell— whether to run through a shell (true,false, or a custom shell binary name)id— optional identifier that exposessteps.<id>.stdoutandsteps.<id>.stderrto later stepsvariables— optional custom variable mapping for command substitutioninputs— optional step-local input overridesshow_progress— optional boolean; set tofalsewhen the command itself is interactive and spinner output would get in the wayalways_run— optional boolean; set totrueto run this step even when a previous step has failed
Prerequisites
Command itself has no built-in prerequisite.
What it can see depends on where you place it:
- after
PrepareRelease, it can consumerelease.*andmanifest.path - after
AffectedPackages, it can consumeaffected.* - after
RetargetRelease, it can consumeretarget.* - after
CommitRelease, it can consumerelease_commit.* - after another named
Command, it can consumesteps.<id>.*
Side effects and outputs
- runs an external command
- records stdout/stderr when
idis present - can act as a consumer or producer step in a workflow chain
Example
[cli.test]
help_text = "Run project tests"
[[cli.test.steps]]
type = "Command"
command = "cargo test --workspace --all-features"
dry_run_command = "cargo test --workspace --all-features --no-run"
shell = true
Composition ideas
Consume prepared release context
[cli.release-with-notes]
help_text = "Prepare a release and print a custom summary"
[[cli.release-with-notes.inputs]]
name = "format"
type = "choice"
choices = ["text", "json"]
default = "text"
[[cli.release-with-notes.steps]]
type = "PrepareRelease"
inputs = ["format"]
[[cli.release-with-notes.steps]]
type = "Command"
command = "echo Releasing {{ release.version }} for {{ released_packages }}"
shell = true
Reuse earlier command output
[cli.release-with-generated-notes]
help_text = "Prepare a release, generate notes, and upload them"
[[cli.release-with-generated-notes.steps]]
type = "PrepareRelease"
[[cli.release-with-generated-notes.steps]]
type = "Command"
id = "notes"
command = "printf 'version=%s\n' '{{ release.version }}'"
shell = true
[[cli.release-with-generated-notes.steps]]
type = "Command"
command = "printf '%s\n' '{{ steps.notes.stdout }}'"
shell = true
Consume repair state
[cli.repair-and-notify]
help_text = "Repair a release and print the retarget result"
[[cli.repair-and-notify.inputs]]
name = "from"
type = "string"
required = true
[[cli.repair-and-notify.inputs]]
name = "target"
type = "string"
default = "HEAD"
[[cli.repair-and-notify.steps]]
type = "RetargetRelease"
inputs = ["from", "target"]
[[cli.repair-and-notify.steps]]
type = "Command"
command = "echo moved {{ retarget.tags }} to {{ retarget.target }} with status {{ retarget.status }}"
shell = true
Why choose Command carefully?
Because it is powerful enough to bypass monochange’s typed guarantees.
That is useful, but it also means:
- validation cannot reason deeply about your command string
- provider-aware dry-run semantics are now partly your responsibility
- shell quoting and portability become part of the workflow design
Recommended usage pattern
A good workflow usually looks like this:
- use built-in steps to create stable state
- use
Commandonly for the final custom integration points - give important custom steps an
idso later steps can consume structured stdout
Common mistakes
- using
Commandto reimplementPublishReleaseorOpenReleaseRequest - forgetting
dry_run_commandwhen the real command would mutate external systems - omitting
idand then having no clean way to reuse the command’s output later - relying on shell features without setting
shell = trueor a custom shell name