Env, dir & timeout

Four settings every step has, whatever it runs and wherever it runs. Every method returns the same builder, so calls chain.

verify.Step("deploy", exec.Command("./deploy.sh", "prod")).
	Env("DEPLOY_ENV", "prod").
	WorkDir("./infra").
	Timeout(5 * time.Minute).
	ContinueOnError()
MethodWhat it does
Env(key, value)One environment variable. Call it again for more.
WorkDir(dir)The directory the command runs in.
Timeout(d)Bounds one attempt, not the whole retry sequence.
ContinueOnError()Dependents run even if this step fails.

Env

One pair per call:

verify.Step("test", exec.Command("go", "test", "./...")).
	Env("CGO_ENABLED", "0").
	Env("GOFLAGS", "-count=1")

It is not variadic, because a variadic run of pairs has an arity the compiler cannot check: Env("A", "1", "B") would compile and mean nothing.

Your step gets exactly what you declared

Build() adds nothing to a step’s environment, not even a PATH. Two developers on the same commit get the same plan, because nothing about their shells leaks into it. That matters most on the exact field a cache key is computed from.

Search-path defaults are the executor’s job instead:

  • The local executor supplies the coordinator’s own PATH to a step that declares none, and nothing else from the parent environment. That is why exec.Command("go", "test", "./...") finds go with no Env call at all.
  • Declaring a PATH yourself replaces that fallback, it does not add to it. If you set PATH=/opt/toolchain/bin, go is no longer on it.

Never put a credential in Env

Use SecretEnv instead, which delivers a file path rather than a value. senro refuses a plan that would route a resolved secret through an environment value, so this is not a style preference.

WorkDir

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

There is no cd in a step, because nothing is shell-interpreted. WorkDir is how you change directory.

When a step mounts a workspace, WorkDir is usually the mount path:

deploy.Step("build", exec.Command("./build.sh")).
	Mount(src.At("/repo", senro.RW)).
	WorkDir("/repo")

Timeout bounds one attempt

A step with Retry(3, ...) and Timeout(5*time.Minute) can take fifteen minutes across three attempts. The bound is per attempt, not per step.

An attempt that outlives the bound settles the step as timed_out.

One exception, on the coordinator. Nothing can force a Go function to return, so a senro.Func step running locally that ignores its context keeps running past the deadline and is merely filed as timed_out when it eventually finishes.

Off the coordinator the function has a process of its own, which ends itself at the deadline. Declare a Timeout on every remote func step.

ContinueOnError is for advisory steps

ContinueOnError() says: if this step fails, its dependents should run anyway, against whatever it did produce. Linting is the usual case.

verify.Step("lint", exec.Command("golangci-lint", "run")).ContinueOnError()
verify.Step("report", exec.Command("./collect-report.sh")).Needs("lint")   // runs either way

It is not a general rescue, and two limits catch people out:

  • It does not apply when the upstream never ran. A dependent of a skipped_condition or skipped_manual step is skipped the same way, because nothing failed and nothing is being excused. ContinueOnError promises a dependent survives a failure, not that it runs against output that was never produced.
  • It still changes the run’s rollup. Without it, the failure makes the run partial or failed.

Both are on Step states.

The rest of the builder

I want to…CallPage
Run this after another stepNeedsOrdering
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

A handler is a *senro.StepBuilder too, and Env, SecretEnv, WorkDir and Timeout are exactly the four settings that work on one.