diff --git a/Build/Program.fs b/Build/Program.fs index 11cc8cb..79a26cd 100644 --- a/Build/Program.fs +++ b/Build/Program.fs @@ -152,27 +152,6 @@ module Documentation = } } - /// Prepends the docs/static/llms.txt header to output/llms.txt, which Nacara generates - /// as a verbatim copy of that same file, so the body appears twice. output/llms-full.txt does not - /// exist, so that half of the merge has no effect. - /// - /// Written for fsdocs, which generated both files at the site root as a link inventory under such a heading. - /// - let llms = input { - let! watch = Baked.Common.watch - return stage "llms" { - when' (not watch) "--watch is set" - run (fun ctx -> - let header = File.ReadAllText(Path.Combine(root, "docs", "static", "llms.txt")).TrimEnd() - for name in [ "llms.txt"; "llms-full.txt" ] do - let path = Path.Combine(root, "output", name) - if File.Exists path then - let body = File.ReadAllLines path |> Array.skipWhile (fun line -> line.Trim() = "" || line.StartsWith "# ") - File.WriteAllLines(path, Array.append [| header; "" |] body) - StageContext.writeLine ctx StdStream.Out $"merged docs/llms.txt into output/{name}") - } - } - module Commands = let build = command "build" { @@ -231,7 +210,6 @@ module Commands = Prelude.restore ProjectManagement.buildAll Documentation.generate - Documentation.llms } } diff --git a/CLAUDE.md b/CLAUDE.md index 4cb41d5..9f34c7e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -303,5 +303,5 @@ a patch bump move it breaks anything not rebuilt in the same pass with `Could no - Public API goes in `[]` modules under `Partas.Build`; the model and engine stay in `Partas.Build.Internal`. A model type that appears in a public signature gets an abbreviation in `ConsumerTypes` (`Conductors.fs`), and a lookup a step needs gets a re-export in the public `StageContext` module. - Human-readable console output uses `Terminal.ansi ()`, which follows the invocation writer. Plain host consoles disable CI profile enrichment to preserve plain text. GitHub workflow commands use `Terminal.out ()` directly to preserve one physical line, enabled by case-insensitive `GITHUB_ACTIONS=true` detection (see `WorkflowCommands` and `printError` in both context modules). - The Nacara site (`docs/Site.fs`, `docs/docs.fsproj`) publishes every page under `docs/content/` and `docs/blog/`, plus the generated API reference, so internal working documents belong in `notes/` (as the `PLAN*.md` files do), not under `docs/`. -- The `.fsx` pages under `docs/content/` `#load` the library's sources in `` order: a file added to `Partas.Build.fsproj` or `Partas.Build.Baked.fsproj` goes into all four lists, or the pages stop type-checking. Check a page with `dotnet fsi .fsx` from its own directory. `docs/static/` is copied to the site root verbatim: `llms.txt` and `AGENTS-snippet.md` (the block a consumer pastes into its `AGENTS.md`, also packed into the `Partas.Build` package root). Nacara generates no `llms-full.txt`. +- Guides under `docs/content/` are Markdown. Compile changed F# examples against the current library in a scratch project before publishing; compile definitions without invoking the example workflows. Run the `docs` command to check navigation and links. `AgentFriendly` generates `llms.txt`, `llms-full.txt`, Markdown copies and alternate links; register link validation after it writes those files. `docs/static/` is copied verbatim, including `AGENTS-snippet.md`, which is also packed into the core package. Keep the agent index generated rather than adding a static copy or a post-build merge. - XML docs carry `` blocks on the entry points an agent hovers (`run`/`runLine`/`runSensitive`, `input`, `whenSome`, `Cmd.argIf`/`argWhenSome`/`cmd`, `stage`/`pipeline`/`command`/`rootCommand`/`rootCommandOfScript`, `Command.root`/`invoke`, `RootCommandDefinition.Invoke`, `RunResult`, `MachineOutput.json`, `Baked.Stages`). They are written to compile: an edit to one is checked by pasting it into a scratch script against the built library. Escape `<` as `<` inside them; `GenerateDocumentationFile` is on and the build stays warning-free. diff --git a/docs/Directives.fs b/docs/Directives.fs new file mode 100644 index 0000000..983c11b --- /dev/null +++ b/docs/Directives.fs @@ -0,0 +1,23 @@ +module Docs.Directives + +open Feliz.ViewEngine +open Nacara.Core +open Nacara.Plugins + +let private cards = + Directive.create "cards" Decode.node + |> Directive.render (fun _ _ contents -> + Html.div [ prop.className "pb-cards"; prop.children contents ]) + +let private card = + Directive.create "card" (Decode.object (fun get -> + {| Title = get.Required.Field "title" Decode.string + Href = get.Required.Field "href" Decode.string |})) + |> Directive.render (fun _ args contents -> + Html.a [ + prop.className "pb-card" + prop.href args.Href + prop.children [ Html.strong args.Title; contents ] + ]) + +let register = Directives.register [ cards; card ] diff --git a/docs/Site.fs b/docs/Site.fs index 85203fe..76b9ca5 100644 --- a/docs/Site.fs +++ b/docs/Site.fs @@ -1,14 +1,19 @@ module Docs.Site open Feliz.ViewEngine +open System.IO open Nacara.Core open Nacara.Plugins -open Nacara.Theme +open Partas.Nacara.Theme let theme = Theme.defaults + |> Theme.favIcon "favicon.svg" + |> Theme.lightTokens (fun t -> { t with Primary = "#165c50"; ContentWidth = "48rem"; Radius = "0.6rem" }) + |> Theme.darkTokens (fun t -> { t with Primary = "#7de0bd"; ContentWidth = "48rem"; Radius = "0.6rem" }) + |> Theme.layerAfter "responsive" "partas" (File.ReadAllText(Path.Combine(__SOURCE_DIRECTORY__, "theme.css"))) |> Theme.navbar [ - NavbarSection("Build", "build", "/build/") + NavbarSection("Guide", "build", "/build/") NavbarSection("Extensions", "extensions", "/extensions/") NavbarSection("External Annotations", "external-annotations", "/external-annotations/") NavbarSection("Reference", "reference", "/reference/") @@ -21,7 +26,42 @@ let theme = NavbarIcon("GitHub", "https://github.com/shayanhabibi/Partas.Build", Icons.github) ] |> Theme.editUrl "https://github.com/shayanhabibi/Partas.Build/edit/master/docs" - |> Theme.footer (Html.p [ Html.text "Built with Nacara" ]) + |> Theme.footer (Html.p [ Html.text "Partas.Build · Build workflows in F# · Built with Nacara" ]) + |> Theme.menu "build" [ + Menu.section "Start here" [ + Menu.page "Build/index.md" + Menu.page "Build/installation.md" + Menu.page "Build/getting-started.md" + ] + Menu.section "Build workflows" [ + Menu.page "Build/inputs.md" + Menu.page "Build/steps.md" + Menu.page "Build/composition.md" + Menu.page "Build/execution.md" + Menu.page "Build/baked.md" + ] + Menu.section "Integrate and inspect" [ + Menu.page "Build/agents.md" + Menu.page "Build/hosting.md" + Menu.page "Build/troubleshooting.md" + ] + Menu.section "Reference" [ + Menu.page "Build/CAPABILITIES.md" + Menu.page "Build/computation-expression-operations.md" + Menu.page "Build/workflow-reference.md" + Menu.page "Build/composition-reference.md" + ] + ] + |> Theme.menu "extensions" [ + Menu.section "Extensions" [ Menu.page "extensions/index.md"; Menu.page "extensions/shipit.md" ] + ] + |> Theme.menu "external-annotations" [ + Menu.section "External annotations" [ + Menu.page "external-annotations/index.md" + Menu.page "external-annotations/external-annotations-recipes.md" + Menu.page "external-annotations/external-annotations-api.md" + ] + ] |> Theme.menu "blog" [ Menu.section "2026" [ Menu.section "September" [ @@ -58,10 +98,21 @@ let plugins = >> FSharpApi.register apiOptions >> Literate.register >> TreeSitter.register + >> Docs.Directives.register >> Sitemap.register + >> AgentFriendly.registerWith ( + AgentFriendly.summary "Composable build workflows and command-line tools in F#." + >> AgentFriendly.details "Start with https://shayanhabibi.github.io/Partas.Build/build/getting-started.md. Commands derive their inputs and help from the stages they run. Inspect a consumer build with --help, --schema --json, or --explain --json before executing it. See https://shayanhabibi.github.io/Partas.Build/build/agents.md for JSON output and exit codes, and https://shayanhabibi.github.io/Partas.Build/AGENTS-snippet.md for repository instructions." + ) + >> OgImage.registerWith ( + OgImage.defaultImage (OgImage.image "/img/sun-ztu.jpeg" |> OgImage.withAlt "Partas.Build") + ) >> LightningCss.register + >> Esbuild.register >> Nuglify.minifyHtml - >> Nuglify.minifyJs + // Validate after AgentFriendly writes Markdown and llms-full.txt. Canonical URLs point to + // this build, not the previously deployed site; local links are still validated normally. + >> LinkValidator.registerWith (LinkValidator.ignoring [ @"^https://shayanhabibi\.github\.io/Partas\.Build/" ]) let collections = Site.collection content diff --git a/docs/content/Build/CAPABILITIES.md b/docs/content/Build/CAPABILITIES.md index 6944626..1bea3e5 100644 --- a/docs/content/Build/CAPABILITIES.md +++ b/docs/content/Build/CAPABILITIES.md @@ -8,7 +8,7 @@ order: 1 One line per custom operation on the four builders, per `Input` combinator, and per `Cmd` argument helper. The [API reference](https://shayanhabibi.github.io/Partas.Build/reference/) has full signatures and remarks. -[Composing reusable blocks](composition.fsx) has worked examples. +[Composing reusable blocks](composition.md) has worked examples. ## How settings resolve @@ -557,8 +557,8 @@ counterpart takes each of those options as an `InputSpec<_>` instead. A skip rep ## Reference -- [Overview](index.fsx) — the layers, and a first pipeline. -- [Composing reusable blocks](composition.fsx) — blocks, nesting, and composition across files. +- [Overview](index.md) — the layers, and a first pipeline. +- [Composing reusable blocks](composition.md) — blocks, nesting, and composition across files. - [Hosting a build in a long-lived session](hosting.md) — `Command.invoke` from SageFs or any F# host. -- [Stage CE run overloads](computation-expression-operations.fsx). +- [Stage CE run overloads](computation-expression-operations.md). - [API reference](https://shayanhabibi.github.io/Partas.Build/reference/) — full signatures and remarks. diff --git a/docs/content/Build/agents.md b/docs/content/Build/agents.md new file mode 100644 index 0000000..eb507ff --- /dev/null +++ b/docs/content/Build/agents.md @@ -0,0 +1,43 @@ +--- +title: Agents and JSON +description: Inspect a command and consume structured execution results. +category: Build +order: 8 +--- + +Use the build's own command model to discover what it accepts and what it will run. + +## Inspect before execution + +```shell +dotnet run --project Build.fsproj -- build --help +dotnet run --project Build.fsproj -- build --schema --json +dotnet run --project Build.fsproj -- build --explain --json +``` + +`--help` describes registered inputs. `--schema --json` returns the command schema. `--explain --json` returns a static execution plan without running steps or effectful conditions. Text `--explain` may evaluate conditions such as a Git branch check. + +## Consume a run result + +```shell +dotnet run --project Build.fsproj -- build --json +``` + +A JSON run ends with one result document containing the outcome, exit code, reports, failures, and timings. Child processes may still print output before that document. + +Detected AI environments default `--json` to true. Detection happens per invocation. `PARTAS_BUILD_DISABLE_AI=true` disables that automatic default; explicit `--json` still works. Use `--json false` to request text for an individual invocation. + +## Handle exit codes + +- `0`: success, including help and inspection. +- `1`: execution failed. +- `2`: invalid command or input; execution did not start. +- `130`: cancelled. + +In an F# host, [invoke a reusable root](hosting.md) to receive a `RunResult` directly. + +## Read these docs as Markdown + +The site publishes [llms.txt](/Partas.Build/llms.txt), [llms-full.txt](/Partas.Build/llms-full.txt), and Markdown copies beside guide pages. Each HTML guide advertises its Markdown URL with an alternate link. + +Copy the [agent instructions](https://shayanhabibi.github.io/Partas.Build/AGENTS-snippet.md) into a consumer repository to document how its build should be inspected. diff --git a/docs/content/Build/baked.md b/docs/content/Build/baked.md new file mode 100644 index 0000000..0029ada --- /dev/null +++ b/docs/content/Build/baked.md @@ -0,0 +1,44 @@ +--- +title: Baked stages +description: Reuse common .NET build operations and inputs. +category: Build +order: 7 +--- + +`Partas.Build.Baked` provides ready-made stages for restore, build, clean, pack, publishing, and tests. + +```fsharp +open Partas.Build +open Partas.Build.Baked + +let projects = [ "src/MyLibrary/MyLibrary.fsproj" ] + +let root = Command.root { + workingDir __SOURCE_DIRECTORY__ + command "build" { + Command.pipeline { + Stages.restore "MyLibrary.slnx" + Stages.build projects + } + } +} +``` + +The stages register the inputs they need. Check `build --help` for the resulting flags. + +## Reuse inputs + +- `Baked.Dotnet.config`: build configuration. +- `Baked.NuGet.apiKey`: NuGet key, with an environment default. +- `Baked.Common.isCI`: CI detection and override. +- `Baked.SemVer.bump`: semantic version increment or explicit version. + +Bind them with the same `input` block used for your own options. + +## Choose a versioning workflow + +The agnostic SemVer bump rewrites project `Version` and sets `AssemblyVersion` to the major version. It does not infer releases from commits. + +For conventional commits and changelogs, use the [ShipIt bump](../extensions/shipit.md#use-the-shipit-backed-bump). It computes the release version and updates configured package versions together. Use one bump workflow per release. + +Full signatures are in [Capabilities](CAPABILITIES.md#baked) and the [workflow reference](workflow-reference.md#baked-the-batteries). diff --git a/docs/content/Build/composition.fsx b/docs/content/Build/composition-reference.md similarity index 83% rename from docs/content/Build/composition.fsx rename to docs/content/Build/composition-reference.md index 7caaccd..31c1e82 100644 --- a/docs/content/Build/composition.fsx +++ b/docs/content/Build/composition-reference.md @@ -1,80 +1,25 @@ -(** --- -title: Composing reusable blocks +title: Composition reference +description: Detailed patterns for reusable blocks and composition across files. category: Build -order: 3 +order: 21 --- -*) -(*** hide ***) -// The sources are #load-ed rather than #r-ing a built DLL, for two reasons: the guide then type-checks against -// the code as written instead of against the last build, and nothing holds a file lock — a #r-ed assembly stays -// loaded for the lifetime of the site watcher (`dotnet run --project Build.fsproj -- docs --watch`), which on -// Windows makes rebuilding the library fail. -// Keep this list in the same order as the items in Partas.Build.fsproj. -#r "nuget: FSharp.Control.AsyncSeq, 4.15.0" -#r "nuget: FsToolkit.ErrorHandling, 5.2.0" -#r "nuget: System.CommandLine, 2.0.11" -#r "nuget: Spectre.Console, 0.57.2" - - -#load "../../../src/Partas.Build.Cmd/Execution.fs" -#load "../../../src/Partas.Build.Cmd/Program.fs" -#load "../../../src/Partas.Build/System.CommandLine/Aliases.fs" -#load "../../../src/Partas.Build/System.CommandLine/Inputs.fs" -#load "../../../src/Partas.Build/Exceptions.fs" -#load "../../../src/Partas.Build/Output.fs" -#load "../../../src/Partas.Build/Terminal.fs" -#load "../../../src/Partas.Build/Annotations.fs" -#load "../../../src/Partas.Build/Environment.fs" -#load "../../../src/Partas.Build/Timing.fs" -#load "../../../src/Partas.Build/Producer.fs" -#load "../../../src/Partas.Build/Failures.fs" -#load "../../../src/Partas.Build/Conductors.fs" -#load "../../../src/Partas.Build/GitHubActions.fs" -#load "../../../src/Partas.Build/Conductors.Runners.fs" -#load "../../../src/Partas.Build/Process.fs" -#load "../../../src/Partas.Build/Operations.fs" -#load "../../../src/Partas.Build/Dependencies.fs" -#load "../../../src/Partas.Build/DependencyPlan.fs" -#load "../../../src/Partas.Build/ExecutionState.fs" -#load "../../../src/Partas.Build/Builders/StageSettings.fs" -#load "../../../src/Partas.Build/Builders/Stage.fs" -#load "../../../src/Partas.Build/Builders/Conditions.fs" -#load "../../../src/Partas.Build/Builders/PipelineSettings.fs" -#load "../../../src/Partas.Build/Builders/Pipeline.fs" -#load "../../../src/Partas.Build/Builders/Inputs.fs" -#load "../../../src/Partas.Build/AiEnvironment.fs" -#load "../../../src/Partas.Build/MachineOutput.fs" -#load "../../../src/Partas.Build/Explain.fs" -#load "../../../src/Partas.Build/Summary.fs" -#load "../../../src/Partas.Build/RunResult.fs" -#load "../../../src/Partas.Build/Builders/Command.fs" -#load "../../../src/Partas.Build.Baked/Program.fs" -#load "../../../src/Partas.Build.Baked/Common.fs" -#load "../../../src/Partas.Build.Baked/NuGet.fs" -#load "../../../src/Partas.Build.Baked/Dotnet.fs" -#load "../../../src/Partas.Build.Baked/SemVer.fs" -#load "../../../src/Partas.Build.Baked/Clean.fs" -#load "../../../src/Partas.Build.Baked/Stages.fs" - -open Partas.Build - -(** # Composing reusable blocks -[The guide](index.fsx) introduces one stage at a time. This page builds a library of reusable *blocks* — +[The guide](index.md) introduces one stage at a time. This page builds a library of reusable *blocks* — stages that carry their own CLI inputs — and assembles them into pipelines and commands. -Every snippet compiles when the docs build, except the two file listings under *Composition across files*, -which are separate scripts. +The file listings under *Composition across files* are separate scripts. Snippets marked with compiler diagnostics show unsupported syntax. ## The shape of a block A block is a function returning a stage. It returns a plain `StageContext` when it needs no CLI flag, or an `InputSpec` from an `input { }` CE when it does. Both are ordinary values and both are yieldable anywhere a stage is. -*) + +```fsharp +open Partas.Build module Options = let config = @@ -116,14 +61,14 @@ module Blocks = run (cmd $"dotnet build {project} -c {config} -v {level}") } } +``` -(** ## Yielding blocks A pipeline takes blocks in declaration order and unions the options they declare, whether a block is a bare stage or a spec: -*) +```fsharp let one = pipeline "one project" { workingDir __SOURCE_DIRECTORY__ @@ -132,16 +77,16 @@ let one = Blocks.restore "MyLib.fsproj" Blocks.build "MyLib.fsproj" } +``` -(** `one` is an `InputSpec` declaring `--quick`, `--configuration` and `--verbose` exactly once, contributed by `build` and `restore`. ## Loops and lists A `for` loop and a yielded list both work; they differ only in where the collection comes from: -*) +```fsharp let projects = [ "MyLib.fsproj"; "MyLib.Tool.fsproj"; "MyLib.Tests.fsproj" ] let looped = @@ -155,8 +100,8 @@ let listed = [ Blocks.restore "MyLib.fsproj" Blocks.build "MyLib.fsproj" ] } +``` -(** Both forms union the inputs of every element: `looped` still declares `--configuration` and `--verbose` once across three stages. @@ -177,8 +122,8 @@ A stage nested inside another stage is one step of its parent, so blocks group w an input declared at any depth surfaces on the command running the pipeline. Here the innermost stage, three levels down, is the only thing that names `--configuration`: -*) +```fsharp let deep = command "ci" { description "Restore, build and test" @@ -204,27 +149,27 @@ let deep = } } } +``` -(** `ci --help` lists `--quick`, `--configuration` and `--verbose`; the stages that read them registered them. A setting placed *after* a nested block still applies to the enclosing stage, so ordering is free: -*) +```fsharp let settingsAfter = stage "compile" { Blocks.build "MyLib.fsproj" timeout 300 whenNot { envVar "SKIP_BUILD" } } +``` -(** ## Blocks that take blocks A block is a value, so a block factory can take other blocks as arguments — the usual way to build a house style: a wrapper adding retries, timing, teardown or a condition to whatever it receives. -*) +```fsharp /// Wraps stages in a named group with a shared timeout, and a teardown that always runs. let group name (seconds: int) (stages: StageContext seq) = stage name { @@ -238,12 +183,12 @@ let grouped = pipeline "grouped" { group "prepare" 60 [ Blocks.clean "MyLib.fsproj" ] } +``` -(** When the wrapped stages carry inputs, the wrapper takes an `InputSpec` list and returns an `InputSpec`, joined through the `input` CE: -*) +```fsharp let inputGroup name (seconds: int) (blocks: InputSpec list) = input { let! stages = InputSpec.sequence blocks @@ -257,16 +202,16 @@ let inputGrouped = pipeline "release" { inputGroup "compile" 600 [ Blocks.restore "MyLib.fsproj"; Blocks.build "MyLib.fsproj" ] } +``` -(** `InputSpec.sequence` turns a list of specs into one spec of a list, unioning the inputs. `InputSpec.traverse fn items` does the same over a mapping. These are the two functions to reach for when writing this kind of wrapper. ## Adding an input of the wrapper's own A wrapper can bind flags the wrapped blocks know nothing about, alongside the sequenced blocks: -*) +```fsharp let skipTests = Input.option "--skip-tests" |> Input.description "Build the tests but do not run them" let testGroup (blocks: InputSpec list) = input { @@ -288,8 +233,8 @@ let tested = testGroup [ Blocks.restore "MyLib.fsproj"; Blocks.build "MyLib.fsproj" ] } } +``` -(** `test --help` now lists `--skip-tests` next to the three the blocks declared. ## What does not compose: a block returning a block's spec @@ -314,8 +259,8 @@ option set that cannot be registered. **Binding is a layer boundary**: one layer binds, the layers above harvest. When two blocks share a body but differ in where a value comes from, pass the *source* in as an `InputSpec` instead of passing a read value out as one. The shared body keeps one `let!`/`and!` group; callers vary only the spec they hand over: -*) +```fsharp module Sources = /// The bump kind as a positional argument — `bump minor`. let bumpArgument = Input.argument "bump" |> Input.def "patch" @@ -339,18 +284,18 @@ let bumpFromArgument project = let bumpFromOption project = let source = InputSpec.ofInput Sources.bumpOption |> InputSpec.map (Option.defaultValue "patch") bumpBlock source project +``` -(** `InputSpec.ofInput` lifts a bare `ActionInput` into a spec; `InputSpec.map` adapts its value, so a source can be defaulted or reshaped before being handed over. Both commands below declare `--configuration`; one declares the argument, the other declares `--bump`: -*) +```fsharp let bumping = [ command "bump" { bumpFromArgument "MyLib.fsproj" } command "release" { bumpFromOption "MyLib.fsproj" } ] +``` -(** The same rule covers a helper needing no source of its own: give it the already-read values as plain arguments and let the caller bind. An `input { }` nested inside a `return` is always the error. @@ -358,8 +303,8 @@ arguments and let the caller bind. An `input { }` nested inside a `return` is al A command needs no explicit `pipeline`. Stages yielded straight into it become one implicit pipeline that takes the command's name and description: -*) +```fsharp let flat = command "build" { description "Build the solution" @@ -367,8 +312,8 @@ let flat = Blocks.restore "MyLib.sln" Blocks.build "MyLib.sln" } +``` -(** Consecutive stages share that one pipeline, its settings, its run and its `whenStage` cross-references. A command can also mix implicit and explicit pipelines; declaration order is preserved. @@ -377,14 +322,14 @@ command can also mix implicit and explicit pipelines; declaration order is prese A command also takes the pipeline settings themselves — `workingDir`, `envVars`, the timeouts, the output operations, the hooks, `post`, `verbosity` — and hands them to every pipeline it runs, including the implicit one. They are defaults: a pipeline that sets the same thing keeps its own value. See -[command-level defaults](index.fsx#command-level-defaults). +[command-level defaults](workflow-reference.md#command-level-defaults). ## Conditional assembly An `if` with no `else` is fine around a whole stage or block; the untaken branch contributes nothing. Custom operations are the exception, since F# forbids those under an `if`. -*) +```fsharp let includeDocs = System.Environment.GetEnvironmentVariable "DOCS" = "1" let conditional = @@ -394,12 +339,12 @@ let conditional = if includeDocs then stage "docs" { run "dotnet run --project docs/docs.fsproj -- build" } } +``` -(** Bind and branch inside the `input` CE for a condition known only after parsing; it is ordinary F# with no such restriction: -*) +```fsharp let maybeClean = input { let! quick = Options.quick @@ -407,13 +352,13 @@ let maybeClean = input { if quick then stage "skip clean" { echo "skipping clean" } else Blocks.clean "MyLib.fsproj" } +``` -(** ## Putting it together A small, complete build assembled entirely from blocks: -*) +```fsharp let mainCommand argv = rootCommand argv { description "MyLib build" @@ -449,8 +394,8 @@ let mainCommand argv = } ] } +``` -(** Each command lists only the flags its own stages read: `build` gets `--configuration` and `--verbose`, `test` adds `--skip-tests` and `--quick`, `release` gets what its blocks declare. @@ -544,6 +489,5 @@ lists them because they exist, and one process resolves packages once. ## Reference -- [Guide](index.fsx) — steps, conditions, inputs, output, timeouts, `Baked`. +- [Guide](index.md) — steps, conditions, inputs, output, timeouts, `Baked`. - [API reference](https://shayanhabibi.github.io/Partas.Build/reference/) — every custom operation, from its XML documentation. -*) diff --git a/docs/content/Build/composition.md b/docs/content/Build/composition.md new file mode 100644 index 0000000..75781ac --- /dev/null +++ b/docs/content/Build/composition.md @@ -0,0 +1,115 @@ +--- +title: Composition +description: Reuse stages, assemble workflows, and share command trees across files. +category: Build +order: 5 +--- + +Stages and pipelines are F# values. A reusable block can return a plain stage or an `InputSpec` when it needs CLI inputs. + +## Define a block + +```fsharp +open Partas.Build + +let configuration = + Input.option "--configuration" + |> Input.def "Release" + +let compile project = input { + let! config = configuration + return stage $"compile {project}" { + run (cmd $"dotnet build {project} -c {config}") + } +} +``` + +## Assemble a workflow + +```fsharp +let projects = [ "src/Core/Core.fsproj"; "src/App/App.fsproj" ] + +let build = pipeline "build" { + workingDir __SOURCE_DIRECTORY__ + stage "restore" { run "dotnet restore" } + for project in projects do + compile project +} + +let root = Command.root { + command "build" { build } +} +``` + +`build --help` registers `--configuration` once, even though several stages read it. The same block can be reused by another command. + +## Nest stages + +```fsharp +let libraries = stage "libraries" { + timeout 300 + for project in projects do + compile project +} +``` + +Place settings before loops. For individually yielded blocks, settings can appear before or after the block. Children inherit unset settings from the enclosing stage. + +## Wrap blocks that carry inputs + +A wrapper returning a stage can accept an input-bearing stage: + +```fsharp +let group name (block: InputSpec) = stage name { + block +} + +let grouped = group "core" (compile "src/Core/Core.fsproj") +``` + +The wrapper retains the inner input set. See the [composition reference](composition-reference.md#blocks-that-take-blocks) for wrappers accepting lists, adding their own inputs, and compiler limits around nested specs. + +## Yield lists + +```fsharp +let blocks = [ for project in projects -> compile project ] + +let listed = pipeline "compile" { + [ yield! blocks ] +} +``` + +Use a list when F# rejects `yield!` mixed with custom operations. A `for` loop is usually the simpler form. + +## Composition across files + +Put reusable definitions in a file that constructs commands without invoking or exiting: + +```fsharp +// tools/wire.defs.fsx +module Wire + +open Partas.Build + +let generate = command "wire" { + stage "generate" { echo "generating" } +} +``` + +Load it from your entry-point script and add the command to the root: + +```fsharp +// build.fsx; the package references precede this #load +#load "tools/wire.defs.fsx" +open Partas.Build + +let root = Command.root { + addCommand Wire.generate +} + +exit (Command.invoke (Args.script ()) root).ExitCode +``` + +Keep command names unique among siblings. An explicit module name avoids guessing the identifier F# derives from a filename. For a reusable root shared with a session, follow [Hosting](hosting.md). + +The [composition reference](composition-reference.md) includes larger examples and standalone script entry points. diff --git a/docs/content/Build/computation-expression-operations.fsx b/docs/content/Build/computation-expression-operations.md similarity index 51% rename from docs/content/Build/computation-expression-operations.fsx rename to docs/content/Build/computation-expression-operations.md index 373a073..a577327 100644 --- a/docs/content/Build/computation-expression-operations.fsx +++ b/docs/content/Build/computation-expression-operations.md @@ -1,66 +1,9 @@ -(** --- title: Stage CE run overloads category: Build order: 4 --- -*) - -(*** hide ***) -// The sources are #load-ed rather than #r-ing a built DLL, for two reasons: the guide then type-checks against -// the code as written instead of against the last build, and nothing holds a file lock — a #r-ed assembly stays -// loaded for the lifetime of the site watcher (`dotnet run --project Build.fsproj -- docs --watch`), which on -// Windows makes rebuilding the library fail. -// Keep this list in the same order as the items in Partas.Build.fsproj. -#r "nuget: FSharp.Control.AsyncSeq, 4.15.0" -#r "nuget: FsToolkit.ErrorHandling, 5.2.0" -#r "nuget: System.CommandLine, 2.0.11" -#r "nuget: Spectre.Console, 0.57.2" -#load "../../../src/Partas.Build.Cmd/Execution.fs" -#load "../../../src/Partas.Build.Cmd/Program.fs" -#load "../../../src/Partas.Build/System.CommandLine/Aliases.fs" -#load "../../../src/Partas.Build/System.CommandLine/Inputs.fs" -#load "../../../src/Partas.Build/Exceptions.fs" -#load "../../../src/Partas.Build/Output.fs" -#load "../../../src/Partas.Build/Terminal.fs" -#load "../../../src/Partas.Build/Annotations.fs" -#load "../../../src/Partas.Build/Environment.fs" -#load "../../../src/Partas.Build/Timing.fs" -#load "../../../src/Partas.Build/Producer.fs" -#load "../../../src/Partas.Build/Failures.fs" -#load "../../../src/Partas.Build/Conductors.fs" -#load "../../../src/Partas.Build/GitHubActions.fs" -#load "../../../src/Partas.Build/Conductors.Runners.fs" -#load "../../../src/Partas.Build/Process.fs" -#load "../../../src/Partas.Build/Operations.fs" -#load "../../../src/Partas.Build/Dependencies.fs" -#load "../../../src/Partas.Build/DependencyPlan.fs" -#load "../../../src/Partas.Build/ExecutionState.fs" -#load "../../../src/Partas.Build/Builders/StageSettings.fs" -#load "../../../src/Partas.Build/Builders/Stage.fs" -#load "../../../src/Partas.Build/Builders/Conditions.fs" -#load "../../../src/Partas.Build/Builders/PipelineSettings.fs" -#load "../../../src/Partas.Build/Builders/Pipeline.fs" -#load "../../../src/Partas.Build/Builders/Inputs.fs" -#load "../../../src/Partas.Build/AiEnvironment.fs" -#load "../../../src/Partas.Build/MachineOutput.fs" -#load "../../../src/Partas.Build/Explain.fs" -#load "../../../src/Partas.Build/Summary.fs" -#load "../../../src/Partas.Build/RunResult.fs" -#load "../../../src/Partas.Build/Builders/Command.fs" -#load "../../../src/Partas.Build.Baked/Program.fs" -#load "../../../src/Partas.Build.Baked/Common.fs" -#load "../../../src/Partas.Build.Baked/NuGet.fs" -#load "../../../src/Partas.Build.Baked/Dotnet.fs" -#load "../../../src/Partas.Build.Baked/SemVer.fs" -#load "../../../src/Partas.Build.Baked/Clean.fs" -#load "../../../src/Partas.Build.Baked/Stages.fs" - - - -open System.Threading -open Partas.Build -(** + # Computation Expression Operations ## Stage @@ -69,12 +12,6 @@ open Partas.Build `run` takes a step and has more overloads than any other operation, which is why it gets its own page. -*) -(*** hide ***) - -let _ = stage "stage" { - -(** ### `run` > A returned `string`-like value runs as a command. @@ -86,49 +23,58 @@ let _ = stage "stage" { ##### `buildStep: StageContext -> BuildStep` ##### `command: string -> ?cancellationToken: CancellationToken` -*) - run "exe args --options" + +```fsharp +run "exe args --options" run "exe args --options" CancellationToken.None +``` -(** ##### `exe: string -> args: string -> ?cancellationToken: CancellationToken` -*) - run "exe" "args --options" +```fsharp +run "exe" "args --options" run "exe" "args --options" CancellationToken.None -(** +``` + ##### `command: Cmd -> ?cancellationToken: CancellationToken` -*) - run (cmd $"exe args --options") + +```fsharp +run (cmd $"exe args --options") run (cmd $"exe args --options") CancellationToken.None +``` -(** ##### `asyncExitCode: Async` -*) - run (async { return 0 }) -(** +```fsharp +run (async { return 0 }) +``` + ##### `asyncAction: Async` -*) - run (async { do () }) -(** + +```fsharp +run (async { do () }) +``` + ##### `exitCodeFn: StageContext -> int` ##### `exitCodeFn: StageContext -> Async` ##### `exitCodeFn: StageContext -> Task` -*) - run (fun _ -> 1) + +```fsharp +run (fun _ -> 1) run (fun _ -> async { return 0 }) run (fun _ -> task { return 99 }) -(** +``` + ##### `actionFn: StageContext -> unit` ##### `actionFn: StageContext -> Async` ##### `actionFn: StageContext -> Task` -*) - run (fun _ -> ()) + +```fsharp +run (fun _ -> ()) run (fun _ -> async { do () }) run (fun _ -> task { do () }) +``` -(** #### `runLine` A function returning a command line is `runLine`, not `run`. `run (fun ctx -> "...")` still compiles, marked @@ -138,61 +84,53 @@ honouring quotes; build a `Cmd` with `cmd $"..."` to keep each interpolation hol ##### `commandFn: StageContext -> string` ##### `commandFn: StageContext -> Async` ##### `commandFn: StageContext -> Task` -*) - runLine (fun _ -> "dotnet build") + +```fsharp +runLine (fun _ -> "dotnet build") runLine (fun _ -> async { return "dotnet build" }) runLine (fun _ -> task { return "dotnet build" }) // With CancellationToken runLine (fun _ -> "dotnet build") CancellationToken.None runLine (fun _ -> async { return "dotnet build" }) CancellationToken.None runLine (fun _ -> task { return "dotnet build" }) CancellationToken.None +``` -(** ##### `commandMaybeFn: StageContext -> string option` ##### `commandMaybeFn: StageContext -> Async` ##### `commandMaybeFn: StageContext -> Task` -*) - runLine (fun _ -> Some "dotnet build") + +```fsharp +runLine (fun _ -> Some "dotnet build") runLine (fun _ -> async { return Option.None }) runLine (fun _ -> task { return Some "dotnet build" }) // With CancellationToken runLine (fun _ -> Some "dotnet build") CancellationToken.None runLine (fun _ -> async { return Some "dotnet build" }) CancellationToken.None runLine (fun _ -> task { return Some "dotnet build" }) CancellationToken.None +``` - -(** ##### `resultFn: StageContext -> Result` ##### `resultFn: StageContext -> Async>` ##### `resultFn: StageContext -> Task>` -*) - run (fun _ -> Error "some error") + +```fsharp +run (fun _ -> Error "some error") run (fun _ -> async { return Ok() }) run (fun _ -> task { return Error "some error" }) +``` -(** ##### `cmdResultFn: StageContext -> Result` ##### `cmdResultFn: StageContext -> Async>` ##### `cmdResultFn: StageContext -> Task>` -*) - // todo - overloads without CancellationToken should not require explicit typing + +```fsharp +// todo - overloads without CancellationToken should not require explicit typing run (fun _ -> Ok (Some (cmd $"dotnet build")) : Result) run (fun _ -> async { return Ok (Some (cmd $"dotnet build")) : Result }) // With CancellationToken run (fun _ -> Ok (Some (cmd $"dotnet build"))) CancellationToken.None run (fun _ -> async { return Ok (Some (cmd $"dotnet build")) }) CancellationToken.None +``` -(** ##### `cmdResultFn: StageContext -> Result` ##### `cmdResultFn: StageContext -> Async>` -*) - // todo - overloads without CancellationToken should not require explicit typing - run (fun _ -> Ok (cmd $"dotnet build"): Result) - run (fun _ -> async { return Ok (cmd $"dotnet build"): Result }) - // With CancellationToken - run (fun _ -> Ok (cmd $"dotnet build")) CancellationToken.None - run (fun _ -> async { return Ok (cmd $"dotnet build") }) CancellationToken.None - -(*** hide ***) -} - diff --git a/docs/content/Build/execution.md b/docs/content/Build/execution.md new file mode 100644 index 0000000..3ff772f --- /dev/null +++ b/docs/content/Build/execution.md @@ -0,0 +1,71 @@ +--- +title: Execution +description: Control conditions, concurrency, timeouts, cleanup, and failures. +category: Build +order: 6 +--- + +## Conditions narrow; settings override + +Every condition on a stage must pass. Use one `whenAny` block when either condition should enable it: + +```fsharp +open Partas.Build + +let publish = stage "publish" { + whenBranch "master" + whenWindows + run "dotnet pack" +} +``` + +This stage runs on Windows **and** master. A second condition never replaces the first. Supply a reason to `when'` so `--explain` can describe a skip. + +Settings such as `workingDir` and `parallel'` use the last value at the same level. A child stage inherits unset settings from its parents and pipeline. Command settings fill in pipeline defaults. + +## Run independent work concurrently + +```fsharp +let compile = stage "compile" { + parallel' 2 + run "dotnet build A.fsproj" + run "dotnet build B.fsproj" + run "dotnet build C.fsproj" +} +``` + +At most two steps run at once. Omit `parallel'` for sequential execution; use `parallel' true` for unbounded concurrency. Avoid parallel builds that write the same referenced project's outputs. + +## Bound execution time + +```fsharp +let test = stage "test" { + timeout 60 + timeoutForStep 30 + run "dotnet test" +} +``` + +A timeout cancels the stage and kills its process tree. Pipelines additionally support `timeoutForStage`. A blocking F# function must cooperate with cancellation itself. + +## Always clean up + +```fsharp +let integration = pipeline "integration" { + stage "start" { run "docker compose up -d" } + stage "test" { run "dotnet test" } + post [ stage "stop" { run "docker compose down" } ] +} +``` + +Post stages run after the main stages, including after failure. + +## Decide how failure propagates + +- `continueStepsOnFailure`: run the remaining steps. +- `continueStageOnFailure`: let the pipeline continue. +- `continueOnStepFailure`: apply both. +- `acceptExitCodes`: accept additional process exit codes. +- `failIfIgnored`: treat a skipped stage as failure. + +For typed outputs and prerequisite ordering, use [producers and dependencies](CAPABILITIES.md#producers-and-dependencies). For recovery policies, see [failure handlers](CAPABILITIES.md#failure-handlers). diff --git a/docs/content/Build/getting-started.md b/docs/content/Build/getting-started.md new file mode 100644 index 0000000..68009a3 --- /dev/null +++ b/docs/content/Build/getting-started.md @@ -0,0 +1,74 @@ +--- +title: Getting started +description: Create a build command with a configuration option and generated help. +category: Build +order: 2 +--- + +Create a build project or script using the [installation guide](installation.md). + +## Define a stage + +```fsharp +open Partas.Build + +let compile = input { + let! config = + Input.option "--configuration" + |> Input.alias "-c" + |> Input.def "Release" + |> Input.acceptOnlyFromAmong [ "Debug"; "Release" ] + + return stage "compile" { + run (cmd $"dotnet build -c {config}") + } +} +``` + +The stage owns `--configuration`. `cmd` keeps each interpolated value as one argument. + +## Add it to a root + +```fsharp +let root = Command.root { + description "My build" + workingDir __SOURCE_DIRECTORY__ + command "build" { + description "Compile the solution" + compile + } +} + +let build args = Command.invoke args root +``` + +`Command.root` constructs the command tree without executing it. Relative process paths resolve against `workingDir`. + +## Invoke it + +For an executable project, place this entry point after the definitions: + +```fsharp +[] +let main argv = (build argv).ExitCode +``` + +```shell +dotnet run --project Build.fsproj -- build --help +dotnet run --project Build.fsproj -- build --explain +dotnet run --project Build.fsproj -- build -c Debug +``` + +For a script, use this final line instead: + +```fsharp +exit (build (Args.script ())).ExitCode +``` + +```shell +dotnet fsi build.fsx -- build --help +``` + +`--help` lists the stage's configuration option automatically. `--explain` prints the workflow without running the build. Exit code `0` means success, `1` a stage failed, `2` invalid input, and `130` cancellation. + +Next: [bind more inputs](inputs.md), [compose stages](composition.md), or use [Baked's .NET stages](baked.md). diff --git a/docs/content/Build/index.fsx b/docs/content/Build/index.fsx deleted file mode 100644 index ced5343..0000000 --- a/docs/content/Build/index.fsx +++ /dev/null @@ -1,719 +0,0 @@ -(** ---- -title: Overview -category: Build -order: 0 ---- -*) -(*** hide ***) -// #load-ed rather than #r-ed: the guide type-checks against the code as written, not the last build, and no -// #r-ed assembly holds a file lock — one held open by the site watcher (`dotnet run --project Build.fsproj -- -// docs --watch`) breaks a rebuild of the library on Windows. -// Keep this list in the same order as the items in Partas.Build.fsproj. -#r "nuget: FSharp.Control.AsyncSeq, 4.15.0" -#r "nuget: FsToolkit.ErrorHandling, 5.2.0" -#r "nuget: System.CommandLine, 2.0.11" -#r "nuget: Spectre.Console, 0.57.2" - -#load "../../../src/Partas.Build.Cmd/Execution.fs" -#load "../../../src/Partas.Build.Cmd/Program.fs" -#load "../../../src/Partas.Build/System.CommandLine/Aliases.fs" -#load "../../../src/Partas.Build/System.CommandLine/Inputs.fs" -#load "../../../src/Partas.Build/Exceptions.fs" -#load "../../../src/Partas.Build/Output.fs" -#load "../../../src/Partas.Build/Terminal.fs" -#load "../../../src/Partas.Build/Annotations.fs" -#load "../../../src/Partas.Build/Environment.fs" -#load "../../../src/Partas.Build/Timing.fs" -#load "../../../src/Partas.Build/Producer.fs" -#load "../../../src/Partas.Build/Failures.fs" -#load "../../../src/Partas.Build/Conductors.fs" -#load "../../../src/Partas.Build/GitHubActions.fs" -#load "../../../src/Partas.Build/Conductors.Runners.fs" -#load "../../../src/Partas.Build/Process.fs" -#load "../../../src/Partas.Build/Operations.fs" -#load "../../../src/Partas.Build/Dependencies.fs" -#load "../../../src/Partas.Build/DependencyPlan.fs" -#load "../../../src/Partas.Build/ExecutionState.fs" -#load "../../../src/Partas.Build/Builders/StageSettings.fs" -#load "../../../src/Partas.Build/Builders/Stage.fs" -#load "../../../src/Partas.Build/Builders/Conditions.fs" -#load "../../../src/Partas.Build/Builders/PipelineSettings.fs" -#load "../../../src/Partas.Build/Builders/Pipeline.fs" -#load "../../../src/Partas.Build/Builders/Inputs.fs" -#load "../../../src/Partas.Build/AiEnvironment.fs" -#load "../../../src/Partas.Build/MachineOutput.fs" -#load "../../../src/Partas.Build/Explain.fs" -#load "../../../src/Partas.Build/Summary.fs" -#load "../../../src/Partas.Build/RunResult.fs" -#load "../../../src/Partas.Build/Builders/Command.fs" -#load "../../../src/Partas.Build.Baked/Program.fs" -#load "../../../src/Partas.Build.Baked/Common.fs" -#load "../../../src/Partas.Build.Baked/NuGet.fs" -#load "../../../src/Partas.Build.Baked/Dotnet.fs" -#load "../../../src/Partas.Build.Baked/SemVer.fs" -#load "../../../src/Partas.Build.Baked/Clean.fs" -#load "../../../src/Partas.Build.Baked/Stages.fs" - -open Partas.Build - -(** - - -Command line and build pipelines in F#. Composable, hints of elderberry, thick in tannins, a glorious vintage. - -A pipeline DSL based on [Fun.Build](https://github.com/slaveOftime/Fun.Build), scaffolded over -[FSharp.SystemCommandLine](https://github.com/jordanmarr/FSharp.SystemCommandLine), for no-nonsense CLIs. CLI -inputs are declared where used and lifted into the command line help of the commands that run them, with -automatic validation and help. - -## Layers - -| Layer | CE | Produces | -|---|---|---| -| Step | `run`, `echo`, … | one action inside a stage | -| Stage | `stage "name" { }` | `StageContext` | -| Inputs | `input { }` | `InputSpec<'T>` | -| Pipeline | `pipeline "name" { }` | `PipelineContext` or `InputSpec` | -| Command | `command "name" { }` | `System.CommandLine.Command` | -| Root | `rootCommand argv { }` | `int` exit code — **it runs immediately** | -| Hosted root | `Command.root { }` | `RootCommandDefinition` — runs on each `Command.invoke args` | - -## A first pipeline -*) - -let hello = - pipeline "hello" { - workingDir __SOURCE_DIRECTORY__ - - stage "greet" { - echo "building" - run "dotnet --version" - } - } - -(** - -`step` -> `stage` -> `stage` -> ... -> `pipeline` -> `command` -> `rootCommand`. Nothing runs until the root -command does. - -## Steps - -A step is anything yielded inside a `stage`. `run` is heavily overloaded; three overloads matter: -*) - -let steps = - stage "steps" { - // a whole command line, split on whitespace honouring quotes - run "dotnet build --no-restore" - - // executable and arguments kept apart - run "dotnet" "build --no-restore" - - // an F# function; also Async<_>, Task<_>, StageContext -> _ and Result-returning variants - run (fun (ctx: StageContext) -> printfn "%s" ctx.Name) - } - -(** -### Interpolation: use `cmd` - -`run $"..."` binds the **`string`** overload, which flattens the holes and re-splits the result on whitespace. -A path containing a space becomes two arguments. Route interpolation through `cmd` instead: it keeps each hole -as exactly one argument and lets the platform do the escaping: -*) - - // v------- will break if directly passed verbatim -let project = "src/My Project/My Project.fsproj" - -let interpolated = - stage "build" { - run (cmd $"dotnet build {project}") // one argument, space and all - } - -(** -`runSensitive` takes a `FormattableString` directly, needs no `cmd`, and masks every hole as `***` in the log -while passing the real value to the process: -*) - -let password = "drowssap" - -let login = - stage "login" { - runSensitive $"docker login -u me -p {password}" - } - -(** -It masks *every* hole. Build a `Cmd` by hand when only one argument is secret. `Secrets` is a set of argument -indices: -*) - -let pushArgs = [ "nuget"; "push"; "bin/x.nupkg"; "--api-key"; password ] - -let push = - stage "push" { - run { Cmd.ofList "dotnet" pushArgs with Secrets = Set.singleton 4 } - } - -(** -*New in >0.2.2*: wrap sensitive strings with `Cmd.secret` or `Cmd.sensitive`. Every command runner picks them -up and quotes any string containing spaces. -*) - -let pushSecret = - stage "push" { - run $"dotnet nuget push bin/x.nupkg --api-key {Cmd.secret password}" - } - -(** -## Conditions - -`when'` and its friends set whether a stage runs. They **conjoin**. A second condition narrows the first -rather than replacing it: -*) - -let conditional = - stage "release only" { - whenBranch "master" - whenNot { envVar "CI" } // master AND not CI - run "dotnet pack" - } - -(** -`whenAll`, `whenAny` and `whenNot` are CEs that combine leaf conditions (`branch`, `branches`, `envVar`, -`platformWindows`, `platformLinux`, `platformOSX`, and a literal `when'`). An empty `whenAll { }` is active, -the identity of `forall`. An empty `whenAny { }` is not. -*) - -let combined = - stage "publish" { - whenAny { - branch "master" - envVar "FORCE_PUBLISH" - } - run "dotnet nuget push" - } - -(** -`when'` also accepts a whole `StageContext`. It runs for real, side effects and console output included, and -its success is the answer. - -A `bool` carries no reason, so `--explain` prints a stage it turns off as `(skipped)`. Name the reason as a second -argument, `when' (not quick) "--quick is set"`, and `--explain` prints `(skipped: --quick is set)`. - -## Inputs - -A stage that needs a CLI flag binds it in an `input` CE. It is then lifted into any command that asks for it, -with no further wiring: -*) - -module Options = - let quick = - Input.option "--quick" - |> Input.alias "-q" - |> Input.description "Skip restores and cleaning" - - let config = - Input.option "--configuration" - |> Input.alias "-c" - |> Input.def "Release" - |> Input.acceptOnlyFromAmong [ "Debug"; "Release" ] - -let restore = - input { - let! quick = Options.quick - - return stage "restore" { - when' (not quick) - run "dotnet restore" - } - } - -(** -Bind several sources with `and!`, never with nested `let!`: -*) - -let build = - input { - let! quick = Options.quick - and! config = Options.config - - return stage "build" { - when' (not quick) - run (cmd $"dotnet build -c {config}") - } - } - -(** -The CE rejects a second `let!`; bind every source in one `let! … and! …` block. Binding this way is what lifts -flags into the command line help without first evaluating pipelines. - -### Harvesting upward - -A pipeline or stage that asks for an input, or nests an `input` request, is wrapped in an `InputSpec<'T>`, -which tracks inputs and unions them by reference. - -`--quick` and `--configuration` appear under `build --help` without being named anywhere but the -stages that read them: - -*) - -let buildCommand = - command "build" { - description "Restores and builds the solution" - - pipeline "build" { - workingDir __SOURCE_DIRECTORY__ - restore - build - } - } - -(** -Flags sit on the commands whose stages read them, not on the root. `addInput` covers the remainder: flags no -pipeline asks for that a root command still wants to expose. - -## Wiring the root - -`rootCommand` parses and invokes immediately, returning the process exit code. It belongs in `main`: -*) - -let main argv = - rootCommand argv { - description "My build" - addCommands [ buildCommand ] - } - -(** -A command with no pipelines is a grouping node: it gets no action, so System.CommandLine reports the missing -subcommand and prints help instead of succeeding silently. - -The exit code tells a mistaken invocation from a broken build: `0` success (help, `--version` and `--explain` -included), `1` a stage failed, `2` the command line did not parse or validate — an unknown option, a missing -subcommand, a producer arrangement dependency validation rejects — and `130` the run was cancelled. The -`ExitCode` module names the four. - -### Invoking from a host - -`rootCommand` reads its arguments once and runs at construction. A long-lived host — an F# interactive -session, a test — builds the root once with `Command.root`, which takes every `rootCommand` operation and runs -nothing, then invokes it as often as it likes. `Command.invoke` answers a `RunResult`: the exit code, its -`RunOutcome`, and each pipeline run's `ScopeReport`s and `StageTiming`s. -*) - -let root = - Command.root { - description "My build" - addCommands [ buildCommand ] - } - -let buildQuick () = - let result = root |> Command.invoke [ "build"; "--quick"; "true" ] - printfn "%d failure(s)" result.Failures.Length - result.ExitCode - -(** -`root.Invoke(args, output = writer, cancellationToken = token)` sends help, `--explain`, the timing summary and -parse errors to `writer` instead of the console, and cancels the running pipeline — its processes with it — -when `token` fires. - -## Composition - -### Stages nest - -A stage can be yielded inside another stage, arbitrarily deep, with no separate grouping concept: -*) - -let nested = - stage "outer" { - run "dotnet --version" - - stage "inner" { - whenWindows - run "dotnet --info" - } - } - -(** -### Settings inherit - -Settings resolve outward: stage, then parent stage, then pipeline. A pipeline-level `workingDir` or `envVars` -defaults every stage that does not override it: -*) - -let inherited = - pipeline "inherited" { - workingDir __SOURCE_DIRECTORY__ - envVars [ ("CI", "true") ] - timeoutForStage 300 - - stage "uses the pipeline's dir and env" { run "dotnet --info" } - stage "overrides just the dir" { - workingDir __SOURCE_DIRECTORY__ - run "dotnet --version" - } - } - -(** -### Stages are values - -A stage is an ordinary value, so reuse is ordinary F#. Return them from functions, put them in lists, iterate -over them: -*) - -let testProject (name: string) = - stage $"test {name}" { run (cmd $"dotnet test {name}") } - -let testAll = - pipeline "test" { - for proj in [ "A.fsproj"; "B.fsproj" ] do - testProject proj - } - -(** -The same works one layer up: a `pipeline` is a value, and a `command` can run several in declaration order. -[Composing reusable blocks](composition.fsx) covers nesting, lists of blocks, and stages that carry their own -inputs. - -### Nameless Pipelines - -A short CLI command often runs a single pipeline named the same as the command. `Command.pipeline` covers -this: it inherits its description and name from the command it is defined within, essentially `pipeline null -{ }`. - -*) - -let namelessPipe = - command "build" { - description "Build projects" - Command.pipeline { - stage "build" { - run "dotnet build" - } - } - } - - - -(** -### Command-level defaults - -A `command` also takes the pipeline settings: `workingDir`, `envVars`, `timeout`, `timeoutForStage`, -`timeoutForStep`, `acceptExitCodes`, `outputTo`, `silentOutput`, `captureOutput`, `redirectOutput`, -`noPrefixForStep`, `noStdRedirectForStep`, `runBeforeEachStage`, `runAfterEachStage`, `post`, `verbosity`, -`verbose` and `quiet`. Set on the command, these are **defaults for every pipeline the command runs**, saving -repetition across pipelines: -*) - -let ciCommand = - command "ci" { - workingDir __SOURCE_DIRECTORY__ - envVars [ ("CI", "true") ] - quiet - - pipeline "build" { stage "build" { run "dotnet build" } } - pipeline "test" { - verbosity Verbosity.Verbose // this pipeline says its own piece; see below - stage "test" { run "dotnet test" } - } - } - -(** -A default never overwrites a pipeline's own setting: the pipeline wins regardless of write order. A default -written *below* the pipelines still applies, because defaults are folded in once the whole command is built, -not as each pipeline is yielded. - -A setting counts as the pipeline's own once it differs from a freshly created pipeline's initial value: - -- `workingDir`, the timeouts, `outputTo` and friends, `verbosity`: hand over as soon as the pipeline names them. -- `post`: hands over as soon as the pipeline declares a post stage. -- `acceptExitCodes`: hands over as soon as it widens the set beyond `0`. -- the hooks: hand over as soon as one is installed. -- `envVars`: merges per variable — keys the pipeline set stay, the rest arrive from the command. -- `noPrefixForStep`, `noStdRedirectForStep`: plain booleans with no "unset" state, so setting one to its - existing value is indistinguishable from not setting it. - -Stages are never touched: a command default fills in pipeline settings only. - -## Advanced - -### Parallelism - -`parallel'` makes a stage run its steps concurrently. It takes a flag, a throttle, or a function returning -either: `StageContext -> bool`, `-> int voption`, `-> Choice`, `-> Choice`. Every -overload resolves to the same `int voption`: - -| You write | Resolves to | Behaviour | -|---|---|---| -| nothing | `ValueNone` | sequential | -| `parallel' false` | `ValueNone` | sequential | -| `parallel' 1` | `ValueSome 1` | sequential | -| `parallel' n` (`n > 1`) | `ValueSome n` | at most `n` steps in flight | -| `parallel'`, `parallel' true` | `ValueSome -1` | unbounded | -| `parallel' 0`, `parallel' -1` | `ValueSome n`, `n < 1` | unbounded | - -*) - -let fanOut = - stage "fan out" { - parallel' 2 - run "dotnet build A.fsproj" - run "dotnet build B.fsproj" - run "dotnet build C.fsproj" - } - -(** -The bound is exact: a stage set to `2` never has a third step in flight. - -To choose a mode at runtime, return the choice from a single condition rather than writing two operations: -*) - -let adaptive = - stage "fan out" { - parallel' (fun (_: StageContext) -> if System.Environment.ProcessorCount > 4 then ValueSome 4 else ValueNone) - run "dotnet build A.fsproj" - run "dotnet build B.fsproj" - } - -(** -### Settings overwrite, conditions conjoin - -The two halves of the stage CE compose differently. Mixing them up is the most common surprise. `parallel'`, -`workingDir`, `timeout` and the rest are **settings**: the last one written wins and an earlier one leaves no -trace. -*) - -let lastWins = - stage "settings" { - parallel' 4 - parallel' false // sequential; the 4 is gone, not combined with - run "dotnet build" - } - -(** -`when'`, `whenBranch`, `whenWindows` and the rest are **conditions**: each narrows the stage to the logical AND -of everything declared so far, so a second condition can only make the stage run less often. -*) - -let narrows = - stage "conditions" { - whenBranch "master" - whenWindows // master AND Windows, not Windows instead of master - run "dotnet pack" - } - -(** -To widen a condition, write **one** `whenAny { }` containing both alternatives: a second operation would -narrow instead. To switch parallel modes, write **one** condition function returning the mode: a second -operation would discard the first. - -### Timeouts and cancellation - -A stage takes `timeout` and `timeoutForStep`; a pipeline takes those plus `timeoutForStage`. A stage -takes plain `int` seconds, `float` seconds or a `TimeSpan`; a pipeline takes `int`, `float` -seconds or a `TimeSpan`. - -A timeout cancels the stage and kills the whole process tree it started, grandchildren included. - -### Post stages - -`post` stages run after the main stages whether or not the pipeline succeeded: the place for teardown. -*) - -let withTeardown = - pipeline "integration" { - stage "up" { run "docker compose up -d" } - stage "test" { run "dotnet test" } - - post [ stage "down" { run "docker compose down" } ] - } - -(** -### Failure control - -- `continueStepsOnFailure` keeps a stage going after a failed step. -- `continueStageOnFailure` keeps the pipeline going after a failed stage. -- `continueOnStepFailure` sets both. -- `acceptExitCodes` widens what counts as success (the default is `0`). -- `failIfIgnored` turns a skipped stage into a failure. - -### Hooks - -`runBeforeEachStage` and `runAfterEachStage` take a `StageContext -> unit` and fire around every stage in the -pipeline. - -### Where step output goes - -By default a step's output goes straight to the console. `outputTo` redirects it, inherited by sub-stages like -any other setting: - -| | | -|---|---| -| `silentOutput` | dropped | -| `captureOutput` | held, and lifted into the error message if a step fails | -| `redirectOutput (fun stream line -> …)` | handed over line by line, as it arrives | -| `outputTo sink` | any of the above as a `StageOutput` value, for when the choice is made at run time | - -The common case is a test run: silent when it passes, and its own output as the reason when it does not. -*) - -let quietTests = - pipeline "test" { - stage "test" { - captureOutput - run "dotnet test" - } - } - -(** -`captureOutput` lifts stderr into the step's error, or everything written if there was none. A failing stage -still says why, on the console and in the GitHub Actions annotation. - -Pass an `OutputCapture` to keep the lines regardless of outcome: - -```fsharp -let log = OutputCapture.create() - -let audited = - pipeline "audit" { - stage "scan" { - captureOutput log - run "dotnet list package --vulnerable" - } - - post [ stage "report" { run (fun _ -> File.WriteAllText ("scan.log", OutputCapture.text log)) } ] - } -``` - -- `OutputCapture.lines`: both streams, in the order they arrived. -- `OutputCapture.errors`: stderr only. -- `OutputCapture.text` / `OutputCapture.errorText`: the same, joined. -- `OutputCapture.failureText`: what a failure lifts. - -It does not cover three things: - -- The pipeline's own log (stage rules, command lines, timings) is `verbosity`, not `outputTo`. A stage wanting - both quiet needs `quiet` *and* `silentOutput`. -- A bare `printfn` inside a step is not routable. Use `echo`, or `StageContext.writeLine ctx StdStream.Out`. -- `noStdRedirectForStep` overrides all of it: without redirection there is no stream to route. - - -## Baked: the batteries - -`Partas.Build.Baked` is the layer of options every build CLI ends up writing anyway, ready made, documented, -and shipped as its own package. Each declaration is a `BuildOption<'T>`: `.option` the flag, `.argument` the -positional equivalent. - -| | | -|---|---| -| `Baked.Dotnet.config` | `configuration`/`-c`, over `release`/`r`/`debug`/`d` case-insensitively, as `string option` | -| `Baked.NuGet.apiKey` | `nuget-key`/`--nuget`/`-k`, help name `APIKEY`, defaulting to `$NUGET_API_KEY` | -| `Baked.SemVer.bump` | `bump`, parsed to a `Bump` DU over `major\|minor\|patch\|alpha\|beta\|rc\|preview\|`, defaulting to `patch` | -| `Baked.Common.isCI` | `--ci`, defaulting to true when the environment looks like CI | - -`BuildOption.map`, `.mapOpt` and `.mapArg` apply an `Input.*` combinator to both forms or to one. Both forms -are `ActionInput` values, so they bind in an `input` CE exactly as a hand-rolled option does: -*) - - -let packaging = - input { - let! config = Baked.Dotnet.config.option - and! key = Baked.NuGet.apiKey.option - - let config = Option.defaultValue "Release" config - - return stage "pack" { - when' key.IsSome - run (cmd $"dotnet pack -c {config}") - } - } - -(** -### Versioning a project file - -`Baked.SemVer.Version` is semantic-version arithmetic over the `Bump` DU. `Baked.SemVer.Version.IO` applies it -to a project file. The `bump` stage is ready made: it binds `--ci`, skips itself when set, and takes the -projects to edit as an `InputSpec`, so the CLI decides what `--project` accepts. - -```fsharp -let bumpAsArgument = Baked.SemVer.Stages.bumpArgument Options.projects // minor -p MyLib -let bumpAsOption = Baked.SemVer.Stages.bumpOption Options.projects // --bump minor -p MyLib -``` - -`Baked.SemVer.Version.IO.writeVersion` rewrites `` and `` in the first -`PropertyGroup`, adding either if absent, and returns the previous ``. It skips the `` -declaration and byte-order mark `XDocument.Save` would otherwise add, so a bump reads as a one-line diff. - -`` is the package version and moves however it is bumped. `` only takes the major. -Moving it on a patch bump breaks anything not rebuilt in the same pass, with `Could not load file or assembly -', Version=…'`. - -Paired with `Baked.Common.isCI`, versions are bumped locally and committed, so CI packs whatever the project -file carries rather than inventing one at build time. - -The arithmetic itself: - -| from | bump | to | -|---|---|---| -| `1.2.3` | `patch` | `1.2.4` | -| `1.2.3` | `minor` | `1.3.0` | -| `1.2.3` | `major` | `2.0.0` | -| `1.2.3` | `alpha` | `1.2.4-alpha.1` | -| `1.2.4-alpha.1` | `alpha` | `1.2.4-alpha.2` | -| `1.2.4-alpha.2` | `rc` | `1.2.4-rc.1` | -| `1.2.4-rc.1` | `patch` | `1.2.4` | -| anything | `Target "2.0.0-nightly.7"` | `2.0.0-nightly.7` | - -A `patch` on a pre-release *releases* it rather than moving past it. `major`/`minor` drop the tag outright. -`Target` is taken verbatim, unparsed. - -## Antipatterns - -**Interpolating straight into `run`.** `run $"dotnet build {path}"` picks the `string` overload and re-splits -on whitespace. Use `run (cmd $"...")`. - -**Nested `let!` in `inputs`.** It does not compile, by design. Use `and!`. A source that depends on another's -value needs a single input with a richer type, or a runtime check inside a step. - -**Custom operations under `if` or `match`.** F# forbids it. Build the value first, then apply the operation -unconditionally: - -```fsharp -// won't compile -stage "publish" { if hasKey then run pushToNuget else run pushToLocal } - -// do this -let push = if hasKey then pushToNuget else pushToLocal -stage "publish" { run push } -``` - -A whole *stage* under an `if` is fine: an untaken branch contributes nothing. - -```fsharp -pipeline "ci" { - stage "build" { run "dotnet build" } - if not skipTests then stage "test" { run "dotnet test" } -} -``` - -**Mixing `yield!` with a custom operation in the same CE.** F# rejects it (`FS3086`). Yield a list instead: -`pipeline "p" { [ yield! blocks; yield extra ] }`, and put the settings on a stage inside. - -**Registering options on the root so every command has them.** `build --help` lists `--configuration` -*because* a build stage binds it. Hand-registering reintroduces the drift the library exists to remove. - -**Expecting a second condition to replace the first.** Conditions conjoin. Settings do not. A second -`whenBranch` narrows to both branches at once (so: never), where a second `parallel'` silently throws the -first away. To widen a condition, put the alternatives in one `whenAny { }`. - -**`runSensitive` with a bound interpolation.** `runSensitive` takes a `FormattableString`, and F# only -converts a `string` when a single overload is in play. It has no `InputSpec` form: bind the value outside the -stage and use `runSensitive $"…"` inside it as normal. - - - -## API reference - -The [API reference](https://shayanhabibi.github.io/Partas.Build/reference/) is generated from the XML -documentation on each custom operation. -*) diff --git a/docs/content/Build/index.md b/docs/content/Build/index.md new file mode 100644 index 0000000..22beebd --- /dev/null +++ b/docs/content/Build/index.md @@ -0,0 +1,46 @@ +--- +title: Overview +description: The building blocks of a Partas.Build command. +category: Build +order: 0 +--- + +Partas.Build turns F# workflow definitions into command-line tools. A stage declares the inputs it reads; each command derives its options, validation, and help from those stages. + +## Start with a command + +[Install the packages](installation.md), then [create your first build](getting-started.md). + +```fsharp +open Partas.Build + +let root = Command.root { + command "build" { + stage "compile" { run "dotnet build" } + } +} + +let build args = Command.invoke args root +``` + +Construction runs nothing. `Command.invoke [ "build" ] root` parses the arguments and runs the selected workflow. + +## The building blocks + +- **Step**: one process or F# function inside a stage. +- **Stage**: named steps, conditions, and execution settings. +- **Input**: an option or argument bound where the workflow needs it. +- **Pipeline**: an ordered workflow of stages. +- **Command**: a CLI entry point containing stages or pipelines. +- **Root**: the command tree, invoked once or reused by a host. + +## Choose your next task + +- [Inputs and help](inputs.md): bind options with `let!` and `and!`. +- [Processes and secrets](steps.md): preserve argument boundaries and mask sensitive values. +- [Composition](composition.md): reuse stages and commands. +- [Execution](execution.md): conditions, parallelism, timeouts, and failure handling. +- [Baked stages](baked.md): common .NET build operations. +- [Agents and JSON](agents.md): inspect and run a build with structured output. + +[Capabilities](CAPABILITIES.md) lists the operations. [Workflow reference](workflow-reference.md) retains the detailed examples, and the [API reference](https://shayanhabibi.github.io/Partas.Build/reference/) supplies full signatures. diff --git a/docs/content/Build/inputs.md b/docs/content/Build/inputs.md new file mode 100644 index 0000000..cc375ba --- /dev/null +++ b/docs/content/Build/inputs.md @@ -0,0 +1,57 @@ +--- +title: Inputs and help +description: Bind CLI options and arguments where they are used. +category: Build +order: 3 +--- + +Declare an input once. Bind it in an `input` block that returns a stage or pipeline. + +```fsharp +open Partas.Build + +let quick = + Input.option "--quick" + |> Input.alias "-q" + |> Input.description "Skip restore" + +let configuration = + Input.option "--configuration" + |> Input.def "Release" + +let compile = input { + let! skipRestore = quick + and! config = configuration + + return pipeline "compile" { + stage "restore" { + when' (not skipRestore) "--quick is set" + run "dotnet restore" + } + stage "build" { run (cmd $"dotnet build -c {config}") } + } +} + +let root = Command.root { + command "build" { compile } +} +``` + +Both options appear in `build --help`. Another command receives them only if its stages bind them. + +## Bind independent inputs together + +Use one `let!` followed by `and!` for additional inputs. A second `let!` is rejected. The complete input set must be known before parsing. + +If one value depends on another, derive it after binding. Keep runtime work inside a stage, rather than in the input reader. + +## Choose the input shape + +- `Input.option<'T> "--name"`: an option. +- `Input.argument<'T> "NAME"`: a positional argument. +- `Input.def value`: a default. +- `Input.acceptOnlyFromAmong values`: validation against a fixed set. +- `Input.map f`: transform a parsed value. +- `Input.sensitive`: hide the value in structured inspection. + +See [Capabilities](CAPABILITIES.md#input-combinators) for the complete list and [Baked](baked.md) for ready-made inputs. diff --git a/docs/content/Build/installation.md b/docs/content/Build/installation.md new file mode 100644 index 0000000..462f404 --- /dev/null +++ b/docs/content/Build/installation.md @@ -0,0 +1,40 @@ +--- +title: Installation +description: Add Partas.Build to a project or an F# script. +category: Build +order: 1 +--- + +Packages are published to the [Partas.Build Cloudsmith feed](https://cloudsmith.io/~shayanhabibi/repos/shayanhabibi-partas-build/packages/). + +## Build project + +Add the source, then the packages you need: + +```shell +dotnet nuget add source https://nuget.cloudsmith.io/shayanhabibi/shayanhabibi-partas-build/v3/index.json --name partas-build +dotnet add Build.fsproj package Partas.Build +dotnet add Build.fsproj package Partas.Build.Baked +``` + +`Partas.Build` is the DSL and execution engine. `Partas.Build.Baked` supplies common build inputs and stages. The [ShipIt extension](../extensions/shipit.md) is optional. + +## F# script + +Reference the feed explicitly: + +```fsharp +#i "nuget: https://nuget.cloudsmith.io/shayanhabibi/shayanhabibi-partas-build/v3/index.json" +#r "nuget: Partas.Build, 0.8.0" +#r "nuget: Partas.Build.Baked, 0.2.0" + +open Partas.Build +``` + +Pin versions for repeatable builds. Add `Partas.Build.EasyBuild.ShipIt, 0.1.0` if you need release workflows. + +## Frameworks + +The build packages target `net10.0`, `net8.0`, and `netstandard2.0`. Use an SDK compatible with your build project's target framework. External tools have their own runtime requirements; the default ShipIt tool requires .NET 10. + +Continue with [Getting started](getting-started.md). diff --git a/docs/content/Build/steps.md b/docs/content/Build/steps.md new file mode 100644 index 0000000..ef08719 --- /dev/null +++ b/docs/content/Build/steps.md @@ -0,0 +1,59 @@ +--- +title: Processes and secrets +description: Run processes and F# functions while keeping arguments and secrets intact. +category: Build +order: 4 +--- + +## Preserve arguments + +Use `cmd` for interpolated process commands. Each hole becomes one argument, including paths with spaces. + +```fsharp +open Partas.Build + +let project = "src/My Library/My Library.fsproj" + +let compile = stage "compile" { + run (cmd $"dotnet build {project}") +} +``` + +Plain `run "dotnet build"` is suitable for a fixed command. `run $"dotnet build {project}"` flattens the interpolation into a string and can split a path into several arguments. + +## Mask secrets + +`runSensitive` accepts an interpolation directly and masks every hole in the command label: + +```fsharp +let token = "example-token" + +let publish = stage "publish" { + runSensitive $"dotnet nuget push package.nupkg --api-key {token}" +} +``` + +For selective masking, construct a `Cmd` and mark only the secret arguments. Masking command labels does not redact output printed by the child process. + +## Run F# work + +```fsharp +let report = stage "report" { + run (fun (ctx: StageContext) -> printfn "Stage: %s" ctx.Name) +} +``` + +Use `echo` or `StageContext.writeLine` when output must follow the stage's configured writer. A bare `printfn` writes directly to the process console. + +## Route process output + +Stages can inherit or override an output sink: + +```fsharp +let quietBuild = stage "compile" { + silentOutput + run "dotnet build" +} +``` + +`silentOutput` controls child output; `quiet` controls pipeline logging. `captureOutput`, `teeOutput`, `outputTo`, and `OutputCapture` support collection and routing. See the [workflow reference](workflow-reference.md#where-step-output-goes) for examples. diff --git a/docs/content/Build/troubleshooting.md b/docs/content/Build/troubleshooting.md new file mode 100644 index 0000000..31f86a1 --- /dev/null +++ b/docs/content/Build/troubleshooting.md @@ -0,0 +1,44 @@ +--- +title: Troubleshooting +description: Fix common input, argument, and composition mistakes. +category: Build +order: 10 +--- + +## A path with spaces becomes several arguments + +Use `run (cmd $"dotnet build {path}")`. Direct string interpolation into `run` loses the argument boundary. + +## A second `let!` does not compile + +Bind inputs in one `let! … and! …` group. Derive dependent values after that group. The parser needs to know every input before reading their values. + +## A custom operation under `if` or `match` does not compile + +Choose a value first, then pass it to the operation: + +```fsharp +open Partas.Build + +let hasKey = false +let push = if hasKey then "dotnet nuget push package.nupkg" else "dotnet pack" +let publish = stage "publish" { run push } +``` + +A conditional that yields an entire stage is supported. + +## `yield!` and custom operations conflict + +Yield a list from the computation expression instead. See [Composition](composition.md). + +## An option is missing from help + +The selected command registers inputs its stages bind. Yield the input-bearing stage or pipeline into that command. Constructing an input elsewhere does not register it. + +## A stage is skipped unexpectedly + +Inspect it with `--explain`. Conditions conjoin; a second `whenBranch` narrows the stage to both branches. Use `whenAny` to express alternatives. + +## Help or explain installs a tool + +Move setup into an execution step. Input readers and module initialization should not perform workflow side effects. The [ShipIt extension](../extensions/shipit.md) supplies explicit setup commands. diff --git a/docs/content/index.fsx b/docs/content/Build/workflow-reference.md similarity index 86% rename from docs/content/index.fsx rename to docs/content/Build/workflow-reference.md index 1691c42..c864a62 100644 --- a/docs/content/index.fsx +++ b/docs/content/Build/workflow-reference.md @@ -1,72 +1,11 @@ -(** --- -title: Partas.Build -order: 0 +title: Workflow reference +description: Detailed examples of stages, inputs, execution settings, and Baked. +category: Build +order: 20 --- -*) -(*** hide ***) -// #load-ed rather than #r-ed: the guide type-checks against the code as written, not the last build, and no -// #r-ed assembly holds a file lock — one held open by the site watcher (`dotnet run --project Build.fsproj -- -// docs --watch`) breaks a rebuild of the library on Windows. -// Keep this list in the same order as the items in Partas.Build.fsproj. -#r "nuget: FSharp.Control.AsyncSeq, 4.15.0" -#r "nuget: FsToolkit.ErrorHandling, 5.2.0" -#r "nuget: System.CommandLine, 2.0.11" -#r "nuget: Spectre.Console, 0.57.2" - -#load "../../src/Partas.Build.Cmd/Execution.fs" -#load "../../src/Partas.Build.Cmd/Program.fs" -#load "../../src/Partas.Build/System.CommandLine/Aliases.fs" -#load "../../src/Partas.Build/System.CommandLine/Inputs.fs" -#load "../../src/Partas.Build/Exceptions.fs" -#load "../../src/Partas.Build/Output.fs" -#load "../../src/Partas.Build/Terminal.fs" -#load "../../src/Partas.Build/Annotations.fs" -#load "../../src/Partas.Build/Environment.fs" -#load "../../src/Partas.Build/Timing.fs" -#load "../../src/Partas.Build/Producer.fs" -#load "../../src/Partas.Build/Failures.fs" -#load "../../src/Partas.Build/Conductors.fs" -#load "../../src/Partas.Build/GitHubActions.fs" -#load "../../src/Partas.Build/Conductors.Runners.fs" -#load "../../src/Partas.Build/Process.fs" -#load "../../src/Partas.Build/Operations.fs" -#load "../../src/Partas.Build/Dependencies.fs" -#load "../../src/Partas.Build/DependencyPlan.fs" -#load "../../src/Partas.Build/ExecutionState.fs" -#load "../../src/Partas.Build/Builders/StageSettings.fs" -#load "../../src/Partas.Build/Builders/Stage.fs" -#load "../../src/Partas.Build/Builders/Conditions.fs" -#load "../../src/Partas.Build/Builders/PipelineSettings.fs" -#load "../../src/Partas.Build/Builders/Pipeline.fs" -#load "../../src/Partas.Build/Builders/Inputs.fs" -#load "../../src/Partas.Build/AiEnvironment.fs" -#load "../../src/Partas.Build/MachineOutput.fs" -#load "../../src/Partas.Build/Explain.fs" -#load "../../src/Partas.Build/Summary.fs" -#load "../../src/Partas.Build/RunResult.fs" -#load "../../src/Partas.Build/Builders/Command.fs" -#load "../../src/Partas.Build.Baked/Program.fs" -#load "../../src/Partas.Build.Baked/Common.fs" -#load "../../src/Partas.Build.Baked/NuGet.fs" -#load "../../src/Partas.Build.Baked/Dotnet.fs" -#load "../../src/Partas.Build.Baked/SemVer.fs" -#load "../../src/Partas.Build.Baked/Clean.fs" -#load "../../src/Partas.Build.Baked/Stages.fs" -open Partas.Build - -(** - - - - -Command line and build pipelines in F#. Composable, hints of elderberry, thick in tannins, a glorious vintage. - -A pipeline DSL based on [Fun.Build](https://github.com/slaveOftime/Fun.Build), scaffolded over -[FSharp.SystemCommandLine](https://github.com/jordanmarr/FSharp.SystemCommandLine), for no-nonsense CLIs. CLI -inputs are declared where used and lifted into the command line help of the commands that run them, with -automatic validation and help. +Detailed examples complement the [task-focused guides](index.md). ## Layers @@ -81,7 +20,9 @@ automatic validation and help. | Hosted root | `Command.root { }` | `RootCommandDefinition` — runs on each `Command.invoke args` | ## A first pipeline -*) + +```fsharp +open Partas.Build let hello = pipeline "hello" { @@ -92,8 +33,7 @@ let hello = run "dotnet --version" } } - -(** +``` `step` -> `stage` -> `stage` -> ... -> `pipeline` -> `command` -> `rootCommand`. Nothing runs until the root command does. @@ -101,8 +41,8 @@ command does. ## Steps A step is anything yielded inside a `stage`. `run` is heavily overloaded; three overloads matter: -*) +```fsharp let steps = stage "steps" { // a whole command line, split on whitespace honouring quotes @@ -114,77 +54,77 @@ let steps = // an F# function; also Async<_>, Task<_>, StageContext -> _ and Result-returning variants run (fun (ctx: StageContext) -> printfn "%s" ctx.Name) } +``` -(** ### Interpolation: use `cmd` `run $"..."` binds the **`string`** overload, which flattens the holes and re-splits the result on whitespace. A path containing a space becomes two arguments. Route interpolation through `cmd` instead: it keeps each hole as exactly one argument and lets the platform do the escaping: -*) - // v------- will break if directly passed verbatim +```fsharp +// v------- will break if directly passed verbatim let project = "src/My Project/My Project.fsproj" let interpolated = stage "build" { run (cmd $"dotnet build {project}") // one argument, space and all } +``` -(** `runSensitive` takes a `FormattableString` directly, needs no `cmd`, and masks every hole as `***` in the log while passing the real value to the process: -*) +```fsharp let password = "drowssap" let login = stage "login" { runSensitive $"docker login -u me -p {password}" } +``` -(** It masks *every* hole. Build a `Cmd` by hand when only one argument is secret. `Secrets` is a set of argument indices: -*) +```fsharp let pushArgs = [ "nuget"; "push"; "bin/x.nupkg"; "--api-key"; password ] let push = stage "push" { run { Cmd.ofList "dotnet" pushArgs with Secrets = Set.singleton 4 } } +``` -(** *New in >0.2.2*: wrap sensitive strings with `Cmd.secret` or `Cmd.sensitive`. Every command runner picks them up and quotes any string containing spaces. -*) +```fsharp let pushSecret = stage "push" { run $"dotnet nuget push bin/x.nupkg --api-key {Cmd.secret password}" } +``` -(** ## Conditions `when'` and its friends set whether a stage runs. They **conjoin**. A second condition narrows the first rather than replacing it: -*) +```fsharp let conditional = stage "release only" { whenBranch "master" whenNot { envVar "CI" } // master AND not CI run "dotnet pack" } +``` -(** `whenAll`, `whenAny` and `whenNot` are CEs that combine leaf conditions (`branch`, `branches`, `envVar`, `platformWindows`, `platformLinux`, `platformOSX`, and a literal `when'`). An empty `whenAll { }` is active, the identity of `forall`. An empty `whenAny { }` is not. -*) +```fsharp let combined = stage "publish" { whenAny { @@ -193,8 +133,8 @@ let combined = } run "dotnet nuget push" } +``` -(** `when'` also accepts a whole `StageContext`. It runs for real, side effects and console output included, and its success is the answer. @@ -205,8 +145,8 @@ argument, `when' (not quick) "--quick is set"`, and `--explain` prints `(skipped A stage that needs a CLI flag binds it in an `input` CE. It is then lifted into any command that asks for it, with no further wiring: -*) +```fsharp module Options = let quick = Input.option "--quick" @@ -228,11 +168,11 @@ let restore = run "dotnet restore" } } +``` -(** Bind several sources with `and!`, never with nested `let!`: -*) +```fsharp let build = input { let! quick = Options.quick @@ -243,8 +183,8 @@ let build = run (cmd $"dotnet build -c {config}") } } +``` -(** The CE rejects a second `let!`; bind every source in one `let! … and! …` block. Binding this way is what lifts flags into the command line help without first evaluating pipelines. @@ -256,8 +196,7 @@ which tracks inputs and unions them by reference. `--quick` and `--configuration` appear under `build --help` without being named anywhere but the stages that read them: -*) - +```fsharp let buildCommand = command "build" { description "Restores and builds the solution" @@ -268,23 +207,23 @@ let buildCommand = build } } +``` -(** Flags sit on the commands whose stages read them, not on the root. `addInput` covers the remainder: flags no pipeline asks for that a root command still wants to expose. ## Wiring the root `rootCommand` parses and invokes immediately, returning the process exit code. It belongs in `main`: -*) +```fsharp let main argv = rootCommand argv { description "My build" addCommands [ buildCommand ] } +``` -(** A command with no pipelines is a grouping node: it gets no action, so System.CommandLine reports the missing subcommand and prints help instead of succeeding silently. @@ -299,8 +238,8 @@ subcommand, a producer arrangement dependency validation rejects — and `130` t session, a test — builds the root once with `Command.root`, which takes every `rootCommand` operation and runs nothing, then invokes it as often as it likes. `Command.invoke` answers a `RunResult`: the exit code, its `RunOutcome`, and each pipeline run's `ScopeReport`s and `StageTiming`s. -*) +```fsharp let root = Command.root { description "My build" @@ -311,8 +250,8 @@ let buildQuick () = let result = root |> Command.invoke [ "build"; "--quick"; "true" ] printfn "%d failure(s)" result.Failures.Length result.ExitCode +``` -(** `root.Invoke(args, output = writer, cancellationToken = token)` sends help, `--explain`, the timing summary and parse errors to `writer` instead of the console, and cancels the running pipeline — its processes with it — when `token` fires. @@ -322,8 +261,8 @@ when `token` fires. ### Stages nest A stage can be yielded inside another stage, arbitrarily deep, with no separate grouping concept: -*) +```fsharp let nested = stage "outer" { run "dotnet --version" @@ -333,14 +272,14 @@ let nested = run "dotnet --info" } } +``` -(** ### Settings inherit Settings resolve outward: stage, then parent stage, then pipeline. A pipeline-level `workingDir` or `envVars` defaults every stage that does not override it: -*) +```fsharp let inherited = pipeline "inherited" { workingDir __SOURCE_DIRECTORY__ @@ -353,14 +292,14 @@ let inherited = run "dotnet --version" } } +``` -(** ### Stages are values A stage is an ordinary value, so reuse is ordinary F#. Return them from functions, put them in lists, iterate over them: -*) +```fsharp let testProject (name: string) = stage $"test {name}" { run (cmd $"dotnet test {name}") } @@ -369,10 +308,10 @@ let testAll = for proj in [ "A.fsproj"; "B.fsproj" ] do testProject proj } +``` -(** The same works one layer up: a `pipeline` is a value, and a `command` can run several in declaration order. -[Composing reusable blocks](composition.fsx) covers nesting, lists of blocks, and stages that carry their own +[Composing reusable blocks](composition.md) covers nesting, lists of blocks, and stages that carry their own inputs. ### Nameless Pipelines @@ -381,8 +320,7 @@ A short CLI command often runs a single pipeline named the same as the command. this: it inherits its description and name from the command it is defined within, essentially `pipeline null { }`. -*) - +```fsharp let namelessPipe = command "build" { description "Build projects" @@ -392,10 +330,8 @@ let namelessPipe = } } } +``` - - -(** ### Command-level defaults A `command` also takes the pipeline settings: `workingDir`, `envVars`, `timeout`, `timeoutForStage`, @@ -403,8 +339,8 @@ A `command` also takes the pipeline settings: `workingDir`, `envVars`, `timeout` `noPrefixForStep`, `noStdRedirectForStep`, `runBeforeEachStage`, `runAfterEachStage`, `post`, `verbosity`, `verbose` and `quiet`. Set on the command, these are **defaults for every pipeline the command runs**, saving repetition across pipelines: -*) +```fsharp let ciCommand = command "ci" { workingDir __SOURCE_DIRECTORY__ @@ -417,8 +353,8 @@ let ciCommand = stage "test" { run "dotnet test" } } } +``` -(** A default never overwrites a pipeline's own setting: the pipeline wins regardless of write order. A default written *below* the pipelines still applies, because defaults are folded in once the whole command is built, not as each pipeline is yielded. @@ -452,8 +388,7 @@ overload resolves to the same `int voption`: | `parallel'`, `parallel' true` | `ValueSome -1` | unbounded | | `parallel' 0`, `parallel' -1` | `ValueSome n`, `n < 1` | unbounded | -*) - +```fsharp let fanOut = stage "fan out" { parallel' 2 @@ -461,48 +396,48 @@ let fanOut = run "dotnet build B.fsproj" run "dotnet build C.fsproj" } +``` -(** The bound is exact: a stage set to `2` never has a third step in flight. To choose a mode at runtime, return the choice from a single condition rather than writing two operations: -*) +```fsharp let adaptive = stage "fan out" { parallel' (fun (_: StageContext) -> if System.Environment.ProcessorCount > 4 then ValueSome 4 else ValueNone) run "dotnet build A.fsproj" run "dotnet build B.fsproj" } +``` -(** ### Settings overwrite, conditions conjoin The two halves of the stage CE compose differently. Mixing them up is the most common surprise. `parallel'`, `workingDir`, `timeout` and the rest are **settings**: the last one written wins and an earlier one leaves no trace. -*) +```fsharp let lastWins = stage "settings" { parallel' 4 parallel' false // sequential; the 4 is gone, not combined with run "dotnet build" } +``` -(** `when'`, `whenBranch`, `whenWindows` and the rest are **conditions**: each narrows the stage to the logical AND of everything declared so far, so a second condition can only make the stage run less often. -*) +```fsharp let narrows = stage "conditions" { whenBranch "master" whenWindows // master AND Windows, not Windows instead of master run "dotnet pack" } +``` -(** To widen a condition, write **one** `whenAny { }` containing both alternatives: a second operation would narrow instead. To switch parallel modes, write **one** condition function returning the mode: a second operation would discard the first. @@ -518,8 +453,8 @@ A timeout cancels the stage and kills the whole process tree it started, grandch ### Post stages `post` stages run after the main stages whether or not the pipeline succeeded: the place for teardown. -*) +```fsharp let withTeardown = pipeline "integration" { stage "up" { run "docker compose up -d" } @@ -527,8 +462,8 @@ let withTeardown = post [ stage "down" { run "docker compose down" } ] } +``` -(** ### Failure control - `continueStepsOnFailure` keeps a stage going after a failed step. @@ -555,8 +490,8 @@ any other setting: | `outputTo sink` | any of the above as a `StageOutput` value, for when the choice is made at run time | The common case is a test run: silent when it passes, and its own output as the reason when it does not. -*) +```fsharp let quietTests = pipeline "test" { stage "test" { @@ -564,8 +499,8 @@ let quietTests = run "dotnet test" } } +``` -(** `captureOutput` lifts stderr into the step's error, or everything written if there was none. A failing stage still says why, on the console and in the GitHub Actions annotation. @@ -613,9 +548,8 @@ positional equivalent. `BuildOption.map`, `.mapOpt` and `.mapArg` apply an `Input.*` combinator to both forms or to one. Both forms are `ActionInput` values, so they bind in an `input` CE exactly as a hand-rolled option does: -*) - +```fsharp let packaging = input { let! config = Baked.Dotnet.config.option @@ -628,8 +562,8 @@ let packaging = run (cmd $"dotnet pack -c {config}") } } +``` -(** ### Versioning a project file `Baked.SemVer.Version` is semantic-version arithmetic over the `Bump` DU. `Baked.SemVer.Version.IO` applies it @@ -711,10 +645,9 @@ first away. To widen a condition, put the alternatives in one `whenAny { }`. converts a `string` when a single overload is in play. It has no `InputSpec` form: bind the value outside the stage and use `runSensitive $"…"` inside it as normal. - + ## API reference The [API reference](https://shayanhabibi.github.io/Partas.Build/reference/) is generated from the XML documentation on each custom operation. -*) diff --git a/docs/content/extensions/shipit.md b/docs/content/extensions/shipit.md index be1fa08..e12b0d6 100644 --- a/docs/content/extensions/shipit.md +++ b/docs/content/extensions/shipit.md @@ -1,5 +1,6 @@ --- title: EasyBuild.ShipIt +description: Conventional-commit releases with changelogs and synchronized package versions. category: Extensions order: 1 --- @@ -8,7 +9,7 @@ order: 1 `Partas.Build.EasyBuild.ShipIt` wraps [EasyBuild.ShipIt](https://github.com/easybuild-org/EasyBuild.ShipIt) as reusable operations, inputs, stages and commands. ShipIt calculates release versions from conventional commits, updates changelogs and configured project files, and can open release pull requests. -The extension supports `net10.0`, `net8.0` and `netstandard2.0`. The default ShipIt tool version, 3.1.0, requires .NET 10. Add the extension as a project/package reference; script consumers can use the [package feed described on the home page](https://shayanhabibi.github.io/Partas.Build/). +The extension supports `net10.0`, `net8.0` and `netstandard2.0`. The default ShipIt tool version, 3.1.0, requires .NET 10. Add the extension as a project/package reference using the [installation guide](../Build/installation.md). ## Add the commands to your build diff --git a/docs/content/external-annotations/external-annotations-api.fsx b/docs/content/external-annotations/external-annotations-api.md similarity index 71% rename from docs/content/external-annotations/external-annotations-api.fsx rename to docs/content/external-annotations/external-annotations-api.md index c4e4ba3..2ad59a0 100644 --- a/docs/content/external-annotations/external-annotations-api.fsx +++ b/docs/content/external-annotations/external-annotations-api.md @@ -1,64 +1,9 @@ -(** --- title: F# surface category: ExternalAnnotations index: 2 --- -*) -(*** hide ***) -// Sources are #load-ed rather than #r-ing built DLLs so the guide type-checks against the code as written, -// and so nothing holds a file lock while the site watcher is running. Keep this list in the same order -// as the items of each project. -#r "nuget: FSharp.Control.AsyncSeq, 4.15.0" -#r "nuget: FsToolkit.ErrorHandling, 5.2.0" -#r "nuget: System.CommandLine, 2.0.11" -#r "nuget: Spectre.Console, 0.57.2" -#r "nuget: System.Reflection.MetadataLoadContext, 10.0.11" - -#load "../../../src/Partas.Build.Cmd/Execution.fs" -#load "../../../src/Partas.Build.Cmd/Program.fs" -#load "../../../src/Partas.Build/System.CommandLine/Aliases.fs" -#load "../../../src/Partas.Build/System.CommandLine/Inputs.fs" -#load "../../../src/Partas.Build/Exceptions.fs" -#load "../../../src/Partas.Build/Output.fs" -#load "../../../src/Partas.Build/Terminal.fs" -#load "../../../src/Partas.Build/Annotations.fs" -#load "../../../src/Partas.Build/Environment.fs" -#load "../../../src/Partas.Build/Timing.fs" -#load "../../../src/Partas.Build/Producer.fs" -#load "../../../src/Partas.Build/Failures.fs" -#load "../../../src/Partas.Build/Conductors.fs" -#load "../../../src/Partas.Build/GitHubActions.fs" -#load "../../../src/Partas.Build/Conductors.Runners.fs" -#load "../../../src/Partas.Build/Process.fs" -#load "../../../src/Partas.Build/Operations.fs" -#load "../../../src/Partas.Build/Dependencies.fs" -#load "../../../src/Partas.Build/DependencyPlan.fs" -#load "../../../src/Partas.Build/ExecutionState.fs" -#load "../../../src/Partas.Build/Builders/StageSettings.fs" -#load "../../../src/Partas.Build/Builders/Stage.fs" -#load "../../../src/Partas.Build/Builders/Conditions.fs" -#load "../../../src/Partas.Build/Builders/PipelineSettings.fs" -#load "../../../src/Partas.Build/Builders/Pipeline.fs" -#load "../../../src/Partas.Build/Builders/Inputs.fs" -#load "../../../src/Partas.Build/AiEnvironment.fs" -#load "../../../src/Partas.Build/MachineOutput.fs" -#load "../../../src/Partas.Build/Explain.fs" -#load "../../../src/Partas.Build/Summary.fs" -#load "../../../src/Partas.Build/RunResult.fs" -#load "../../../src/Partas.Build/Builders/Command.fs" -#load "../../../src/Partas.Build.Baked/Program.fs" -#load "../../../src/Partas.Build.Baked/Common.fs" -#load "../../../src/Partas.Build.Baked/NuGet.fs" -#load "../../../src/Partas.Build.Baked/Dotnet.fs" -#load "../../../src/Partas.Build.Baked/SemVer.fs" -#load "../../../Partas.ExternalAnnotations/Library.fs" -#load "../../../Partas.Build.ExternalAnnotations/Library.fs" -open Partas.Build -open Partas.Build.ExternalAnnotations - -(** # External Annotations — F# surface [The overview](index.md) covers what external annotations are and why a sidecar is the only reliable way to @@ -86,7 +31,10 @@ forms sit over one implementation. Generation belongs after the build and before the pack. `generateTo` still binds `--strict` itself: the pipeline is an `InputSpec`, and the flag appears under the command's `--help` without being registered anywhere: -*) + +```fsharp +open Partas.Build +open Partas.Build.ExternalAnnotations let packPipeline = pipeline "pack" { @@ -98,8 +46,8 @@ let packPipeline = verifyPackageOf 596 "bin/My.Lib.1.0.0.nupkg" } +``` -(** `verifyPackage` is the same check with a floor of `0`: every assembly under `lib/` must have a sidecar beside it, but an empty sidecar passes. `verifyPackageOf` adds the floor and catches the characteristic failure: a green build, a sidecar generated from the wrong assembly, and a package that annotates nothing. @@ -111,28 +59,28 @@ Verification opens the `.nupkg` a consumer would download rather than any proxy The three commands are values. Hand them to your own root command and you have the tool, without installing it: -*) +```fsharp let main argv = rootCommand argv { description "My build" addCommands [ generateCommand; verifyCommand; initCommand ] } +``` -(** That is, modulo the description, the entire `partas-annotations` executable. To expose one operation under your own name and defaults, wrap the option-driven stage instead: -*) +```fsharp let annotationsCommand = command "annotations" { description "Generates the external annotations sidecar" Command.pipeline { generateStage } } +``` -(** > `Partas.Build.ExternalAnnotations` is `[]` and exposes its own `Options` module (`strict`, > `assembly`, `output`, `package`, `minMembers`, `directory`, `annotationsTool`, `force`). If your build CLI has > a module by that name, open the namespace before defining yours, or qualify. @@ -141,22 +89,22 @@ let annotationsCommand = `initIn` is the pipeline form of `init`. It writes `Directory.Build.targets` into the given directory and refuses to overwrite an existing one without `--force`: -*) +```fsharp let initPipeline = pipeline "init" { initIn "src/My.Lib" } +``` -(** Note the directory: MSBuild takes the first `Directory.Build.targets` it finds walking up, so writing it beside the packable project scopes annotations to that project and leaves test and sample projects untouched. `writeTargets` is the same thing without a stage around it: a path and an optional command to pin inside the file: -*) +```fsharp let writeItSomewhereElse () = writeTargets "build/Partas.ExternalAnnotations.targets" (Some "dotnet partas-annotations") +``` -(** Pinning the command means the committed artifact says what runs, instead of depending on an ambient MSBuild property set correctly on every machine. @@ -164,14 +112,14 @@ property set correctly on every machine. `packArgs` builds the `-p:CustomAfterMicrosoftCommonTargets=` argument that injects the same MSBuild logic into a single `dotnet pack`, with no file added to the project. The path must be absolute; `packArgs` makes it so: -*) +```fsharp let packWithInjection = stage "pack" { run (Cmd.ofList "dotnet" ([ "pack"; "-c"; "Release" ] @ packArgs "build/Partas.ExternalAnnotations.targets")) } +``` -(** This is the exception, not the rule. A committed `Directory.Build.targets` is what makes *every* pack correct, including the ones that never go through your pipeline. @@ -216,4 +164,3 @@ Both are ordinary functions; `ExternalAnnotationGenerator` sits under them for t ## Recipes Concrete end-to-end setups are on the [recipes page](external-annotations-recipes.md). -*) diff --git a/docs/content/external-annotations/external-annotations-recipes.md b/docs/content/external-annotations/external-annotations-recipes.md index 6a325bb..e5e8d8a 100644 --- a/docs/content/external-annotations/external-annotations-recipes.md +++ b/docs/content/external-annotations/external-annotations-recipes.md @@ -7,7 +7,7 @@ index: 3 # External Annotations — Recipes Concrete setups. Background is on the [overview](index.md); the F# API is -[here](external-annotations-api.fsx). +[here](external-annotations-api.md). ## Ship annotations from a repo you own diff --git a/docs/content/external-annotations/index.md b/docs/content/external-annotations/index.md index 1b2d040..c6339c5 100644 --- a/docs/content/external-annotations/index.md +++ b/docs/content/external-annotations/index.md @@ -1,4 +1,4 @@ ---- +--- title: Overview category: ExternalAnnotations index: 1 @@ -39,7 +39,7 @@ sites resolve. | `Partas.Build.ExternalAnnotations` | Partas.Build stages and commands over the generator, plus the MSBuild `.targets` as an embedded resource and a packed `build/` asset. | | `Partas.ExternalAnnotations.Tool` | The `partas-annotations` dotnet tool: a `rootCommand` over the library's three commands. | -The tool is what MSBuild shells out to during pack. See [the F# surface](external-annotations-api.fsx) to own +The tool is what MSBuild shells out to during pack. See [the F# surface](external-annotations-api.md) to own the behaviour in your own build instead. Your assembly is never loaded for execution and its target framework is irrelevant. Any tool host produces diff --git a/docs/content/index.md b/docs/content/index.md new file mode 100644 index 0000000..2671070 --- /dev/null +++ b/docs/content/index.md @@ -0,0 +1,57 @@ +--- +title: Partas.Build +description: Composable build workflows and command-line tools in F#. +layout: splash +order: 0 +--- + +
+
+F# · Build workflows · Command-line tools +

A build you can
read and reuse.

+

Define stages once. Compose them into commands. The inputs each stage reads become your CLI options, validation, and help.

+ +
+
+
Build.fs
+
let build = input {
+    let! config = Input.option<string> "--configuration"
+                  |> Input.def "Release"
+    return stage "compile" {
+        run (cmd $"dotnet build -c {config}")
+    }
+}
+
+let root = Command.root {
+    command "build" { build }
+}
+
$ build build --help
Options: --configuration <value> [default: Release]
+
+
+ +## One definition, several ways to use it + +::::cards +:::card title="Compose your workflow" href="/Partas.Build/build/composition/" +Stages and pipelines are F# values. Nest them, yield lists, and share them across commands and files. +::: +:::card title="Start with ready-made stages" href="/Partas.Build/build/baked/" +Baked supplies restore, build, pack, test, and publish stages. ShipIt adds changelog-driven release workflows. +::: +:::card title="Inspect before you run" href="/Partas.Build/build/agents/" +Generated help, command schemas, execution plans, and JSON results make the same build usable by people and agents. +::: +:::: + +
+ +## From a script to a hosted build + +Use a `.fsx` script or a build project. Keep a reusable root in a long-lived F# session and invoke it with typed results. Partas.Build targets `net10.0`, `net8.0`, and `netstandard2.0`. + +[Installation](Build/installation.md) · [Hosting](Build/hosting.md) · [ShipIt extension](extensions/shipit.md) · [External annotations](external-annotations/index.md) + +
diff --git a/docs/docs.fsproj b/docs/docs.fsproj index 1f05ba3..41ac2ce 100644 --- a/docs/docs.fsproj +++ b/docs/docs.fsproj @@ -3,8 +3,10 @@ Exe net10.0 false + true + @@ -26,6 +28,12 @@ - + + + + + + + diff --git a/docs/static/favicon.svg b/docs/static/favicon.svg new file mode 100644 index 0000000..99b47e9 --- /dev/null +++ b/docs/static/favicon.svg @@ -0,0 +1 @@ + diff --git a/docs/static/llms.txt b/docs/static/llms.txt deleted file mode 100644 index be9634e..0000000 --- a/docs/static/llms.txt +++ /dev/null @@ -1,30 +0,0 @@ -# Partas.Build - -> An F# build-pipeline DSL whose stages declare the CLI options they read. Commands derive their -> System.CommandLine option set from the pipelines they activate, so options, validation and help text -> come from the pipeline definition instead of manual registration. Runs from a `.fsx` script or a build -> project. - -## Start here - -- [Capabilities](https://shayanhabibi.github.io/Partas.Build/build/capabilities/): every operation and Input combinator, one line each -- [Index](https://shayanhabibi.github.io/Partas.Build/build/): the layers, and a first pipeline -- [Composition](https://shayanhabibi.github.io/Partas.Build/build/composition/): nesting stages, and reusing commands across files -- [Hosting](https://shayanhabibi.github.io/Partas.Build/build/hosting/): running a build from a long-lived F# session (SageFs) through `Command.invoke` -- [ShipIt extension](https://shayanhabibi.github.io/Partas.Build/extensions/shipit/): ShipIt-backed package bumps, release commands and explicit setup - -## Ask the script rather than reading it - -Every command generates its own `--help` from the stages it runs, and the flags listed there are exactly the -flags those stages bind. `