Executors

An executor is where a workflow’s steps run. You pick one with senro.On, and every step in that workflow runs there.

deploy := p.Workflow("deploy", senro.Needs("verify"), senro.On(senro.Local()))

The executor belongs to the workflow, not to individual steps. Every target below is described relative to the coordinator, the machine that calls senro.Run and hosts the engine, whether that’s your laptop in local development or the CI runner in a pipeline job. There are four targets today:

TargetPackageRuns every step of the workflow
senro.Local()built in; the defaultas processes on the coordinator’s own machine
container.Image(ref)github.com/xavidop/senro/executor/containerin a container on a local Docker daemon
k8s.Pod(ref, k8s.Namespace(ns))github.com/xavidop/senro/executor/k8sas a pod in a Kubernetes cluster
ssh.Host(dest)github.com/xavidop/senro/executor/sshas a process on a remote machine

Choose one

LocalContainerKubernetesSSH
Setupnonea container daemon on this machinefive environment variables and RBACyour own ~/.ssh/config
Workspacesthe coordinator’s own directoriesbind mounts of themcarried in and back, tar over the apiservercarried in and back, tar over the connection
senro.RO enforcedno, detected afterwardsyes, a read-only bindyes, readOnly in the podno, detected on read-back
Secretsa file under the runtime directorythat file, bind-mounted at /run/senro/secretsa namespaced Secret, projected at 0400a file on the host, delivered over stdin
Scratch cachesthe coordinator’s own directorythat directory, bind-mountedcarried in and back, two full transfers per stepcarried in and back, two full transfers per step
Func stepsin this processyes, the binary is bind-mountedyes, the binary is sent in per podyes, the binary is staged per host
senro shellyesyesyes, in a pod of its ownyes
senro shell --ttyyesyesyes, a pty the runtime allocatesexecutor_no_terminal
stdout and stderrkept apartkept apartmerged into one streamkept apart
Private registriesnothing to pullcontainer.RegistryAuththe node pulls, via an imagePullSecret you setnothing to pull
Cache classlocal/<os>/<arch>platform and resolved image digestimage digest and platform, never the namespacessh/<os>/<arch>, or your ssh.CacheClass

For a full comparison of secret delivery, see Secret channels. For senro shell and its refusal codes, see Shell.

Read-only mounts are enforced on two of the four

ws.At(path, senro.RO) means the same thing on every executor, but each one enforces it differently. Don’t assume the strongest behavior applies everywhere. This table shows what actually happens on each one.

ExecutorWhat senro.RO doesWhen a write through one is caught
ContainerA real read-only bind mountAt the write. The write fails
KubernetesreadOnly on the pod’s volume mount; the kubelet refusesAt the write. The write fails
LocalNothing at mount time; a workspace is a directory with no per-step modeRight afterwards, when the workspace’s content is found changed under a mount that promised it would not be
SSHNothing on the far side, which senro is not root onOn read-back. The copy is hashed but not written over yours, so the write is reported rather than carried home

On the local and SSH executors, read-only is a request that senro checks after the fact, not a rule the kernel enforces. Keep credentials and other sensitive input out of any workspace a step could overwrite by mistake.

What every executor shares

A step behaves the same no matter where it runs. These things are handled above the executor level, so no target re-implements them and they can’t differ between executors:

  • retries, Timeout, ContinueOnError, and the end-state taxonomy (Failure states)
  • OnFailure and Always handlers, which run on their parent’s executor and can’t declare their own (Handlers)
  • workspace snapshots and the action cache (Caching)
  • secret resolution, delivery as a file, and redaction of every log stream (Secrets)
  • the trace context (TRACEPARENT, and TRACESTATE when the run has one)

If your build doesn’t include a given executor, targeting it is rejected at Build(). It never falls back to running locally.

Refusals worth knowing before you pick

What you wroteWhere it is refused
One scratch cache mounted by a remote step and by a local or container step, with nothing ordering themBuild(). The local step would write that directory while the remote step is tarring it. A Needs between them makes it a hand-off, and legal
An unpinned image tag on k8s.PodBuild(). Pin it to a digest
k8s.Pod with no k8s.NamespaceBuild(). There is no fallback to default
An executor on a handlerBuild(). A handler runs where its parent ran
container.RegistryAuth on any other executorBuild(). Only the container executor pulls an image itself
DOCKER_HOST naming tcp://Run start. Every container mount is a bind mount of a coordinator directory

The executor is set per workflow, not per step, because it decides what a mount, a secret, and a shell actually mean on that machine. Steps that share a machine share an executor. Grouping steps by where they run costs nothing: see Ordering.

Where to go next

  • Containers: container.Image, the daemon it needs, container.User, and private registries.
  • Kubernetes: cluster configuration, workspaces across the apiserver, and delegated secrets.
  • SSH: your own SSH configuration, where things land on the host, and the cache class.
  • Func steps off the coordinator: staging the pipeline binary, cross-compiling, and cgo.