A babashka-compatible Clojure library for building idempotent devops CLIs: desired state in EDN, workflows as step graphs threaded by one map, Selmer-scaffolded configuration files, OpenTofu as the muscle, and Ansible for SSH provisioning when you need it.
Docs: specification (repo) · source tour (repo).
(require '[green.workflow :as wf]
'[green.cli :as cli])
(defn wire-fn [step run-opts] ;; static graph for this run
(case step
:zk/start [start-step :zk/node]
:zk/node [node-step :zk/zoo-cfg]
:zk/zoo-cfg [zoo-cfg-step]))
(defn next-fn [step default-next opts] ;; dynamic router: fan-out, error routing
(cond
(pos? (:green/exit opts 0)) []
(= step :zk/start) (for [n (:zk/servers opts)]
[:zk/node (assoc opts :zk/node n)])
:else (map (fn [s] [s opts]) default-next)))
(def workflow
(wf/workflow {:start :zk/start :wire-fn wire-fn :next-fn next-fn}))
(cli/exec workflow) ;; ./green create | ./green delete- A step is a function
opts -> opts, named by a qualified keyword. - Outcomes are Unix-style:
:green/exit(0 ok),:green/err,:green/trace. Thrown exceptions and non-map step returns are converted to that contract. The engine stamps:green/stepbefore each step runs. wire-fnis called as(wire-fn step run-opts), whererun-optsis the initial opts for this run. It may switch the static graph on stable inputs such as:green/event; step-result-dependent routing belongs innext-fn.- Multiple successors run in parallel using
futures. Branches converging on a step join it once with branch results under:green/branches. If a branch fails inside a fork, running siblings finish their current step, the join is skipped, and the worst exit propagates. - Advice is Emacs
nadvice-style, workflow-scoped, and pure:wf/advice-addtargets one step andwf/advice-add-alltargets every step. Supportedhowvalues are:around,:override,:before,:after,:before-while,:before-until,:after-while,:after-until,:filter-args, and:filter-return. At equal:depth, newest advice is outermost; lower:depthruns farther outside. - Composition:
(wf/step sub-workflow {:in … :out …})turns a workflow into an ordinary step — wire it, advise it, and fan it out. Parent advice is inherited by embedded workflows; same step names match flat at any depth, and a parent advice entry with the same id replaces the child's entry.wf/advice-planshows the composed stack. green.scaffold/scaffoldrenders flat Selmer file specs on create; on:deletethe same specs name targets to remove, pruning immediate empty parent directories.green.tofu/tofu-steprunstofu init+applyfor any non-:deleteevent andinit+destroyfor:delete. Apply outputs land under:tofu/outputsby default. Backends are explicit:beforeadvices:backend-advice,local-backend-advice,s3-backend-advice, andgcs-backend-advice.green.ansible/ansible-steprunsansible-playbookevent-aware (create.ymlfor non-delete,delete.ymlfor delete), parses PLAY RECAP under:ansible/recap, and pairs withinventory-advicefor generated INI inventories.green.dry-run/advise+--dry-runskips the named side-effecting steps and prints what would run.green.progress/adviseadds all-step timing output.
The scheduler is a small fork/join workflow runner:
- Start with one live task: the workflow's
:startstep and the initial opts. - Keep two piles:
livebranches still running andfinishedterminal branches. - Group live branches by step. Multiple branches waiting at the same step may need to join.
- A step is ready only when no other live branch can still statically reach
that same step through this run's
wire-fnedges. This lets longer branches arrive at a join before the join runs. - Ready work runs concurrently in
futures. - After a step, zero next pairs terminates, one continues, several fork.
- Branches from different origins at the same step join: the join step runs
once from the fork-point opts with
:green/branchesattached. - A failed fork collapses: siblings finish their current step, no new fork work starts, the join is skipped, and the result carries the worst exit plus all branch results.
- When no live branches remain, the final result is the single terminal opts; with multiple terminals, the first failure wins, otherwise the last success.
green has not been published to Clojars yet. Use a git dependency with an
explicit commit SHA:
io.github.amiorin/green {:git/sha "REPLACE_WITH_COMMIT_SHA"}In-repo examples use :local/root "../.." for development. Publishing for a
future Clojars release: clojure -T:build jar (or install / deploy; deploy
reads CLOJARS_USERNAME/CLOJARS_PASSWORD). Do not recommend :mvn/version
for consumers until a Clojars release exists.
Each example's ./green is a self-contained babashka script. Mock examples use
OpenTofu configs made only of locals/output blocks, so non-dry-run paths
need tofu on PATH but create no real infrastructure.
examples/zookeeper— dynamic fan-out/join, scaffold + tofu, backend-as-advice, dry-run.examples/multi-zookeeper— two clusters from one workflow viawf/step; parent advice reaches embedded steps.examples/once— a Basecamp ONCE-style single-VPS PaaS: provider-swap advice,compute ∥ smtp → dns → smtp-post → (ansible-local ∥ ansible-remote), threaded opts, per-step tofu state, and scaffold-only Ansible config. Seeexamples/once/SPEC.md.examples/multi-once— many ONCE boxes from oneonce-wf; the parent swaps inherited::providerand::backendadvice, using S3 state keys isolated by deployment and step. S3 is demonstration-only;createneeds a real bucket.examples/floci-zookeeper— the real example: OpenTofu's AWS provider talks to floci onlocalhost:4566, creating Docker-backed EC2 instances, thengreen.ansibleprovisions a real ZooKeeper ensemble over SSH and a health step verifies quorum. Linux-only at runtime;--dry-runworks offline.
cd examples/zookeeper
./green create --dry-run
./green create
./green delete
cd ../multi-zookeeper
./green create --dry-run
./green create
./green delete
cd ../once
./green create --dry-run
./green create
./green delete
cd ../multi-once
./green create --dry-run # offline path; real create needs an S3 bucket
./green create
./green delete
cd ../floci-zookeeper
./green create --dry-run # validates and prints; touches nothing
./green create # real local cluster on floci + Ansible
./green delete # Ansible delete.yml first, then tofu destroysbb test # under babashka
clojure -X:test # under the JVMtest/green/zookeeper_test.clj drives real tofu when it is on PATH over
resource-free HCL. test/green/tofu_test.clj and
test/green/ansible_test.clj cover backend/inventory/playbook helpers without
invoking tofu or ansible-playbook.