cli

package
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 25, 2026 License: MIT Imports: 53 Imported by: 0

Documentation

Overview

Package cli implements steps' command-line grammar and every command behind it. `run` executes one job once and `test` every job; `web` is the daemon that holds the pipelines `pipeline set` uploads, serves the UI, polls trigger: true resources and auto-runs every job a changed resource affects.

It lives below main rather than in it so the end-to-end suite in ./e2e can drive the whole stack through Run, which is the only entry point that spans it.

Index

Constants

View Source
const DefaultDaemonState = ".steps/steps.db"

DefaultDaemonState is where a daemon and the commands that read its database look when --db says nothing.

A fixed name rather than one derived from a file, because there is no file: a daemon is handed pipelines by `steps pipeline set`, and the commands that read what it recorded are handed a NAME. See docs/web.md.

View Source
const DefaultLogLevel = "info"

DefaultLogLevel is what InitLogging runs at before the CLI's own --log-level/STEPS_LOG_LEVEL has been parsed (during kong construction itself, and as CLI.LogLevel's own default) — never debug, so a parse failure (or any other pre-parse code path) can't fall back to printing every subsequent command/output dump by accident.

Variables

View Source
var BuildVersion = "dev"

BuildVersion is the version string steps --version prints. Overridden at build time via -ldflags "-X github.com/jtarchie/steps/internal/cli.BuildVersion=..."; "dev" covers `go run`/unversioned `go build` invocations.

Functions

func InitLogging

func InitLogging(level string)

InitLogging installs a slog handler on stderr as the default logger, separate from this tool's plain stdout progress lines ("get: prs (version: ...)", "task: review"), at the given level (see parseLogLevel). Debug is what previously ran unconditionally on every invocation: shell.go/ docker.go log the full rendered command and complete captured stdout/ stderr at that level, so any resource check/in/out command whose templated source: or output embeds a credential was written to stderr on every ordinary run, with no way to suppress it. Defaulting to info (see CLI.LogLevel) makes that opt-in, via --log-level debug or STEPS_LOG_LEVEL=debug, rather than the permanent default.

func PipelineName

func PipelineName(pipeline string) string

PipelineName is a pipeline's identity inside a state database: the YAML's base name without its extension.

The same string web.Slugify and config.Slugify produce, and deliberately so — the UI's /p/<slug> route, the database's pipelines.name and the Config's own name are one identity, not three that have to be kept in agreement.

func RecordRevision

func RecordRevision(ctx context.Context, st store.Revisions, cfg *config.Config) error

RecordRevision writes down WHICH configuration the runs opened against this handle will have executed.

Called from the same funnel as SetSourcePath, and for the same reason: it is the only place holding both the configuration that was parsed and the handle its runs are recorded against. A command that only READS history never comes through there and records no revision, which is right — it resolved no configuration.

func Run

func Run(args []string) error

Run parses args as the steps command line and executes the command they name. It is the whole CLI behind one call, which is what lets the end-to-end suite drive a real invocation in-process rather than spawning a binary.

Types

type AnswerCmd

type AnswerCmd struct {
	ReadFlags `embed:""`
	ID        int64 `arg:""   help:"the question id, from steps questions"`
	// Variadic so an answer can be written without quoting it, which is what
	// somebody typing a sentence back at a parked step will do.
	Answer []string `arg:"" help:"the answer — one of the offered options, or your own words"`
}

AnswerCmd supplies the fact a parked agent step is waiting on.

⚠️ Same v1 scope as ApproveCmd, stated deliberately: anyone who can run this command can answer. The recorded answerer is the OS user, which is an audit record and not an authorization check.

func (*AnswerCmd) Run

func (a *AnswerCmd) Run() error

Run answers the named question.

type ApprovalsCmd

type ApprovalsCmd struct {
	List    ApprovalsListCmd `cmd:"" default:"withargs"                      help:"list approval: steps waiting for a decision"`
	Approve ApproveCmd       `cmd:"" help:"approve a waiting approval: step"`
	Reject  RejectCmd        `cmd:"" help:"reject a waiting approval: step"`
}

ApprovalsCmd lists approval: steps waiting for a decision.

A parked approval that nobody is told about is useless in practice, so this is the "what is waiting on me?" command. It reads the same rows the audit trail is made of.

type ApprovalsListCmd

type ApprovalsListCmd struct {
	ReadFlags `embed:""`
}

ApprovalsListCmd is the listing itself, and the group's default: bare `steps approvals -p <pipeline>` still answers "what is waiting on me?".

func (*ApprovalsListCmd) Run

func (a *ApprovalsListCmd) Run() error

Run prints every pending approval.

type ApproveCmd

type ApproveCmd struct {
	ReadFlags `embed:""`
	ID        int64  `arg:""                                       help:"the approval id, from steps approvals"`
	Reason    string `help:"note to record alongside the decision"`
}

ApproveCmd records a yes.

⚠️ v1 scope, stated deliberately rather than discovered: anyone who can run this command can approve. There is no separate identity system — the recorded approver is the OS user, which is an audit record, not an authorization check. Someone will ask "can anyone approve?" the day this ships, and the answer is yes, on purpose, for now.

func (*ApproveCmd) Run

func (a *ApproveCmd) Run() error

Run approves the named approval.

type CLI

type CLI struct {
	LogLevel       string            `` /* 191-byte string literal not displayed */
	Version        kong.VersionFlag  `help:"print the steps version and exit" name:"version"`
	Run            RunCmd            `` /* 141-byte string literal not displayed */
	Test           TestCmd           `cmd:""                                  help:"run every job (force) and verify assert directives"`
	Validate       ValidateCmd       `cmd:""                                  help:"check a pipeline for errors without running anything"`
	Runs           RunsCmd           `cmd:""                                  help:"show what past runs recorded"`
	Plan           PlanCmd           `cmd:""                                  help:"show which steps a run would execute or skip"`
	MCP            MCPCmd            `cmd:""                                  help:"inspect or authorize a pipeline's mcp_servers: entries"`
	Jobs           JobsCmd           `cmd:""                                  help:"jobs the circuit breaker has paused, and taking one out of it"`
	Approvals      ApprovalsCmd      `cmd:""                                  help:"approval: steps waiting for a decision, and deciding them"`
	Questions      QuestionsCmd      `cmd:""                                  help:"ask_user questions waiting for an answer, and answering them"`
	Memory         MemoryCmd         `cmd:""                                  help:"what agent steps remember between runs, and deleting it"`
	ClearTaskCache ClearTaskCacheCmd `cmd:""                                  help:"empty a step's caches: so its next run starts cold"`
	Web            WebCmd            `cmd:""                                  help:"serve the UI, poll trigger: true resources, and run affected jobs"`
	Pipeline       PipelineCmd       `cmd:""                                  help:"tell a steps web daemon which pipelines to serve"`
	Docs           DocsCmd           `cmd:""                                  help:"read the docs in the terminal (no page name lists them)"`
}

CLI is the pipeline runner's command-line grammar, parsed by kong. Run is default:"withargs" so today's flat invocation (steps pipeline.yml --job x) keeps working unchanged, routed to it implicitly. LogLevel is a global flag (available to every subcommand, not just Run) rather than living on RunCmd/TestCmd individually, since it configures the process-wide slog default logger before any subcommand's Run method executes — see InitLogging.

type ClearTaskCacheCmd added in v0.2.0

type ClearTaskCacheCmd struct {
	VarFlags  `embed:""`
	Pipeline  string            `` /* 126-byte string literal not displayed */
	Job       string            `` /* 136-byte string literal not displayed */
	Step      string            `` /* 136-byte string literal not displayed */
	CachePath string            `` /* 136-byte string literal not displayed */
	Worker    map[string]string `help:"map a step tag to a worker, as steps run takes it; a placed step's cache is there" name:"worker"`
	Start     bool              `help:"start a stopped aws:// or gcp:// worker to clear it, and stop it again"            name:"start"`
	// Name alone of StateFlags: the cache is keyed by the pipeline's name, and nothing here opens the state database.
	Name map[string]string `help:"name the pipeline as steps run --name does, e.g. --name infra=infra/pipeline.yml" name:"name"`
}

ClearTaskCacheCmd is fly clear-task-cache: it empties a step's caches: so its next run starts cold.

func (*ClearTaskCacheCmd) Run added in v0.2.0

func (c *ClearTaskCacheCmd) Run() error

Run clears what it can and fails if anything it found could not be removed.

type DB

type DB string

DB is the --db value: the state database as a bare sqlite path or as a url whose scheme picks the driver, the way --worker's ssh:// and aws:// pick a transport — sqlite:// or postgres:// (postgresql:// too). The switch is here in the CLI rather than in internal/store because the contract package cannot import its own drivers; db.go is where it is made.

Checked while the flag is parsed, so an unknown scheme is a usage error before any command runs: opened as a file, `mysql://…` would have been a freshly created sqlite database of that name, and a job recorded into it.

No refusal here repeats the value: a network url carries credentials, and a usage error lands in shell history and CI logs.

func (*DB) UnmarshalText

func (d *DB) UnmarshalText(text []byte) error

UnmarshalText is kong's parse hook for the flag.

type DeliverFlags added in v0.2.0

type DeliverFlags struct {
	Deliver map[string]string `` /* 127-byte string literal not displayed */
}

DeliverFlags is --deliver, apart from ExecFlags because only the local commands take it: a daemon receives deliveries at POST /p/<pipeline>/hooks/<resource>.

type DocsCmd

type DocsCmd struct {
	Page    string `` /* 128-byte string literal not displayed */
	NoPager bool   `help:"print to stdout instead of paging long output" name:"no-pager"`
}

DocsCmd renders the embedded documentation (docs/*.md — the same pages the web UI serves at /docs) in the terminal. No page name renders the index, which lists the rest.

func (*DocsCmd) Run

func (d *DocsCmd) Run() error

Run prints the requested page's markdown, paged when stdout is a terminal (see pageOutput). Raw rather than styled: a terminal markdown renderer linked a second chroma and goldmark beside the web UI's, and their lexer and style registries were half of every invocation's startup heap.

type ExecFlags

type ExecFlags struct {
	Worker        map[string]string `help:"map a step tag to a worker, e.g. --worker gpu=ssh://jt@box (repeatable)"                           name:"worker"`
	ArtifactStore string            `` /* 126-byte string literal not displayed */
	Answer        []string          `help:"answer an ask_user question in advance, e.g. --answer 'which bump=minor' (repeatable)"             name:"answer"`
	KeepWorkspace bool              `` /* 168-byte string literal not displayed */
	NoPreflight   bool              `help:"skip the pre-run health check of the models and MCP servers a job needs"                           name:"no-preflight"`
}

ExecFlags shape how a command that RUNS steps executes them: which machines tags: resolve to, where cached outputs are mirrored, what a parked question is answered with, whether the workspace survives, and whether the pre-run health check happens at all.

One embed rather than five, because they are one idea — what the operator asked for about this execution — and because Apply is then the single place that threads them, which no embedder can forget without failing to compile.

func (ExecFlags) Apply

Apply folds into the context the flags a poll reads as well as a run, and returns the rest as the Run every job this invocation starts begins from.

Workers and answers are PARSED here rather than at first use, so a typo is reported with everything else wrong with the invocation instead of mid-plan, when some step happens to reach for it.

type HistoryFlags

type HistoryFlags struct {
	VersionHistory int `help:"how many versions of each resource to remember (pipeline defaults.version_history wins)" name:"version-history"`
	RunHistory     int `help:"how many runs of each job to keep (pipeline defaults.run_history wins)"                  name:"run-history"`
}

HistoryFlags bound what a build leaves behind.

Both limits belong on every command that records: a manual run writes the same nodes, events and transcripts a triggered one does. --version-history was once declared on `run` and threaded nowhere, which is why declaring and applying are the same embed now.

func (HistoryFlags) Apply

func (h HistoryFlags) Apply(cfg *config.Config)

Apply lets the flags stand in for limits the pipeline did not set. The pipeline wins where it spoke: it is the thing under version control.

type JobsCmd

type JobsCmd struct {
	List    JobsListCmd    `cmd:"" default:"withargs"                                          help:"list jobs the circuit breaker is holding"`
	Release JobsReleaseCmd `cmd:"" help:"release a held job, so new versions trigger it again"`
}

JobsCmd inspects and clears the watch circuit breaker.

It exists because a held job is otherwise invisible: the trigger loop stops triggering it and says so once, in output that has long since scrolled past by the time anyone wonders why the nightly summary stopped arriving.

type JobsListCmd

type JobsListCmd struct {
	ReadFlags `embed:""`
}

JobsListCmd is the listing, and the group's default: bare `steps jobs <pipeline>` still answers "what has the breaker stopped?".

func (*JobsListCmd) Run

func (j *JobsListCmd) Run() error

Run prints every held job.

type JobsReleaseCmd

type JobsReleaseCmd struct {
	ReadFlags `embed:""`
	Job       string `arg:""   help:"held job to release"`
}

JobsReleaseCmd clears the breaker for one job. Not "resume": that word is --resume <run>'s, continuing a failed run.

A subcommand rather than `jobs --release <name>`: a flag that turns a listing into a write reads as configuration and behaves as a mutation, and the verb should say which one it is.

func (*JobsReleaseCmd) Run

func (j *JobsReleaseCmd) Run() error

Run releases the named job.

type MCPCmd

type MCPCmd struct {
	List  MCPListCmd  `cmd:"" help:"list the pipeline's mcp servers and whether each one answers"`
	Tools MCPToolsCmd `cmd:"" help:"list an mcp server's tools and their argument schemas"`
	Login MCPLoginCmd `cmd:"" help:"interactively authorize an oauth-configured mcp server"`
}

MCPCmd groups the two mcp_servers:-related subcommands: `tools` (list a server's tools — works for any auth type, and is the discovery/preflight step a pipeline author runs before writing a tool reference or a resource type's mcp: block) and `login` (the only interactive, state-writing command in this group — see internal/mcp/login.go). Neither `run` nor `watch` ever prompts; a headless process that hits an unauthorized oauth server just surfaces the actionable error naming this login command.

type MCPListCmd

type MCPListCmd struct {
	Pipeline string            `` /* 184-byte string literal not displayed */
	Offline  bool              `` /* 161-byte string literal not displayed */
	Name     map[string]string `` /* 158-byte string literal not displayed */
	DB       DB                `` /* 202-byte string literal not displayed */
}

MCPListCmd is the inventory: every mcp_servers: entry, how it connects, who consumes it, and — unless --offline — whether it answers right now.

`mcp tools` answers "what can this one server do", which presumes you already know which servers exist and which are working. This is the step before that, and the reason it probes by default: a server is configured in YAML but broken on this machine (binary not on PATH, token never obtained, endpoint moved), and the file alone cannot tell you which.

It reports rather than gates — a server that does not answer is a row with an ✗, not a non-zero exit. `steps validate --live` is the command that fails.

func (*MCPListCmd) Run

func (m *MCPListCmd) Run() error

Run prints one row per configured server.

type MCPLoginCmd

type MCPLoginCmd struct {
	TargetFlags      `embed:""`
	PipelineNameFlag `embed:""`
	Server           string `` /* 126-byte string literal not displayed */
	Config           string `` /* 148-byte string literal not displayed */
	// Name is which pipeline a -c login is filed under, the same name `steps run --name` gives its state.
	Name map[string]string `` /* 158-byte string literal not displayed */
	DB   DB                `` /* 176-byte string literal not displayed */
}

MCPLoginCmd interactively authorizes an auth: {type: oauth} mcp_servers: entry — the only command in this CLI that opens a browser or blocks on user interaction outside a pipeline run.

func (*MCPLoginCmd) Run

func (m *MCPLoginCmd) Run() error

Run is one of two logins, chosen by which pipeline was named. -c is a file here, so the login runs here: a loopback listener catches the redirect and the token lands in this user's config dir. -p is a pipeline a DAEMON serves, so the daemon runs it (daemon_login.go) — it is the machine that will spend the token, and one with no browser cannot be the loopback a redirect comes back to.

type MCPToolsCmd

type MCPToolsCmd struct {
	Pipeline string            `` /* 184-byte string literal not displayed */
	Server   string            `` /* 177-byte string literal not displayed */
	Name     map[string]string `` /* 158-byte string literal not displayed */
	DB       DB                `` /* 202-byte string literal not displayed */
}

MCPToolsCmd lists the tools a configured mcp_servers: entry exposes.

func (*MCPToolsCmd) Run

func (m *MCPToolsCmd) Run() error

Run loads the pipeline, resolves the named server, connects (per its configured auth), and prints each of its tools' name, description, and argument schema. An unauthorized oauth server surfaces oauthTokenSource's own actionable "run steps mcp login" error, unchanged.

type MemoryCmd

type MemoryCmd struct {
	List MemoryListCmd `cmd:"" default:"withargs"                        help:"list memory scopes, or one scope's entries with --scope"`
	Rm   MemoryRmCmd   `cmd:"" help:"delete entries from a memory scope"`
}

MemoryCmd is what agent steps keep about people between runs — readable and deletable by whoever runs the pipeline, not only by the model that wrote it.

type MemoryListCmd

type MemoryListCmd struct {
	ReadFlags `embed:""`
	Scope     string `help:"list this scope's entries instead of every scope" name:"scope"`
}

MemoryListCmd lists scopes, or one scope's entries.

func (*MemoryListCmd) Run

func (m *MemoryListCmd) Run() error

Run prints the listing.

type MemoryRmCmd

type MemoryRmCmd struct {
	ReadFlags `embed:""`
	Scope     string  `help:"the scope to delete from"                                     name:"scope"                                required:""`
	All       bool    `help:"delete every entry of the scope — everything kept about it"   name:"all"`
	IDs       []int64 `arg:""                                                              help:"entry ids, from steps memory --scope" optional:""`
}

MemoryRmCmd deletes named entries of one scope, or all of it.

func (*MemoryRmCmd) Run

func (m *MemoryRmCmd) Run() error

Run deletes what was named.

type PipelineCmd

type PipelineCmd struct {
	Set     PipelineSetCmd     `cmd:"" help:"upload a pipeline's configuration to a daemon"`
	List    PipelineListCmd    `cmd:"" default:"withargs"                                          help:"what the daemon is serving"`
	Get     PipelineGetCmd     `cmd:"" help:"print the configuration the daemon is serving"`
	Destroy PipelineDestroyCmd `cmd:"" help:"forget a pipeline and everything recorded under it"`
	Pause   PipelinePauseCmd   `cmd:"" help:"stop polling, admitting and triggering this pipeline"`
	Unpause PipelineUnpauseCmd `cmd:"" help:"let it run again"`
	Rename  PipelineRenameCmd  `cmd:"" help:"give a pipeline a new name, keeping its history"`
}

PipelineCmd groups the verbs that tell a daemon what to serve.

type PipelineDestroyCmd

type PipelineDestroyCmd struct {
	TargetFlags      `embed:""`
	PipelineNameFlag `embed:""`
	NonInteractive   bool `help:"do not ask" name:"non-interactive" short:"n"`
}

PipelineDestroyCmd forgets a pipeline and everything recorded under it.

func (*PipelineDestroyCmd) Run

func (p *PipelineDestroyCmd) Run() error

Run destroys the named pipeline.

type PipelineGetCmd

type PipelineGetCmd struct {
	TargetFlags      `embed:""`
	PipelineNameFlag `embed:""`
}

PipelineGetCmd prints what a daemon serves, which after a local edit is the only place that configuration still exists.

func (*PipelineGetCmd) Run

func (p *PipelineGetCmd) Run() error

Run prints the served source and names the files it carried.

type PipelineListCmd

type PipelineListCmd struct {
	TargetFlags `embed:""`
}

PipelineListCmd is what the daemon holds.

func (*PipelineListCmd) Run

func (p *PipelineListCmd) Run() error

Run prints one row per served pipeline.

type PipelineNameFlag

type PipelineNameFlag struct {
	Pipeline string `help:"the pipeline's name on the daemon" name:"pipeline" short:"p"`
}

PipelineNameFlag is which pipeline on that daemon a verb is about.

type PipelinePauseCmd

type PipelinePauseCmd struct {
	TargetFlags      `embed:""`
	PipelineNameFlag `embed:""`
}

PipelinePauseCmd throws the pipeline-level circuit breaker.

func (*PipelinePauseCmd) Run

func (p *PipelinePauseCmd) Run() error

Run pauses the named pipeline.

type PipelineRenameCmd

type PipelineRenameCmd struct {
	TargetFlags      `embed:""`
	PipelineNameFlag `embed:""`
	To               string `help:"the new name" name:"to" required:""`
}

PipelineRenameCmd moves a pipeline's identity, keeping its history.

func (*PipelineRenameCmd) Run

func (p *PipelineRenameCmd) Run() error

Run renames the named pipeline.

type PipelineSetCmd

type PipelineSetCmd struct {
	TargetFlags      `embed:""`
	PipelineNameFlag `embed:""`
	VarFlags         `embed:""`
	Config           string `help:"the pipeline YAML to upload" name:"config" required:"" short:"c" type:"path"`
	// The prompt exists because a set is a deploy, and a deploy nobody looked at is the file watcher this command replaced.
	NonInteractive bool `help:"do not show a diff or ask; apply it"                                        name:"non-interactive" short:"n"`
	Pause          bool `help:"pause the pipeline as part of the set, so it serves nothing until unpaused" name:"pause"`
}

PipelineSetCmd uploads a configuration, which is the only way a served pipeline changes.

func (*PipelineSetCmd) Run

func (p *PipelineSetCmd) Run() error

Run substitutes, bundles, diffs, asks, and uploads.

type PipelineUnpauseCmd

type PipelineUnpauseCmd struct {
	TargetFlags      `embed:""`
	PipelineNameFlag `embed:""`
}

PipelineUnpauseCmd releases it.

func (*PipelineUnpauseCmd) Run

func (p *PipelineUnpauseCmd) Run() error

Run unpauses the named pipeline.

type PlanCmd

type PlanCmd struct {
	StateFlags `embed:""`
	VarFlags   `embed:""`
	Pipeline   string            `arg:""                                                   help:"path to the pipeline YAML file"`
	Job        string            `help:"job to plan (defaults to the pipeline's only job)"`
	Pin        map[string]string `help:"pin a version field, e.g. number=87 (repeatable)"  name:"pin"`
	// Worker alone of ExecFlags: planning runs a tagged resource's check where
	// a run would, and nothing else of a run.
	Worker map[string]string `help:"map a resource tag to a worker, e.g. --worker vpc=ssh://jt@box (repeatable)" name:"worker"`
}

PlanCmd shows which steps a run would execute and which it would skip, without executing any of them.

It is a distinct verb rather than a --dry-run flag on `run`: previewing is a read, and a flag on the run command reads as "and then run it".

The planner already computes this and acts on it immediately, so finding out what a run would skip meant starting one — the wrong trade when the question is "is my cache in the state I think it is?".

func (*PlanCmd) Run

func (p *PlanCmd) Run() error

Run loads the pipeline, plans the selected job, and prints one line per step. Resource check commands run (planning has always resolved get versions), but no step executes and nothing is recorded.

type ProgressFlags

type ProgressFlags struct {
	Progress string `` /* 163-byte string literal not displayed */
}

ProgressFlags chooses how a run is drawn on the terminal that started it.

type QuestionsCmd

type QuestionsCmd struct {
	List   QuestionsListCmd `cmd:"" default:"withargs"                        help:"list ask_user questions waiting for an answer"`
	Answer AnswerCmd        `cmd:"" help:"answer a waiting ask_user question"`
}

QuestionsCmd is the "what is waiting on me?" command for questions, the way ApprovalsCmd is for approvals. It reads the same rows the audit trail is made of.

Separate from `steps approvals` because the two park for different reasons and are answered differently: an approval takes a yes or a no, a question takes a fact — and a listing that mixed them would have to leave out the options, which are most of what makes a question one keystroke to answer.

type QuestionsListCmd

type QuestionsListCmd struct {
	ReadFlags `embed:""`
}

QuestionsListCmd is the listing itself, and the group's default.

func (*QuestionsListCmd) Run

func (q *QuestionsListCmd) Run() error

Run prints every question waiting for an answer.

type ReadFlags

type ReadFlags struct {
	DB       DB     `` /* 132-byte string literal not displayed */
	Pipeline string `help:"the pipeline's name in the state database"                                                   name:"pipeline" short:"p"`
}

ReadFlags is what a command that only READS a daemon's database needs: which database, and which pipeline in it.

A NAME rather than a path, because a served pipeline no longer has a file here — `steps pipeline set` uploaded it, and the name is what the daemon, the /p/<slug> route and every row it wrote agree on. The database defaults to the daemon's own for the same reason: there is no YAML to derive one from.

type RejectCmd

type RejectCmd struct {
	ReadFlags `embed:""`
	ID        int64  `arg:""                                    help:"the approval id, from steps approvals"`
	Reason    string `help:"why — recorded with the decision"`
}

RejectCmd records a no.

func (*RejectCmd) Run

func (r *RejectCmd) Run() error

Run rejects the named approval.

type RunCmd

type RunCmd struct {
	StateFlags    `embed:""`
	VarFlags      `embed:""`
	ExecFlags     `embed:""`
	DeliverFlags  `embed:""`
	HistoryFlags  `embed:""`
	ProgressFlags `embed:""`
	Pipeline      string            `` /* 164-byte string literal not displayed */
	Job           string            `help:"job name to run (defaults to the pipeline's only job)"`
	Pin           map[string]string `` /* 137-byte string literal not displayed */
	Force         bool              `` /* 126-byte string literal not displayed */
	Resume        string            `` /* 140-byte string literal not displayed */
	Replay        string            `` /* 140-byte string literal not displayed */
	From          string            `` /* 138-byte string literal not displayed */
	Rerun         string            `` /* 139-byte string literal not displayed */
}

RunCmd runs a single job's plan once, exactly as steps has always done.

func (*RunCmd) Run

func (r *RunCmd) Run() error

Run loads the pipeline, selects a job, and runs it once via pipeline.RunJob.

type RunsAbortCmd

type RunsAbortCmd struct {
	TargetFlags      `embed:""`
	PipelineNameFlag `embed:""`
	RunID            string `arg:""                                                      help:"the run to stop" optional:""`
	Queued           string `help:"instead, drop this job's queued run before it starts" name:"queued"          placeholder:"JOB"`
}

RunsAbortCmd goes over HTTP rather than writing a row, because what it stops is a context inside the daemon's process that no database write can reach.

func (*RunsAbortCmd) Run

func (a *RunsAbortCmd) Run() error

Run stops the named run, or drops the job's queued one.

type RunsCmd

type RunsCmd struct {
	List   RunsListCmd   `cmd:"" default:"withargs"                                                 help:"runs, newest first"`
	Steps  RunsStepsCmd  `cmd:"" help:"individual steps, with what each one recorded"`
	Queue  RunsQueueCmd  `cmd:"" help:"what the trigger loop has queued"`
	Cost   RunsCostCmd   `cmd:"" help:"what a pipeline's agent steps spent"`
	Where  RunsWhereCmd  `cmd:"" help:"the machines a run's placed steps ran on"`
	Abort  RunsAbortCmd  `cmd:"" help:"stop a run on a steps web daemon, or drop a queued one"`
	Follow RunsFollowCmd `cmd:"" help:"watch a run to its end: live on a terminal, lines elsewhere"`
}

RunsCmd is what past runs recorded, in five views.

The store has always written all of this and offered no way to read it: the only route to "why did my last run fail" was opening .steps/state.db in sqlite and knowing the schema, which the vendored pure-Go driver means may not even be installed.

Subcommands rather than a flag switch, and the difference is not cosmetic: each view differs in what it needs NAMED. `list` reads one pipeline or every pipeline in a --db file; the other four are questions about one pipeline and cannot be anything else — a trigger queue belongs to a pipeline, a step's job name means nothing without one, and --run already refuses an id belonging to a neighbour in the same file. As flags on one command that distinction was a runtime table of which combinations to refuse; as subcommands it is the grammar, and kong enforces it.

type RunsCostCmd

type RunsCostCmd struct {
	ReadFlags `embed:""`
	RunID     string `arg:""       help:"break this one run down per step" optional:""`
	Limit     int    `default:"20" help:"maximum number of rows to show"`
}

RunsCostCmd is what agent steps spent: per run, or per step within one run.

The run id is a positional rather than a --run flag, because naming a run IS choosing the deeper view. As a flag it had to imply --cost to mean anything, which is a flag that reads as configured while binding nothing.

func (*RunsCostCmd) Run

func (r *RunsCostCmd) Run() error

Run prints per-run totals, or one run's steps.

type RunsFollowCmd

type RunsFollowCmd struct {
	ReadFlags     `embed:""`
	ProgressFlags `embed:""`
	RunID         string `arg:""                                   help:"the run to follow (default: the newest)" optional:""`
	Job           string `help:"follow the newest run of this job"`
}

RunsFollowCmd attaches the live view to a run some other process is executing — a daemon's, or a `steps run` in another terminal — by polling what it records, the way fly watch attaches to a build.

ponytail: reads the state database, so it cannot follow a daemon on another host; a JSON event stream under /api/pipelines/:p/runs/:run/events is the upgrade.

func (*RunsFollowCmd) Run

func (r *RunsFollowCmd) Run() error

Run follows one run to its end and exits as that run did.

type RunsListCmd

type RunsListCmd struct {
	ReadFlags `embed:""`
	Job       string `help:"only show runs of this job"`
	Limit     int    `default:"20"                      help:"maximum number of rows to show"`
}

RunsListCmd is the default view: runs, newest first — and the one view that answers for a whole state file when no pipeline is named.

func (*RunsListCmd) Run

func (r *RunsListCmd) Run() error

Run prints one pipeline's job runs, or every pipeline's in a shared file.

type RunsQueueCmd

type RunsQueueCmd struct {
	ReadFlags `embed:""`
	Limit     int `default:"20" help:"maximum number of rows to show"`
}

RunsQueueCmd is what the trigger loop has queued and not yet run.

func (*RunsQueueCmd) Run

func (r *RunsQueueCmd) Run() error

Run prints the trigger queue.

type RunsStepsCmd

type RunsStepsCmd struct {
	ReadFlags `embed:""`
	RunID     string `arg:""                             help:"what this run completed — the steps a --resume skips"   optional:""`
	Job       string `help:"only show steps of this job"`
	Limit     int    `default:"20"                       help:"maximum number of rows to show in the listing"`
}

RunsStepsCmd is the per-step detail: what previous runs actually did.

The run id is a positional for the reason RunsCostCmd's is: naming a run is asking for the deeper view — what that run completed, per build.

func (*RunsStepsCmd) Run

func (r *RunsStepsCmd) Run() error

Run prints recorded steps, newest first, or what one run completed.

type RunsWhereCmd

type RunsWhereCmd struct {
	ReadFlags `embed:""`
	RunID     string `arg:""                                 help:"the run to report on (default: the newest)" optional:""`
	Job       string `help:"take the newest run of this job"`
}

RunsWhereCmd is which machines a run's placed steps ran on.

func (*RunsWhereCmd) Run

func (r *RunsWhereCmd) Run() error

Run prints one run's placements.

type State

type State string

State is a resolved state database: a sqlite file path, or a postgres url.

A type rather than a string so that printing one is safe by default: a url may carry a password, and String — what %s and %v call — never shows it. Opening one goes through db.go, which is the one place that reads the raw value.

func DaemonStatePath

func DaemonStatePath(db DB) State

DaemonStatePath is the state database `steps web` and the read commands use.

func StatePath

func StatePath(pipeline string, db DB) State

StatePath returns the sqlite database path for pipeline's persisted job state: under .steps/ beside the pipeline YAML, named for the FILE — unless --db names one, which is how several pipelines come to share a file. A sqlite:// url and the bare path it wraps are one file.

Per file BY DEFAULT, not per directory, and that default is load-bearing on its own. Two pipelines in one folder are two namespaces, and a `.steps/state.db` that merged them by accident of layout was a bug: one pipeline's version change could enqueue a job the other then claimed and ran. Sharing is now something an operator asks for, and the database keeps them apart when they do — every row is scoped to a pipelines row (see internal/store/schema.go).

An older database is upgraded by the store's migrations; only one older than the oldest upgradable schema, or newer than this build, is refused.

func (State) LogValue

func (s State) LogValue() slog.Value

LogValue is String for slog, whose JSON handler would otherwise write the raw string a State is.

func (State) String

func (s State) String() string

String is the state database as it may be printed.

type StateFlags

type StateFlags struct {
	DB   DB                `` /* 149-byte string literal not displayed */
	Name map[string]string `` /* 131-byte string literal not displayed */
}

StateFlags are the two flags every command that opens a state database carries: WHERE the database is, and WHAT this pipeline is called inside it.

Embedded rather than repeated because they always travel together — a --db naming a shared file is exactly when --name starts to matter.

type TargetFlags

type TargetFlags struct {
	Target string `` /* 141-byte string literal not displayed */
}

TargetFlags is which daemon a verb talks to. No login and no saved targets, because there is nothing to log in to when the daemon binds the loopback this defaults to; a daemon that does ask takes its credentials from this URL's userinfo, which is a string an operator can keep in a password manager and a script can keep in one variable.

func (TargetFlags) Validate

func (f TargetFlags) Validate() error

Validate is kong's hook. A target that does not parse cannot have its credentials taken off, and every message after this one would quote it whole — url.Parse's own error included.

type TestCmd

type TestCmd struct {
	StateFlags    `embed:""`
	VarFlags      `embed:""`
	ExecFlags     `embed:""`
	DeliverFlags  `embed:""`
	ProgressFlags `embed:""`
	Pipeline      string `arg:""   help:"path to the pipeline YAML file"`
}

TestCmd runs every job in the pipeline (force, so nothing is skipped and the recorded execution order is deterministic, and versions an every-get already took are re-opened so a rerun is too) and verifies its assert: directives — each job's own assert.execution is checked inside RunJob, and a top-level assert.execution of job names is checked here. It's the entry point for a self-verifying fixture — every runnable example in docs/*.md is one (see docs_test.go).

func (*TestCmd) Run

func (t *TestCmd) Run() error

Run loads the pipeline, runs every job (force), and reports pass/fail per job plus the pipeline-level assert.execution. It returns a non-nil error if any job failed or the pipeline assert mismatched, so the process exits non-zero.

type ValidateCmd

type ValidateCmd struct {
	VarFlags `embed:""`
	Pipeline string `arg:""   help:"path to the pipeline YAML file"`
	// SyntaxOnly skips the checks about THIS MACHINE — credentials and MCP
	// binaries — leaving only the checks about the file. It exists for the
	// lint-in-CI case: a pre-commit hook or a build that checks a pipeline it
	// has no intention of running should not need that pipeline's production
	// credentials on hand.
	SyntaxOnly bool `help:"check the file only; skip credential and MCP-binary checks about this machine" name:"syntax-only"`
	// Live goes the other way: past what is knowable locally, out to the
	// models and MCP servers themselves. It was `steps preflight`, which is
	// the same read at a different depth — and a verb whose only difference
	// from this one was how far it looked.
	Live bool   `help:"also probe the models and MCP servers, live"                                    name:"live"`
	Job  string `help:"with --live, probe only this job's models and MCP servers (default: every job)"`
	// Name is here because --live dials oauth servers with the pipeline's own login, which is filed under this name.
	Name map[string]string `` /* 158-byte string literal not displayed */
	DB   DB                `` /* 176-byte string literal not displayed */
}

ValidateCmd checks a pipeline and prints what's wrong with it, without running any of it.

Every check it performs already existed; the only way to reach them was to start a run, which opens a state store, builds a workspace, preflights docker, and then begins executing steps. That made "is my YAML right?" an expensive, side-effecting question — worst while writing the pipeline, which is exactly when it gets asked. This command answers it with no store, no workspace, no containers, and nothing written to disk.

func (*ValidateCmd) Run

func (v *ValidateCmd) Run() error

Run loads the pipeline (which runs every config-level validator) and then checks artifact flow for each job, joining the failures so one invocation reports everything wrong with the file.

type VarFlags

type VarFlags struct {
	Var      map[string]string `help:"set a pipeline var, e.g. --var repo_uri=https://..." name:"var"       short:"v"`
	VarsFile string            `help:"YAML file of pipeline vars"                          name:"vars-file"`
}

VarFlags carry ((name)) substitutions into a pipeline load.

Declared once and embedded rather than repeated, because five commands take them and a copy that drifts is a flag that silently means something else on one verb. Load is part of the embed for the same reason: a command that takes these flags and loads the pipeline some other way has declared an input it does not read, which is the shape of bug this consolidation exists to make impossible.

func (VarFlags) Load

func (v VarFlags) Load(path string, name string) (*config.Config, error)

Load reads the pipeline at path, under the identity name, with these vars applied.

name is passed rather than derived from path because --name lives on StateFlags and this embed cannot see it: a caller resolves the identity once (resolvePipelineName) and hands the same string to the store and to here, so the two cannot disagree. Positional for the reason config.Load makes it so.

type WebCmd

type WebCmd struct {
	ExecFlags    `embed:""`
	HistoryFlags `embed:""`
	// Its own --db rather than StateFlags, whose --name binds nothing here: a name is chosen by `steps pipeline set -p`, and a flag that parses and threads nowhere reads as configured.
	DB            DB                `` /* 220-byte string literal not displayed */
	Listen        string            `` /* 153-byte string literal not displayed */
	Interval      time.Duration     `` /* 176-byte string literal not displayed */
	MaxConcurrent int               `` /* 193-byte string literal not displayed */
	MaxRuns       int               `` /* 202-byte string literal not displayed */
	Pin           map[string]string `` /* 137-byte string literal not displayed */
	Force         bool              `` /* 126-byte string literal not displayed */
	// A statement about the BROWSER's surface only; `steps pipeline set` is the deployment path and is deliberately not withheld — see docs/web.md.
	ReadOnly bool `` /* 173-byte string literal not displayed */
	// Both or neither, refused below: half a pair is a daemon somebody believes is protected. Env vars because a password on a command line is in every ps listing and every shell history.
	BasicAuthUsername string   `` /* 305-byte string literal not displayed */
	BasicAuthPassword string   `` /* 305-byte string literal not displayed */
	LocalUser         []string `` /* 302-byte string literal not displayed */
	ActivityHistory   int      `` /* 302-byte string literal not displayed */
	Role              []string `` /* 302-byte string literal not displayed */
	GitHubClientID    string   `` /* 295-byte string literal not displayed */
	GitHubSecretEnv   string   `` /* 329-byte string literal not displayed */
	GoogleClientID    string   `` /* 295-byte string literal not displayed */
	GoogleSecretEnv   string   `` /* 329-byte string literal not displayed */
	LocalUsersFile    string   `` /* 330-byte string literal not displayed */
	// Implied on a loopback listen (hostOptions); elsewhere opt-in, because a reverse proxy forwards the client's Host, so only the operator knows this daemon's names; see docs/web.md#security.
	AllowedHost []string `` /* 217-byte string literal not displayed */
	// No env: binding: a STEPS_*-shaped name invites confusion with the variable this one sets.
	ExternalURL string `help:"the URL steps publish to every step as STEPS_URL (default http://<listen>)" name:"external-url"`
	// contains filtered or unexported fields
}

WebCmd is the daemon: it serves the pipeline UI, holds whatever pipelines have been set into it, polls their trigger: true resources, and runs the jobs both of those enqueue.

It takes no pipeline arguments, and that is the whole shape of it: a pipeline arrives by `steps pipeline set` and by nothing else, so vars are per-set rather than process-wide, a pipeline's identity is a name somebody chose rather than a filename, and a change happens when somebody asks for it rather than when a file moves. Nothing here watches a file.

It binds loopback by default and has no authentication, because there is nothing to authenticate against — this is the local runner's own front end, in the same trust domain as the shell that started it. `steps pipeline set` makes that sharper than it was: a pipeline is arbitrary commands, so the set endpoint is a remote shell. --listen exists for the person who has decided that is what they want, not as a default.

func (*WebCmd) Run

func (w *WebCmd) Run() error

Run serves until canceled, holding whatever the state database says was set.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL