Documentation
Everything you need to go from one task on your laptop to a release across a fleet. For every last option, the reference lives in the repository docs.
Install
The install script puts the latest release on your PATH. It installs the instrumented build, which adds OpenTelemetry support that stays off until you set an OTEL_* endpoint.
curl -fsSL https://raw.githubusercontent.com/jenmud/gorun/master/scripts/install.sh | sh For the standard build without OpenTelemetry:
curl -fsSL https://raw.githubusercontent.com/jenmud/gorun/master/scripts/install.sh | INSTALL_VARIANTS=1 shPrebuilt binaries are on the releases page, built for Linux, macOS and Windows. Or build it yourself:
git clone https://github.com/jenmud/gorun && cd gorun && go build -o gorun ./cmdNothing is installed on your servers: gorun connects over plain SSH.
Quick start
gorun -dump > Gorunfile # a starting Gorunfile that runs as-is on this machine
gorun Gorunfile # pick what to run: space selects, enter runs
gorun Gorunfile --run # or run everything, without the pickerThe starting file documents every option in comments and runs on your own machine: its tasks are marked local = true. To run on a real server, add one with an ssh table and remove local = true from the tasks it should run:
[servers.hosts.web-01]
hostname = "web-01.example.com"
[servers.hosts.web-01.ssh]
username = "deploy"
identityfile = "~/.ssh/id_ed25519"Then check what will happen with gorun Gorunfile --list-servers before you run anything.
The Gorunfile
A Gorunfile is TOML with four kinds of thing in it: a library of tasks, bundles (named task lists), server groups (shared settings) and servers. Servers pick tasks from the library by name.
title = "Provision app servers" # shown in the picker and the history
[tasks.packages] # the task library: named shell commands
cmd = "apt-get update && apt-get install -y curl jq"
sudo = true
retries = 1
[tasks.app]
cmd = "systemctl restart app"
sudo = true
depends_on = ["packages"] # runs after packages, on the same server
[bundles.baseline] # a named task list, used as "@baseline"
tasks = ["packages"]
[server_groups.web] # shared settings for servers that join it
tags = ["web"]
tasks = ["@baseline", "app"]
[server_groups.web.ssh]
username = "deploy"
[servers]
tasks = ["@baseline"] # the default for servers without their own list
[servers.hosts.web-01]
hostname = "web-01.example.com"
groups = ["web"]The more specific setting wins: task, then server, then its groups, then the top of the file.
Tasks
Commands and templates
A task runs cmd (a command), cmd_file (a script), or cmd_template, a Go template that sees the server it runs on (.Name, .Hostname, .Tags, .EnvVars) and the task's own vars.
[tasks.greet]
cmd_template = "echo 'hello from {{.Name}} on {{.Hostname}}'"
[tasks.nginx-port]
cmd_template = "sed -i 's/listen .*/listen {{.vars.port}};/' /etc/nginx/sites-enabled/app"
vars = { port = 8080 }Dependencies
depends_on pulls in what a task needs and orders it first. A server's tasks run one at a time by default, in dependency order; max_task_parallel = -1 runs independent tasks together. If a task fails, the tasks after it on that server are skipped and every other server carries on.
Environment and secrets
envvars are exported to the task, also under sudo. Values expand $VAR from gorun's own environment (and a .env file), so secrets stay out of the Gorunfile. They're sent over stdin: not in logs, history, events or the process list.
envvars = { APP_ENV = "production" } # every task
[tasks.registry-login]
cmd = 'printf %s "$TOKEN" | docker login -u me --password-stdin'
envvars = { TOKEN = "${REGISTRY_TOKEN}" } # read from gorun's environment or .envTimeouts and retries
Every task times out after 30 minutes unless it says otherwise. Retries are off unless you ask for them; mark tasks that are dangerous to repeat with rerun = "unsafe".
[tasks.migrate]
cmd = "docker compose run --rm migrate"
timeout = "45m" # default is 30m; a timeout exits 124
rerun = "unsafe" # never retried automatically
[tasks.packages]
cmd = "apt-get install -y nginx"
retries = 2 # up to three attempts
retry_delay = "1s"Local tasks
A server with no ssh table is the machine running gorun. A task marked local = true runs only on local servers, and every other task runs only over SSH, so a build step can never land on production by mistake.
[tasks.build]
local = true # runs on the machine running gorun
cmd = "go build -o dist/app ./cmd/app"
[servers.hosts.laptop] # a server with no ssh table is localServers, groups & bundles
- Servers set a
hostname,sshsettings,tags,groupsand a task list.add_tasksandskip_tasksadjust what they inherit. - Groups hold defaults (SSH user, tags, env, task list) for every server that joins them. Selecting a group in the picker selects all its servers.
- Bundles are named task lists, written
@nameanywhere a task list is accepted, and can include other bundles. - Large fleets split across files with
include:
include = ["groups/*.toml", "hosts/*.toml", "tasks/*.toml"]Running
The picker
gorun Gorunfile opens the picker. Nothing runs until you select it. Type to fuzzy-filter (wp finds web-prod-1), press space to select a server, group or tag, tab to check and untick the server's tasks, and enter to run. ctrl+k lists every shortcut.
Watching a run
The live view shows a progress bar per server. ↑/↓ move, enter shows a server's tasks, v switches to the verbose view listing every task, where enter opens a task's output. A failure shows a red box with the exit code and the end of stderr.
Choosing servers and tasks from the command line
gorun Gorunfile --server 'web-*' --exclude-server 'web-canary-*'
gorun Gorunfile --tag prod --group eu # servers matching all of these
gorun Gorunfile --task @baseline # one bundle (and what it needs)
gorun Gorunfile --bundles baseline --tag prod
gorun Gorunfile -r --parallel 4 # at most 4 servers at a timeCareful: --task X runs X on every selected server, even ones that don't normally run it. Aim it with --server, --tag or --group.
Check before you run
gorun Gorunfile --list-servers # each server and the tasks it will run
gorun Gorunfile --list-tasks # every task, as a dependency tree
gorun Gorunfile --list-bundles # every bundle and what it expands to
gorun Gorunfile --dump-tasks DIR # every task as a runnable shell scriptScripts, cron and CI
--run skips the picker. Without a terminal, gorun prints plain lines and refuses to run anything you didn't ask for with --run.
gorun Gorunfile --run --plain --tag prod # plain lines, exit status 1 on failureHistory
Every run is saved to SQLite with its commands, output, exit codes and timings. Open it from the picker's History tab (ctrl+h), with gorun -history, or in the web UI. Press y on a task to copy its command.
The web UI
gorun -web serves a dashboard for the same Gorunfile: tick servers and tasks or bundles, start and stop runs, watch them live, browse tasks, bundles and history, and edit the Gorunfile. Live updates come over NATS, which is also how terminal runs appear in the browser.
gorun -web Gorunfile # http://localhost:8080
GORUN_ADDRESS=127.0.0.1:9090 gorun -web Gorunfile
ssh -L 8080:localhost:8080 ops-box # reach it safely from your laptop127.0.0.1 and use an SSH tunnel, or keep it on a trusted network.Using it well
- Start with
--list-servers. It shows exactly what each server will run, with your filters applied. - Try new tasks on one server.
--server web-01 --task new-thing, then widen with--tag. - Make tasks safe to repeat. Write idempotent commands, and mark the ones that aren't with
rerun = "unsafe"so they're never retried. - Use bundles for intent.
@baseline,@deploy,@rollbackread better than long task lists, and the picker shows them. - Keep secrets in the environment.
${TOKEN}inenvvars, never the value. - Give long tasks long timeouts. And set
--parallelwhen a shared service can't take every server at once. - Review with
--dump-tasks. Every task as a plain script makes a readable diff for a change review. - Read the history after incidents. It has the exact command each server ran and what it printed.
Go library
The CLI is a thin layer over the gorun package. Load a Gorunfile, select servers the way the flags do, and receive every event as it happens. This website's playground uses it to check your Gorunfile.
var cfg gorun.Config
if err := cfg.LoadFile("Gorunfile"); err != nil {
return err
}
cfg.ResolvePaths(filepath.Dir("Gorunfile"))
servers, err := cfg.SelectServers(gorun.ServerFilter{Tags: []string{"web"}})
if err != nil {
return err
}
cfg.OnEvent = func(event any) {
if ev, ok := event.(gorun.TaskEvent); ok && ev.State != gorun.TaskRunning {
fmt.Printf("%s: %s exit %d\n", ev.Server, ev.Task, ev.ExitCode)
}
}
return cfg.Run(ctx, servers...)Reference
- Tutorial: from hello world to a full deployment
- The Gorunfile: every option
- Running gorun: picker, live view, CI mode, history
- Integrations: web UI, OpenTelemetry, NATS events, completion
- Reference: every flag, key, environment variable and shortcut
- Recipes: bigger examples against a lab of a hundred SSH servers