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 sh

Prebuilt 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 ./cmd

Nothing 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 picker

The 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 .env

Timeouts 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 local

Servers, groups & bundles

  • Servers set a hostname, ssh settings, tags, groups and a task list. add_tasks and skip_tasks adjust 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 @name anywhere 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 time

Careful: --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 script

Scripts, 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 failure

History

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 laptop
No login. Anyone who can reach the port can run tasks and edit the Gorunfile. Bind it to 127.0.0.1 and use an SSH tunnel, or keep it on a trusted network.

Using it well

  1. Start with --list-servers. It shows exactly what each server will run, with your filters applied.
  2. Try new tasks on one server. --server web-01 --task new-thing, then widen with --tag.
  3. Make tasks safe to repeat. Write idempotent commands, and mark the ones that aren't with rerun = "unsafe" so they're never retried.
  4. Use bundles for intent. @baseline, @deploy, @rollback read better than long task lists, and the picker shows them.
  5. Keep secrets in the environment. ${TOKEN} in envvars, never the value.
  6. Give long tasks long timeouts. And set --parallel when a shared service can't take every server at once.
  7. Review with --dump-tasks. Every task as a plain script makes a readable diff for a change review.
  8. 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