README
¶
oprun
A small, configurable task runner for interactive developer workflows.
Flows are declared in YAML. Each step is a node in a tree — exec commands, ask
yes/no, pick from a menu, collect free-text input, loop over a selection, or
jump to another node by id. Prompts use
charmbracelet/huh, so the UX feels like
gum without shelling out to gum. Inputs and selections are persisted between
runs so the next invocation defaults to the last answers you gave.
oprun is meant for the kind of recipe you used to keep in a scratch file and run by copy-pasting: build an image, optionally push it, optionally redeploy, pick one or more test files, then run them. Turning that into a YAML flow gives you a repeatable, documented, reviewable routine without having to write a real CLI tool.
Install
go install
go install github.com/wufe/oprun@latest
Make sure $(go env GOBIN) (or $(go env GOPATH)/bin, typically ~/go/bin)
is on your $PATH.
From source
git clone https://github.com/wufe/oprun.git
cd oprun
go build ./...
# the binary is ./oprun
Quick start
Put a flow YAML in one of the search paths below, then:
oprun list # see what's available
oprun <flow-name> # run it
oprun run <flow-name> # same thing, explicit
oprun run ./my-flow.yaml # run a yaml file directly by path
Flow search order
First match wins. Specificity goes cwd → monorepo root → home, so a project-specific flow shadows a repo-shared one, which in turn shadows your personal collection.
./.oprun/flows/<name>.yaml./.flows/<name>.yaml./flows/<name>.yaml<repo-root>/.oprun/flows/<name>.yaml(when cwd is inside a git repo)<repo-root>/.flows/<name>.yaml<repo-root>/flows/<name>.yaml~/.oprun/flows/<name>.yaml
Steps 4–6 use the same bounded .git ancestor walk as the
from_repo_root
flow setting, so the search is silently skipped when cwd is not inside a repo
(or the walk is stopped by the system-parent blocklist). Duplicates are
deduped, so when cwd is the repo root, steps 1–3 cover steps 4–6.
Both .yaml and .yml extensions are accepted.
Saved answers
After each run — including runs that fail — oprun writes the inputs you
submitted to ~/.local/state/oprun/<flow-name>.json (respecting
$XDG_STATE_HOME if set). On the next run those values are pre-filled:
- declared
vars:andinputnodes are re-asked with the last value as the editable default choosepre-selects what you picked last time (filtered against the currently available options); formulti: true, the prior selection order is restored too — each selected option shows its 1-based pick number ([1],[2], …)confirmpre-highlights your last Yes/No (keyed by nodeid— no id, no persistence){foo}lazy references skip the prompt entirely if a value was saved
Writing flows
Full step-by-step guide:
FLOWS.md. It covers every node type in detail, variable lifecycle, persistence rules, conditional execution withwhen:, a cookbook of common patterns, and a troubleshooting table. The section below is a tour; reach forFLOWS.mdwhen you're actually authoring.
A flow has a name, an optional set of variables prompted up-front, and an
ordered list of nodes. The example below tags a release, optionally runs
tests, builds for one or more targets, and uploads the resulting artifacts —
it exercises every node type (confirm, choose, foreach, exec) and both
flavours of choose (static options and dynamic options_cmd).
name: release
description: Tag a release and publish build artifacts
vars:
- name: version
prompt: Version tag (e.g. v1.2.3)
nodes:
- id: prechecks
type: confirm
prompt: Run the test suite first?
on_yes:
- run: make test
- type: choose
prompt: Build target?
options:
- label: linux/amd64
do:
- run: GOOS=linux GOARCH=amd64 go build -o dist/app-linux-amd64
- label: darwin/arm64
do:
- run: GOOS=darwin GOARCH=arm64 go build -o dist/app-darwin-arm64
- label: both
do:
- run: GOOS=linux GOARCH=amd64 go build -o dist/app-linux-amd64
- run: GOOS=darwin GOARCH=arm64 go build -o dist/app-darwin-arm64
- type: confirm
prompt: Create tag {version} and push it?
on_yes:
- run: git tag {version}
- run: git push origin {version}
- type: choose
prompt: Which artifacts to attach to the release?
multi: true
options_cmd: ls dist/ | awk '{print $0 "\t" "dist/" $0}'
store: artifact
- type: foreach
var: artifact
do:
- run: gh release upload {version} {artifact}
Reading top to bottom: ask for a version, optionally run tests, pick one or
more build targets (each option has its own subtree), confirm tagging, then
multi-select artifacts from dist/ (displayed as filenames, stored as paths
via the tab-separator) and upload each one.
Control flow
- Top-level nodes run in declaration order.
- Branch subtrees (
on_yes,on_no,options[].do,foreach.do) are nested node lists — completing one falls through to the parent's next sibling. type: gotojumps anywhere byid; execution resumes linearly from there.- Omitting
on_yes/on_noon a confirm is equivalent to "do nothing on that answer, fall through" — a common pattern. when:on any node gates whether it runs. The string is run through{var}substitution and evaluated as truthy: empty /no/false/0/off(case-insensitive) skip the node; anything else runs it. Useful for gating on a flag captured by an earlierexec— e.g.when: "{rebuilt}"after capturingrebuilt: yes|noin aconfirm'son_yes/on_nobranches.from_repo_root: trueat the flow's top level resolves relativedir:values (and the defaultexeccwd) from the nearest.gitancestor instead of the process cwd — useful when the same flow is invoked from various subdirectories of a monorepo. SeeFLOWS.mdfor the full semantics.
Node types
| type | required fields | notes |
|---|---|---|
exec |
run |
Default when type is omitted. Runs via bash -c. Optional dir, capture (string var), capture_lines (list var, one entry per non-empty stdout line). |
confirm |
prompt |
Optional on_yes, on_no. Answer persisted if node has id. |
choose |
prompt + exactly one of options / options_cmd / options_var |
multi: true for multi-select; press a in the picker to toggle all on/off. Selection order is preserved (numbered in the UI, in the executed do: subtrees, and in the stored list). Store selection with store:. options_var consumes a list var (typically from capture_lines) as its option source. default_all: true (multi only) pre-selects every option on the first run; once anything is persisted, saved state takes over. |
input |
store |
Prompts for a string; stored in the named variable. |
goto |
goto |
Target id must exist at the top level. |
foreach |
var, do |
Iterates a list variable (typically from a multi-select choose). |
Variables and substitution
- Declare upfront in
vars:— each one is prompted at flow start. - Set inline with
input,capture:on anexecnode, orstore:on achoosenode. - Reference as
{name}inrun,dir,prompt, oroptions_cmd. - An unset
{name}is prompted lazily the first time it's referenced.
Dynamic options (options_cmd)
Each line of the command's stdout becomes an option. A line containing a tab
splits into <display label>\t<stored value>; a line with no tab is used for
both. This lets you show short labels but store richer values (full paths,
JSON, etc.):
options_cmd: ls /tests/*.yaml | awk -F/ '{print $NF "\t" $0}'
# ^ stdout lines like: test-a.yaml <TAB> /tests/test-a.yaml
Schema / editor support
A JSON Schema lives at flow.schema.json. Point the
Red Hat YAML extension
at it either per-file:
# yaml-language-server: $schema=../flow.schema.json
name: my-flow
...
...or project-wide in .vscode/settings.json:
{
"yaml.schemas": {
"./flow.schema.json": "flows/*.yaml"
}
}
You get field completion, hover docs, and early errors on typos and missing required fields.
Contributing
Pull requests and issues welcome.
Local setup
git clone https://github.com/wufe/oprun.git
cd oprun
go build ./...
./oprun list
Standard Go tooling applies:
go test ./...
go vet ./...
gofmt -w .
Project layout
.
├── main.go # CLI entry; flow search and dispatch
├── flow.go # YAML types + loader + type defaulting
├── runner.go # execution engine (sequencing, branches, goto, foreach)
├── prompt.go # huh-based Confirm/Choose/Input wrappers
├── state.go # per-flow JSON state (~/.local/state/oprun/<flow>.json)
└── flow.schema.json # JSON schema for editor tooling
Adding a node type
- Add the field(s) to
Nodeinflow.go. - Add a
caseinRunner.runNodeinrunner.go. - Extend
flow.schema.jsonwith anif/thenbranch describing the new type. - Document it in the README table and, if needed, add a short example.
Reporting issues
When filing a bug, a minimal reproducing flow YAML and the state JSON (if any)
from ~/.local/state/oprun/<flow>.json make debugging much easier.
License
MIT.
Documentation
¶
There is no documentation for this package.