線路 senro, railway track
A pipeline engine, defined in Go.
Write your pipeline as ordinary Go code. Run it locally, in containers, on Kubernetes or over SSH, and watch it live from a second terminal while it works.
go get github.com/xavidop/senro
Point a coding agent at senro: install the skill, or hand it the whole documentation as one file.
Add the agent skill
npx skills add xavidop/senro
Or paste this prompt
Add senro to my Go project. Docs: https://xavidop.github.io/senro/llms.txt
test and lint start together, build waits for both. Here it hits a dropped connection, retries once, and recovers before deploy runs.Why
YAML has none of the tools you'd want for programming.
A dependency graph, retries, conditional branching, failure recovery: that's real programming, asked for in a format with no functions, no types, no tests, and no debugger. Teams end up either fighting the YAML or routing around it with shell scripts the YAML merely calls, which just moves the real logic out of the tool meant to run it.
senro is a pipeline engine first. CI/CD is the obvious first thing to build with it, not the limit of what it's for.
senro starts from the other end: the pipeline is the Go program. You write the graph, build it into a plan, and hand that plan to senro to run. Your code never runs a step directly, which is exactly what lets a second process (an attached terminal, in particular) watch, retry into, and replay the exact thing that's running. You get a real language's tooling, compiler, types, go test, a debugger, for the code that decides whether your build ships.
Quick start
Two steps, one retry rule.
exec.Command runs any command exactly as written: no shell, no surprises. Add Needs to order steps, and Retry(3, retry.OnInfra()) to retry only a dropped connection or the like, never a failing test.
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())
senro.Run(ctx, p)Features
Everything a pipeline needs, in the box.
Each card is one docs page deep. All of it ships in this build; the docs keep an honest list of what does not, so nothing here is aspirational.
Four executors
Run a workflow locally, in a container on your Docker daemon, as one pod per step on Kubernetes, or on a remote host over your own SSH config. One option picks it, per workflow.
Kubernetes & SSH →Go functions as steps
senro.Func runs a registered, typed Go function with the same retries, conditions and caching a command gets: on the coordinator, on an SSH host, or in a container.
Func steps →Fan-out for monorepos
Expand builds one step per module over eight unit graphs, with per-unit edges, duration-balanced shards, and runs narrowed to what a change actually affects.
Fan-out →A cache a fleet can share
Snapshotted workspaces, an opt-in Pure() action cache, and a shared tier over an S3 bucket or an OCI registry, so a fresh runner starts warm. senro verify audits purity claims.
Shared cache →Secrets stay out of logs
A secret arrives as a file, never argv or an environment value. Output is redacted, and channels nothing could clean up afterwards are refused before anything runs.
Secrets →Triggers without a mirror
The pipeline binary is its own matcher: hand it a GitHub, GitLab, Bitbucket or Gitea event and it decides to run, not run (exit 78), or error. No config duplicating what the code declares.
Triggers →Watch and steer a live run
Attach a terminal UI to a pipeline while it runs. Focus a step, read its log, retry it, skip it, pause the run, set a breakpoint, or open a shell inside the step's own sandbox.
Attach →A browser view too
senro ui serves a live run on loopback from a one-time link. The page is Go compiled to WebAssembly, folding the same events as the TUI, with the same controls.
Browser UI →Retries that know what broke
retry.OnInfra() retries a dropped connection or a registry hiccup, and never a failing test. Retrying a real failure until it passes only deletes what the test told you.
Retries →Real outcomes, not pass or fail
A step ends in one of ten states. One that needed three tries is recovered, not succeeded, so flaky infrastructure cannot hide inside a green build.
Step states →Notifications and traces
Slack, GitHub Checks, webhooks, or a destination you write against one small interface; OpenTelemetry-style traces exported straight from the event stream.
Notifications →Failure analysis, gated
Hand a failed step to an analyzer you wrote, with any model behind it. Its proposal applies only when a person or an explicit policy accepts it, never on its own.
Analyzers →See it running
A pipeline you can watch, not just launch.
One line before senro.Run lets a second terminal attach while the run is happening. A pipeline that never adds it pays nothing: no open port, no background process.
$ senro attach
ci run 20260807T101503-a1b2c3
┌─ steps ──────────────┐ ┌─ build ──────────────────────┐
│ ✓ test succeeded│ │ go build ./... │
│ ▸ build running │ │ #cgo CFLAGS: -O2 │
│ │ │ compiling ./cmd/senro ... │
└──────────────────────┘ └──────────────────────────────┘
enter focus · r retry · c cancel · q detachfailedrunning / retryingsucceededcached / skipped
How it's built
Four decisions everything else follows from.
Write the pipeline, run the pipeline
Build a plan from ordinary Go code, then run it. Your code describes the work; senro is the one thing that actually executes it, so a second process can always watch what's happening.
One timeline, always accurate
Every step starting, finishing, or retrying shows up on one timeline. The terminal UI, a finished run you reopen later, and anything else that watches all read that same timeline, so they never disagree about what happened.
Two kinds of step, both running today
Wrap any command in exec.Command and it runs on any of the four executors. Register a Go function and senro.Func runs it, with the same retries, conditions and caching a command step gets, on the coordinator or on an SSH host, where senro stages a copy of this binary and re-enters it there.
Content-addressed, and shareable
Mark a step Pure() and declare its inputs, and a re-run with nothing changed skips it entirely, restored from the content store. Point SENRO_REMOTE_CACHE at an S3-compatible bucket or an OCI registry and a fresh CI runner starts warm on what another machine already built.