Skip to content

Capabilities

Capabilities

One line per custom operation on the four builders, per Input combinator, and per Cmd argument helper. The API reference has full signatures and remarks. Composing reusable blocks has worked examples.

How settings resolve

A stage setting resolves by walking upward: the stage, its parent stage, the parent's parent, then the pipeline. The first level that sets it wins, so a pipeline's workingDir covers every stage under it, and a nested stage overrides it for itself and its children. This applies to workingDir, envVars, the timeouts, acceptExitCodes, the output sink, noPrefixForStep, noStdRedirectForStep and verbosity.

Conditions are the exception. when', whenEnvVar, whenBranch and the platform operations conjoin: a second condition on the same stage narrows it to the logical AND of both. Use whenAny { } to widen.

A command's copies of the pipeline settings are defaults, not overrides: they reach every pipeline the command runs, but only where that pipeline left the setting alone, regardless of write order. noPrefixForStep and noStdRedirectForStep are plain bools with no unset state, so a pipeline setting either one to the value PipelineContext.create already gives it is indistinguishable from leaving it untouched — the command default overwrites it either way.

Timeouts

Three names. Meaning shifts with the builder they sit on.

BuildertimeouttimeoutForStagetimeoutForStep
stagethis stage as a whole—each step of this stage
pipelinethe whole pipeline runeach stage's defaulteach step's default
command / rootCommandpipeline default for the whole runpipeline default for each stagepipeline default for each step

The unit differs by builder:

  • pipeline — int<second>, float seconds or a TimeSpan
  • stage — plain int seconds, float seconds or a TimeSpan
  • command / rootCommand — int seconds or a TimeSpan

Stage operations

Available inside stage, and inside whenStage, which accepts everything stage does.

OperationWhat it does
runAdds a step. Takes a literal command line, a Cmd, or a function of the StageContext returning unit, int, Result<unit, string>, a Cmd, an Async<_> or a Task<_> of any of those, optionally wrapped in option
runSensitiveAdds a step from an interpolated command line with every hole masked as *** wherever the library prints it
runOperationAdds a step from an Operation<unit>, with an optional label for --explain. Runs under the stage's working directory, environment, acceptable exit codes and output routing
runHttpHealthCheckAdds a step that polls a URL until it answers or the stage is cancelled
echoAdds a step that prints a message through the stage's output sink
when'Runs the stage only when a bool holds, or only when a given StageContext succeeds
whenEnvVarRuns the stage only when an environment variable is set, or set to a given value; also takes an EnvArg
whenBranch / whenBranchesRuns the stage only on the named git branch. Reads git branch --show-current in the stage's working directory; a missing git evaluates false rather than throwing
whenWindows / whenLinux / whenOSXRuns the stage only on that platform. Pass false to invert
whenPlatformThe same over an OSPlatform value
workingDirThe directory this stage's child processes start in. Takes a string or a DirectoryInfo
envVarsEnvironment variables for this stage's child processes. Applied to ProcessStartInfo, so the host process's own environment is untouched
timeoutCancels the stage after the given duration
timeoutForStepCancels any one step of the stage after the given duration
retryRuns the stage's steps again after a failing attempt, up to the given count. timeout remains the budget for the whole stage, retries included
parallel'Runs the stage's steps concurrently. true/0/-1 unbounded, 1/false sequential, n throttled to exactly n in flight; also takes a StageContext -> _ condition
consumesAdds a step that reads a DependencySpec<'D> and runs an Operation<unit> over its value, and adds the required producers to this stage's prerequisites. See Producers and dependencies
onFailureRegisters a handler that runs once, after this stage's own retry attempts are exhausted. See Failure handlers
acceptExitCodesThe exit codes that count as success. Replaces the default [0]
failIfIgnoredFails the pipeline when this stage is inactive, instead of skipping it
failIfNoActiveSubStageFails the pipeline when none of this stage's sub-stages is active
continueStepsOnFailureRuns the remaining steps after one fails
continueStageOnFailureRuns the remaining stages after this one fails
continueOnStepFailureBoth of the above at once
outputToSends this stage's step output to a StageOutput — Console, Silent, Captured or Redirect
silentOutputDrops this stage's step output. A failure still reports its exit code
captureOutputHolds this stage's step output back and lifts it into the error message when a step fails. Takes an optional OutputCapture to keep the lines either way
redirectOutputHands each line to StdStream -> string -> unit as it arrives, from both streams' reader threads
noPrefixForStepStops each step's output being prefixed with its stage and step index
noStdRedirectForStepStops redirecting the child's stdout/stderr — the mechanism every output operation above depends on — and overrides all of them
shuffleExecuteSequenceRandomises step order at each run
verbosityHow much of the pipeline's own log this stage prints. Takes Verbosity.Quiet, Normal or Verbose
verbose / quietverbosity Verbose and verbosity Quiet

A stage nested inside another is one step of its parent. Stages nest to any depth. A block is a value that stage, pipeline or command can yield.

Running a command from inside a step

run and runSensitive cover the case where a command's exit code is the whole result. A step built with run (fun ctx -> ...) reaches for one of these Operation<'T> functions when it needs the command's output as a value:

FunctionWhat it does
execute cmdRuns cmd, streaming its output through the stage's own output routing. Fails on an exit code the stage does not accept; carries no captured text
executeCapture cmdRuns cmd, capturing stdout and stderr instead of streaming them. Fails on an unaccepted exit code the same way execute does, with the captured CommandResult attached to the failure as evidence
attemptCapture cmdRuns cmd, capturing stdout and stderr, and always answers the CommandResult — an unaccepted exit code included. A process-start failure and a cancellation remain outcomes of their own, never a CommandResult; only a process that ran to completion produces one, and branching on its exit code is the caller's

Captured stdout and stderr are the child's raw bytes, ahead of any prefix or display formatting — application data that can hold secrets. Printing a successful capture is the caller's decision. Neither executeCapture nor attemptCapture does it automatically.

Pipeline operations

Available inside pipeline "name" { } and inside Command.pipeline { }, which takes the name and description of the command that runs it.

OperationWhat it does
descriptionThe pipeline's description. Discarded in Command.pipeline { }, which always takes the command's own name and description instead
timeoutCancels the whole pipeline after the given duration
timeoutForStageThe default timeout of each stage
timeoutForStepThe default timeoutForStep of each stage
workingDirThe default working directory of every stage. Takes a string or a DirectoryInfo
envVarsEnvironment variables every stage inherits. Appends to the pipeline's map rather than replacing it
acceptExitCodesThe exit codes that count as success. Replaces the default [0]
outputToThe default output sink of every stage
silentOutputDrops every stage's step output
captureOutputHolds every stage's step output back, lifting it into the error message on failure
redirectOutputHands every line of step output to StdStream -> string -> unit
noPrefixForStepStops step output being prefixed with the stage and step index
noStdRedirectForStepStops redirecting child stdout/stderr
runBeforeEachStageA StageContext -> unit hook run before each stage. Replaces the previous hook
runAfterEachStageA StageContext -> unit hook run after each stage. Replaces the previous hook
postThe stages that run after the main stages whether or not the pipeline succeeded — the teardown slot. Replaces any post stages already declared
verbosityHow much the pipeline prints. Takes Verbosity.Quiet, Normal or Verbose
verbose / quietverbosity Verbose and verbosity Quiet
onFailureRegisters a handler that runs once per failed run, after the handlers of every stage of that run. See Failure handlers

command/rootCommand carries no onFailure default. Such a default would have to cover a failure of InputSpec.Read or of CLI parsing, ahead of every pipeline it runs. Nothing observes that failure today.

Producers and dependencies

A Producer<'T> is a typed, named unit of deferred work with its own CLI inputs and its own prerequisites. Declaring one registers its identity and harvests those inputs. Nothing runs until a consumer schedules it.

FunctionWhat it does
Producer.define name inputs dependencies executeDeclares a producer: its own InputSpec<'I>, a DependencySpec<'D> of prerequisites, and the work computing 'T from both
Producer.stagePlaces a producer at this exact point of a pipeline or parent stage, rather than leaving its placement implicit
Producer.emptyDefine name executeDeclares a producer with no inputs or dependencies; identifies the operation as one that should only be run once.
DependencySpec.emptyA specification with no prerequisites
DependencySpec.require producerA specification requiring one producer and reading its result
DependencySpec.map fn specThe prerequisites of spec, its value read through fn
DependencySpec.map2 fn first secondThe prerequisites and inputs of both specifications, unioned, their values read through fn
DependencySpec.zip first secondA specification requiring both producers and reading their results as a pair
Stage.consuming name dependencies executeA stage whose one step is execute run over dependencies, with no CLI inputs of its own
Stage.consumingWith name inputs dependencies executeThe same, plus CLI inputs the stage itself declares

A producer's handle carries an identity allocated when declared. Two declarations sharing a name and arguments are distinct producers with distinct results. Depending on the same handle from more than one consumer runs it once per invocation and shares that result. An unlisted producer required by a stage runs immediately before that stage, after its own prerequisites. Listing it explicitly (Producer.stage, or yielding it into a pipeline) fixes its position instead. A consumer running under a parallel' or shuffleExecuteSequence scope reads a value published before that scope began. Placing a producer inside such a scope fails validation, naming the producer and the scope.

A required producer that is skipped or fails leaves its consumers skipped, carrying a dependency reason. An Option/ValueOption result models an intentional absence, distinct from a failed producer, and a consumer can handle it directly. Retrying a consumer through retry reuses the successful results of producers outside the retried scope. A producer owned by the retried scope gets a fresh result on each attempt, and a failed attempt leaves no value for the next one to read.

Failure handlers

onFailure registers a FailureContext -> unit handler on a stage or a pipeline. It runs once per failed execution of that scope, after the scope exhausts its retry attempts, and inner handlers run before outer ones — a stage's handler before the pipeline's. A recovered retry, or a cancelled stage, runs no handler. A stage's own timeout and timeoutForStep count as failures of that stage. A cancellation from an ancestor's token, the pipeline's, or the invocation's runs no handler.

The handler reads FailureContext.Primary/.Secondary for the causes recorded, and FailureContext.TryGetOutput producer for a value the invocation has already published — answering ValueNone for a producer that has not run. An exception out of a handler becomes one more cause of the same scope. The original failure stays primary, and successful reporting preserves it. A failing handler triggers no second run of itself.

Known limitations:

  • A handler is synchronous and unbounded, with no cleanup operation or budget separate from its own body.
  • onFailure has no equivalent on command/rootCommand. A failure of InputSpec.Read or of CLI parsing reaches no handler.
  • A stage with no timeoutForStep runs its steps under the attempt's own cancellation source, not a budget of its own. A step still in flight when its scope unwinds keeps its sources undisposed, rather than racing a straggler that may still read them.
  • FailureCause.summarise, used in ScopeReports, keeps only the first line of a multi-line capture. Later lines are lost from the report, not merely hidden from the rendering.
  • OperationFailedException's message is written by hand for each FailureCause case rather than through FailureCause.describe, so the two can drift.
  • whenStageSucceeds (the body of whenStage) reads the policy-folded outcome of the condition stage: one carrying continueStageOnFailure reports itself as succeeded even where it failed.
  • The gate placing an unlisted producer immediately before a whenStage consumer evaluates that consumer's condition stage a second time, beyond whenStage's own evaluation.
  • PipelineContext.run, called directly rather than through a command's own invocation, skips DependencyPlan.validate: an arrangement validation would reject — a producer inside a parallel' scope, say — runs instead of failing up front.

Migrating work out of InputSpec.Read

InputSpec<'T>.Read is a projection, ParseResult -> 'T, called once per invocation to bind the CLI values a stage declared. Effects belong in a step or a producer, not in Read: a Read that shells out or writes a file runs on every path that resolves inputs, --help and --explain included, since resolution happens ahead of the flag check.

Before, doing the work inside Read:

let publish =
    input {
        let! tag = Input.option<string> "--tag" |> Input.def "v0.0.0"
        // Runs on every resolution of this input, --help and --explain included.
        let manifest = fetchManifest tag
        return stage "publish" {
            run (cmd $"deploy --version {manifest.Version}")
        }
    }

After, the same CLI option feeding a producer, and the stage consuming its typed result:

let tag = Input.option<string> "--tag" |> Input.def "v0.0.0"

let manifest: Producer<Manifest> =
    Producer.define "manifest" (InputSpec.ofInput tag) DependencySpec.empty (fun tag () ->
        Operation.ofAsync (fetchManifestAsync tag))

let publish =
    pipeline "release" {
        stage "publish" {
            retry 2
            onFailure (fun context ->
                context.TryGetOutput manifest
                |> ValueOption.iter (fun manifest -> printfn $"publish failed for {manifest.Version}"))
            consumes (DependencySpec.require manifest) (fun manifest ->
                execute (cmd $"deploy --version {manifest.Version}"))
        }
    }

Read now binds only the option. --help and --explain resolve it without running fetchManifest or deploy, since a producer runs only where its consumer is scheduled. The consumer's retry repeats the deploy alone: manifest is required, not retried, so a failing deploy re-reads the same published value rather than re-fetching it. Its onFailure reads that same value back out of the failure it is given. The CLI layer stays applicative: tag is still an ordinary ActionInput<string>, readable without a ParseResult, exactly as before.

Command operations

Available inside command "name" { } and rootCommand argv { } / rootCommandOfScript { }, except for the three marked as root-only.

OperationWhat it does
descriptionThe command's description, shown in help
alias / aliasesAlternative names for the command. These accumulate
hiddenKeeps the command out of help output
addCommand / addCommandsAdds subcommands. Yielding a Command value does the same
addInput / addInputsRegisters an option or argument no pipeline asks for. Options a stage binds are registered already
timeoutPipeline default: the whole run. Takes int seconds or a TimeSpan
timeoutForStagePipeline default: each stage. Takes int seconds or a TimeSpan
timeoutForStepPipeline default: each step. Takes int seconds or a TimeSpan
workingDirPipeline default: the directory commands run in
envVarsPipeline default, per key: a pipeline that sets one of these keys itself keeps its own value and the rest still apply
acceptExitCodesPipeline default: the exit codes that count as success
outputTo / silentOutput / captureOutput / redirectOutputPipeline default: where step output goes
noPrefixForStep / noStdRedirectForStepPipeline default: prefixing and child stream redirection
runBeforeEachStage / runAfterEachStagePipeline default: the per-stage hooks
postPipeline default: the teardown stages
verbosity / verbose / quietPipeline default: how much the pipeline prints
nameRoot only. What the root command calls itself in help and usage. Defaults to the script's filename
parserConfigurationRoot only. A System.CommandLine ParserConfiguration
invocationConfigurationRoot only. A System.CommandLine InvocationConfiguration

A command yields stages directly — command "test" { Stages.restore; Stages.test } — and consecutive stages become one implicit pipeline carrying the command's name and description. Command.pipeline { } is that same pipeline written out, for when it needs pipeline-level settings. pipeline "name" { } is for several pipelines under one command, or a pipeline that needs its own name.

Condition builders

whenAll { }, whenAny { } and whenNot { } take these. Each yields a single condition to a stage. An empty whenAll/whenNot is always active. An empty whenAny never is.

OperationWhat it does
when'A bool, or a StageContext that must succeed
envVarAn environment variable by name, by name and value, or as an EnvArg
branch / branchesThe current git branch
platformWindows / platformLinux / platformOSXThe running platform. Pass false to invert
platformThe same over an OSPlatform value

whenEnv { } describes one environment variable in place of a wall of overloads: name, description, value, acceptValues and optional. whenStage "name" { } runs a stage for its result — everything stage accepts is accepted, and the stage runs for real, side effects included.

whenSome value build and whenOk value build are functions rather than operations. Each returns a StageContext list: the stage built from the bound value, or []. The absent case is an empty list, not an inactive stage requiring a name. build receives the value already unwrapped, inside the condition that guards it.

Input combinators

Declaring functions:

FunctionWhat it makes
Input.option<'T> "--name"An option bound as 'T
Input.optionMaybe<'T> "--name"An option bound as 'T option, None when absent
Input.argument<'T> "name"A positional argument bound as 'T
Input.argumentMaybe<'T> "name"A positional argument bound as 'T option
Input.contextInjects the ActionContext — the ParseResult and a cancellation token
Input.inject valueInjects a value that is not parsed from the command line
Input.ofOption / Input.ofArgumentLifts a raw System.CommandLine Option<'T> / Argument<'T>

Shaping combinators, all ActionInput<'T> -> ActionInput<'T> and all pipeable:

FunctionWhat it does
Input.alias / Input.aliasesAdds alternative names. Options only
Input.description, Input.descThe help text
Input.helpNameThe value placeholder in help — <Debug\|Release>
Input.defaultValue, Input.defThe value used when the token is absent
Input.defaultValueFactoryThe same, computed from the ArgumentResult
Input.arityHow many values are accepted: ExactlyOne, OneOrMore, Zero, ZeroOrMore, ZeroOrOne, or ArgumentArity (min, max)
Input.requiredMarks an option required
Input.recursiveApplies the option to the command and, recursively, its subcommands
Input.hiddenKeeps it out of help output
Input.allowMultipleArgumentsPerTokenLets one identifier token carry several values
Input.acceptOnlyFromAmongRestricts to a set of legal strings, ordinally
Input.addCompletion / Input.addCompletionsAdds tab-completion suggestions without restricting what is accepted
Input.mapFromAmong<'T> [ "key", value ]An option over a known set, each key bound to a typed value
Input.mapFromAmongWith<'T> comparermapFromAmong under an explicit StringComparer
Input.mapFromMany / mapFromManyWithThe repeatable forms, binding 'T list
Input.acceptLegalFileNamesOnly / Input.acceptLegalFilePathsOnlyRestricts to legal file names / paths
Input.validateA 'T -> Result<unit, string> check; Error becomes a CLI validation message
Input.validateFileExists / Input.validateDirectoryExistsThe two common cases, over FileInfo / DirectoryInfo
Input.addValidatorA raw SymbolResult -> unit validator
Input.customParserAn ArgumentResult -> 'T parser
Input.tryParseAn ArgumentResult -> Result<'T, string> parser; Error becomes a parse diagnostic instead of an exception
Input.editOption / Input.editArgumentReaches the underlying Option<'T> / Argument<'T> for anything not covered above

InputSpec<'T>

InputSpec<'T> is public at Partas.Build. A stage factory parameterised by an option needs no open Partas.Build.Internal:

let build (projects: InputSpec<string list>) = input {
    let! projects = projects
    and! config = Options.config
    ...
}
FunctionWhat it does
InputSpec.ofInputLifts an ActionInput<'T> into a spec
InputSpec.retA spec that reads nothing and returns a constant
InputSpec.mapReshapes the value a spec reads
InputSpec.map2Combines two specs, unioning their inputs
InputSpec.sequenceA list of specs into one spec of a list
InputSpec.traversesequence over the results of a mapping
InputSpec.unionConcatenates input lists, keeping the first occurrence of each

The input { let! … and! … return … } CE is the usual way to build one; inputs is the same builder under a second name (src/Partas.Build/Builders/Inputs.fs binds both). It is applicative: bind every source in a single let!/and! group. A sequential second let! is a compile error (FS0708): the input set must be readable before anything is parsed. An input { } nested inside another's return produces an InputSpec<InputSpec<_>>, which nothing accepts — pass the source in as an InputSpec instead.

Cmd

A Cmd keeps the executable and its arguments apart all the way to ProcessStartInfo.ArgumentList, so the platform does the escaping.

FunctionWhat it does
cmd $"dotnet build {project}"Each hole becomes exactly one argument, whatever it contains. run $"..." binds to the string overload and flattens the holes, so interpolate through cmd
Cmd.ofStringSplits a whole command line, honouring " and '
Cmd.create exe argsThe executable exactly as given, plus an argument string split as ofString does
Cmd.ofList exe argsBoth exactly as given
Cmd.arg / Cmd.argsAppends arguments exactly as given
Cmd.argIf cond valuesAppends only when cond holds — one line instead of two whole command lines under an if
Cmd.argWhenSome value renderAppends the arguments rendered from a Some, and nothing from a None
Cmd.secretArgAppends one argument whose value is masked wherever the command is printed
Cmd.secretOption flag valueAppends a visible flag and a masked value: -k ***
Cmd.secretOptionWhenSome flag valueThe same when the value exists, appending nothing otherwise
Cmd.secret / Cmd.sensitiveMarks a string unprintable before it goes into a cmd hole
Cmd.ofFormattable secretThe interpolation reader behind cmd and runSensitive
Cmd.toLogStringHow the command prints: secrets masked, whitespace-carrying arguments quoted

Args

The arguments a script was given, as distinct from the ones its host was given.

FunctionWhat it answers
Args.script ()The running script's own arguments. rootCommandOfScript { } is rootCommand (Args.script ()) { }
Args.scriptName ()The running script's filename, when it was launched as one
Args.afterScript argvEverything after the .fsx in argv, or after argv[0] when there is none. A leading -- is dropped
Args.take argvEverything after the first --
Args.nameOf argvThe filename of the first .fsx in argv

dotnet fsi build.fsx -- test --quick does not reach the process with its -- intact: the dotnet driver consumes one before fsi sees the command line. Args.script locates the script's own filename instead of splitting on a separator.

Baked

Ready-made declarations for the options every build CLI ends up wanting. They ship in their own package, Partas.Build.Baked.

Each declaration is a BuildOption<'T> carrying both forms: .option is the flag, .argument the positional equivalent. BuildOption.map, .mapOpt and .mapArg apply an Input.* combinator to both forms or to one.

ValueWhat it declares
Baked.NuGet.apiKeynuget-key (aliases --nuget, -k) as string option, defaulting to the NUGET_API_KEY environment variable
Baked.Dotnet.configconfiguration (alias -c) as string option, over release/r/debug/d case-insensitively
Baked.SemVer.bumpbump as Bump option, over major\|minor\|patch\|alpha\|beta\|rc\|preview\|<SEMVER>, defaulting to Patch
Baked.Common.isCI--ci, defaulting to true when any of the usual CI environment variables is set
FunctionWhat it does
Baked.SemVer.Version.apply bump versionSemantic version arithmetic over a Bump
Baked.SemVer.Version.assembly versionThe assembly version that goes with a package version: its major, and nothing else
Baked.SemVer.Version.IO.writeVersion / setVersionRewrites <Version> and <AssemblyVersion> in a project file
Baked.SemVer.Version.IO.bumpVersion projPath bumpApplies a bump to a project file in place, answering the versions before and after
Baked.SemVer.Stages.bumpArgument projectsA bump stage taking the bump kind as a positional argument
Baked.SemVer.Stages.bumpOption projectsThe same with the bump kind as --bump

Reference

Edit this page