Steps

A step is one action with an id: a command, or a registered Go function. Steps are what a pipeline is made of, and every page in this section configures one.

p := senro.New("ci")

verify := p.Workflow("verify")
verify.Step("test", exec.Command("go", "test", "./...")).
	Timeout(5 * time.Minute)

plan, err := p.Build()

Workflow(name, opts...) adds a named group and returns a *senro.WorkflowBuilder. Step(id, action) adds a step to it and returns a *senro.StepBuilder. Every builder method returns the same builder, so calls chain. Build() resolves the whole pipeline into a validated, immutable Plan; see Concepts for why that boundary exists.

Step ids are unique across the whole pipeline, not per workflow, because a plan is flat. Build() refuses a duplicate, and refuses a step with no action.

The two step kinds

KindWhat it runs
exec.Command(name, args...)A command, exactly as given
senro.Func(name, params)A Go function registered under name. See Func steps

Both kinds are built, scheduled, retried, cached and handled by exactly the same code, so reach for senro.Func whenever the work is “call this Go function”, not “shell out to a program”.

exec.Command interprets no shell

exec.Command("go", "test", "./...") runs the program go with two arguments, exactly as written. There is no shell in between, so none of the things a shell does happen:

You writeWhat a shell would doWhat senro does
exec.Command("ls", "*.go")Expand *.go to your filesPasses the literal *.go to ls
exec.Command("echo", "$HOME")Substitute your home directoryPasses the literal $HOME
exec.Command("go build && ls")Run two programs in sequenceLooks for one program whose whole name is go build && ls
exec.Command("cd", "web")Change directoryRuns /bin/cd, which changes nothing

Each has a direct replacement:

  • Globs, pipes, redirection, &&: ask for a shell in so many words.

    verify.Step("test", exec.Command("sh", "-c", "go test ./... | tee test.log"))
  • Environment variables: declare them with Env, which the step sees for real.

    verify.Step("test", exec.Command("go", "test", "./...")).Env("CGO_ENABLED", "0")
  • Changing directory: use WorkDir.

    verify.Step("build", exec.Command("pnpm", "build")).WorkDir("./web")

Both are on Env, dir & timeout.

This is a feature, not a restriction to work around. An argument that is never re-parsed cannot be split on a space you did not expect, and a filename with a space in it is just a filename.

What you can configure

Everything below is a method on the *senro.StepBuilder that Step(...) returned.

I want to…CallPage
Run this after another stepNeedsOrdering
Set env vars, a directory, a time limitEnv, WorkDir, TimeoutEnv, dir & timeout
Let dependents run even if this failsContinueOnErrorEnv, dir & timeout
Try again when it breaksRetry, RetryPolicyRetries
Clean up or collect logs afterwardsOnFailure, AlwaysFailure handlers
Skip it unless something is trueWhenConditions
Give it files, and keep what it wroteMount, NoSnapshotWorkspaces
Skip it when nothing changedPure, Inputs, Outputs, CacheEnvCaching a step
Give it a credentialSecretEnvSecrets

Two things that are not step settings

Where a step runs belongs to its workflow. senro.On(...) on the Workflow call picks the executor for every step in it, so a step never carries one of its own:

remote := p.Workflow("remote", senro.On(ssh.Host("build@ci-1")))
remote.Step("build", exec.Command("make", "build"))   // runs on ci-1

See Executors.

Generating steps instead of writing them is a workflow-level call too. If your repository has many apps, modules or packages and you want one step each, that is Monorepos, not a step setting.

Where to go next