Overview
Overview
Usage
Usage
Examples
Examples
GitHub
Section

Usage

The CLI: commands, global flags, targeting, dry-run, the deploy lifecycle, rollback, concurrency, locking, logging and secret masking.

Every invocation has the shape:

whoosh <stage> <action> [flags]

The stage is the name of a deploy/<stage>.yml file (it’s just data, so any file you add becomes a usable stage). The action is a built-in command, or the name of a task from your Deployfile. The two stage-less commands are whoosh init and whoosh version.

Commands

CommandDescription
whoosh initScaffold Deployfile.yml, deploy/<stage>.yml files, and deploy/scripts/.
whoosh versionPrint the version.
whoosh <stage> deployBuild and publish a new release.
whoosh <stage> deploy:rollback [--cleanup]Repoint current at the previous release (--cleanup removes the rolled-back release).
whoosh <stage> deploy:checkValidate connectivity, ensure the directory tree exists, and verify every linked_files entry is present in shared/.
whoosh <stage> deploy:unlockClear a stale deploy lock on the primary host.
whoosh <stage> releasesList the releases present on each host.
whoosh <stage> deploy:hostsPrint the stage’s resolved hosts (including deploy: false) as a table. Provided by the default-on print-inventory plugin.
whoosh <stage> configPrint the fully resolved, merged configuration.
whoosh <stage> validateValidate the configuration offline (parse + merge + schema checks + plugin param templates). No host or cloud access, no dynamic inventory. A fast CI / pre-commit gate.
whoosh <stage> run "<cmd>"Run an ad-hoc command on the stage’s hosts.
whoosh <stage> <task>Run a named task from the Deployfile.

Task commands are discovered from your Deployfile per stage, so whoosh production --help lists the tasks available for that stage (tasks marked hidden: true are omitted but still runnable).

Global flags

Available on any <stage> <action>:

FlagMeaning
--dry-runPrint the exact plan without contacting a host (see below).
-v, --verboseVerbose output (logs each command before running it).
--roles <r1,r2>Restrict to hosts filling these roles.
-H, --host <host>Restrict to specific hosts (repeatable / comma-separated).
--concurrency <n>Max hosts to run a command on at once (0 = all, the default).
--deployfile <path>Use a specific Deployfile instead of auto-discovery.

Logging flags:

FlagMeaning
--log-leveldebug / info (default) / warn / error.
--log-formattext (default) or json.
--log-outputstdout, stderr, or a file path.
--log-colorOn by default, auto-suppressed when output is a file or a pipe.
--log-fileAlso write a deploy log to this file, in addition to --log-output. Empty (default) = console only.
--log-file-formatFormat for --log-file: text (default) or json. The file is never colorized.

--log-file adds a second destination rather than replacing the console one, so a single run keeps colored output on the terminal and a log on disk. What the file contains depends on --log-file-format:

  • text (default) - full transcript. Whoosh’s narrative (phases, tasks, results) and all host command output - the raw remote/local output that normally streams only to stdout - exactly as the console shows it, minus color. This is what you want to capture a complete record of a deploy (e.g. why an asset compile failed on one host).
  • json - narrative only. Whoosh’s events as one JSON object per line, for machine parsing / log shipping. Host command output is deliberately excluded: interleaving raw bytes would break the JSON lines. Use text if you need the command output captured.
whoosh production deploy --log-file deploy.log
# console: colored text   -   deploy.log: full transcript (narrative + command output)

whoosh production deploy --log-file events.json --log-file-format json
# console: colored text   -   events.json: structured narrative, one JSON object per line

(Secret masking applies to the file too - command output written to the transcript goes through the same redactor as the console.)

Log settings in the Deployfile

The --log-* flags can also be set in the Deployfile under log:, so a project logs consistently without repeating flags. A command-line --log-* flag you set explicitly always overrides the Deployfile value:

log:
  level: info             # debug, info, warn, error
  format: json            # text or json
  output: stdout          # stdout, stderr, or a file path
  color: true             # colorize text logs (terminal only)
  file: deploy.log        # also write a deploy log here (like --log-file)
  file_format: text       # text or json for `file`
  raw_remote_log: true    # true (default): stream command output raw, false: emit it through the logger

Put log: in deploy/<stage>.yml to vary logging per stage (e.g. JSON in CI).

Command output as structured logs (raw_remote_log)

By default whoosh streams host command output raw, prefixed by host, as it arrives - and the json log channel deliberately excludes it (raw bytes would break the JSON lines). That is great for a human watching a terminal, but means a json log stream you ship to a collector doesn’t contain what the commands actually printed.

Set raw_remote_log: false to flip this: each line of command output is emitted through the logger as a structured record instead of streamed raw, so it joins the JSON stream and can be shipped and parsed. The echoed command becomes an exec record too. For example, with log.format: json and raw_remote_log: false:

{
  "time": "...",
  "level": "INFO",
  "msg": "exec",
  "task": "hello",
  "host": "local",
  "command": "echo hi"
}
{
  "time": "...",
  "level": "INFO",
  "msg": "output",
  "task": "hello",
  "host": "10.4.20.204",
  "output": "hi"
}

Each record carries the host it came from and the task that produced it. masking still applies, so secrets stay masked in the shipped logs. (Dry-run plans are unaffected - they remain raw, since they are for interactive inspection.)

Targeting: roles and host

A task with roles: [db] already runs only on db hosts. The flags narrow any action further, on top of that:

whoosh production deploy --roles web        # only web hosts
whoosh production deploy --host web1.example.com
whoosh production migrate                   # the task's own roles: [db] applies
whoosh production run "uptime" --roles app

Dry run

--dry-run prints the complete plan - every command, on every host, with the rendered environment - and contacts no host:

whoosh production deploy --dry-run

Use it to review a change before applying it, or as a CI pre-step. In dry-run, run-time-only values (like captured task state) render as <no value> instead of erroring, and action tasks print their call without reaching AWS. (Note: startup plugins like aws:ec2:inventory do run on every command, including dry-run, since they populate the host list.)

Deploy lifecycle

whoosh <stage> deploy runs these phases in order, each as a barrier across all target hosts (with your hooks.before/hooks.after wrapped around it):

PhaseWhat it does
deploy:startingAcquire the deploy lock on the primary host.
deploy:checkEnsure the <deploy_to> directory tree exists (creating linked_dirs), and verify every linked_files entry exists in shared/ - fail early if one is missing.
deploy:init(marker - provision the host: install software / deps. Hook tasks need an explicit dir:, e.g. "{{.deploy_to}}".)
deploy:started(marker - hook anchor, no built-in step)
deploy:updatingUpdate the git mirror, create the release, record REVISION/REVISION_TIME, resolve the commit SHA.
deploy:symlinkLink linked_files/linked_dirs from shared/ into the new release.
deploy:updated(marker - “release built & linked, not yet live”)
deploy:publishingAtomically swap the current symlink to the new release.
deploy:published(marker - “release is live”)
deploy:finishingAppend to revisions.log, prune old releases beyond keep_releases.
deploy:finished(marker - done)

On any step or hook error, the special deploy:failed hook runs (best-effort, for notifications) and the deploy returns the error. See Configuration -> Hooks & phases for attaching tasks.

On-target layout

<deploy_to>/
  repo/                       # git mirror cache
  releases/<timestamp>/       # one dir per release
    REVISION                  # deployed git SHA
    REVISION_TIME             # deploy time (RFC3339)
  shared/                     # linked_files / linked_dirs persist here
  current -> releases/<timestamp>
  revisions.log               # one line appended per deploy

Each deploy appends to revisions.log: Branch main (at <sha>) deployed as release <timestamp> by <user>.

Rollback

whoosh production deploy:rollback            # repoint current at the previous release
whoosh production deploy:rollback --cleanup  # also delete the rolled-back release
whoosh production releases                   # inspect what's available first

Rollback fires before/after deploy:rollback hooks around the swap. The after hooks run with current already pointing at the restored release (use them to fix up shared state - see examples/07-rails-assets). Rolling back past the oldest release fails cleanly.

Concurrency

For a given command, all target hosts run in parallel, with a barrier between phases (output is streamed, prefixed by host). Bound the fan-out with --concurrency:

whoosh production deploy --concurrency 5     # at most 5 hosts at a time

0 (default) means unbounded - all hosts at once. Within a single host, steps run sequentially.

Locking

A deploy takes a lock on the primary (first) host to block concurrent deploys of the same stage. If a deploy is interrupted and leaves a stale lock:

whoosh production deploy:unlock

The lock-holding primary is implicitly required, so it’s never dropped by on_unreachable: skip.

Logging & secret masking

Whoosh’s own narrative (phases, tasks, results, warnings) goes through slog. Raw remote/local command output and structured dumps (config, releases, --dry-run plans) stream to stdout, prefixed by host.

Each task command is echoed before it runs, host-prefixed, so the console and the --log-file transcript show what was sent to the host, not just its output:

[10.0.0.5] $ bundle exec rake assets:precompile
[10.0.0.5] ... compiling ...

(cmds are echoed. Multi-line scripts are announced by name and only echoed in full under --verbose. Built-in lifecycle commands - git, symlink swaps - also echo only under --verbose.)

When color is enabled (--log-color, on by default) and the output is a terminal, the [host] prefix is shown in green on both the command’s stdout and its stderr - the prefix marks which host a line came from, not severity. (It deliberately doesn’t turn red on stderr: tools like git and npm write normal progress to stderr, and each line streams before the command’s exit status is known, so the prefix can’t reliably flag failure. A command that actually fails still surfaces in red through whoosh’s own ERROR/WARN log line.) Color is suppressed when output is redirected, piped, or tee’d to --log-file, so transcripts never get ANSI codes. (With raw_remote_log: false there is no raw prefix to color - output becomes structured log records instead.)

Secret masking: command output, the echoed commands, dry-run plans, and verbose command logs are scrubbed before reaching the console (or the transcript file). Two layers:

  • Built-in patterns for well-known secret formats - AWS keys, GitHub/Slack/Stripe/SendGrid/Google/npm tokens, JWTs, key=secret pairs, and credentials embedded in URLs (https://user:TOKEN@host -> https://user:[FILTERED]@host). Pattern-based and best-effort.

  • User-marked secrets for anything the patterns don’t recognize: mark a value sensitive in a template and its exact value is masked everywhere whoosh prints it.

    | Helper | Use | | — | — | | {{ envSecret "NAME" }} | Like sprig’s env, but the value is registered as sensitive and always redacted. | | {{ sensitive .db_password }} | Mark any value (a var, an expression) sensitive. |

    cmds:
      # the token is used in the command but shows as [FILTERED] in logs/echo:
      - bundle config set --global rubygems.pkg.github.com {{ envSecret "REG_TOKEN" }}
    

(Function names can’t contain -, so it’s envSecret, not env-sens. Values shorter than 4 characters are ignored so a near-empty var can’t blank the logs.)

Debug disables masking

masking is turned off at --log-level debug so you can see raw output when debugging - including user-marked secrets. Don’t ship debug logs.

Cancellation & liveness

Ctrl-C / SIGTERM cancels cleanly: in-flight commands are signalled and the deploy lock is released (deferred cleanup runs), rather than the process being killed outright.

A host that dies mid-deploy (power loss, partition) surfaces as an error and fails the run fast instead of hanging: a new connection times out after ~15s, and on an established connection whoosh sends a keepalive every 10s, dropping the host after ~30s of silence. To finish on the survivors instead of aborting, use on_unreachable: skip.

Common workflows

First deploy to a new stage:

whoosh production deploy:check       # connectivity + create the tree
whoosh production deploy --dry-run   # review the plan
whoosh production deploy

Routine deploy:

whoosh production deploy

Roll back a bad release:

whoosh production deploy:rollback

Run a one-off task or command:

whoosh production migrate                 # a Deployfile task
whoosh production run "bin/rails runner 'puts User.count'" --roles app

Inspect without changing anything:

whoosh production config                  # resolved, merged config
whoosh production deploy:hosts            # the host table
whoosh production releases                # releases per host