Running targets side by side

A command's composed targets run in order. A parallel stage runs its targets side by side instead: each starts once the stages before it have finished, and the stages after it wait for all of them.

C#, on a RunnaApp<TUnion>:

C#
Release => app.Command()
    .Requires(new Test())
    .Parallel(new PackNuget(), new DocsBuild(), new DockerImage())
    .Composes(new Publish()),

C#, on a RunnaBuild class:

C#
public Command Release => _ => _
    .Requires(Test)
    .Parallel(PackNuget, DocsBuild, DockerImage)
    .Composes(Publish);

F#, in a command { } block, where the operation is concurrently because F# reserves parallel:

Fsharp
| Release -> command {
      requires [Test { Filter = None }]
      concurrently [Pkg Generate; Docs Build; Docker Image]
      composes [Publish] }

Stages run in the order they are declared. Requires always run before the composed stages. A target two stages share runs once, the first time it is reached.

--graph draws a parallel stage as a parallel group. A command's --help numbers the targets of one stage the same.

-j / --jobs caps how many targets run at once. The default is one per processor. -j 1 runs a parallel stage one target at a time, in the order it is declared, which helps when the output of two targets is hard to tell apart.

Shell
./build.cs release -j 4
./build.cs release -j 1

The first failure stops the run. The targets still running are cancelled, and no more start. The run exits with the failed target's exit code.

-k / --keep-going lets the targets that do not depend on the failure finish. A target that waits for the failed one does not start. The run still exits with the first failure's exit code.

A target body stops when it is cancelled only if it watches for it:

  • Shell.Exec in C#, and sh in an F# target { } block, watch for it and stop the process they started.
  • An async body can take the token: app.Target(async ct => await Task.Delay(1000, ct)), or read RunnaContext.Current.CancellationToken.

While targets run side by side, each line of their output carries the target's name:

Text
Starting  pkg generate
Starting  docs build
[pkg generate] Successfully created package 'Example.1.0.0.nupkg'.
[docs build] Build succeeded.
Finished  docs build (4.1s)
Finished  pkg generate (6.3s)

  docs build        4.1s
  pkg generate      6.3s
  Took 6.4s for 10.4s of work

--output grouped holds a target's lines back and writes them together when it finishes. On GitHub Actions each target's lines become a collapsible group.

The summary at the end lists each target's time, and the wall-clock time against their sum.

A command run with Shell.Exec streams its output through the target's lines. Its output is then a pipe, not a terminal, so a tool that colours only for a terminal prints without colour. A run with one job, or without a parallel stage, leaves every command on the console as before.

In F#, the sh commands of a target { } block running side by side do the same: their lines carry the target's name, and a failure elsewhere stops them. So do the commands of a command's executes { } block.

A global option the build declares itself keeps its name. A build with its own -k flag keeps it, and --keep-going is the long form left for the switch.