Define a pipeline as ordinary Go code, run it locally, in containers, on Kubernetes or over SSH, and attach to it live from a second terminal or a browser while it runs.
senro (線路, Japanese for railway track) is a pipeline engine you program instead of
configure. Your Go code builds an immutable graph of workflows and steps, senro resolves it into
a plan, executes it, and exposes one append-only event stream that a terminal UI, a browser page,
or a script can attach to, live, to watch and steer the run.
CI/CD is the most familiar thing to build with it, but not the boundary: a data pipeline, a batch job, an infrastructure rollout or a release script is built the same way, out of steps with dependencies, retries, failure handlers, and one event stream.
CI systems ask you to describe real programming (dependency graphs, retries, branching, recovery) in YAML, which has no functions, no types, no tests, and no debugger. So the logic ends up in shell scripts the YAML merely invokes.
senro starts from the other end: the pipeline is the Go program. For the thing that decides
whether your build ships, you get a real language's tooling: the compiler, types, go test, and a
debugger.
- Pipelines as code.
Workflow,Step,Needs,Retry,Timeout,OnFailure,Always, andWhenconditions that prune the graph per branch or run parameter: typed, composable, testable. See Pipelines and Conditions. - Definition, plan, execution.
Build()resolves your code into an immutable, validatedPlan; the engine executes the plan, never your code directly. That is what makes a run inspectable, replayable and attachable. See Concepts. - Live attach. A terminal UI (
senro attach), a browser UI (senro ui, a Go client compiled to WebAssembly), or plain streaming lines, all folding the same event stream, live over a socket or replayed from disk with the same client. See Attach. - Control, not just watch. Cancel, pause and resume a run; retry, skip and re-run from a step; set breakpoints; open an interactive shell inside a live step's sandbox. See Control operations and The shell.
- Four executors. Local processes, containers on a local Docker daemon, one pod per step on
Kubernetes, and processes on a remote host over your own SSH configuration. Chosen per workflow
with
senro.On(...). See Kubernetes and SSH. - Two step kinds.
exec.Commandruns a command on any executor;senro.Funcruns a registered Go function, on the coordinator, on an SSH host, or in a container, by staging the pipeline binary there and re-entering it. See Func steps off the coordinator. - Fan-out for monorepos.
Expandbuilds one step per unit over eight unit graphs (glob,gowork,cargo,jswork,maven,gradle,pyproject,bazel), with per-unit edges (NeedsEach), duration-balanced sharding (Partition), and change-scoped runs (Affected). See Fan-out and Monorepos. - Graphs decided at runtime. When the list only exists once something has run,
Generatesreturns aFragmentthe engine splices into the plan, so each item becomes a real step with its own log, state, cache entry and retry, instead of a shell loop senro cannot see into. See Generate a subgraph. - Caching that tells the truth. Named workspaces snapshotted between steps (and, with bounds,
between runs), an opt-in
Pure()action cache keyed off declared inputs, a best-effort scratch cache, and a shared remote tier over an S3-compatible bucket or an OCI registry so a fleet starts warm.senro verify --recheck-pureaudits purity claims empirically. See Shared cache. - Runs that outlive the runner. With the shared cache on, each step's output and the sealed
event ledger are archived as the run goes;
senro logs fetch <id>brings a destroyed CI runner's failed build back to read locally. See Archiving a run. - Secrets that stay out of logs. Values are delivered as files, never argv or environment values; output is redacted, unsafe channels are refused outright before anything runs. Built on mamori. See Secrets.
- Triggers. The pipeline binary is its own matcher: hand it a GitHub, GitLab, Bitbucket or
Gitea webhook event (or your own provider) and it decides whether to run, with exit code 78 for
"not my business". Or be the endpoint yourself:
trigger.FromRequestverifies and parses a delivery straight off the wire, per source, with no event file in between. See Triggers and Run it as a server. - Notifications and export. Slack, GitHub Checks, webhooks, or your own destination through a
small
Renderer/Requesterseam; traces exported from the event stream through aSink. See Notifications. - Failure analysis, gated. Hand a failed step to your own analyzer (your SDK, your model, no
provider dependency in senro); its proposal applies only when a human or an explicit policy
accepts it.
contrib/genkitanalyzeris one you can install, in its own module so senro's graph stays free of it. See Writing a failure analyzer and An AI analyzer. - Honest failure states. A step ends in one of ten states, never a boolean;
recovered(flaky but passed on retry) is notsucceeded, andretry.OnInfra()retries broken infrastructure but never a failing test. See Failure handling. - Embeddable. senro is a library first: no global state, no
os.Exit, no reading your argv; attach is one opt-in call. The wire contract lives ingithub.com/xavidop/senro/apiwith no dependency beyond the standard library. See Embedding.
The CLI:
brew install xavidop/tap/senro
# or
go install github.com/xavidop/senro/cmd/senro@latestThe library, which is all a pipeline needs:
go get github.com/xavidop/senroReleased binaries for linux and darwin on amd64 and arm64, with checksums, SBOMs and SLSA provenance, are on the releases page. See Install for verifying a download.
Requires Go 1.26+ on Linux or macOS. Windows is deliberately unsupported; see Platform support.
A pipeline is a Go package with a main that builds the pipeline and calls senro.Run. This one
runs go test and, once that passes, go build, retrying only infrastructure failures, never a
failing test:
// Command ci is a two-step pipeline: test, then build.
package main
import (
"context"
"log"
"os"
"github.com/xavidop/senro"
"github.com/xavidop/senro/attach"
"github.com/xavidop/senro/exec"
"github.com/xavidop/senro/retry"
)
func main() {
ctx := context.Background()
p := senro.New("ci")
verify := p.Workflow("verify")
verify.Step("test", exec.Command("go", "test", "./..."))
verify.Step("build", exec.Command("go", "build", "./...")).
Needs("test").
Retry(3, retry.OnInfra()) // retries a dropped connection, never a failing test
// Opens a unix socket a second terminal can attach to while this runs.
att, err := attach.Listen(ctx, attach.Options{Bind: attach.AutoUnixSocket})
if err != nil {
log.Fatal(err)
}
defer att.Close()
log.Printf("attach with: senro attach --pid %d", os.Getpid())
if err := senro.Run(ctx, p, senro.WithAttach(att)); err != nil {
log.Print(err)
os.Exit(1)
}
}Run it with the CLI, which builds the package, execs it, and attaches automatically (a TUI on a TTY, plain lines otherwise):
senro run ./ciOr run the binary yourself and attach from a second terminal, or open the browser view:
go run ./ci &
senro attach # terminal UI
senro ui # browser UI, prints a one-time linkEvery run writes runs/<id>/ with events.jsonl and per-step logs.
senro attach --run <id> reopens a finished run from disk over the exact same protocol; there is
no separate offline mode. From here, the
getting started guide continues.
The full documentation lives under site/, an Astro site (make site-dev
serves it locally with Node 22 via nvm). It is the primary reference for what is implemented:
- Start: Introduction, Getting started, Concepts
- Build: Pipelines, Conditions, Fan-out, Monorepos, Subgraphs, Failure handling, Secrets, Kubernetes, SSH
- Attach: The protocol, TUI, Browser UI, Security
- Use: Embedding, Triggers, Notifications, Shared cache, Archiving, CLI, Reading a failed run
- Extend: Write a unit graph, a trigger source, a notifier, a trace exporter, a failure analyzer
Runnable examples live in examples/, each a small main package with a doc comment
saying what it demonstrates. An agent skill ships in-repo for AI coding tools.
senro targets Linux and macOS. Windows is not supported, deliberately: attach's security
boundary is a kernel peer-credential check with no Windows equivalent implemented, and senro fails
closed rather than advertising a feature it cannot secure. See
Attach security for the full reasoning.
Contributions are welcome. make all runs formatting, go vet, the linters and the test suite;
run it before sending a change. See CONTRIBUTING.md for the repository layout,
what the container, Kubernetes and SSH executors' tests need, and how releases are cut.
MIT © Xavier Portilla Edo
senro is built by Xavier Portilla Edo, and mirrors the structure,
tooling and release process of mamori (typed, watchable
configuration and secrets for Go), which senro's secrets support is built on.