Partas.Build
0.6.2
See the version list below for details.
dotnet add package Partas.Build --version 0.6.2
NuGet\Install-Package Partas.Build -Version 0.6.2
<PackageReference Include="Partas.Build" Version="0.6.2" />
<PackageVersion Include="Partas.Build" Version="0.6.2" />
<PackageReference Include="Partas.Build" />
paket add Partas.Build --version 0.6.2
#r "nuget: Partas.Build, 0.6.2"
#:package Partas.Build@0.6.2
#addin nuget:?package=Partas.Build&version=0.6.2
#tool nuget:?package=Partas.Build&version=0.6.2
Partas.Build
An F# build-pipeline DSL: a stage declares the CLI options it reads, and a command derives its
System.CommandLine option set from the stages it runs. Options, validation, and help text generate from the
pipeline definition instead of by hand. Runs from a .fsx script or a build project.
- Documentation: https://shayanhabibi.github.io/Partas.Build
- Every operation, one line each:
docs/content/Build/CAPABILITIES.md(rendered) - Agents: start at https://shayanhabibi.github.io/Partas.Build/llms.txt
The entire
FSharp.SystemCommandLinelibrary is copied directly into this repo. All credit to the original author. Much of the pipeline implementation is copied fromFun.Build. All credit to the original author.
Options are declared where they are read
A flag that a stage reads but the CLI does not accept is not expressible. Neither is a flag registered on a
command whose stages ignore it. Both are routine failures in hand-wired System.CommandLine setups. The
option set of a command is the union of what its stages bind — the two agree by construction — and two
stages binding the same option register it once.
module Options =
let quick =
Input.option<bool> "--quick"
|> Input.alias "-q"
|> Input.desc "Skip restores and cleaning"
let config =
Input.option<string> "--configuration"
|> Input.alias "-c"
|> Input.def "Release"
|> Input.helpName "Debug|Release"
|> Input.acceptOnlyFromAmong [ "Debug"; "Release" ]
|> Input.desc "Build configuration"
module Stages =
let restore = input {
let! quick = Options.quick
return stage "restore" {
when' (not quick)
run "dotnet restore"
}
}
let build = input {
let! config = Options.config
return stage "build" {
run (cmd $"dotnet build -c {config}")
}
}
let test = input {
let! config = Options.config
return stage "test" {
run (cmd $"dotnet test -c {config} --no-build")
}
}
do exit (
rootCommandOfScript {
description "The repository build"
command "build" {
description "Restore and build"
Stages.restore
Stages.build
}
command "test" {
description "Restore, build and test"
Stages.restore
Stages.build
Stages.test
}
})
The command registers nothing. dotnet fsi build.fsx -- test --help:
Description:
Restore, build and test
Usage:
build.fsx test [options]
Options:
-q, --quick Skip restores and cleaning
-c, --configuration <Debug|Release> Build configuration [default: Release]
-?, -h, --help Show help and usage information
--configuration is bound by two of the three stages and appears once. --quick reaches test only because
Stages.restore is in it: delete that one line and the flag leaves test --help in the same edit.
Did you look for this?
| You want | Use |
|---|---|
| An environment variable for one stage and its children | envVars on the stage — it is applied to the child process, so your own environment is untouched and needs no restore |
| A secret in a command line | runSensitive $"...", or Cmd.secretOption — every hole is masked *** wherever the library prints it |
| A stage's output only when it fails | captureOutput |
| Another script's commands as subcommands | #load it and yield the Command value — see Composition |
| An option with a fixed set of legal values, each bound to a typed value | Input.mapFromAmong |
| A flag added to a command line only sometimes | Cmd.argIf, or Cmd.argWhenSome |
| A working directory for a stage's children | workingDir on the parent — it is inherited |
| A stage that exists only when an option has a value | whenSome, which yields no stage for None rather than an inactive one |
| A block of stages parameterised by an option someone else declares | Take an InputSpec<'T> parameter and let! it |
| The root command to call itself something other than the script's filename | name on rootCommand |
The right-hand column in full is docs/content/Build/CAPABILITIES.md
(rendered).
Composition
command { stage; stage } is the common form. Consecutive stages yielded into a command become one pipeline
that takes the command's name and description — what a build script usually wants.
pipeline "name" { } is for the two cases that shape does not cover: running several pipelines under one
command, and giving a pipeline a name and description of its own. Command.pipeline { } is the middle ground
— an explicit block, so the pipeline-level settings have somewhere to go, still named after the command:
let test =
command "test" {
description "Builds and runs the test suite"
Command.pipeline {
workingDir root
Prelude.restore
ProjectManagement.buildAll
Tests.execute
}
}
Stages nest to any depth — a stage inside a stage is one step of its parent — and a command tree is an
ordinary value, so a Command built in one script is yielded into another after a #load. See
Composing reusable blocks.
Producers, consumers and failure handlers
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; nothing runs until a consumes stage requires it:
let tag = Input.option<string> "--tag" |> Input.def "v0.0.0"
let manifest =
Producer.define "manifest" (InputSpec.ofInput tag) DependencySpec.empty (fun tag () ->
Operation.ofAsync (fetchManifestAsync tag))
let publish =
stage "publish" {
retry 2
onFailure (fun context -> printfn "%A" context.Primary)
consumes (DependencySpec.require manifest) (fun manifest ->
execute (cmd $"deploy --version {manifest.Version}"))
}
A producer runs once per invocation and shares that one result with every consumer that requires it. Retrying
the consumer through retry re-runs the deploy alone, not the fetch. onFailure registers a handler on a
stage or a pipeline that runs once the scope's own retries are exhausted, and reads what a producer
published through context.TryGetOutput.
execute above streams the command's output and reports pass/fail. A step that needs the process's stdout as
a value reaches for executeCapture (fails on a rejected exit code, keeping the capture as evidence) or
attemptCapture (always answers the capture, rejected exit codes included, and leaves branching to the caller)
— see Running a command from inside a step in
docs/content/Build/CAPABILITIES.md.
Work that does not belong in InputSpec.Read — whose job is to bind CLI values, not run them — moves to a
producer or a step. See Migrating work out of InputSpec.Read in
docs/content/Build/CAPABILITIES.md for a worked
before/after, and the same file's Producers and dependencies and Failure handlers sections for the full
operation list, scope-retry ownership, and remaining limitations.
Motivation
I hate CI/CD and CLI plumbing, but it saves me the headache of returning to old projects later.
System.CommandLine is great and comes with batteries included; FSharp.SystemCommandLine wraps it well.
Fun.Build reads like GitHub Actions YAML for building workflows, but its command-line parsing is outdated
and untyped.
So this repo combines Fun.Build's shape with FSharp.SystemCommandLine's strong typing, and dogfoods the
result on its own CI/CD.
Do I get friends now?
No. This still sounds useless.
Rude.
Development
Build CLI
Every repository task runs through the Build project rather than a script, so
the tasks are typed, debuggable, and discoverable:
dotnet run --project Build.fsproj -- --help
| Command | What it does |
|---|---|
build |
Restores and builds the solution |
test |
Builds and runs the Expecto suites |
publish |
Packs and pushes to NuGet (--nuget-key; falls back to the local feed) |
bump |
Rewrites <Version> in a project file (-p <project>) |
docs |
Builds the Nacara site in docs/ (--watch to serve it) |
Flags belong to the commands whose stages read them: --quick skips restores
and the clean, --skip-tests skips the suites, --configuration picks the
configuration. None of them is registered by hand — see Adding a step.
Versioning
Versions live in the project files, not in a notes file or on the command line:
dotnet run --project Build.fsproj -- bump -p build # patch, the default
dotnet run --project Build.fsproj -- bump minor -p build external-annotations
dotnet run --project Build.fsproj -- bump rc -p build # 0.2.0 -> 0.2.1-rc.1
dotnet run --project Build.fsproj -- bump 2.0.0-nightly.7 -p build
Each packable project carries a <Version> and an <AssemblyVersion>, and a
bump rewrites both — the second as <major>.0.0.0, so it only moves when the
major does. An assembly's version is its identity to everything already
compiled against it: moving it on a patch bump breaks anything not rebuilt in
the same pass.
pack passes no version property, so CI publishes what the project file says.
bump is skipped when --ci is set — which it is by default under GitHub
Actions — so a version is bumped locally and committed, never invented on a
runner. Add a project to Project.allProjects in Build/Program.fs to make it
a bump target and have it packed.
Layout
Build.fsproj the build CLI
Build/
Program.fs the repository paths, options, stages and commands
src/Partas.Build/ the library
src/Partas.Build.Cmd/ the command value and the process runner
src/Partas.Build.Baked/ ready-made options, stages and semver helpers
docs/ the Nacara site (Site.fs, docs.fsproj, content/, blog/, static/)
tests/ the Expecto suites
Adding a project
Build/Program.fs addresses the repository through
Partas.TypeProvider.BuildHelper, so paths are checked when the build project
compiles. After adding a project, register it in Project.allProjects, which is
both what bump can version and what pack packs:
module Project =
let allProjects =
[
"build", Repo.Project.``Partas.Build``.Path
"new-thing", Repo.Project.``Partas.NewThing``.Path
]
A typo, or a project renamed without updating the build, then fails at compile time rather than halfway through a release.
Adding a step
A step is a stage of a pipeline. A stage that needs a flag binds it in an
input { } block, which also makes the flag appear in --help:
let myStep = input {
let! quick = Options.quick
return stage "my step" {
when' (not quick)
run (cmd $"dotnet ... {Repo.Project.``Partas.Build``.Path}")
}
}
Yield it into any command. The condition stays in the stage, so the command
carries no flags of its own, and adding the stage to a second command registers
--quick there too.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- FSharp.Control.AsyncSeq (>= 4.15.0)
- FSharp.Core (>= 10.1.400)
- FsToolkit.ErrorHandling (>= 5.2.0)
- Partas.Build.Cmd (>= 0.3.1)
- Spectre.Console (>= 0.57.2)
- System.CommandLine (>= 2.0.11)
-
net10.0
- FSharp.Control.AsyncSeq (>= 4.15.0)
- FSharp.Core (>= 10.1.400)
- FsToolkit.ErrorHandling (>= 5.2.0)
- Partas.Build.Cmd (>= 0.3.1)
- Spectre.Console (>= 0.57.2)
- System.CommandLine (>= 2.0.11)
-
net8.0
- FSharp.Control.AsyncSeq (>= 4.15.0)
- FSharp.Core (>= 10.1.400)
- FsToolkit.ErrorHandling (>= 5.2.0)
- Partas.Build.Cmd (>= 0.3.1)
- Spectre.Console (>= 0.57.2)
- System.CommandLine (>= 2.0.11)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Partas.Build:
| Package | Downloads |
|---|---|
|
Partas.Build.ExternalAnnotations
Package Description |
|
|
Partas.Build.Baked
Prebaked options, arguments, and composed stages for Partas.Build |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.6.5 | 36 | 9/21/2026 |
| 0.6.4 | 31 | 9/21/2026 |
| 0.6.3 | 40 | 9/19/2026 |
| 0.6.2 | 49 | 9/19/2026 |
| 0.6.1 | 39 | 9/19/2026 |
| 0.6.1-alpha.1 | 36 | 9/18/2026 |
| 0.5.0 | 67 | 9/16/2026 |
| 0.4.0-alpha.3 | 113 | 9/5/2026 |
| 0.4.0-alpha.2 | 47 | 9/4/2026 |
| 0.4.0-alpha.1 | 52 | 9/3/2026 |
| 0.3.0 | 147 | 8/29/2026 |
| 0.2.3 | 99 | 8/27/2026 |
| 0.2.2 | 88 | 8/27/2026 |
| 0.2.1 | 90 | 8/26/2026 |
| 0.2.0 | 98 | 8/25/2026 |
| 0.1.4 | 164 | 8/25/2026 |
| 0.1.3 | 104 | 8/23/2026 |
| 0.1.2 | 96 | 8/23/2026 |
| 0.1.1 | 97 | 8/22/2026 |
| 0.1.0 | 96 | 8/22/2026 |