From d642356d8543fa021743e7587f3e97c4c511e312 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Fri, 11 Sep 2026 11:48:28 +0200 Subject: [PATCH 1/5] Update to Fantomas 8 --- .config/dotnet-tools.json | 5 +- .editorconfig | 8 +- Fun.Build.Cli/History.fs | 123 ++-- Fun.Build.Cli/Pipeline.fs | 110 ++-- Fun.Build.Tests/ConditionsBuilderTests.fs | 701 ++++++++++------------ Fun.Build.Tests/ExceptionHandlingTests.fs | 90 ++- Fun.Build.Tests/PipelineBuilderTests.fs | 442 ++++++-------- Fun.Build.Tests/SequenceTests.fs | 389 ++++++------ Fun.Build.Tests/StageContextExtensions.fs | 418 ++++++------- Fun.Build.Tests/TimeoutTests.fs | 216 +++---- Fun.Build/BuiltinCmds.fs | 272 +++++---- Fun.Build/Changelog.fs | 13 +- Fun.Build/ConditionsBuilder.fs | 153 +++-- Fun.Build/PipelineBuilder.fs | 69 ++- Fun.Build/PipelineContextExtensions.fs | 52 +- Fun.Build/ProcessExtensions.fs | 15 +- Fun.Build/StageBuilder.fs | 303 +++++----- Fun.Build/StageContextExtensions.fs | 42 +- Fun.Build/Types.fs | 26 +- Fun.Build/Windows.fs | 53 +- build.fsx | 4 +- demo.fsx | 18 +- 22 files changed, 1661 insertions(+), 1861 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 4d88e0f..8758b7a 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,10 +3,11 @@ "isRoot": true, "tools": { "fantomas": { - "version": "5.2.1", + "version": "8.0.0-beta-002", "commands": [ "fantomas" - ] + ], + "rollForward": false } } } \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index 1f7ae51..f511c15 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,8 +9,10 @@ fsharp_max_record_width=60 fsharp_max_array_or_list_width=120 fsharp_max_value_binding_width=120 fsharp_max_function_binding_width=120 -fsharp_max_dot_get_expression_width=120 -fsharp_multiline_block_brackets_on_same_column=true +fsharp_multiline_bracket_style=stroustrup +fsharp_newline_before_multiline_computation_expression=false fsharp_multi_line_lambda_closing_newline=true fsharp_blank_lines_around_nested_multiline_expressions=false -fsharp_experimental_stroustrup_style=true \ No newline at end of file + +[*.fsx] +fsharp_newline_before_multiline_computation_expression=true diff --git a/Fun.Build.Cli/History.fs b/Fun.Build.Cli/History.fs index 898e7e8..5f66187 100644 --- a/Fun.Build.Cli/History.fs +++ b/Fun.Build.Cli/History.fs @@ -101,67 +101,66 @@ type History = ?withLastArgs: bool, ?promptForContinuation: bool, ?continuationItems: Collections.Generic.List - ) = - asyncResult { - let withLastArgs = defaultArg withLastArgs false - let scriptFile = Path.GetFileName history.Pipeline.Script - let scriptDir = Path.GetDirectoryName history.Pipeline.Script - let promptForContinuation = defaultArg promptForContinuation false - - let table = Table() - table.ShowHeaders <- false - table.AddColumns("", "") |> ignore - table.AddRow("Script", $"[green]{scriptFile}[/]") |> ignore - table.AddRow("Pipeline", $"[bold green]{history.Pipeline.Name}[/]") |> ignore - table.AddRow("WorkingDir", $"[green]{scriptDir}[/]") |> ignore - table.AddRow("Arguments", history.Args) |> ignore - - AnsiConsole.Write table - AnsiConsole.MarkupLine("New arguments (leave empty to use the one from history):") - - let args = - if withLastArgs then - history.Args - else - let text = TextPrompt("> ") - text.AllowEmpty <- true - let newArgs = AnsiConsole.Prompt text - if String.IsNullOrEmpty newArgs then history.Args else newArgs - + ) = asyncResult { + let withLastArgs = defaultArg withLastArgs false + let scriptFile = Path.GetFileName history.Pipeline.Script + let scriptDir = Path.GetDirectoryName history.Pipeline.Script + let promptForContinuation = defaultArg promptForContinuation false + + let table = Table() + table.ShowHeaders <- false + table.AddColumns("", "") |> ignore + table.AddRow("Script", $"[green]{scriptFile}[/]") |> ignore + table.AddRow("Pipeline", $"[bold green]{history.Pipeline.Name}[/]") |> ignore + table.AddRow("WorkingDir", $"[green]{scriptDir}[/]") |> ignore + table.AddRow("Arguments", history.Args) |> ignore + + AnsiConsole.Write table + AnsiConsole.MarkupLine("New arguments (leave empty to use the one from history):") + + let args = + if withLastArgs then + history.Args + else + let text = TextPrompt("> ") + text.AllowEmpty <- true + let newArgs = AnsiConsole.Prompt text + if String.IsNullOrEmpty newArgs then history.Args else newArgs + + AnsiConsole.MarkupLine("") + + let currentItems = continuationItems |> Option.defaultWith (fun () -> Collections.Generic.List()) + currentItems.Add({ history with Args = args }) + + if promptForContinuation && not withLastArgs then + if AnsiConsole.Confirm("Do you want to select another history to continue?", defaultValue = false) then + match History.PromptSelect() with + | ValueSome item -> do! History.Run(ctx, item, continuationItems = currentItems, promptForContinuation = true) + | _ -> () AnsiConsole.MarkupLine("") - let currentItems = continuationItems |> Option.defaultWith (fun () -> Collections.Generic.List()) - currentItems.Add({ history with Args = args }) - - if promptForContinuation && not withLastArgs then - if AnsiConsole.Confirm("Do you want to select another history to continue?", defaultValue = false) then - match History.PromptSelect() with - | ValueSome item -> do! History.Run(ctx, item, continuationItems = currentItems, promptForContinuation = true) - | _ -> () - AnsiConsole.MarkupLine("") - - // If there is no continuation commands need to fill, then we consider it should be executed now - if continuationItems.IsNone then - for index, history in Seq.indexed currentItems do - let scriptFile = Path.GetFileName history.Pipeline.Script - let scriptDir = Path.GetDirectoryName history.Pipeline.Script - let isHelpCommand = history.Args.Trim().ToLower() = "-h" - - if not isHelpCommand then - History.Add { history with StartedTime = DateTime.Now } - - if index = 0 then - AnsiConsole.Console.MarkupLine("[green]Start executing...[/]") - else - AnsiConsole.Console.MarkupLine("[green]Continue executing...[/]") - AnsiConsole.Console.WriteLine() - - try - do! - ctx.RunCommand($"dotnet fsi \"{scriptFile}\" -- -p {history.Pipeline.Name} {history.Args}", workingDir = scriptDir) - |> Async.map (ignore >> Ok) - with _ -> - () - - if currentItems.Count = 1 && isHelpCommand then do! History.Run(ctx, history) - } + // If there is no continuation commands need to fill, then we consider it should be executed now + if continuationItems.IsNone then + for index, history in Seq.indexed currentItems do + let scriptFile = Path.GetFileName history.Pipeline.Script + let scriptDir = Path.GetDirectoryName history.Pipeline.Script + let isHelpCommand = history.Args.Trim().ToLower() = "-h" + + if not isHelpCommand then + History.Add { history with StartedTime = DateTime.Now } + + if index = 0 then + AnsiConsole.Console.MarkupLine("[green]Start executing...[/]") + else + AnsiConsole.Console.MarkupLine("[green]Continue executing...[/]") + AnsiConsole.Console.WriteLine() + + try + do! + ctx.RunCommand($"dotnet fsi \"{scriptFile}\" -- -p {history.Pipeline.Name} {history.Args}", workingDir = scriptDir) + |> Async.map (ignore >> Ok) + with _ -> + () + + if currentItems.Count = 1 && isHelpCommand then do! History.Run(ctx, history) + } diff --git a/Fun.Build.Cli/Pipeline.fs b/Fun.Build.Cli/Pipeline.fs index 0071aef..bebdcff 100644 --- a/Fun.Build.Cli/Pipeline.fs +++ b/Fun.Build.Cli/Pipeline.fs @@ -157,68 +157,66 @@ type Pipeline with pipeline: Pipeline, ?promptForContinuation: bool, ?continuationCommands: Collections.Generic.List<{| Pipeline: Pipeline; Args: string |}> - ) = - asyncResult { - let promtForContinuation = defaultArg promptForContinuation false - let scriptFile = Path.GetFileName pipeline.Script - let scriptDir = Path.GetDirectoryName pipeline.Script + ) = asyncResult { + let promtForContinuation = defaultArg promptForContinuation false + let scriptFile = Path.GetFileName pipeline.Script + let scriptDir = Path.GetDirectoryName pipeline.Script - let table = Table() - table.ShowHeaders <- false - table.AddColumns("", "") |> ignore - table.AddRow("Script", $"[green]{scriptFile}[/]") |> ignore - table.AddRow("Pipeline", $"[bold green]{pipeline.Name}[/]") |> ignore - table.AddRow("WorkingDir", $"[green]{scriptDir}[/]") |> ignore - if String.IsNullOrEmpty(pipeline.Description.Trim()) |> not then - table.AddRow("Description", $"[green]{pipeline.Description}[/]") |> ignore + let table = Table() + table.ShowHeaders <- false + table.AddColumns("", "") |> ignore + table.AddRow("Script", $"[green]{scriptFile}[/]") |> ignore + table.AddRow("Pipeline", $"[bold green]{pipeline.Name}[/]") |> ignore + table.AddRow("WorkingDir", $"[green]{scriptDir}[/]") |> ignore + if String.IsNullOrEmpty(pipeline.Description.Trim()) |> not then + table.AddRow("Description", $"[green]{pipeline.Description}[/]") |> ignore - AnsiConsole.Write table + AnsiConsole.Write table - let textPrompt = TextPrompt("Arguments (-h for help): ") - textPrompt.AllowEmpty <- true - let args = AnsiConsole.Prompt textPrompt + let textPrompt = TextPrompt("Arguments (-h for help): ") + textPrompt.AllowEmpty <- true + let args = AnsiConsole.Prompt textPrompt - let currentCommands = continuationCommands |> Option.defaultWith (fun () -> Collections.Generic.List()) - currentCommands.Add({| Pipeline = pipeline; Args = args |}) + let currentCommands = continuationCommands |> Option.defaultWith (fun () -> Collections.Generic.List()) + currentCommands.Add({| Pipeline = pipeline; Args = args |}) + AnsiConsole.MarkupLine("") + + if promtForContinuation then + if AnsiConsole.Confirm("Do you want to select another pipeline to continue?", defaultValue = false) then + match Pipeline.PromptSelect() with + | ValueSome item -> do! Pipeline.Run(ctx, item, continuationCommands = currentCommands, promptForContinuation = true) + | _ -> () AnsiConsole.MarkupLine("") - if promtForContinuation then - if AnsiConsole.Confirm("Do you want to select another pipeline to continue?", defaultValue = false) then - match Pipeline.PromptSelect() with - | ValueSome item -> do! Pipeline.Run(ctx, item, continuationCommands = currentCommands, promptForContinuation = true) - | _ -> () - AnsiConsole.MarkupLine("") - - if continuationCommands.IsNone then - for index, command in Seq.indexed currentCommands do - let scriptFile = Path.GetFileName command.Pipeline.Script - let scriptDir = Path.GetDirectoryName command.Pipeline.Script - let isHelpCommand = command.Args.Trim().ToLower() = "-h" - - if not isHelpCommand then - History.Add - { - Pipeline = pipeline - Args = args - StartedTime = DateTime.Now - } - - if index = 0 then - AnsiConsole.Console.MarkupLine("[green]Start executing...[/]") - else - AnsiConsole.Console.MarkupLine("[green]Continue executing...[/]") - AnsiConsole.Console.WriteLine() - - try - do! - ctx.RunCommand($"dotnet fsi \"{scriptFile}\" -- -p {command.Pipeline.Name} {command.Args}", workingDir = scriptDir) - |> Async.map (ignore >> Ok) - with _ -> - () - - if isHelpCommand && currentCommands.Count = 1 then - do! Pipeline.Run(ctx, pipeline) - } + if continuationCommands.IsNone then + for index, command in Seq.indexed currentCommands do + let scriptFile = Path.GetFileName command.Pipeline.Script + let scriptDir = Path.GetDirectoryName command.Pipeline.Script + let isHelpCommand = command.Args.Trim().ToLower() = "-h" + + if not isHelpCommand then + History.Add { + Pipeline = pipeline + Args = args + StartedTime = DateTime.Now + } + + if index = 0 then + AnsiConsole.Console.MarkupLine("[green]Start executing...[/]") + else + AnsiConsole.Console.MarkupLine("[green]Continue executing...[/]") + AnsiConsole.Console.WriteLine() + + try + do! + ctx.RunCommand($"dotnet fsi \"{scriptFile}\" -- -p {command.Pipeline.Name} {command.Args}", workingDir = scriptDir) + |> Async.map (ignore >> Ok) + with _ -> + () + + if isHelpCommand && currentCommands.Count = 1 then + do! Pipeline.Run(ctx, pipeline) + } diff --git a/Fun.Build.Tests/ConditionsBuilderTests.fs b/Fun.Build.Tests/ConditionsBuilderTests.fs index 4aa2eae..b7b6135 100644 --- a/Fun.Build.Tests/ConditionsBuilderTests.fs +++ b/Fun.Build.Tests/ConditionsBuilderTests.fs @@ -9,386 +9,320 @@ open Fun.Build.PipelineContextExtensionsInternal [] let ``whenCmd should work`` () = - shouldNotBeCalled (fun call -> - pipeline "" { - stage "" { - whenCmd { name "test1" } - run call - } - runImmediate - } - ) - - shouldBeCalled (fun call -> - pipeline "" { - cmdArgs [ "test1" ] - stage "" { - whenCmd { name "test1" } - run call - } - runImmediate - } - ) - - shouldBeCalled (fun call -> - pipeline "" { - cmdArgs [ "test1"; "v1" ] - stage "" { + shouldNotBeCalled (fun call -> pipeline "" { + stage "" { + whenCmd { name "test1" } + run call + } + runImmediate + }) + + shouldBeCalled (fun call -> pipeline "" { + cmdArgs [ "test1" ] + stage "" { + whenCmd { name "test1" } + run call + } + runImmediate + }) + + shouldBeCalled (fun call -> pipeline "" { + cmdArgs [ "test1"; "v1" ] + stage "" { + whenCmd { + name "test1" + acceptValues [ "v1"; "v2" ] + } + run call + } + runImmediate + }) + + shouldBeCalled (fun call -> pipeline "" { + cmdArgs [ "-t"; "v1" ] + stage "" { + whenAll { whenCmd { name "test1" + alias "-t" acceptValues [ "v1"; "v2" ] } - run call - } - runImmediate - } - ) - - shouldBeCalled (fun call -> - pipeline "" { - cmdArgs [ "-t"; "v1" ] - stage "" { - whenAll { - whenCmd { - name "test1" - alias "-t" - acceptValues [ "v1"; "v2" ] - } - } - run call } - runImmediate + run call } - ) + runImmediate + }) - shouldBeCalled (fun call -> - pipeline "" { - stage "" { - whenCmd { - name "test1" - optional - } - run call + shouldBeCalled (fun call -> pipeline "" { + stage "" { + whenCmd { + name "test1" + optional } - runImmediate + run call } - ) + runImmediate + }) [] let ``whenCmdArg should work`` () = - shouldNotBeCalled (fun call -> - pipeline "" { - stage "" { - whenCmdArg "test1" - run call - } - runImmediate + shouldNotBeCalled (fun call -> pipeline "" { + stage "" { + whenCmdArg "test1" + run call } - ) + runImmediate + }) - shouldBeCalled (fun call -> - pipeline "" { - cmdArgs [ "test1" ] - stage "" { - whenCmdArg "test1" - run call - } - runImmediate + shouldBeCalled (fun call -> pipeline "" { + cmdArgs [ "test1" ] + stage "" { + whenCmdArg "test1" + run call } - ) + runImmediate + }) - shouldBeCalled (fun call -> - pipeline "" { - stage "" { - whenCmdArg "test1" "value" "description" true - run call - } - runImmediate + shouldBeCalled (fun call -> pipeline "" { + stage "" { + whenCmdArg "test1" "value" "description" true + run call } - ) + runImmediate + }) - shouldNotBeCalled (fun call -> - pipeline "" { - stage "" { - whenCmdArg "test1" "value" "description" false - run call - } - runImmediate + shouldNotBeCalled (fun call -> pipeline "" { + stage "" { + whenCmdArg "test1" "value" "description" false + run call } - ) + runImmediate + }) - shouldBeCalled (fun call -> - pipeline "" { - cmdArgs [ "test1"; "value" ] - stage "" { - whenCmdArg "test1" "value" "description" false - run call - } - runImmediate + shouldBeCalled (fun call -> pipeline "" { + cmdArgs [ "test1"; "value" ] + stage "" { + whenCmdArg "test1" "value" "description" false + run call } - ) + runImmediate + }) [] let ``whenEnv should work`` () = - shouldNotBeCalled (fun call -> - pipeline "" { - stage "" { - whenEnv { name "test1" } - run call - } - runImmediate - } - ) - - shouldBeCalled (fun call -> - pipeline "" { - envVars [ "test1", "" ] - stage "" { - whenEnv { name "test1" } - run call - } - runImmediate - } - ) - - shouldBeCalled (fun call -> - pipeline "" { - envVars [ "test1", "v1" ] - stage "" { + shouldNotBeCalled (fun call -> pipeline "" { + stage "" { + whenEnv { name "test1" } + run call + } + runImmediate + }) + + shouldBeCalled (fun call -> pipeline "" { + envVars [ "test1", "" ] + stage "" { + whenEnv { name "test1" } + run call + } + runImmediate + }) + + shouldBeCalled (fun call -> pipeline "" { + envVars [ "test1", "v1" ] + stage "" { + whenEnv { + name "test1" + acceptValues [ "v1"; "v2" ] + } + run call + } + runImmediate + }) + + shouldBeCalled (fun call -> pipeline "" { + envVars [ "test1", "v1" ] + stage "" { + whenAll { whenEnv { name "test1" acceptValues [ "v1"; "v2" ] } - run call - } - runImmediate - } - ) - - shouldBeCalled (fun call -> - pipeline "" { - envVars [ "test1", "v1" ] - stage "" { - whenAll { - whenEnv { - name "test1" - acceptValues [ "v1"; "v2" ] - } - } - run call } - runImmediate + run call } - ) + runImmediate + }) - shouldBeCalled (fun call -> - pipeline "" { - stage "" { - whenEnv { - name "test1" - optional - } - run call + shouldBeCalled (fun call -> pipeline "" { + stage "" { + whenEnv { + name "test1" + optional } - runImmediate + run call } - ) + runImmediate + }) [] let ``whenEnvVar should work`` () = - shouldNotBeCalled (fun call -> - pipeline "" { - stage "" { - whenEnvVar "test1" - run call - } - runImmediate + shouldNotBeCalled (fun call -> pipeline "" { + stage "" { + whenEnvVar "test1" + run call } - ) + runImmediate + }) - shouldBeCalled (fun call -> - pipeline "" { - envVars [ "test1", "" ] - stage "" { - whenEnvVar "test1" - run call - } - runImmediate + shouldBeCalled (fun call -> pipeline "" { + envVars [ "test1", "" ] + stage "" { + whenEnvVar "test1" + run call } - ) + runImmediate + }) [] let ``when' stage should use stage execution result as when' condition for stage`` () = - shouldNotBeCalled (fun call -> - pipeline "" { - stage "" { - when' (stage "" { run (fun ctx -> 1) }) - run call - } - runImmediate + shouldNotBeCalled (fun call -> pipeline "" { + stage "" { + when' (stage "" { run (fun ctx -> 1) }) + run call } - ) + runImmediate + }) - shouldBeCalled (fun call -> - pipeline "" { - stage "" { - when' (stage "" { run (fun ctx -> 0) }) - run call - } - runImmediate + shouldBeCalled (fun call -> pipeline "" { + stage "" { + when' (stage "" { run (fun ctx -> 0) }) + run call } - ) + runImmediate + }) - shouldNotBeCalled (fun call -> - pipeline "" { - stage "" { - whenStage "" { run (fun _ -> 1) } - run call - } - runImmediate + shouldNotBeCalled (fun call -> pipeline "" { + stage "" { + whenStage "" { run (fun _ -> 1) } + run call } - ) + runImmediate + }) - shouldBeCalled (fun call -> - pipeline "" { - stage "" { - whenStage "" { run (fun _ -> 0) } - run call - } - runImmediate + shouldBeCalled (fun call -> pipeline "" { + stage "" { + whenStage "" { run (fun _ -> 0) } + run call } - ) + runImmediate + }) [] let ``when' stage should have parent context in execution mode`` () = - shouldBeCalled (fun call -> - pipeline "" { - envVars [ "ENV", "0" ] - stage "" { - when' (stage "" { run (fun ctx -> ctx.GetEnvVar("ENV") |> int) }) - run call - } - runImmediate + shouldBeCalled (fun call -> pipeline "" { + envVars [ "ENV", "0" ] + stage "" { + when' (stage "" { run (fun ctx -> ctx.GetEnvVar("ENV") |> int) }) + run call } - ) + runImmediate + }) - shouldBeCalled (fun call -> - pipeline "" { - envVars [ "ENV", "0" ] - stage "" { - whenStage "" { run (fun ctx -> ctx.GetEnvVar("ENV") |> int) } - run call - } - runImmediate + shouldBeCalled (fun call -> pipeline "" { + envVars [ "ENV", "0" ] + stage "" { + whenStage "" { run (fun ctx -> ctx.GetEnvVar("ENV") |> int) } + run call } - ) + runImmediate + }) [] let ``when' stage should use stage execution result as when' condition for nested stage`` () = - shouldNotBeCalled (fun call -> - pipeline "" { - stage "" { - stage "nested" { - when' (stage "" { run (fun ctx -> 1) }) - run call - } + shouldNotBeCalled (fun call -> pipeline "" { + stage "" { + stage "nested" { + when' (stage "" { run (fun ctx -> 1) }) + run call } - runImmediate } - ) + runImmediate + }) - shouldBeCalled (fun call -> - pipeline "" { - stage "" { - stage "nested" { - when' (stage "" { run (fun ctx -> 0) }) - run call - } + shouldBeCalled (fun call -> pipeline "" { + stage "" { + stage "nested" { + when' (stage "" { run (fun ctx -> 0) }) + run call } - runImmediate } - ) + runImmediate + }) [] let ``when' stage should use stage execution result as when' condition in composed when`` () = - shouldNotBeCalled (fun call -> - pipeline "" { - stage "" { - whenAll { when' (stage "" { run (fun ctx -> 1) }) } - run call - } - runImmediate + shouldNotBeCalled (fun call -> pipeline "" { + stage "" { + whenAll { when' (stage "" { run (fun ctx -> 1) }) } + run call } - ) + runImmediate + }) - shouldBeCalled (fun call -> - pipeline "" { - stage "" { - whenAll { when' (stage "" { run (fun ctx -> 0) }) } - run call - } - runImmediate + shouldBeCalled (fun call -> pipeline "" { + stage "" { + whenAll { when' (stage "" { run (fun ctx -> 0) }) } + run call } - ) + runImmediate + }) - shouldNotBeCalled (fun call -> - pipeline "" { - stage "" { - whenAll { whenStage "" { run (fun _ -> 1) } } - run call - } - runImmediate + shouldNotBeCalled (fun call -> pipeline "" { + stage "" { + whenAll { whenStage "" { run (fun _ -> 1) } } + run call } - ) + runImmediate + }) - shouldBeCalled (fun call -> - pipeline "" { - stage "" { - whenAll { whenStage "" { run (fun _ -> 0) } } - run call - } - runImmediate + shouldBeCalled (fun call -> pipeline "" { + stage "" { + whenAll { whenStage "" { run (fun _ -> 0) } } + run call } - ) + runImmediate + }) [] let ``when' stage should work in pipeline directly`` () = - Assert.Throws(fun _ -> - pipeline "" { - when' (stage "" { run (fun _ -> 1) }) - runImmediate - } - ) + Assert.Throws(fun _ -> pipeline "" { + when' (stage "" { run (fun _ -> 1) }) + runImmediate + }) |> ignore - shouldBeCalled (fun call -> - pipeline "" { - when' (stage "" { run (fun _ -> 0) }) - stage "" { run call } - runImmediate - } - ) + shouldBeCalled (fun call -> pipeline "" { + when' (stage "" { run (fun _ -> 0) }) + stage "" { run call } + runImmediate + }) - Assert.Throws(fun _ -> - pipeline "" { - whenStage "" { run (fun _ -> 1) } - runImmediate - } - ) + Assert.Throws(fun _ -> pipeline "" { + whenStage "" { run (fun _ -> 1) } + runImmediate + }) |> ignore - shouldBeCalled (fun call -> - pipeline "" { - whenStage "" { run (fun _ -> 0) } - stage "" { run call } - runImmediate - } - ) + shouldBeCalled (fun call -> pipeline "" { + whenStage "" { run (fun _ -> 0) } + stage "" { run call } + runImmediate + }) [] let ``whenAny should work`` () = @@ -399,20 +333,23 @@ let ``whenAny should work`` () = let pipeline = PipelineContext.Create "" - { StageContext.Create "" with - ParentContext = pipeline |> StageParent.Pipeline |> ValueSome + { + StageContext.Create "" with + ParentContext = pipeline |> StageParent.Pipeline |> ValueSome } |> condition.Invoke |> Assert.False - { StageContext.Create "" with - ParentContext = { pipeline with CmdArgs = [ "test1" ] } |> StageParent.Pipeline |> ValueSome + { + StageContext.Create "" with + ParentContext = { pipeline with CmdArgs = [ "test1" ] } |> StageParent.Pipeline |> ValueSome } |> condition.Invoke |> Assert.True - { StageContext.Create "" with - ParentContext = { pipeline with EnvVars = Map.ofList [ "test2", "" ] } |> StageParent.Pipeline |> ValueSome + { + StageContext.Create "" with + ParentContext = { pipeline with EnvVars = Map.ofList [ "test2", "" ] } |> StageParent.Pipeline |> ValueSome } |> condition.Invoke |> Assert.True @@ -429,15 +366,17 @@ let ``whenAny should work`` () = } // All commands are optional, so it should return true when no command is provided - { StageContext.Create "" with - ParentContext = pipeline |> StageParent.Pipeline |> ValueSome + { + StageContext.Create "" with + ParentContext = pipeline |> StageParent.Pipeline |> ValueSome } |> condition2.Invoke |> Assert.True // All commands are optional, so it should return true if one command is provided - { StageContext.Create "" with - ParentContext = { pipeline with CmdArgs = [ "test1"; "v1" ] } |> StageParent.Pipeline |> ValueSome + { + StageContext.Create "" with + ParentContext = { pipeline with CmdArgs = [ "test1"; "v1" ] } |> StageParent.Pipeline |> ValueSome } |> condition2.Invoke |> Assert.True @@ -454,15 +393,17 @@ let ``whenAny should work`` () = } // Because at least one command is optional it should return true when no command is provided - { StageContext.Create "" with - ParentContext = pipeline |> StageParent.Pipeline |> ValueSome + { + StageContext.Create "" with + ParentContext = pipeline |> StageParent.Pipeline |> ValueSome } |> condition3.Invoke |> Assert.True // It should return true if a non-optional command is provided - { StageContext.Create "" with - ParentContext = { pipeline with CmdArgs = [ "test1" ] } |> StageParent.Pipeline |> ValueSome + { + StageContext.Create "" with + ParentContext = { pipeline with CmdArgs = [ "test1" ] } |> StageParent.Pipeline |> ValueSome } |> condition3.Invoke |> Assert.True @@ -479,32 +420,37 @@ let ``whenAll should work`` () = let pipeline = PipelineContext.Create "" - { StageContext.Create "" with - ParentContext = pipeline |> StageParent.Pipeline |> ValueSome + { + StageContext.Create "" with + ParentContext = pipeline |> StageParent.Pipeline |> ValueSome } |> condition.Invoke |> Assert.False - { StageContext.Create "" with - ParentContext = { pipeline with CmdArgs = [ "test1" ] } |> StageParent.Pipeline |> ValueSome + { + StageContext.Create "" with + ParentContext = { pipeline with CmdArgs = [ "test1" ] } |> StageParent.Pipeline |> ValueSome } |> condition.Invoke |> Assert.False - { StageContext.Create "" with - ParentContext = { pipeline with EnvVars = Map.ofList [ "test2", "" ] } |> StageParent.Pipeline |> ValueSome + { + StageContext.Create "" with + ParentContext = { pipeline with EnvVars = Map.ofList [ "test2", "" ] } |> StageParent.Pipeline |> ValueSome } |> condition.Invoke |> Assert.False - { StageContext.Create "" with - ParentContext = - { pipeline with - CmdArgs = [ "test1" ] - EnvVars = Map.ofList [ "test2", "" ] - } - |> StageParent.Pipeline - |> ValueSome + { + StageContext.Create "" with + ParentContext = + { + pipeline with + CmdArgs = [ "test1" ] + EnvVars = Map.ofList [ "test2", "" ] + } + |> StageParent.Pipeline + |> ValueSome } |> condition.Invoke |> Assert.True @@ -519,32 +465,37 @@ let ``whenNot should work`` () = let pipeline = PipelineContext.Create "" - { StageContext.Create "" with - ParentContext = pipeline |> StageParent.Pipeline |> ValueSome + { + StageContext.Create "" with + ParentContext = pipeline |> StageParent.Pipeline |> ValueSome } |> condition.Invoke |> Assert.True - { StageContext.Create "" with - ParentContext = { pipeline with CmdArgs = [ "test1" ] } |> StageParent.Pipeline |> ValueSome + { + StageContext.Create "" with + ParentContext = { pipeline with CmdArgs = [ "test1" ] } |> StageParent.Pipeline |> ValueSome } |> condition.Invoke |> Assert.False - { StageContext.Create "" with - ParentContext = { pipeline with EnvVars = Map.ofList [ "test2", "" ] } |> StageParent.Pipeline |> ValueSome + { + StageContext.Create "" with + ParentContext = { pipeline with EnvVars = Map.ofList [ "test2", "" ] } |> StageParent.Pipeline |> ValueSome } |> condition.Invoke |> Assert.False - { StageContext.Create "" with - ParentContext = - { pipeline with - CmdArgs = [ "test1" ] - EnvVars = Map.ofList [ "test2", "" ] - } - |> StageParent.Pipeline - |> ValueSome + { + StageContext.Create "" with + ParentContext = + { + pipeline with + CmdArgs = [ "test1" ] + EnvVars = Map.ofList [ "test2", "" ] + } + |> StageParent.Pipeline + |> ValueSome } |> condition.Invoke |> Assert.False @@ -567,26 +518,30 @@ let ``when compose should work`` () = let pipeline = PipelineContext.Create "" - { StageContext.Create "" with - ParentContext = pipeline |> StageParent.Pipeline |> ValueSome + { + StageContext.Create "" with + ParentContext = pipeline |> StageParent.Pipeline |> ValueSome } |> condition.Invoke |> Assert.False - { StageContext.Create "" with - ParentContext = { pipeline with CmdArgs = [ "test1" ] } |> StageParent.Pipeline |> ValueSome + { + StageContext.Create "" with + ParentContext = { pipeline with CmdArgs = [ "test1" ] } |> StageParent.Pipeline |> ValueSome } |> condition.Invoke |> Assert.True - { StageContext.Create "" with - ParentContext = { pipeline with CmdArgs = [ "test2" ] } |> StageParent.Pipeline |> ValueSome + { + StageContext.Create "" with + ParentContext = { pipeline with CmdArgs = [ "test2" ] } |> StageParent.Pipeline |> ValueSome } |> condition.Invoke |> Assert.True - { StageContext.Create "" with - ParentContext = { pipeline with CmdArgs = [ "test3" ] } |> StageParent.Pipeline |> ValueSome + { + StageContext.Create "" with + ParentContext = { pipeline with CmdArgs = [ "test3" ] } |> StageParent.Pipeline |> ValueSome } |> condition.Invoke |> Assert.False @@ -626,8 +581,9 @@ let ``condition builder should follow the sequence`` () = ) } - { StageContext.Create "" with - ParentContext = PipelineContext.Create "" |> StageParent.Pipeline |> ValueSome + { + StageContext.Create "" with + ParentContext = PipelineContext.Create "" |> StageParent.Pipeline |> ValueSome } |> condition.Invoke |> Assert.False @@ -660,8 +616,9 @@ let ``condition builder should follow the sequence`` () = ) } - { StageContext.Create "" with - ParentContext = PipelineContext.Create "" |> StageParent.Pipeline |> ValueSome + { + StageContext.Create "" with + ParentContext = PipelineContext.Create "" |> StageParent.Pipeline |> ValueSome } |> condition.Invoke |> Assert.False @@ -671,42 +628,38 @@ let ``condition builder should follow the sequence`` () = [] let ``for top level condition of stage or pipeline it should combine all condition with && rule`` () = - shouldBeCalled (fun call -> - pipeline "" { - cmdArgs [ "-t" ] - envVars [ "ENV", "" ] + shouldBeCalled (fun call -> pipeline "" { + cmdArgs [ "-t" ] + envVars [ "ENV", "" ] + + when' true + whenCmdArg "-t" + whenEnvVar "ENV" + stage "" { when' true whenCmdArg "-t" whenEnvVar "ENV" - stage "" { - when' true - whenCmdArg "-t" - whenEnvVar "ENV" + run (fun _ -> call ()) + } - run (fun _ -> call ()) - } + runImmediate + }) - runImmediate - } - ) + shouldNotBeCalled (fun call -> pipeline "" { + cmdArgs [ "-t" ] - shouldNotBeCalled (fun call -> - pipeline "" { - cmdArgs [ "-t" ] + when' true + whenCmdArg "-t" + stage "" { when' true whenCmdArg "-t" + whenEnvVar "ENV" - stage "" { - when' true - whenCmdArg "-t" - whenEnvVar "ENV" - - run (fun _ -> call ()) - } - - runImmediate + run (fun _ -> call ()) } - ) + + runImmediate + }) diff --git a/Fun.Build.Tests/ExceptionHandlingTests.fs b/Fun.Build.Tests/ExceptionHandlingTests.fs index 5139db9..2b66c97 100644 --- a/Fun.Build.Tests/ExceptionHandlingTests.fs +++ b/Fun.Build.Tests/ExceptionHandlingTests.fs @@ -9,18 +9,16 @@ let ``exception handling should work for sync steps`` () = let mutable exn = ValueNone try - shouldNotBeCalled (fun call -> - pipeline "" { - stage "" { - run (fun _ -> - failwith "test" - () - ) - run call - } - runImmediate + shouldNotBeCalled (fun call -> pipeline "" { + stage "" { + run (fun _ -> + failwith "test" + () + ) + run call } - ) + runImmediate + }) with :? PipelineFailedException as ex -> exn <- ValueSome ex @@ -33,25 +31,23 @@ let ``exception handling should work for parallel steps`` () = let mutable exn = ValueNone try - shouldNotBeCalled (fun call -> - pipeline "" { + shouldNotBeCalled (fun call -> pipeline "" { + stage "" { + paralle + run (fun _ -> async { + do! Async.Sleep 10 + failwith "test" + () + }) stage "" { - paralle run (fun _ -> async { - do! Async.Sleep 10 - failwith "test" - () + do! Async.Sleep 100 + call () }) - stage "" { - run (fun _ -> async { - do! Async.Sleep 100 - call () - }) - } } - runImmediate } - ) + runImmediate + }) with :? PipelineFailedException as ex -> exn <- ValueSome ex @@ -64,19 +60,17 @@ let ``PipelineFailedException should fail fast`` () = let mutable exn = ValueNone try - shouldNotBeCalled (fun call -> - pipeline "" { - stage "" { - continueOnStepFailure - run (fun _ -> - raise (PipelineFailedException("demo")) - () - ) - } - post [ stage "" { run call } ] - runImmediate + shouldNotBeCalled (fun call -> pipeline "" { + stage "" { + continueOnStepFailure + run (fun _ -> + raise (PipelineFailedException("demo")) + () + ) } - ) + post [ stage "" { run call } ] + runImmediate + }) with :? PipelineFailedException as ex -> exn <- ValueSome ex @@ -89,19 +83,17 @@ let ``PipelineCancelledException should fail fast`` () = let mutable exn = ValueNone try - shouldNotBeCalled (fun call -> - pipeline "" { - stage "" { - continueOnStepFailure - run (fun _ -> - raise (PipelineCancelledException("demo")) - () - ) - } - post [ stage "" { run call } ] - runImmediate + shouldNotBeCalled (fun call -> pipeline "" { + stage "" { + continueOnStepFailure + run (fun _ -> + raise (PipelineCancelledException("demo")) + () + ) } - ) + post [ stage "" { run call } ] + runImmediate + }) with :? PipelineCancelledException as ex -> exn <- ValueSome ex diff --git a/Fun.Build.Tests/PipelineBuilderTests.fs b/Fun.Build.Tests/PipelineBuilderTests.fs index 914b832..e597031 100644 --- a/Fun.Build.Tests/PipelineBuilderTests.fs +++ b/Fun.Build.Tests/PipelineBuilderTests.fs @@ -8,133 +8,113 @@ open System.Threading.Tasks [] let ``pipeline should world with multiple stages with different conditions`` () = - shouldNotBeCalled (fun call -> - pipeline "" { - stage "" { - whenCmdArg "test1" - run call - } - stage "" { - whenEnvVar "test2" - run call - } - runImmediate + shouldNotBeCalled (fun call -> pipeline "" { + stage "" { + whenCmdArg "test1" + run call } - ) + stage "" { + whenEnvVar "test2" + run call + } + runImmediate + }) - shouldBeCalled (fun call -> - pipeline "" { - cmdArgs [ "test1" ] - stage "" { - whenCmdArg "test1" - run call - } - runImmediate + shouldBeCalled (fun call -> pipeline "" { + cmdArgs [ "test1" ] + stage "" { + whenCmdArg "test1" + run call } - ) + runImmediate + }) - shouldBeCalled (fun call -> - pipeline "" { - envVars [ "test2", "" ] - stage "" { - whenEnvVar "test2" - run call - } - runImmediate + shouldBeCalled (fun call -> pipeline "" { + envVars [ "test2", "" ] + stage "" { + whenEnvVar "test2" + run call } - ) + runImmediate + }) - shouldNotBeCalled (fun call -> - pipeline "" { - stage "" { - whenAll { - cmdArg "test1" - envVar "test2" - } - run call + shouldNotBeCalled (fun call -> pipeline "" { + stage "" { + whenAll { + cmdArg "test1" + envVar "test2" } - runImmediate + run call } - ) + runImmediate + }) - shouldNotBeCalled (fun call -> - pipeline "" { - cmdArgs [ "test1" ] - stage "" { - whenAll { - cmdArg "test1" - envVar "test2" - } - run call + shouldNotBeCalled (fun call -> pipeline "" { + cmdArgs [ "test1" ] + stage "" { + whenAll { + cmdArg "test1" + envVar "test2" } - runImmediate + run call } - ) + runImmediate + }) - shouldBeCalled (fun call -> - pipeline "" { - cmdArgs [ "test1" ] - envVars [ "test2", "" ] - stage "" { - timeout 1000 - whenAll { - cmdArg "test1" - envVar "test2" - } - timeout 1000 - run call + shouldBeCalled (fun call -> pipeline "" { + cmdArgs [ "test1" ] + envVars [ "test2", "" ] + stage "" { + timeout 1000 + whenAll { + cmdArg "test1" + envVar "test2" } - runImmediate + timeout 1000 + run call } - ) + runImmediate + }) - shouldBeCalled (fun call -> - pipeline "" { - stage "" { run call } - runImmediate - } - ) + shouldBeCalled (fun call -> pipeline "" { + stage "" { run call } + runImmediate + }) - shouldBeCalled (fun call -> - pipeline "" { - stage "" { run ignore } - stage "" { run call } - stage "" { - paralle - run "dotnet --version" - run "dotnet --list-sdks" - } - runImmediate + shouldBeCalled (fun call -> pipeline "" { + stage "" { run ignore } + stage "" { run call } + stage "" { + paralle + run "dotnet --version" + run "dotnet --list-sdks" } - ) + runImmediate + }) [] let ``post stage should always run when other stage is failed`` () = Assert.Throws(fun _ -> - shouldBeCalled (fun call -> - pipeline "" { - stage "" { - run (fun _ -> - failwith "test" - () - ) - } - post [ stage "" { run call } ] - runImmediate + shouldBeCalled (fun call -> pipeline "" { + stage "" { + run (fun _ -> + failwith "test" + () + ) } - ) + post [ stage "" { run call } ] + runImmediate + }) ) |> ignore Assert.Throws(fun _ -> - shouldBeCalled (fun call -> - pipeline "" { - stage "" { run (fun _ -> -1) } - post [ stage "" { run call } ] - runImmediate - } - ) + shouldBeCalled (fun call -> pipeline "" { + stage "" { run (fun _ -> -1) } + post [ stage "" { run call } ] + runImmediate + }) ) |> ignore @@ -143,58 +123,48 @@ let ``post stage should always run when other stage is failed`` () = [] let ``all post stages should always run when some post stages are failed`` () = Assert.Throws(fun _ -> - shouldBeCalled (fun call -> - pipeline "" { - post [ - stage "" { - run (fun _ -> - failwith "test" - () - ) - } - stage "" { run call } - ] - runImmediate - } - ) + shouldBeCalled (fun call -> pipeline "" { + post [ + stage "" { + run (fun _ -> + failwith "test" + () + ) + } + stage "" { run call } + ] + runImmediate + }) ) |> ignore Assert.Throws(fun _ -> - shouldBeCalled (fun call -> - pipeline "" { - post [ stage "" { run (fun _ -> -1) }; stage "" { run call } ] - runImmediate - } - ) + shouldBeCalled (fun call -> pipeline "" { + post [ stage "" { run (fun _ -> -1) }; stage "" { run call } ] + runImmediate + }) ) |> ignore [] let ``runIfOnlySpecified should work`` () = - shouldBeCalled (fun call -> - pipeline "demo" { - cmdArgs [ "-p"; "demo" ] - stage "" { run call } - runIfOnlySpecified - } - ) + shouldBeCalled (fun call -> pipeline "demo" { + cmdArgs [ "-p"; "demo" ] + stage "" { run call } + runIfOnlySpecified + }) - shouldNotBeCalled (fun call -> - pipeline "demo" { - stage "" { run call } - runIfOnlySpecified - } - ) + shouldNotBeCalled (fun call -> pipeline "demo" { + stage "" { run call } + runIfOnlySpecified + }) - shouldBeCalled (fun call -> - pipeline "demo" { - timeout 1 - stage "" { run call } - runIfOnlySpecified false - } - ) + shouldBeCalled (fun call -> pipeline "demo" { + timeout 1 + stage "" { run call } + runIfOnlySpecified false + }) [] let ``runIfOnlySpecified should work for multiple -p`` () = @@ -324,167 +294,141 @@ let ``Syntax check`` () = [] let ``Verification should work`` () = Assert.Throws(fun _ -> - shouldNotBeCalled (fun call -> - pipeline "" { - whenCmdArg "123" - stage "" { run call } - post [ stage "" { run call } ] - runImmediate - } - ) + shouldNotBeCalled (fun call -> pipeline "" { + whenCmdArg "123" + stage "" { run call } + post [ stage "" { run call } ] + runImmediate + }) ) |> ignore Assert.Throws(fun _ -> - shouldNotBeCalled (fun call -> - pipeline "" { - verify (fun _ -> false) - stage "" { run call } - post [ stage "" { run call } ] - runImmediate - } - ) - ) - |> ignore - - shouldBeCalled (fun call -> - pipeline "" { - cmdArgs [ "123" ] - whenAll { cmdArg "123" } + shouldNotBeCalled (fun call -> pipeline "" { + verify (fun _ -> false) stage "" { run call } post [ stage "" { run call } ] runImmediate - } + }) ) + |> ignore + + shouldBeCalled (fun call -> pipeline "" { + cmdArgs [ "123" ] + whenAll { cmdArg "123" } + stage "" { run call } + post [ stage "" { run call } ] + runImmediate + }) [] let ``when' stage should use stage execution result as verification condition for pipeline`` () = let mutable numChecks = 0 Assert.Throws(fun _ -> - shouldNotBeCalled (fun call -> - pipeline "" { - when' ( - stage "whenStageShouldFail" { - run (fun ctx -> - numChecks <- numChecks + 1 - 1 - ) - } - ) - stage "" { run call } - runImmediate - } - ) + shouldNotBeCalled (fun call -> pipeline "" { + when' ( + stage "whenStageShouldFail" { + run (fun ctx -> + numChecks <- numChecks + 1 + 1 + ) + } + ) + stage "" { run call } + runImmediate + }) ) |> ignore Assert.Equal(1, numChecks) - shouldBeCalled (fun call -> - pipeline "" { - when' (stage "whenStageShouldPass" { run (fun ctx -> 0) }) - stage "" { run call } - runImmediate - } - ) + shouldBeCalled (fun call -> pipeline "" { + when' (stage "whenStageShouldPass" { run (fun ctx -> 0) }) + stage "" { run call } + runImmediate + }) [] let ``Should fail if stage is ignored`` () = Assert.Throws(fun _ -> - shouldNotBeCalled (fun call -> - pipeline "" { - stage "" { - whenCmdArg "123" - failIfIgnored - run call - } - runImmediate + shouldNotBeCalled (fun call -> pipeline "" { + stage "" { + whenCmdArg "123" + failIfIgnored + run call } - ) + runImmediate + }) ) |> ignore Assert.Throws(fun _ -> - shouldNotBeCalled (fun call -> - pipeline "" { + shouldNotBeCalled (fun call -> pipeline "" { + stage "" { stage "" { - stage "" { - whenCmdArg "123" - failIfIgnored - run call - } - } - runImmediate - } - ) - ) - |> ignore - - shouldBeCalled (fun call -> - pipeline "1" { - cmdArgs [ "123" ] - stage "2" { - stage "3" { whenCmdArg "123" failIfIgnored run call } } runImmediate - } + }) ) + |> ignore + + shouldBeCalled (fun call -> pipeline "1" { + cmdArgs [ "123" ] + stage "2" { + stage "3" { + whenCmdArg "123" + failIfIgnored + run call + } + } + runImmediate + }) [] let ``whenCmdArg should work`` () = Assert.Throws(fun _ -> - shouldNotBeCalled (fun call -> - pipeline "" { - whenCmdArg "test1" - stage "" { run call } - runImmediate - } - ) - ) - |> ignore - - shouldBeCalled (fun call -> - pipeline "" { - cmdArgs [ "test1" ] + shouldNotBeCalled (fun call -> pipeline "" { whenCmdArg "test1" stage "" { run call } runImmediate - } + }) ) + |> ignore - shouldBeCalled (fun call -> - pipeline "" { - whenCmdArg "test1" "value" "description" true - stage "" { run call } - runImmediate - } - ) + shouldBeCalled (fun call -> pipeline "" { + cmdArgs [ "test1" ] + whenCmdArg "test1" + stage "" { run call } + runImmediate + }) - Assert.Throws(fun _ -> - shouldNotBeCalled (fun call -> - pipeline "" { - whenCmdArg "test1" "value" "description" false - stage "" { run call } - runImmediate - } - ) - ) - |> ignore + shouldBeCalled (fun call -> pipeline "" { + whenCmdArg "test1" "value" "description" true + stage "" { run call } + runImmediate + }) - shouldBeCalled (fun call -> - pipeline "" { - cmdArgs [ "test1"; "value" ] + Assert.Throws(fun _ -> + shouldNotBeCalled (fun call -> pipeline "" { whenCmdArg "test1" "value" "description" false stage "" { run call } runImmediate - } + }) ) + |> ignore + + shouldBeCalled (fun call -> pipeline "" { + cmdArgs [ "test1"; "value" ] + whenCmdArg "test1" "value" "description" false + stage "" { run call } + runImmediate + }) diff --git a/Fun.Build.Tests/SequenceTests.fs b/Fun.Build.Tests/SequenceTests.fs index 5d56575..2d29f12 100644 --- a/Fun.Build.Tests/SequenceTests.fs +++ b/Fun.Build.Tests/SequenceTests.fs @@ -136,36 +136,33 @@ let ``nested stages should run in sequence`` () = Assert.Equal([ 1; 2; 3; 4; 5; 6; 7 ], calls) [] -let ``custom exit code should pass for stage`` () = - pipeline "" { - stage "" { - acceptExitCodes [| 99 |] - run (fun _ -> async { return 99 }) - } - runImmediate +let ``custom exit code should pass for stage`` () = pipeline "" { + stage "" { + acceptExitCodes [| 99 |] + run (fun _ -> async { return 99 }) } + runImmediate +} [] -let ``custom exit code should pass for pipeline`` () = - pipeline "" { - acceptExitCodes [| 99 |] - stage "" { run (fun _ -> async { return 99 }) } - runImmediate - } +let ``custom exit code should pass for pipeline`` () = pipeline "" { + acceptExitCodes [| 99 |] + stage "" { run (fun _ -> async { return 99 }) } + runImmediate +} [] -let ``custom exit code should pass for nested stage`` () = - pipeline "" { - stage "" { - acceptExitCodes [| 99 |] - stage "nested" { run (fun _ -> async { return 99 }) } - stage "nested2" { - acceptExitCodes [ 123 ] - run (fun _ -> 123) - } +let ``custom exit code should pass for nested stage`` () = pipeline "" { + stage "" { + acceptExitCodes [| 99 |] + stage "nested" { run (fun _ -> async { return 99 }) } + stage "nested2" { + acceptExitCodes [ 123 ] + run (fun _ -> 123) } - runImmediate } + runImmediate +} [] @@ -223,91 +220,81 @@ let ``continueOnStepFailure should work`` () = } Assert.Equal([ 1; 2; 3; 4 ], list) - shouldBeCalled (fun fn -> - pipeline "" { - stage "" { - paralle - continueOnStepFailure - run (fun _ -> Ok()) - run (fun _ -> Error "") - run (fun _ -> Ok()) - } - stage "" { run fn } - runImmediate + shouldBeCalled (fun fn -> pipeline "" { + stage "" { + paralle + continueOnStepFailure + run (fun _ -> Ok()) + run (fun _ -> Error "") + run (fun _ -> Ok()) } - ) + stage "" { run fn } + runImmediate + }) list.Clear() Assert.Throws(fun _ -> - shouldNotBeCalled (fun fn -> - pipeline "" { - stage "" { - continueOnStepFailure false - run (fun _ -> list.Add(1)) - run (fun _ -> - list.Add(2) - Error "" - ) - run (fun _ -> Ok()) - } - stage "" { run fn } - runImmediate + shouldNotBeCalled (fun fn -> pipeline "" { + stage "" { + continueOnStepFailure false + run (fun _ -> list.Add(1)) + run (fun _ -> + list.Add(2) + Error "" + ) + run (fun _ -> Ok()) } - ) + stage "" { run fn } + runImmediate + }) ) |> ignore Assert.Equal([ 1; 2 ], list) Assert.Throws(fun _ -> - shouldNotBeCalled (fun fn -> - pipeline "" { - stage "" { - paralle - continueOnStepFailure false - run (fun _ -> Ok()) - run (fun _ -> Error "") - run (fun _ -> Ok()) - } - stage "" { run fn } - runImmediate + shouldNotBeCalled (fun fn -> pipeline "" { + stage "" { + paralle + continueOnStepFailure false + run (fun _ -> Ok()) + run (fun _ -> Error "") + run (fun _ -> Ok()) } - ) + stage "" { run fn } + runImmediate + }) ) |> ignore list.Clear() Assert.Throws(fun _ -> - shouldNotBeCalled (fun fn -> - pipeline "" { - stage "" { - run (fun _ -> list.Add(1)) - run (fun _ -> - list.Add(2) - Error "" - ) - run (fun _ -> Ok()) - } - stage "" { run fn } - runImmediate + shouldNotBeCalled (fun fn -> pipeline "" { + stage "" { + run (fun _ -> list.Add(1)) + run (fun _ -> + list.Add(2) + Error "" + ) + run (fun _ -> Ok()) } - ) + stage "" { run fn } + runImmediate + }) ) |> ignore Assert.Equal([ 1; 2 ], list) Assert.Throws(fun _ -> - shouldNotBeCalled (fun fn -> - pipeline "" { - stage "" { - paralle - run (fun _ -> Ok()) - run (fun _ -> Error "") - run (fun _ -> Ok()) - } - stage "" { run fn } - runImmediate + shouldNotBeCalled (fun fn -> pipeline "" { + stage "" { + paralle + run (fun _ -> Ok()) + run (fun _ -> Error "") + run (fun _ -> Ok()) } - ) + stage "" { run fn } + runImmediate + }) ) |> ignore @@ -334,9 +321,27 @@ let ``continueOnStepFailure should work`` () = [] let ``continueStepsOnFailure should work`` () = let list = System.Collections.Generic.List() + Assert.Throws(fun _ -> pipeline "" { + stage "" { + continueStepsOnFailure + run (fun _ -> list.Add(1)) + run (fun _ -> + list.Add(2) + Error "" + ) + run (fun _ -> list.Add(3)) + } + stage "" { run (fun _ -> list.Add(4)) } + runImmediate + }) + |> ignore + Assert.Equal([ 1; 2; 3 ], list) + + list.Clear() Assert.Throws(fun _ -> - pipeline "" { + shouldNotBeCalled (fun fn -> pipeline "" { stage "" { + paralle continueStepsOnFailure run (fun _ -> list.Add(1)) run (fun _ -> @@ -345,76 +350,50 @@ let ``continueStepsOnFailure should work`` () = ) run (fun _ -> list.Add(3)) } - stage "" { run (fun _ -> list.Add(4)) } + stage "" { run fn } runImmediate - } + }) ) |> ignore - Assert.Equal([ 1; 2; 3 ], list) + Assert.Equal([ 1; 2; 3 ], Seq.sort list) list.Clear() - Assert.Throws(fun _ -> - shouldNotBeCalled (fun fn -> - pipeline "" { - stage "" { - paralle - continueStepsOnFailure - run (fun _ -> list.Add(1)) - run (fun _ -> - list.Add(2) - Error "" - ) - run (fun _ -> list.Add(3)) - } - stage "" { run fn } - runImmediate - } - ) - ) + Assert.Throws(fun _ -> pipeline "" { + stage "" { + continueStepsOnFailure false + run (fun _ -> list.Add(1)) + run (fun _ -> + list.Add(2) + Error "" + ) + run (fun _ -> list.Add(3)) + } + stage "" { run (fun _ -> list.Add(4)) } + runImmediate + }) |> ignore - Assert.Equal([ 1; 2; 3 ], Seq.sort list) + Assert.Equal([ 1; 2 ], list) list.Clear() Assert.Throws(fun _ -> - pipeline "" { + shouldNotBeCalled (fun fn -> pipeline "" { stage "" { + paralle continueStepsOnFailure false run (fun _ -> list.Add(1)) - run (fun _ -> + run (fun _ -> async { + do! Async.Sleep 1000 list.Add(2) - Error "" - ) - run (fun _ -> list.Add(3)) + return Error "" + }) + run (fun _ -> async { + do! Async.Sleep 3000 + list.Add(3) + }) } - stage "" { run (fun _ -> list.Add(4)) } + stage "" { run fn } runImmediate - } - ) - |> ignore - Assert.Equal([ 1; 2 ], list) - - list.Clear() - Assert.Throws(fun _ -> - shouldNotBeCalled (fun fn -> - pipeline "" { - stage "" { - paralle - continueStepsOnFailure false - run (fun _ -> list.Add(1)) - run (fun _ -> async { - do! Async.Sleep 1000 - list.Add(2) - return Error "" - }) - run (fun _ -> async { - do! Async.Sleep 3000 - list.Add(3) - }) - } - stage "" { run fn } - runImmediate - } - ) + }) ) |> ignore Assert.Equal([ 1; 2 ], list) @@ -461,46 +440,42 @@ let ``continueStageOnFailure should work`` () = Assert.Equal([ 1; 2; 4 ], Seq.sort list) list.Clear() - Assert.Throws(fun _ -> - pipeline "" { - stage "" { - continueStepsOnFailure true - continueStageOnFailure false - run (fun _ -> list.Add(1)) - run (fun _ -> - list.Add(2) - Error "" - ) - run (fun _ -> list.Add(3)) - } - stage "" { run (fun _ -> list.Add(4)) } - runImmediate + Assert.Throws(fun _ -> pipeline "" { + stage "" { + continueStepsOnFailure true + continueStageOnFailure false + run (fun _ -> list.Add(1)) + run (fun _ -> + list.Add(2) + Error "" + ) + run (fun _ -> list.Add(3)) } - ) + stage "" { run (fun _ -> list.Add(4)) } + runImmediate + }) |> ignore Assert.Equal([ 1; 2; 3 ], list) list.Clear() - Assert.Throws(fun _ -> - pipeline "" { - stage "" { - paralle - continueStepsOnFailure true - continueStageOnFailure false - run (fun _ -> list.Add(1)) - run (fun _ -> - list.Add(2) - Error "" - ) - run (fun _ -> async { - do! Async.Sleep 200 - list.Add(3) - }) - } - stage "" { run (fun _ -> list.Add(4)) } - runImmediate + Assert.Throws(fun _ -> pipeline "" { + stage "" { + paralle + continueStepsOnFailure true + continueStageOnFailure false + run (fun _ -> list.Add(1)) + run (fun _ -> + list.Add(2) + Error "" + ) + run (fun _ -> async { + do! Async.Sleep 200 + list.Add(3) + }) } - ) + stage "" { run (fun _ -> list.Add(4)) } + runImmediate + }) |> ignore Assert.Equal([ 1; 2; 3 ], Seq.sort list) @@ -508,23 +483,21 @@ let ``continueStageOnFailure should work`` () = [] let ``continueStepsOnFailure for nested stage should work`` () = let list = System.Collections.Generic.List() - Assert.Throws(fun _ -> - pipeline "" { + Assert.Throws(fun _ -> pipeline "" { + stage "" { + continueStepsOnFailure + run (fun _ -> list.Add(1)) stage "" { - continueStepsOnFailure - run (fun _ -> list.Add(1)) - stage "" { - run (fun _ -> - list.Add(2) - Error "" - ) - } - run (fun _ -> list.Add(3)) + run (fun _ -> + list.Add(2) + Error "" + ) } - stage "" { run (fun _ -> list.Add(4)) } - runImmediate + run (fun _ -> list.Add(3)) } - ) + stage "" { run (fun _ -> list.Add(4)) } + runImmediate + }) |> ignore Assert.Equal([ 1; 2; 3 ], list) @@ -550,26 +523,24 @@ let ``continueStepsOnFailure for nested stage should work`` () = [] let ``pipeline should be terminated correctly when parallel step are failed`` () = - Assert.Throws(fun _ -> - pipeline "" { + Assert.Throws(fun _ -> pipeline "" { + stage "" { + paralle + stage "timer" { + whenWindows + run (fun ctx -> ctx.RunCommand("timeout /t 10")) + } + stage "timer" { + whenNot { platformWindows } + run (fun ctx -> ctx.RunCommand("sleep 10")) + } stage "" { - paralle - stage "timer" { - whenWindows - run (fun ctx -> ctx.RunCommand("timeout /t 10")) - } - stage "timer" { - whenNot { platformWindows } - run (fun ctx -> ctx.RunCommand("sleep 10")) - } - stage "" { - run (Async.Sleep 1000) - run (fun _ -> - failwith "Demo" - () - ) - } + run (Async.Sleep 1000) + run (fun _ -> + failwith "Demo" + () + ) } - runImmediate } - ) + runImmediate + }) diff --git a/Fun.Build.Tests/StageContextExtensions.fs b/Fun.Build.Tests/StageContextExtensions.fs index 6766521..139b718 100644 --- a/Fun.Build.Tests/StageContextExtensions.fs +++ b/Fun.Build.Tests/StageContextExtensions.fs @@ -7,12 +7,11 @@ open Fun.Build.StageContextExtensionsInternal [] let ``TryGetCmdArgOrEnvVar should work`` () = - let pipeline = - pipeline "" { - envVars [ "test1", "e1"; "test2", "e1"; "test3", "e1" ] - cmdArgs [ "test3"; "c1" ] - stage "" { envVars [ "test2", "e2" ] } - } + let pipeline = pipeline "" { + envVars [ "test1", "e1"; "test2", "e1"; "test3", "e1" ] + cmdArgs [ "test3"; "c1" ] + stage "" { envVars [ "test2", "e2" ] } + } Assert.Equal(ValueNone, pipeline.Stages[0].TryGetCmdArgOrEnvVar "abc") Assert.Equal(ValueSome "e1", pipeline.Stages[0].TryGetCmdArgOrEnvVar "test1") @@ -22,11 +21,10 @@ let ``TryGetCmdArgOrEnvVar should work`` () = [] let ``GetAllEnvVars should work`` () = - let pipeline = - pipeline "" { - envVars [ "test1", "e1"; "test2", "e1"; "test3", "e3" ] - stage "" { envVars [ "test2", "e2" ] } - } + let pipeline = pipeline "" { + envVars [ "test1", "e1"; "test2", "e1"; "test3", "e3" ] + stage "" { envVars [ "test2", "e2" ] } + } let result = pipeline.Stages[0].GetAllEnvVars() Assert.Equal("e1", Map.find "test1" result) @@ -36,23 +34,21 @@ let ``GetAllEnvVars should work`` () = [] let ``GetAllCmdArgs should work`` () = - let pipeline = - pipeline "" { - cmdArgs [ "test3"; "c1" ] - stage "" { echo "" } - } + let pipeline = pipeline "" { + cmdArgs [ "test3"; "c1" ] + stage "" { echo "" } + } Assert.Equal([ "test3"; "c1" ], pipeline.Stages[0].GetAllCmdArgs()) [] let ``workingDir should work`` () = - let pipeline = - pipeline "" { - workingDir "test1" - stage "" { workingDir "test2" } - stage "" { run ignore } - } + let pipeline = pipeline "" { + workingDir "test1" + stage "" { workingDir "test2" } + stage "" { run ignore } + } Assert.Equal(ValueSome "test1", pipeline.Stages[1].GetWorkingDir()) Assert.Equal(ValueSome "test2", pipeline.Stages[0].GetWorkingDir()) @@ -60,21 +56,19 @@ let ``workingDir should work`` () = [] let ``noPrefixForStep should work`` () = - let pipeline1 = - pipeline "" { - noPrefixForStep false - stage "" { noPrefixForStep false } - stage "" { run ignore } - } + let pipeline1 = pipeline "" { + noPrefixForStep false + stage "" { noPrefixForStep false } + stage "" { run ignore } + } Assert.Equal(false, pipeline1.Stages[0].GetNoPrefixForStep()) Assert.Equal(false, pipeline1.Stages[1].GetNoPrefixForStep()) - let pipeline2 = - pipeline "" { - stage "" { noPrefixForStep false } - stage "" { run ignore } - } + let pipeline2 = pipeline "" { + stage "" { noPrefixForStep false } + stage "" { run ignore } + } Assert.Equal(false, pipeline2.Stages[0].GetNoPrefixForStep()) Assert.Equal(true, pipeline2.Stages[1].GetNoPrefixForStep()) @@ -82,171 +76,161 @@ let ``noPrefixForStep should work`` () = [] let ``noStdRedirectForStep should work`` () = - let pipeline1 = - pipeline "" { - noStdRedirectForStep - stage "" { noStdRedirectForStep } - stage "" { run ignore } - } + let pipeline1 = pipeline "" { + noStdRedirectForStep + stage "" { noStdRedirectForStep } + stage "" { run ignore } + } Assert.Equal(true, pipeline1.Stages[0].GetNoStdRedirectForStep()) Assert.Equal(true, pipeline1.Stages[1].GetNoStdRedirectForStep()) - let pipeline2 = - pipeline "" { - stage "" { noStdRedirectForStep } - stage "" { run ignore } - } + let pipeline2 = pipeline "" { + stage "" { noStdRedirectForStep } + stage "" { run ignore } + } Assert.Equal(true, pipeline2.Stages[0].GetNoStdRedirectForStep()) Assert.Equal(false, pipeline2.Stages[1].GetNoStdRedirectForStep()) [] -let ``RunCommandCaptureOutput should work`` () = - pipeline "" { - stage "" { - whenAny { - platformOSX - platformLinux - } - run (fun ctx -> async { - let! result = ctx.RunCommandCaptureOutput "echo 42" - Assert.Equal(Ok "42\n", result) - }) +let ``RunCommandCaptureOutput should work`` () = pipeline "" { + stage "" { + whenAny { + platformOSX + platformLinux } - stage "" { - whenWindows - run (fun ctx -> async { - let! result = ctx.RunCommandCaptureOutput "powershell echo 42" - Assert.Equal(Ok "42\r\n", result) - }) - } - runImmediate + run (fun ctx -> async { + let! result = ctx.RunCommandCaptureOutput "echo 42" + Assert.Equal(Ok "42\n", result) + }) + } + stage "" { + whenWindows + run (fun ctx -> async { + let! result = ctx.RunCommandCaptureOutput "powershell echo 42" + Assert.Equal(Ok "42\r\n", result) + }) } + runImmediate +} [] -let ``RunCommandCaptureOutput in working directory should work`` () = - pipeline "" { - stage "" { - whenAny { - platformOSX - platformLinux - } - run (fun ctx -> async { - let tmpFolder = System.IO.Path.GetTempPath() - let! result = ctx.RunCommandCaptureOutput("echo 42", workingDir = tmpFolder) - Assert.Equal(Ok "42\n", result) - }) - } - stage "" { - whenWindows - run (fun ctx -> async { - let! result = ctx.RunCommandCaptureOutput "powershell echo 42" - Assert.Equal(Ok "42\r\n", result) - }) +let ``RunCommandCaptureOutput in working directory should work`` () = pipeline "" { + stage "" { + whenAny { + platformOSX + platformLinux } - runImmediate + run (fun ctx -> async { + let tmpFolder = System.IO.Path.GetTempPath() + let! result = ctx.RunCommandCaptureOutput("echo 42", workingDir = tmpFolder) + Assert.Equal(Ok "42\n", result) + }) + } + stage "" { + whenWindows + run (fun ctx -> async { + let! result = ctx.RunCommandCaptureOutput "powershell echo 42" + Assert.Equal(Ok "42\r\n", result) + }) } + runImmediate +} [] let ``RunCommandCaptureOutput should return an error if command failed`` () = Assert.Throws(fun _ -> - shouldBeCalled (fun call -> - pipeline "" { - stage "" { - run (fun ctx -> async { - let! result = ctx.RunCommandCaptureOutput "thisCmdDoesNotExist" - return () - }) - } - runImmediate + shouldBeCalled (fun call -> pipeline "" { + stage "" { + run (fun ctx -> async { + let! result = ctx.RunCommandCaptureOutput "thisCmdDoesNotExist" + return () + }) } - ) + runImmediate + }) ) |> ignore [] -let ``RunCommandCaptureAll should return exit code, stdout and stderr`` () = - pipeline "" { - stage "" { - whenAny { - platformOSX - platformLinux - } - run (fun ctx -> async { - let! result = ctx.RunCommandCaptureAll "sh -c \"echo out; echo err >&2; exit 3\"" - Assert.Equal(3, result.ExitCode) - Assert.Equal("out\n", result.StandardOutput) - Assert.Equal("err\n", result.StandardError) - }) - } - stage "" { - whenWindows - run (fun ctx -> async { - let! result = ctx.RunCommandCaptureAll "powershell -Command \"echo out; [Console]::Error.WriteLine('err'); exit 3\"" - Assert.Equal(3, result.ExitCode) - Assert.Equal("out\r\n", result.StandardOutput) - Assert.Equal("err\r\n", result.StandardError) - }) +let ``RunCommandCaptureAll should return exit code, stdout and stderr`` () = pipeline "" { + stage "" { + whenAny { + platformOSX + platformLinux } - runImmediate + run (fun ctx -> async { + let! result = ctx.RunCommandCaptureAll "sh -c \"echo out; echo err >&2; exit 3\"" + Assert.Equal(3, result.ExitCode) + Assert.Equal("out\n", result.StandardOutput) + Assert.Equal("err\n", result.StandardError) + }) } + stage "" { + whenWindows + run (fun ctx -> async { + let! result = ctx.RunCommandCaptureAll "powershell -Command \"echo out; [Console]::Error.WriteLine('err'); exit 3\"" + Assert.Equal(3, result.ExitCode) + Assert.Equal("out\r\n", result.StandardOutput) + Assert.Equal("err\r\n", result.StandardError) + }) + } + runImmediate +} [] let ``RunCommandCaptureAll should not fail the stage on a non zero exit code`` () = - shouldBeCalled (fun call -> - pipeline "" { - stage "" { - whenAny { - platformOSX - platformLinux - } - run (fun ctx -> async { - let! result = ctx.RunCommandCaptureAll "sh -c \"exit 1\"" - Assert.Equal(1, result.ExitCode) - call () - }) - } - stage "" { - whenWindows - run (fun ctx -> async { - let! result = ctx.RunCommandCaptureAll "powershell -Command \"exit 1\"" - Assert.Equal(1, result.ExitCode) - call () - }) - } - runImmediate - } - ) - -[] -let ``RunSensitiveCommandCaptureAll should work`` () = - pipeline "" { + shouldBeCalled (fun call -> pipeline "" { stage "" { whenAny { platformOSX platformLinux } run (fun ctx -> async { - let! result = ctx.RunSensitiveCommandCaptureAll $"""echo {"42"}""" - Assert.Equal(0, result.ExitCode) - Assert.Equal("42\n", result.StandardOutput) - Assert.Equal("", result.StandardError) + let! result = ctx.RunCommandCaptureAll "sh -c \"exit 1\"" + Assert.Equal(1, result.ExitCode) + call () }) } stage "" { whenWindows run (fun ctx -> async { - let! result = ctx.RunSensitiveCommandCaptureAll $"""powershell echo {"42"}""" - Assert.Equal(0, result.ExitCode) - Assert.Equal("42\r\n", result.StandardOutput) - Assert.Equal("", result.StandardError) + let! result = ctx.RunCommandCaptureAll "powershell -Command \"exit 1\"" + Assert.Equal(1, result.ExitCode) + call () }) } runImmediate + }) + +[] +let ``RunSensitiveCommandCaptureAll should work`` () = pipeline "" { + stage "" { + whenAny { + platformOSX + platformLinux + } + run (fun ctx -> async { + let! result = ctx.RunSensitiveCommandCaptureAll $"""echo {"42"}""" + Assert.Equal(0, result.ExitCode) + Assert.Equal("42\n", result.StandardOutput) + Assert.Equal("", result.StandardError) + }) } + stage "" { + whenWindows + run (fun ctx -> async { + let! result = ctx.RunSensitiveCommandCaptureAll $"""powershell echo {"42"}""" + Assert.Equal(0, result.ExitCode) + Assert.Equal("42\r\n", result.StandardOutput) + Assert.Equal("", result.StandardError) + }) + } + runImmediate +} [] @@ -254,102 +238,90 @@ let ``Soft cancel should work`` () = let mutable i = 0 let mutable j = 0 - shouldBeCalled (fun call -> - pipeline "" { - timeout 2 - stage "" { - paralle - run (fun _ -> async { - while true do - do! Async.Sleep 100 - }) - run (fun ctx -> async { - while true do - do! Async.Sleep 100 - j <- j + 1 - printfn $"task2 {i}" - if i > 3 then ctx.SoftCancelStep() - }) - run (fun ctx -> async { - while true do - do! Async.Sleep 100 - i <- i + 1 - printfn $"task1 {i}" - if i > 5 then ctx.SoftCancelStage() - }) - } - stage "" { run call } - runImmediate + shouldBeCalled (fun call -> pipeline "" { + timeout 2 + stage "" { + paralle + run (fun _ -> async { + while true do + do! Async.Sleep 100 + }) + run (fun ctx -> async { + while true do + do! Async.Sleep 100 + j <- j + 1 + printfn $"task2 {i}" + if i > 3 then ctx.SoftCancelStep() + }) + run (fun ctx -> async { + while true do + do! Async.Sleep 100 + i <- i + 1 + printfn $"task1 {i}" + if i > 5 then ctx.SoftCancelStage() + }) } - ) + stage "" { run call } + runImmediate + }) Assert.True(10 <= i + j) [] let ``FailIfNoActiveSubStage`` () = - Assert.Throws(fun _ -> - pipeline "" { - stage "" { - failIfNoActiveSubStage - stage "" { when' false } - } - runImmediate + Assert.Throws(fun _ -> pipeline "" { + stage "" { + failIfNoActiveSubStage + stage "" { when' false } } - ) + runImmediate + }) |> ignore - Assert.Throws(fun _ -> - pipeline "" { - stage "" { - failIfNoActiveSubStage - echo "" - } - runImmediate + Assert.Throws(fun _ -> pipeline "" { + stage "" { + failIfNoActiveSubStage + echo "" } - ) + runImmediate + }) |> ignore - shouldBeCalled (fun call -> - pipeline "" { + shouldBeCalled (fun call -> pipeline "" { + stage "" { + failIfNoActiveSubStage + stage "" { when' false } stage "" { - failIfNoActiveSubStage - stage "" { when' false } - stage "" { - when' true - run (ignore >> call) - } + when' true + run (ignore >> call) } - runImmediate } - ) + runImmediate + }) - shouldBeCalled (fun call -> - pipeline "" { + shouldBeCalled (fun call -> pipeline "" { + stage "" { + failIfNoActiveSubStage + echo "" stage "" { - failIfNoActiveSubStage - echo "" - stage "" { - when' true - run (ignore >> call) - } + when' true + run (ignore >> call) } - runImmediate } - ) + runImmediate + }) [] let ``runHttpHealthCheck should work`` () = - shouldBeCalled (fun fn -> - pipeline "" { - stage "" { - runHttpHealthCheck "https://www.bing.com/" - run fn - } - runImmediate + shouldBeCalled (fun fn -> pipeline "" { + stage "" { + runHttpHealthCheck "https://www.bing.com/" + run fn } - ) + runImmediate + }) Assert.Throws(fun _ -> use cts = new System.Threading.CancellationTokenSource(3000) diff --git a/Fun.Build.Tests/TimeoutTests.fs b/Fun.Build.Tests/TimeoutTests.fs index ce255d7..5104da8 100644 --- a/Fun.Build.Tests/TimeoutTests.fs +++ b/Fun.Build.Tests/TimeoutTests.fs @@ -6,102 +6,88 @@ open Fun.Build [] let ``timeout should work`` () = - shouldBeCalled (fun call -> - pipeline "timeout" { + shouldBeCalled (fun call -> pipeline "timeout" { + timeout 1 + stage "timeout" { + run ( + async { + do! Async.Sleep 100 + call () + } + ) + } + runImmediate + }) + + Assert.Throws(fun _ -> + shouldNotBeCalled (fun call -> pipeline "timeout" { timeout 1 stage "timeout" { run ( async { - do! Async.Sleep 100 + do! Async.Sleep 2000 call () } ) } runImmediate - } - ) - - Assert.Throws(fun _ -> - shouldNotBeCalled (fun call -> - pipeline "timeout" { - timeout 1 - stage "timeout" { - run ( - async { - do! Async.Sleep 2000 - call () - } - ) - } - runImmediate - } - ) + }) ) |> ignore - Assert.Throws(fun _ -> - pipeline "timeout" { - timeout 2 - stage "timeout1" { run (Async.Sleep 1000) } - stage "timeout2" { run (Async.Sleep 1100) } - runImmediate - } - ) + Assert.Throws(fun _ -> pipeline "timeout" { + timeout 2 + stage "timeout1" { run (Async.Sleep 1000) } + stage "timeout2" { run (Async.Sleep 1100) } + runImmediate + }) [] let ``timeoutForStage should work`` () = - Assert.Throws(fun _ -> - pipeline "timeoutForStage" { - timeoutForStage 1 - stage "timeoutForStage" { run (Async.Sleep 2100) } - runImmediate - } - ) + Assert.Throws(fun _ -> pipeline "timeoutForStage" { + timeoutForStage 1 + stage "timeoutForStage" { run (Async.Sleep 2100) } + runImmediate + }) |> ignore - Assert.Throws(fun _ -> - pipeline "timeoutForStage" { - timeoutForStage 2 - stage "timeoutForStage" { - timeout 1 - run (Async.Sleep 1100) - } - runImmediate + Assert.Throws(fun _ -> pipeline "timeoutForStage" { + timeoutForStage 2 + stage "timeoutForStage" { + timeout 1 + run (Async.Sleep 1100) } - ) + runImmediate + }) [] let ``timeoutForStep should work`` () = Assert.Throws(fun _ -> - shouldNotBeCalled (fun call -> - pipeline "timeoutForStep" { - timeoutForStep 1 - stage "timeoutForStep" { - run (Async.Sleep 500) - run (Async.Sleep 1100) - run call - } - runImmediate + shouldNotBeCalled (fun call -> pipeline "timeoutForStep" { + timeoutForStep 1 + stage "timeoutForStep" { + run (Async.Sleep 500) + run (Async.Sleep 1100) + run call } - ) + runImmediate + }) ) |> ignore Assert.Throws(fun _ -> - shouldNotBeCalled (fun call -> - pipeline "timeoutForStep" { - timeoutForStep 2 - stage "timeoutForStep" { - timeoutForStep 1 - run (Async.Sleep 500) - run (Async.Sleep 1100) - run call - } - runImmediate + shouldNotBeCalled (fun call -> pipeline "timeoutForStep" { + timeoutForStep 2 + stage "timeoutForStep" { + timeoutForStep 1 + run (Async.Sleep 500) + run (Async.Sleep 1100) + run call } - ) + runImmediate + }) ) @@ -109,94 +95,82 @@ let ``timeoutForStep should work`` () = [] let ``nested stage timeout should work`` () = Assert.Throws(fun _ -> - shouldNotBeCalled (fun call -> - pipeline "timeout" { - timeout 1 + shouldNotBeCalled (fun call -> pipeline "timeout" { + timeout 1 + stage "" { stage "" { - stage "" { - run (Async.Sleep 500) - run (Async.Sleep 1100) - run call - } + run (Async.Sleep 500) + run (Async.Sleep 1100) + run call } - runImmediate } - ) + runImmediate + }) ) |> ignore Assert.Throws(fun _ -> - shouldNotBeCalled (fun call -> - pipeline "timeout" { - timeout 1 + shouldNotBeCalled (fun call -> pipeline "timeout" { + timeout 1 + stage "" { + timeout 2 stage "" { - timeout 2 - stage "" { - run (Async.Sleep 500) - run (Async.Sleep 1100) - run call - } + run (Async.Sleep 500) + run (Async.Sleep 1100) + run call } - runImmediate } - ) + runImmediate + }) ) |> ignore [] let ``nested stage timeoutForStep should work`` () = - Assert.Throws(fun _ -> - pipeline "timeoutForStep" { - timeoutForStep 1 - stage "" { stage "" { run (Async.Sleep 1100) } } - runImmediate - } - ) + Assert.Throws(fun _ -> pipeline "timeoutForStep" { + timeoutForStep 1 + stage "" { stage "" { run (Async.Sleep 1100) } } + runImmediate + }) |> ignore Assert.Throws(fun _ -> - shouldBeCalled (fun call -> - pipeline "timeoutForStep" { - timeoutForStep 1 + shouldBeCalled (fun call -> pipeline "timeoutForStep" { + timeoutForStep 1 + stage "" { stage "" { - stage "" { - timeoutForStep 2 - run (Async.Sleep 1100) - run call - } + timeoutForStep 2 + run (Async.Sleep 1100) + run call } - runImmediate } - ) + runImmediate + }) ) |> ignore [] let ``nested stage timeoutForStage should work`` () = - Assert.Throws(fun _ -> - pipeline "timeoutForStage" { - timeoutForStage 1 - stage "" { stage "" { run (Async.Sleep 1100) } } - runImmediate - } - ) + Assert.Throws(fun _ -> pipeline "timeoutForStage" { + timeoutForStage 1 + stage "" { stage "" { run (Async.Sleep 1100) } } + runImmediate + }) |> ignore Assert.Throws(fun _ -> - shouldBeCalled (fun call -> - pipeline "timeoutForStage" { - timeoutForStage 1 + shouldBeCalled (fun call -> pipeline "timeoutForStage" { + timeoutForStage 1 + stage "" { stage "" { - stage "" { - timeout 2 - run (Async.Sleep 1100) - run call - } + timeout 2 + run (Async.Sleep 1100) + run call } - runImmediate } - ) + runImmediate + }) ) |> ignore diff --git a/Fun.Build/BuiltinCmds.fs b/Fun.Build/BuiltinCmds.fs index a7c4913..1a1a8a3 100644 --- a/Fun.Build/BuiltinCmds.fs +++ b/Fun.Build/BuiltinCmds.fs @@ -50,40 +50,39 @@ module BuiltinCmdsInternal = ?disablePrintOutput: bool, ?disablePrintCommand: bool, ?cancellationToken: CancellationToken - ) : Async = - async { - let disablePrintOutput = defaultArg disablePrintOutput false - let disablePrintCommand = defaultArg disablePrintCommand false - let command = ctx.BuildCommand(commandStr, ?workingDir = workingDir) - let noPrefixForStep = ctx.GetNoPrefixForStep() - let prefix = - if noPrefixForStep then - "" - else - match step with - | Some i -> ctx.BuildStepPrefix i - | None -> ctx.GetNamePath() - - if not noPrefixForStep then AnsiConsole.Markup $"[green]{prefix}[/] " - if not disablePrintCommand then AnsiConsole.WriteLine commandLogString - - let ct = defaultArg cancellationToken CancellationToken.None - - return! - Process.StartAsync( - command, - commandLogString, - prefix, - printOutput = (not disablePrintOutput && not (ctx.GetNoStdRedirectForStep())), - captureOutput = true, - cancellationToken = ct - ) - } + ) : Async = async { + let disablePrintOutput = defaultArg disablePrintOutput false + let disablePrintCommand = defaultArg disablePrintCommand false + let command = ctx.BuildCommand(commandStr, ?workingDir = workingDir) + let noPrefixForStep = ctx.GetNoPrefixForStep() + let prefix = + if noPrefixForStep then + "" + else + match step with + | Some i -> ctx.BuildStepPrefix i + | None -> ctx.GetNamePath() + + if not noPrefixForStep then AnsiConsole.Markup $"[green]{prefix}[/] " + if not disablePrintCommand then AnsiConsole.WriteLine commandLogString + + let ct = defaultArg cancellationToken CancellationToken.None + + return! + Process.StartAsync( + command, + commandLogString, + prefix, + printOutput = (not disablePrintOutput && not (ctx.GetNoStdRedirectForStep())), + captureOutput = true, + cancellationToken = ct + ) + } /// Add command to context - member ctx.AddCommandStep(commandStrFn: StageContext -> Async, ?cancellationToken: CancellationToken) = - { ctx with + member ctx.AddCommandStep(commandStrFn: StageContext -> Async, ?cancellationToken: CancellationToken) = { + ctx with Steps = ctx.Steps @ [ @@ -92,7 +91,7 @@ module BuiltinCmdsInternal = return! ctx.RunCommand(commandStr, i, cancellationToken = defaultArg cancellationToken CancellationToken.None) }) ] - } + } [] @@ -112,40 +111,39 @@ module BuiltinCmds = ?disablePrintOutput: bool, ?disablePrintCommand: bool, ?cancellationToken: CancellationToken - ) = - async { - let disablePrintOutput = defaultArg disablePrintOutput false - let disablePrintCommand = defaultArg disablePrintCommand false - let command = ctx.BuildCommand(commandStr, ?workingDir = workingDir) - let noPrefixForStep = ctx.GetNoPrefixForStep() - let prefix = - if noPrefixForStep then - "" - else - match step with - | Some i -> ctx.BuildStepPrefix i - | None -> ctx.GetNamePath() - - if not noPrefixForStep then AnsiConsole.Markup $"[green]{prefix}[/] " - if not disablePrintCommand then AnsiConsole.WriteLine commandStr - - let ct = defaultArg cancellationToken CancellationToken.None - - let! result = - Process.StartAsync( - command, - commandStr, - prefix, - printOutput = (not disablePrintOutput && not (ctx.GetNoStdRedirectForStep())), - cancellationToken = ct - ) - - return - if ct.IsCancellationRequested then - Ok() - else - ctx.MapExitCodeToResult result.ExitCode - } + ) = async { + let disablePrintOutput = defaultArg disablePrintOutput false + let disablePrintCommand = defaultArg disablePrintCommand false + let command = ctx.BuildCommand(commandStr, ?workingDir = workingDir) + let noPrefixForStep = ctx.GetNoPrefixForStep() + let prefix = + if noPrefixForStep then + "" + else + match step with + | Some i -> ctx.BuildStepPrefix i + | None -> ctx.GetNamePath() + + if not noPrefixForStep then AnsiConsole.Markup $"[green]{prefix}[/] " + if not disablePrintCommand then AnsiConsole.WriteLine commandStr + + let ct = defaultArg cancellationToken CancellationToken.None + + let! result = + Process.StartAsync( + command, + commandStr, + prefix, + printOutput = (not disablePrintOutput && not (ctx.GetNoStdRedirectForStep())), + cancellationToken = ct + ) + + return + if ct.IsCancellationRequested then + Ok() + else + ctx.MapExitCodeToResult result.ExitCode + } /// /// Run a command string with current context, and return the standard output if the exit code is acceptable. @@ -161,42 +159,41 @@ module BuiltinCmds = ?disablePrintOutput: bool, ?disablePrintCommand: bool, ?cancellationToken: CancellationToken - ) = - async { - let disablePrintOutput = defaultArg disablePrintOutput false - let disablePrintCommand = defaultArg disablePrintCommand false - let command = ctx.BuildCommand(commandStr, ?workingDir = workingDir) - let noPrefixForStep = ctx.GetNoPrefixForStep() - let prefix = - if noPrefixForStep then - "" - else - match step with - | Some i -> ctx.BuildStepPrefix i - | None -> ctx.GetNamePath() - - if not noPrefixForStep then AnsiConsole.Markup $"[green]{prefix}[/] " - if not disablePrintCommand then AnsiConsole.WriteLine commandStr - - let ct = defaultArg cancellationToken CancellationToken.None - - let! result = - Process.StartAsync( - command, - commandStr, - prefix, - printOutput = (not disablePrintOutput && not (ctx.GetNoStdRedirectForStep())), - captureOutput = true, - cancellationToken = ct - ) - - if ct.IsCancellationRequested then - return Ok result.StandardOutput - else if ctx.IsAcceptableExitCode result.ExitCode then - return Ok result.StandardOutput + ) = async { + let disablePrintOutput = defaultArg disablePrintOutput false + let disablePrintCommand = defaultArg disablePrintCommand false + let command = ctx.BuildCommand(commandStr, ?workingDir = workingDir) + let noPrefixForStep = ctx.GetNoPrefixForStep() + let prefix = + if noPrefixForStep then + "" else - return Error "Exit code is not indicating as successful." - } + match step with + | Some i -> ctx.BuildStepPrefix i + | None -> ctx.GetNamePath() + + if not noPrefixForStep then AnsiConsole.Markup $"[green]{prefix}[/] " + if not disablePrintCommand then AnsiConsole.WriteLine commandStr + + let ct = defaultArg cancellationToken CancellationToken.None + + let! result = + Process.StartAsync( + command, + commandStr, + prefix, + printOutput = (not disablePrintOutput && not (ctx.GetNoStdRedirectForStep())), + captureOutput = true, + cancellationToken = ct + ) + + if ct.IsCancellationRequested then + return Ok result.StandardOutput + else if ctx.IsAcceptableExitCode result.ExitCode then + return Ok result.StandardOutput + else + return Error "Exit code is not indicating as successful." + } /// Run a command string with current context, and encrypt the string for logging @@ -208,45 +205,44 @@ module BuiltinCmds = ?disablePrintOutput: bool, ?disablePrintCommand: bool, ?cancellationToken: CancellationToken - ) : Async> = - async { - let disablePrintOutput = defaultArg disablePrintOutput false - let disablePrintCommand = defaultArg disablePrintCommand false - let command = ctx.BuildCommand(commandStr.ToString(), ?workingDir = workingDir) - let noPrefixForStep = ctx.GetNoPrefixForStep() - let args: obj[] = Array.create commandStr.ArgumentCount "*" - let encryptiedStr = String.Format(commandStr.Format, args) - - let prefix = - if noPrefixForStep then - "" - else - match step with - | Some i -> ctx.BuildStepPrefix i - | None -> ctx.GetNamePath() - - if not noPrefixForStep then AnsiConsole.Markup $"[green]{prefix}[/] " - if not disablePrintCommand then AnsiConsole.WriteLine encryptiedStr - - let ct = defaultArg cancellationToken CancellationToken.None - - let! result = - Process.StartAsync( - command, - encryptiedStr, - prefix, - printOutput = (not disablePrintOutput && not (ctx.GetNoStdRedirectForStep())), - captureOutput = true, - cancellationToken = ct - ) + ) : Async> = async { + let disablePrintOutput = defaultArg disablePrintOutput false + let disablePrintCommand = defaultArg disablePrintCommand false + let command = ctx.BuildCommand(commandStr.ToString(), ?workingDir = workingDir) + let noPrefixForStep = ctx.GetNoPrefixForStep() + let args: obj[] = Array.create commandStr.ArgumentCount "*" + let encryptiedStr = String.Format(commandStr.Format, args) - if ct.IsCancellationRequested then - return Ok result.StandardOutput - else if ctx.IsAcceptableExitCode result.ExitCode then - return Ok result.StandardOutput + let prefix = + if noPrefixForStep then + "" else - return Error "Exit code is not indicating as successful." - } + match step with + | Some i -> ctx.BuildStepPrefix i + | None -> ctx.GetNamePath() + + if not noPrefixForStep then AnsiConsole.Markup $"[green]{prefix}[/] " + if not disablePrintCommand then AnsiConsole.WriteLine encryptiedStr + + let ct = defaultArg cancellationToken CancellationToken.None + + let! result = + Process.StartAsync( + command, + encryptiedStr, + prefix, + printOutput = (not disablePrintOutput && not (ctx.GetNoStdRedirectForStep())), + captureOutput = true, + cancellationToken = ct + ) + + if ct.IsCancellationRequested then + return Ok result.StandardOutput + else if ctx.IsAcceptableExitCode result.ExitCode then + return Ok result.StandardOutput + else + return Error "Exit code is not indicating as successful." + } /// /// Run a command string with current context, and return the exit code, standard output and standard error diff --git a/Fun.Build/Changelog.fs b/Fun.Build/Changelog.fs index 370cf35..90d9a1d 100644 --- a/Fun.Build/Changelog.fs +++ b/Fun.Build/Changelog.fs @@ -51,10 +51,9 @@ type Changelog = match version with | None -> None | Some v -> - Some - {| - Version = v - Preview = preview - ReleaseNotes = releaseNotes.ToString() - DateTime = dateTime - |} + Some {| + Version = v + Preview = preview + ReleaseNotes = releaseNotes.ToString() + DateTime = dateTime + |} diff --git a/Fun.Build/ConditionsBuilder.fs b/Fun.Build/ConditionsBuilder.fs index 2c21d8f..b37b024 100644 --- a/Fun.Build/ConditionsBuilder.fs +++ b/Fun.Build/ConditionsBuilder.fs @@ -35,10 +35,10 @@ module Internal = member ctx.WhenStage(stage: StageContext) = match ctx.GetMode() with | Mode.Execution -> - let stage = - { stage with + let stage = { + stage with ParentContext = ValueSome(StageParent.Stage ctx) - } + } let result, _ = stage.Run(StageIndex.Condition, System.Threading.CancellationToken.None) result | Mode.Verification -> @@ -79,15 +79,12 @@ module Internal = member ctx.WhenEnvArg(name: string, ?argValue, ?description, ?isOptional) = let argValue = defaultArg argValue "" - ctx.WhenEnvArg - { - EnvArg.Name = name - Description = description - Values = [ - if String.IsNullOrEmpty argValue |> not then argValue - ] - IsOptional = defaultArg isOptional false - } + ctx.WhenEnvArg { + EnvArg.Name = name + Description = description + Values = [ if String.IsNullOrEmpty argValue |> not then argValue ] + IsOptional = defaultArg isOptional false + } member ctx.WhenCmdArg(info: CmdArg) = @@ -124,15 +121,12 @@ module Internal = member ctx.WhenCmdArg(name: CmdName, ?argValue: string, ?description, ?isOptional) = let argValue = defaultArg argValue "" - ctx.WhenCmdArg - { - CmdArg.Name = name - Description = description - Values = [ - if String.IsNullOrEmpty argValue |> not then argValue - ] - IsOptional = defaultArg isOptional false - } + ctx.WhenCmdArg { + CmdArg.Name = name + Description = description + Values = [ if String.IsNullOrEmpty argValue |> not then argValue ] + IsOptional = defaultArg isOptional false + } member ctx.WhenBranch(branches: string seq) = @@ -267,13 +261,8 @@ type ConditionsBuilder() = [] member inline _.cmdArg - ( - [] builder: BuildConditions, - argKeyLongName: string, - argValue: string, - description: string, - isOptional: bool - ) = + ([] builder: BuildConditions, argKeyLongName: string, argValue: string, description: string, isOptional: bool) + = buildConditions builder (fun ctx -> ctx.WhenCmdArg(CmdName.LongName argKeyLongName, argValue = argValue, description = description, isOptional = isOptional)) @@ -444,13 +433,8 @@ type PipelineBuilder with /// Set if pipeline can run by check the command line args. [] member inline _.whenCmdArg - ( - [] build: BuildPipeline, - argKeyLongName: string, - argValue: string, - description: string, - isOptional: bool - ) = + ([] build: BuildPipeline, argKeyLongName: string, argValue: string, description: string, isOptional: bool) + = buildPipelineVerification build (fun ctx -> @@ -498,14 +482,14 @@ type WhenAnyBuilder() = | Mode.CommandHelp { Verbose = true } -> AnsiConsole.MarkupLine $"[olive]{ctx.BuildIndent()}when any below conditions are met[/]" | _ -> () - let indentCtx = - { StageContext.Create " " with + let indentCtx = { + StageContext.Create " " with ParentContext = ctx.ParentContext - } - let newCtx = - { ctx with + } + let newCtx = { + ctx with ParentContext = ValueSome(StageParent.Stage indentCtx) - } + } builder.Invoke [] |> Seq.iter (fun fn -> fn newCtx |> ignore) false ) @@ -524,14 +508,14 @@ type WhenAllBuilder() = | Mode.CommandHelp { Verbose = true } -> AnsiConsole.MarkupLine $"[olive]{ctx.BuildIndent()}when all below conditions are met[/]" | _ -> () - let indentCtx = - { StageContext.Create " " with + let indentCtx = { + StageContext.Create " " with ParentContext = ctx.ParentContext - } - let newCtx = - { ctx with + } + let newCtx = { + ctx with ParentContext = ValueSome(StageParent.Stage indentCtx) - } + } builder.Invoke [] |> Seq.iter (fun fn -> fn newCtx |> ignore) false ) @@ -551,14 +535,14 @@ type WhenNotBuilder() = AnsiConsole.MarkupLine $"[olive]{ctx.BuildIndent()}when all below conditions are [bold red]NOT[/] met[/]" | _ -> () - let indentCtx = - { StageContext.Create " " with + let indentCtx = { + StageContext.Create " " with ParentContext = ctx.ParentContext - } - let newCtx = - { ctx with + } + let newCtx = { + ctx with ParentContext = ValueSome(StageParent.Stage indentCtx) - } + } builder.Invoke [] |> Seq.iter (fun fn -> fn newCtx |> ignore) false ) @@ -569,14 +553,13 @@ type WhenCmdBuilder() = member _.Run(build: BuildCmdInfo) = BuildStageIsActive(fun ctx -> let cmdInfo = - build.Invoke - { - // We should carefully procees the empty string in this build type - Name = CmdName.ShortName "" - Description = None - Values = [] - IsOptional = false - } + build.Invoke { + // We should carefully procees the empty string in this build type + Name = CmdName.ShortName "" + Description = None + Values = [] + IsOptional = false + } ctx.WhenCmdArg(cmdInfo) ) @@ -588,11 +571,10 @@ type WhenCmdBuilder() = /// Short name, long name [] member inline _.fullName([] build: BuildCmdInfo, shortName: string, longName: string) = - BuildCmdInfo(fun info -> - { build.Invoke(info) with + BuildCmdInfo(fun info -> { + build.Invoke(info) with Name = CmdName.FullName(shortName, longName) - } - ) + }) /// It is the same as shortName [] @@ -607,12 +589,13 @@ type WhenCmdBuilder() = member _.shortName(build: BuildCmdInfo, x: string) = BuildCmdInfo(fun info -> let info = build.Invoke(info) - { info with - Name = - match info.Name with - | CmdName.FullName(_, longName) - | CmdName.LongName longName when not (String.IsNullOrEmpty longName) -> CmdName.FullName(x, longName) - | _ -> CmdName.ShortName x + { + info with + Name = + match info.Name with + | CmdName.FullName(_, longName) + | CmdName.LongName longName when not (String.IsNullOrEmpty longName) -> CmdName.FullName(x, longName) + | _ -> CmdName.ShortName x } ) @@ -620,12 +603,13 @@ type WhenCmdBuilder() = member _.longName(build: BuildCmdInfo, x: string) = BuildCmdInfo(fun info -> let info = build.Invoke(info) - { info with - Name = - match info.Name with - | CmdName.FullName(shortName, _) - | CmdName.ShortName shortName when not (String.IsNullOrEmpty shortName) -> CmdName.FullName(shortName, x) - | _ -> CmdName.LongName x + { + info with + Name = + match info.Name with + | CmdName.FullName(shortName, _) + | CmdName.ShortName shortName when not (String.IsNullOrEmpty shortName) -> CmdName.FullName(shortName, x) + | _ -> CmdName.LongName x } ) @@ -650,14 +634,13 @@ type WhenEnvBuilder() = member _.Run(build: BuildEnvInfo) = BuildStageIsActive(fun ctx -> let arg = - build.Invoke - { - // We should carefully procees the empty string in this build type - Name = "" - Description = None - Values = [] - IsOptional = false - } + build.Invoke { + // We should carefully procees the empty string in this build type + Name = "" + Description = None + Values = [] + IsOptional = false + } ctx.WhenEnvArg(arg) ) diff --git a/Fun.Build/PipelineBuilder.fs b/Fun.Build/PipelineBuilder.fs index 6627b80..cc427c5 100644 --- a/Fun.Build/PipelineBuilder.fs +++ b/Fun.Build/PipelineBuilder.fs @@ -21,21 +21,20 @@ type PipelineBuilder(name: string) = member _.Run(build: BuildPipeline) = let ctx = build.Invoke(PipelineContext.Create name) - { ctx with - Stages = - ctx.Stages - |> List.map (fun x -> - { x with - ParentContext = ValueSome(StageParent.Pipeline ctx) - } - ) - PostStages = - ctx.PostStages - |> List.map (fun x -> - { x with - ParentContext = ValueSome(StageParent.Pipeline ctx) - } - ) + { + ctx with + Stages = + ctx.Stages + |> List.map (fun x -> { + x with + ParentContext = ValueSome(StageParent.Pipeline ctx) + }) + PostStages = + ctx.PostStages + |> List.map (fun x -> { + x with + ParentContext = ValueSome(StageParent.Pipeline ctx) + }) } @@ -70,11 +69,10 @@ type PipelineBuilder(name: string) = member inline _.Yield([] condition: BuildStageIsActive) = condition member inline _.Delay([] fn: unit -> BuildStageIsActive) = - BuildPipeline(fun ctx -> - { ctx with + BuildPipeline(fun ctx -> { + ctx with Verify = fun ctx -> fn().Invoke(ctx.MakeVerificationStage()) - } - ) + }) member inline _.Combine([] condition: BuildStageIsActive, [] build: BuildPipeline) = buildPipelineVerification build condition.Invoke @@ -104,8 +102,9 @@ type PipelineBuilder(name: string) = member inline _.timeout([] build: BuildPipeline, seconds: int) = BuildPipeline(fun ctx -> let ctx = build.Invoke ctx - { ctx with - Timeout = ValueSome(TimeSpan.FromSeconds seconds) + { + ctx with + Timeout = ValueSome(TimeSpan.FromSeconds seconds) } ) @@ -123,8 +122,9 @@ type PipelineBuilder(name: string) = member inline _.timeoutForStage([] build: BuildPipeline, seconds: int) = BuildPipeline(fun ctx -> let ctx = build.Invoke ctx - { ctx with - TimeoutForStage = ValueSome(TimeSpan.FromSeconds seconds) + { + ctx with + TimeoutForStage = ValueSome(TimeSpan.FromSeconds seconds) } ) @@ -142,8 +142,9 @@ type PipelineBuilder(name: string) = member inline _.timeoutForStep([] build: BuildPipeline, seconds: int) = BuildPipeline(fun ctx -> let ctx = build.Invoke ctx - { ctx with - TimeoutForStep = ValueSome(TimeSpan.FromSeconds seconds) + { + ctx with + TimeoutForStep = ValueSome(TimeSpan.FromSeconds seconds) } ) @@ -161,8 +162,9 @@ type PipelineBuilder(name: string) = member inline _.envVars([] build: BuildPipeline, kvs: seq) = BuildPipeline(fun ctx -> let ctx = build.Invoke ctx - { ctx with - EnvVars = kvs |> Seq.fold (fun state (k, v) -> Map.add k v state) ctx.EnvVars + { + ctx with + EnvVars = kvs |> Seq.fold (fun state (k, v) -> Map.add k v state) ctx.EnvVars } ) @@ -183,9 +185,10 @@ type PipelineBuilder(name: string) = let argsInfo = resolveCmdArgsAndRemainings args - { ctx with - CmdArgs = argsInfo.CmdArgs - RemainingCmdArgs = argsInfo.RemainingArgs + { + ctx with + CmdArgs = argsInfo.CmdArgs + RemainingCmdArgs = argsInfo.RemainingArgs } ) @@ -278,11 +281,11 @@ type PipelineBuilder(name: string) = else args[index .. pipelineIndexes[i + 1] - 1] let argInfo = resolveCmdArgsAndRemainings args - let ctx = - { ctx with + let ctx = { + ctx with CmdArgs = argInfo.CmdArgs RemainingCmdArgs = argInfo.RemainingArgs - } + } if isHelp then ctx.RunCommandHelp(verbose) else ctx.Run() with | :? PipelineFailedException diff --git a/Fun.Build/PipelineContextExtensions.fs b/Fun.Build/PipelineContextExtensions.fs index 98141af..7c07bfd 100644 --- a/Fun.Build/PipelineContextExtensions.fs +++ b/Fun.Build/PipelineContextExtensions.fs @@ -56,10 +56,10 @@ module PipelineContextExtensionsInternal = | _ -> ValueNone - member this.MakeVerificationStage() = - { StageContext.Create("") with + member this.MakeVerificationStage() = { + StageContext.Create("") with ParentContext = ValueSome(StageParent.Pipeline this) - } + } member this.PrintError(msg: string) = @@ -75,11 +75,10 @@ module PipelineContextExtensionsInternal = let stages = stages - |> Seq.map (fun x -> - { x with + |> Seq.map (fun x -> { + x with ParentContext = ValueSome(StageParent.Pipeline this) - } - ) + }) |> Seq.toList let mutable i = 0 @@ -239,25 +238,25 @@ module PipelineContextExtensionsInternal = match step with | Step.StepFn _ -> () | Step.StepOfStage s -> - run - { s with + run { + s with ParentContext = ValueSome(StageParent.Stage stage) - } + } pipeline.Stages |> List.iter (fun stage -> - run - { stage with + run { + stage with ParentContext = ValueSome(StageParent.Pipeline pipeline) - } + } ) pipeline.PostStages |> List.iter (fun stage -> - run - { stage with + run { + stage with ParentContext = ValueSome(StageParent.Pipeline pipeline) - } + } ) if not verbose then @@ -299,15 +298,16 @@ module PipelineContextExtensionsInternal = let inline buildPipelineVerification ([] build: BuildPipeline) ([] conditionFn) = BuildPipeline(fun ctx -> let newCtx = build.Invoke ctx - { newCtx with - Verify = - fun ctx -> - match ctx.Mode with - | Mode.Execution -> newCtx.Verify ctx && conditionFn (ctx.MakeVerificationStage()) - | Mode.Verification - | Mode.CommandHelp _ -> - newCtx.Verify ctx |> ignore - conditionFn (ctx.MakeVerificationStage()) |> ignore - false + { + newCtx with + Verify = + fun ctx -> + match ctx.Mode with + | Mode.Execution -> newCtx.Verify ctx && conditionFn (ctx.MakeVerificationStage()) + | Mode.Verification + | Mode.CommandHelp _ -> + newCtx.Verify ctx |> ignore + conditionFn (ctx.MakeVerificationStage()) |> ignore + false } ) diff --git a/Fun.Build/ProcessExtensions.fs b/Fun.Build/ProcessExtensions.fs index e2b4b9e..339002e 100644 --- a/Fun.Build/ProcessExtensions.fs +++ b/Fun.Build/ProcessExtensions.fs @@ -101,14 +101,7 @@ type Process with static member StartAsync - ( - startInfo: ProcessStartInfo, - commandLogString: string, - logPrefix: string, - ?printOutput, - ?captureOutput, - ?cancellationToken: CancellationToken - ) = + (startInfo: ProcessStartInfo, commandLogString: string, logPrefix: string, ?printOutput, ?captureOutput, ?cancellationToken: CancellationToken) = async { let printOutput = defaultArg printOutput true let captureOutput = defaultArg captureOutput false @@ -163,10 +156,10 @@ type Process with Native.kill result ) :> IDisposable - | _ -> - { new IDisposable with + | _ -> { + new IDisposable with member _.Dispose() = () - } + } if shouldRedirectOutput then result.BeginOutputReadLine() diff --git a/Fun.Build/StageBuilder.fs b/Fun.Build/StageBuilder.fs index 762ff0c..7d5c40e 100644 --- a/Fun.Build/StageBuilder.fs +++ b/Fun.Build/StageBuilder.fs @@ -50,25 +50,25 @@ type StageBuilder(name: string) = member inline _.Yield([] builder: BuildStep) = builder member inline _.Delay([] fn: unit -> BuildStep) = - BuildStage(fun ctx -> - { ctx with + BuildStage(fun ctx -> { + ctx with Steps = ctx.Steps @ [ Step.StepFn(fn().Invoke) ] - } - ) + }) member inline _.Combine([] builder: BuildStep, [] build: BuildStage) = BuildStage(fun ctx -> - build.Invoke - { ctx with + build.Invoke { + ctx with Steps = ctx.Steps @ [ Step.StepFn builder.Invoke ] - } + } ) member inline _.For([] build: BuildStage, [] fn: unit -> BuildStep) = BuildStage(fun ctx -> let ctx = build.Invoke ctx - { ctx with - Steps = ctx.Steps @ [ Step.StepFn(fn().Invoke) ] + { + ctx with + Steps = ctx.Steps @ [ Step.StepFn(fn().Invoke) ] } ) @@ -94,8 +94,9 @@ type StageBuilder(name: string) = member inline _.envVars([] build: BuildStage, kvs: seq) = BuildStage(fun ctx -> let ctx = build.Invoke ctx - { ctx with - EnvVars = kvs |> Seq.fold (fun state (k, v) -> Map.add k v state) ctx.EnvVars + { + ctx with + EnvVars = kvs |> Seq.fold (fun state (k, v) -> Map.add k v state) ctx.EnvVars } ) @@ -158,9 +159,10 @@ type StageBuilder(name: string) = BuildStage(fun ctx -> let ctx = build.Invoke ctx let shouldContinue = defaultArg flag true - { ctx with - ContinueStepsOnFailure = shouldContinue - ContinueStageOnFailure = shouldContinue + { + ctx with + ContinueStepsOnFailure = shouldContinue + ContinueStageOnFailure = shouldContinue } ) @@ -171,8 +173,9 @@ type StageBuilder(name: string) = member inline _.timeout([] build: BuildStage, seconds: int) = BuildStage(fun ctx -> let ctx = build.Invoke ctx - { ctx with - Timeout = ValueSome(TimeSpan.FromSeconds seconds) + { + ctx with + Timeout = ValueSome(TimeSpan.FromSeconds seconds) } ) @@ -189,8 +192,9 @@ type StageBuilder(name: string) = member inline _.timeoutForStep([] build: BuildStage, seconds: int) = BuildStage(fun ctx -> let ctx = build.Invoke ctx - { ctx with - TimeoutForStep = ValueSome(TimeSpan.FromSeconds seconds) + { + ctx with + TimeoutForStep = ValueSome(TimeSpan.FromSeconds seconds) } ) @@ -218,11 +222,10 @@ type StageBuilder(name: string) = /// Set if the steps in current stage should run in concurrent. Alias of paralle. [] member inline _.concurrent([] build: BuildStage, ?value: bool) = - BuildStage(fun ctx -> - { build.Invoke ctx with + BuildStage(fun ctx -> { + build.Invoke ctx with IsParallel = fun _ -> defaultArg value true - } - ) + }) /// Set workding dir for all steps under the stage. @@ -266,15 +269,16 @@ type StageBuilder(name: string) = member _.run(build: BuildStage, buildStep: StageContext -> BuildStep) = BuildStage(fun ctx -> let ctx = build.Invoke ctx - { ctx with - Steps = - ctx.Steps - @ [ - Step.StepFn(fun (ctx, i) -> async { - let builder = buildStep ctx - return! builder.Invoke(ctx, i) - }) - ] + { + ctx with + Steps = + ctx.Steps + @ [ + Step.StepFn(fun (ctx, i) -> async { + let builder = buildStep ctx + return! builder.Invoke(ctx, i) + }) + ] } ) @@ -335,14 +339,15 @@ type StageBuilder(name: string) = member inline _.runSensitive([] build: BuildStage, command: FormattableString, ?cancellationToken: CancellationToken) = BuildStage(fun ctx -> let ctx = build.Invoke ctx - { ctx with - Steps = - ctx.Steps - @ [ - Step.StepFn(fun (ctx, step) -> - ctx.RunSensitiveCommand(command, step, cancellationToken = defaultArg cancellationToken CancellationToken.None) - ) - ] + { + ctx with + Steps = + ctx.Steps + @ [ + Step.StepFn(fun (ctx, step) -> + ctx.RunSensitiveCommand(command, step, cancellationToken = defaultArg cancellationToken CancellationToken.None) + ) + ] } ) @@ -352,15 +357,16 @@ type StageBuilder(name: string) = member _.run(build: BuildStage, step: Async) = BuildStage(fun ctx -> let ctx = build.Invoke ctx - { ctx with - Steps = - ctx.Steps - @ [ - Step.StepFn(fun _ -> async { - do! step - return Ok() - }) - ] + { + ctx with + Steps = + ctx.Steps + @ [ + Step.StepFn(fun _ -> async { + do! step + return Ok() + }) + ] } ) @@ -369,15 +375,16 @@ type StageBuilder(name: string) = member _.run(build: BuildStage, step: Async) = BuildStage(fun ctx -> let ctx = build.Invoke ctx - { ctx with - Steps = - ctx.Steps - @ [ - Step.StepFn(fun (ctx, _) -> async { - let! exitCode = step - return ctx.MapExitCodeToResult exitCode - }) - ] + { + ctx with + Steps = + ctx.Steps + @ [ + Step.StepFn(fun (ctx, _) -> async { + let! exitCode = step + return ctx.MapExitCodeToResult exitCode + }) + ] } ) @@ -387,15 +394,16 @@ type StageBuilder(name: string) = member _.run(build: BuildStage, step: StageContext -> unit) = BuildStage(fun ctx -> let ctx = build.Invoke ctx - { ctx with - Steps = - ctx.Steps - @ [ - Step.StepFn(fun (ctx, _) -> async { - step ctx - return Ok() - }) - ] + { + ctx with + Steps = + ctx.Steps + @ [ + Step.StepFn(fun (ctx, _) -> async { + step ctx + return Ok() + }) + ] } ) @@ -404,8 +412,9 @@ type StageBuilder(name: string) = member _.run(build: BuildStage, step: StageContext -> int) = BuildStage(fun ctx -> let ctx = build.Invoke ctx - { ctx with - Steps = ctx.Steps @ [ Step.StepFn(fun (ctx, _) -> async { return ctx.MapExitCodeToResult(step ctx) }) ] + { + ctx with + Steps = ctx.Steps @ [ Step.StepFn(fun (ctx, _) -> async { return ctx.MapExitCodeToResult(step ctx) }) ] } ) @@ -415,15 +424,16 @@ type StageBuilder(name: string) = member _.run(build: BuildStage, step: StageContext -> Async) = BuildStage(fun ctx -> let ctx = build.Invoke ctx - { ctx with - Steps = - ctx.Steps - @ [ - Step.StepFn(fun (ctx, _) -> async { - do! step ctx - return Ok() - }) - ] + { + ctx with + Steps = + ctx.Steps + @ [ + Step.StepFn(fun (ctx, _) -> async { + do! step ctx + return Ok() + }) + ] } ) @@ -432,15 +442,16 @@ type StageBuilder(name: string) = member _.run(build: BuildStage, step: StageContext -> Async) = BuildStage(fun ctx -> let ctx = build.Invoke ctx - { ctx with - Steps = - ctx.Steps - @ [ - Step.StepFn(fun (ctx, _) -> async { - let! exitCode = step ctx - return ctx.MapExitCodeToResult exitCode - }) - ] + { + ctx with + Steps = + ctx.Steps + @ [ + Step.StepFn(fun (ctx, _) -> async { + let! exitCode = step ctx + return ctx.MapExitCodeToResult exitCode + }) + ] } ) @@ -450,8 +461,9 @@ type StageBuilder(name: string) = member _.run(build: BuildStage, step: StageContext -> Async>) = BuildStage(fun ctx -> let ctx = build.Invoke ctx - { ctx with - Steps = ctx.Steps @ [ Step.StepFn(fun (ctx, _) -> step ctx) ] + { + ctx with + Steps = ctx.Steps @ [ Step.StepFn(fun (ctx, _) -> step ctx) ] } ) @@ -460,8 +472,9 @@ type StageBuilder(name: string) = member _.run(build: BuildStage, step: StageContext -> Task>) = BuildStage(fun ctx -> let ctx = build.Invoke ctx - { ctx with - Steps = ctx.Steps @ [ Step.StepFn(fun (ctx, _) -> step ctx |> Async.AwaitTask) ] + { + ctx with + Steps = ctx.Steps @ [ Step.StepFn(fun (ctx, _) -> step ctx |> Async.AwaitTask) ] } ) @@ -470,8 +483,9 @@ type StageBuilder(name: string) = member _.run(build: BuildStage, step: StageContext -> Result) = BuildStage(fun ctx -> let ctx = build.Invoke ctx - { ctx with - Steps = ctx.Steps @ [ Step.StepFn(fun (ctx, _) -> async { return step ctx }) ] + { + ctx with + Steps = ctx.Steps @ [ Step.StepFn(fun (ctx, _) -> async { return step ctx }) ] } ) @@ -481,15 +495,16 @@ type StageBuilder(name: string) = member _.run(build: BuildStage, step: StageContext -> Task) = BuildStage(fun ctx -> let ctx = build.Invoke ctx - { ctx with - Steps = - ctx.Steps - @ [ - Step.StepFn(fun (ctx, _) -> async { - do! step ctx |> Async.AwaitTask - return Ok() - }) - ] + { + ctx with + Steps = + ctx.Steps + @ [ + Step.StepFn(fun (ctx, _) -> async { + do! step ctx |> Async.AwaitTask + return Ok() + }) + ] } ) @@ -498,15 +513,16 @@ type StageBuilder(name: string) = member _.run(build: BuildStage, step: StageContext -> Task) = BuildStage(fun ctx -> let ctx = build.Invoke ctx - { ctx with - Steps = - ctx.Steps - @ [ - Step.StepFn(fun (ctx, _) -> async { - do! step ctx |> Async.AwaitTask - return Ok() - }) - ] + { + ctx with + Steps = + ctx.Steps + @ [ + Step.StepFn(fun (ctx, _) -> async { + do! step ctx |> Async.AwaitTask + return Ok() + }) + ] } ) @@ -515,15 +531,16 @@ type StageBuilder(name: string) = member _.run(build: BuildStage, step: StageContext -> Task) = BuildStage(fun ctx -> let ctx = build.Invoke ctx - { ctx with - Steps = - ctx.Steps - @ [ - Step.StepFn(fun (ctx, _) -> async { - let! exitCode = step ctx |> Async.AwaitTask - return ctx.MapExitCodeToResult exitCode - }) - ] + { + ctx with + Steps = + ctx.Steps + @ [ + Step.StepFn(fun (ctx, _) -> async { + let! exitCode = step ctx |> Async.AwaitTask + return ctx.MapExitCodeToResult exitCode + }) + ] } ) @@ -533,18 +550,19 @@ type StageBuilder(name: string) = member _.runHttpHealthCheck(build: BuildStage, url: string, ?configRequest, ?cancellationToken) = BuildStage(fun ctx -> let ctx = build.Invoke ctx - { ctx with - Steps = - ctx.Steps - @ [ - Step.StepFn(fun (ctx, _) -> - ctx.RunHttpHealthCheck( - url, - configRequest = defaultArg configRequest ignore, - cancellationToken = defaultArg cancellationToken CancellationToken.None + { + ctx with + Steps = + ctx.Steps + @ [ + Step.StepFn(fun (ctx, _) -> + ctx.RunHttpHealthCheck( + url, + configRequest = defaultArg configRequest ignore, + cancellationToken = defaultArg cancellationToken CancellationToken.None + ) ) - ) - ] + ] } ) @@ -554,18 +572,19 @@ type StageBuilder(name: string) = member inline _.echo([] build: BuildStage, msg: StageContext -> string) = BuildStage(fun ctx -> let ctx = build.Invoke ctx - { ctx with - Steps = - ctx.Steps - @ [ - Step.StepFn(fun (ctx, i) -> async { - if ctx.GetNoPrefixForStep() then - printfn "%s" (msg ctx) - else - printfn "%s %s" (ctx.BuildStepPrefix i) (msg ctx) - return Ok() - }) - ] + { + ctx with + Steps = + ctx.Steps + @ [ + Step.StepFn(fun (ctx, i) -> async { + if ctx.GetNoPrefixForStep() then + printfn "%s" (msg ctx) + else + printfn "%s %s" (ctx.BuildStepPrefix i) (msg ctx) + return Ok() + }) + ] } ) diff --git a/Fun.Build/StageContextExtensions.fs b/Fun.Build/StageContextExtensions.fs index 201c059..7d7d50c 100644 --- a/Fun.Build/StageContextExtensions.fs +++ b/Fun.Build/StageContextExtensions.fs @@ -170,13 +170,13 @@ module StageContextExtensionsInternal = if not isActive && stage.FailIfIgnored then let msg = $"Stage ({stage.GetNamePath()}) cannot be ignored (inactive)" AnsiConsole.MarkupLineInterpolated $"[red]{msg}[/]" - let verifyStage = - { stage with + let verifyStage = { + stage with ParentContext = match stage.ParentContext with | ValueSome(StageParent.Pipeline p) -> ValueSome(StageParent.Pipeline { p with Mode = Mode.Verification }) | x -> x - } + } stage.IsActive(verifyStage) |> ignore raise (PipelineFailedException msg) @@ -236,10 +236,10 @@ module StageContextExtensionsInternal = match step with | Step.StepFn _ -> stage.BuildStepPrefix(i) | Step.StepOfStage s -> - let subStage = - { s with + let subStage = { + s with ParentContext = ValueSome(StageParent.Stage stage) - } + } subStage.BuildCurrentStepPrefix() + ">" @@ -265,10 +265,10 @@ module StageContextExtensionsInternal = | Ok _ -> return true } | Step.StepOfStage subStage -> async { - let subStage = - { subStage with + let subStage = { + subStage with ParentContext = ValueSome(StageParent.Stage stage) - } + } let isSuccess, es = subStage.Run(StageIndex.Step i, linkedStepCTS.Token) exns.AddRange es return isSuccess @@ -375,8 +375,7 @@ module StageContextExtensionsInternal = ) | StageIndex.Stage i -> AnsiConsole.Write( - Rule($"""[grey50]STAGE #{i} [bold {color}]{namePath}[/] finished. {stageSW.ElapsedMilliseconds}ms.[/]""") - .LeftJustified() + Rule($"""[grey50]STAGE #{i} [bold {color}]{namePath}[/] finished. {stageSW.ElapsedMilliseconds}ms.[/]""").LeftJustified() ) | StageIndex.Step _ -> AnsiConsole.MarkupLineInterpolated( @@ -405,16 +404,17 @@ module StageContextExtensionsInternal = let inline buildStageIsActive ([] build: BuildStage) ([] conditionFn) = BuildStage(fun ctx -> let newCtx = build.Invoke ctx - { newCtx with - IsActive = - fun ctx -> - match ctx.GetMode() with - | Mode.Execution -> newCtx.IsActive ctx && conditionFn ctx - | Mode.Verification - | Mode.CommandHelp _ -> - newCtx.IsActive ctx |> ignore - conditionFn ctx |> ignore - false + { + newCtx with + IsActive = + fun ctx -> + match ctx.GetMode() with + | Mode.Execution -> newCtx.IsActive ctx && conditionFn ctx + | Mode.Verification + | Mode.CommandHelp _ -> + newCtx.IsActive ctx |> ignore + conditionFn ctx |> ignore + false } ) diff --git a/Fun.Build/Types.fs b/Fun.Build/Types.fs index 2de1ac4..d5cb1d1 100644 --- a/Fun.Build/Types.fs +++ b/Fun.Build/Types.fs @@ -34,13 +34,12 @@ type CmdName = | LongName x -> [ x ] | FullName(s, l) -> [ s; l ] -type CmdArg = - { - Name: CmdName - Values: string list - Description: string option - IsOptional: bool - } +type CmdArg = { + Name: CmdName + Values: string list + Description: string option + IsOptional: bool +} with static member Create(?shortName: string, ?longName: string, ?description: string, ?values, ?isOptional: bool) = { Name = @@ -60,13 +59,12 @@ type CmdArg = member this.WithOptional x = { this with IsOptional = x } -type EnvArg = - { - Name: string - Values: string list - Description: string option - IsOptional: bool - } +type EnvArg = { + Name: string + Values: string list + Description: string option + IsOptional: bool +} with static member Create(name: string, ?description: string, ?values, ?isOptional: bool) = { Name = name diff --git a/Fun.Build/Windows.fs b/Fun.Build/Windows.fs index 896bf58..0feb54c 100644 --- a/Fun.Build/Windows.fs +++ b/Fun.Build/Windows.fs @@ -12,33 +12,32 @@ module Windows = appName = EnvArg.Create("IIS_RECYCLE_APP_NAME") |} - let stage_deployToIIS (zippedPackageFile: string) = - stage "deploy to iis" { - whenEnvVar deployToIISOptions.host - whenEnvVar deployToIISOptions.user - whenEnvVar deployToIISOptions.pwd - whenEnvVar deployToIISOptions.siteName - whenEnvVar deployToIISOptions.targetDir - whenEnvVar deployToIISOptions.appName - run (fun ctx -> asyncResult { - let host = ctx.GetEnvVar deployToIISOptions.host.Name - let user = ctx.GetEnvVar deployToIISOptions.user.Name - let pwd = ctx.GetEnvVar deployToIISOptions.pwd.Name - let siteName = ctx.GetEnvVar deployToIISOptions.siteName.Name - let targetDir = ctx.GetEnvVar deployToIISOptions.targetDir.Name - let appName = ctx.GetEnvVar deployToIISOptions.appName.Name + let stage_deployToIIS (zippedPackageFile: string) = stage "deploy to iis" { + whenEnvVar deployToIISOptions.host + whenEnvVar deployToIISOptions.user + whenEnvVar deployToIISOptions.pwd + whenEnvVar deployToIISOptions.siteName + whenEnvVar deployToIISOptions.targetDir + whenEnvVar deployToIISOptions.appName + run (fun ctx -> asyncResult { + let host = ctx.GetEnvVar deployToIISOptions.host.Name + let user = ctx.GetEnvVar deployToIISOptions.user.Name + let pwd = ctx.GetEnvVar deployToIISOptions.pwd.Name + let siteName = ctx.GetEnvVar deployToIISOptions.siteName.Name + let targetDir = ctx.GetEnvVar deployToIISOptions.targetDir.Name + let appName = ctx.GetEnvVar deployToIISOptions.appName.Name - let msdeploy (x: string) = ctx.RunSensitiveCommand($"'C:/Program Files (x86)/IIS/Microsoft Web Deploy V3/msdeploy.exe' {x}") + let msdeploy (x: string) = ctx.RunSensitiveCommand($"'C:/Program Files (x86)/IIS/Microsoft Web Deploy V3/msdeploy.exe' {x}") - do! - msdeploy - $"-verb:sync -allowUntrusted -source:recycleApp -dest:recycleApp=\"{appName}\",recycleMode=\"StopAppPool\",computerName=\"{host}/msdeploy.axd?site={siteName}\",username=\"{user}\",password=\"{pwd}\",AuthType=\"Basic\"" + do! + msdeploy + $"-verb:sync -allowUntrusted -source:recycleApp -dest:recycleApp=\"{appName}\",recycleMode=\"StopAppPool\",computerName=\"{host}/msdeploy.axd?site={siteName}\",username=\"{user}\",password=\"{pwd}\",AuthType=\"Basic\"" - do! - msdeploy - $"-verb:sync -allowUntrusted -source:package=\"{zippedPackageFile}\" -dest:contentPath=\"{targetDir}\",computerName=\"{host}/msdeploy.axd?site={siteName}\",username=\"{user}\",password=\"{pwd}\",AuthType=\"Basic\" -enableRule:DoNotDeleteRule" - do! - msdeploy - $"-verb:sync -allowUntrusted -source:recycleApp -dest:recycleApp=\"{appName}\",recycleMode=\"StartAppPool\",computerName=\"{host}/msdeploy.axd?site={siteName}\",username=\"{user}\",password=\"{pwd}\",AuthType=\"Basic\"" - }) - } + do! + msdeploy + $"-verb:sync -allowUntrusted -source:package=\"{zippedPackageFile}\" -dest:contentPath=\"{targetDir}\",computerName=\"{host}/msdeploy.axd?site={siteName}\",username=\"{user}\",password=\"{pwd}\",AuthType=\"Basic\" -enableRule:DoNotDeleteRule" + do! + msdeploy + $"-verb:sync -allowUntrusted -source:recycleApp -dest:recycleApp=\"{appName}\",recycleMode=\"StartAppPool\",computerName=\"{host}/msdeploy.axd?site={siteName}\",username=\"{user}\",password=\"{pwd}\",AuthType=\"Basic\"" + }) + } diff --git a/build.fsx b/build.fsx index 3ba2c80..75b5e71 100644 --- a/build.fsx +++ b/build.fsx @@ -18,10 +18,10 @@ let stage_checkEnv = stage "Check environment" { run "dotnet tool restore" } let stage_lint = stage "Lint" { - stage "Format" { run "dotnet fantomas . -r" } + stage "Format" { run "dotnet fantomas" } stage "Check" { whenGithubAction - run "dotnet fantomas . -r --check" + run "dotnet fantomas check" } } diff --git a/demo.fsx b/demo.fsx index 9ba4b48..1cd1a25 100644 --- a/demo.fsx +++ b/demo.fsx @@ -21,10 +21,12 @@ let demo1 = run (fun ctx -> "dotnet --version") run (fun ctx -> async { return "dotnet --version" }) // You use use the RunCommand to run multiple command according to your logics - run (fun ctx -> asyncResult { - do! ctx.RunCommand "dotnet --version" - do! ctx.RunCommand "dotnet --version" - }) + run (fun ctx -> + asyncResult { + do! ctx.RunCommand "dotnet --version" + do! ctx.RunCommand "dotnet --version" + } + ) // You can run async functions run (Async.Sleep 1000) run (fun _ -> Async.Sleep 1000) @@ -106,9 +108,11 @@ pipeline "Fun.Build" { stage "Post stage" { echo "You are finished 😂" echo (fun ctx -> sprintf "You are finished here: %A" (ctx.GetWorkingDir())) - run (fun _ -> async { - return 0 // do something - }) + run (fun _ -> + async { + return 0 // do something + } + ) } ] // You can have multiple pipelines, sometimes you only want to run it only if the command specified the pipeline name. From ded711c2c1567c6d323a47bfafe6a700bb8fac9a Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Fri, 11 Sep 2026 11:48:42 +0200 Subject: [PATCH 2/5] Use dotnet 10 SDK --- .github/workflows/Build and publish nuget package.yml | 8 +++++--- .github/workflows/Run tests for PR.yml | 8 +++++--- global.json | 6 ++++++ 3 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 global.json diff --git a/.github/workflows/Build and publish nuget package.yml b/.github/workflows/Build and publish nuget package.yml index 25fc4ed..1c5f539 100644 --- a/.github/workflows/Build and publish nuget package.yml +++ b/.github/workflows/Build and publish nuget package.yml @@ -14,12 +14,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Setup .NET Core - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: - dotnet-version: 8.0.x + dotnet-version: | + 8.0.x + 10.0.x - name: Use build.fsx env: diff --git a/.github/workflows/Run tests for PR.yml b/.github/workflows/Run tests for PR.yml index fdbe8bf..4b3b8ed 100644 --- a/.github/workflows/Run tests for PR.yml +++ b/.github/workflows/Run tests for PR.yml @@ -10,12 +10,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Setup .NET Core - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: - dotnet-version: 8.0.x + dotnet-version: | + 8.0.x + 10.0.x - name: Use build.fsx run: dotnet fsi ./build.fsx -- -p test diff --git a/global.json b/global.json new file mode 100644 index 0000000..01cb658 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.400", + "rollForward": "latestFeature" + } +} From 8e9dbadf93cb55a1521b8a042854dd8bdee62cba Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Fri, 11 Sep 2026 12:01:11 +0200 Subject: [PATCH 3/5] Loosen test --- Fun.Build.Tests/PipelineBuilderTests.fs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Fun.Build.Tests/PipelineBuilderTests.fs b/Fun.Build.Tests/PipelineBuilderTests.fs index e597031..54e5b73 100644 --- a/Fun.Build.Tests/PipelineBuilderTests.fs +++ b/Fun.Build.Tests/PipelineBuilderTests.fs @@ -209,7 +209,11 @@ let ``parallel should work`` () = } runImmediate } - Assert.InRange(sw.ElapsedMilliseconds, 1000, 2000) + // Upper bounds are deliberately loose: these are wall-clock assertions and CI runners + // are shared and slow. What each bound encodes: + // parallel - must stay well under the 3000ms the sequential version needs + // sequential - must be at least 2500ms, which is what proves it did not run in parallel + Assert.InRange(sw.ElapsedMilliseconds, 1000, 2400) sw.Restart() pipeline "" { @@ -221,7 +225,7 @@ let ``parallel should work`` () = runImmediate } let elapsed = sw.ElapsedMilliseconds - Assert.InRange(elapsed, 2500, 4000) + Assert.InRange(elapsed, 2500, 10000) [] From 69a537f3b2fae840ee2fcfef63025224c1ab0e8b Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Fri, 11 Sep 2026 14:58:42 +0200 Subject: [PATCH 4/5] Fix: sensitive commands mask secrets in the child's own output #95 Since #94 started draining the child's stderr, that output reaches the log for the first time. The RunSensitive* members only ever masked the command line, so anything that echoes a credential back (curl -v, ssh -v, git with GIT_TRACE, a tool warning about a token on argv) printed it in full directly under the masked line, which made the log look like masking had worked. Only master is affected; 1.1.18 predates #94. The interpolated values of a FormattableString are now replaced with * in the child's standard output and standard error as well, not just in the command that gets logged. Captured output is handed back verbatim, since the caller asked for it and already knows those values. Masking is a plain substring replacement, so a short value is replaced wherever it appears in that output, which is noted on the three members. The child's standard error is also written to our standard error instead of our standard output. Sending it to stdout left err.txt empty for a run redirected as `>out.txt 2>err.txt`, so any CI wrapper that classifies a failure by "stderr is non-empty" saw a clean run. That split gives up the serialisation the two pumps used to get for free from sharing Console.Out, so they now take a lock and a line from one stream can no longer be cut in half by the other. Ordering between the two streams stays best effort: they are pumped by separate threads, and merging the streams to make it deterministic would undo the separation above. --- Fun.Build.Tests/StageContextExtensions.fs | 92 +++++++++++++ Fun.Build.Tests/Utils.fs | 31 +++++ Fun.Build/BuiltinCmds.fs | 25 +++- Fun.Build/CHANGELOG.md | 2 + Fun.Build/ProcessExtensions.fs | 161 ++++++++++++---------- 5 files changed, 238 insertions(+), 73 deletions(-) diff --git a/Fun.Build.Tests/StageContextExtensions.fs b/Fun.Build.Tests/StageContextExtensions.fs index 139b718..94d09d2 100644 --- a/Fun.Build.Tests/StageContextExtensions.fs +++ b/Fun.Build.Tests/StageContextExtensions.fs @@ -233,6 +233,98 @@ let ``RunSensitiveCommandCaptureAll should work`` () = pipeline "" { } +// The markers below wrap the secret without any whitespace so that the whole thing survives as a +// single token through both `sh -c` and `powershell -Command`, and so the assertion can prove the +// line was printed *and* that the secret inside it was replaced. +[] +let ``RunSensitiveCommand should mask the secret out of the child's own output`` () = + let secret = "SUPERSECRET95" + let mutable stdout = "" + let mutable stderr = "" + + shouldBeCalled (fun call -> + let out, err = + captureConsole (fun () -> pipeline "" { + stage "" { + whenAny { + platformOSX + platformLinux + } + run (fun ctx -> async { + // disablePrintCommand keeps the (already masked) command line out of the + // buffers, so what we assert on can only have come from the child. + do! + ctx.RunSensitiveCommand( + $"sh -c \"echo out95-{secret}-end95; echo err95-{secret}-end95 >&2\"", + disablePrintCommand = true + ) + |> Async.Ignore + call () + }) + } + stage "" { + whenWindows + run (fun ctx -> async { + do! + ctx.RunSensitiveCommand( + $"powershell -Command \"echo out95-{secret}-end95; [Console]::Error.WriteLine('err95-{secret}-end95')\"", + disablePrintCommand = true + ) + |> Async.Ignore + call () + }) + } + runImmediate + }) + stdout <- out + stderr <- err + ) + + Assert.DoesNotContain(secret, stdout) + Assert.DoesNotContain(secret, stderr) + Assert.Contains("out95-*-end95", stdout) + Assert.Contains("err95-*-end95", stderr) + + +[] +let ``The child's stderr should be written to stderr and not to stdout`` () = + let marker = "stderr95routing" + let mutable stdout = "" + let mutable stderr = "" + + shouldBeCalled (fun call -> + let out, err = + captureConsole (fun () -> pipeline "" { + stage "" { + whenAny { + platformOSX + platformLinux + } + run (fun ctx -> async { + let! result = ctx.RunCommandCaptureAll($"sh -c \"echo {marker} >&2\"", disablePrintCommand = true) + Assert.Equal(0, result.ExitCode) + call () + }) + } + stage "" { + whenWindows + run (fun ctx -> async { + let! result = + ctx.RunCommandCaptureAll($"powershell -Command \"[Console]::Error.WriteLine('{marker}')\"", disablePrintCommand = true) + Assert.Equal(0, result.ExitCode) + call () + }) + } + runImmediate + }) + stdout <- out + stderr <- err + ) + + Assert.Contains(marker, stderr) + Assert.DoesNotContain(marker, stdout) + + [] let ``Soft cancel should work`` () = let mutable i = 0 diff --git a/Fun.Build.Tests/Utils.fs b/Fun.Build.Tests/Utils.fs index af6b0fc..baff4c8 100644 --- a/Fun.Build.Tests/Utils.fs +++ b/Fun.Build.Tests/Utils.fs @@ -1,6 +1,9 @@ [] module Fun.Build.Tests.Utils +open System +open System.IO +open Spectre.Console open Xunit @@ -18,3 +21,31 @@ let shouldNotBeCalled fn = fn (fun _ -> isCalled <- true) Assert.False isCalled + + +/// Run fn with this process' stdout and stderr redirected, and hand back what each one received. +/// xUnit runs collections in parallel, so output from other tests can land in these buffers too. +/// Assert on a marker unique to your own command rather than on the buffer as a whole. +let captureConsole (fn: unit -> unit) = + // Deliberately not disposed. Spectre hands the writer to a cached console and goes on holding it, + // so disposing here would make every later test die on a closed TextWriter. + let out = new StringWriter() + let err = new StringWriter() + let syncOut = TextWriter.Synchronized out + let originalOut = Console.Out + let originalErr = Console.Error + let originalAnsiConsole = AnsiConsole.Console + + try + Console.SetOut syncOut + Console.SetError(TextWriter.Synchronized err) + // Redirecting Console.Out is not enough on its own: AnsiConsole was built around whatever + // Console.Out was the first time it was touched, and keeps writing there. + AnsiConsole.Console <- AnsiConsole.Create(AnsiConsoleSettings(Out = AnsiConsoleOutput(syncOut))) + fn () + finally + AnsiConsole.Console <- originalAnsiConsole + Console.SetOut originalOut + Console.SetError originalErr + + out.ToString(), err.ToString() diff --git a/Fun.Build/BuiltinCmds.fs b/Fun.Build/BuiltinCmds.fs index 1a1a8a3..a3b9906 100644 --- a/Fun.Build/BuiltinCmds.fs +++ b/Fun.Build/BuiltinCmds.fs @@ -12,6 +12,13 @@ open Fun.Build.StageContextExtensionsInternal module BuiltinCmdsInternal = + /// The interpolated argument values, so they can be replaced in the child's own output too. + /// Masking the logged command alone leaves the log looking safe while the child echoes the + /// secret straight back on stdout or stderr. + let maskedCommandValues (commandStr: FormattableString) = + commandStr.GetArguments() |> Array.map string |> Array.filter (String.IsNullOrEmpty >> not) + + type StageContext with /// Build a ProcessStartInfo object for a command string. If your command is a file path with white space, you should quote it with ' or ". @@ -49,7 +56,8 @@ module BuiltinCmdsInternal = ?workingDir: string, ?disablePrintOutput: bool, ?disablePrintCommand: bool, - ?cancellationToken: CancellationToken + ?cancellationToken: CancellationToken, + ?maskedValues: string seq ) : Async = async { let disablePrintOutput = defaultArg disablePrintOutput false let disablePrintCommand = defaultArg disablePrintCommand false @@ -75,7 +83,8 @@ module BuiltinCmdsInternal = prefix, printOutput = (not disablePrintOutput && not (ctx.GetNoStdRedirectForStep())), captureOutput = true, - cancellationToken = ct + cancellationToken = ct, + ?maskedValues = maskedValues ) } @@ -197,6 +206,8 @@ module BuiltinCmds = /// Run a command string with current context, and encrypt the string for logging + /// The interpolated arguments are also replaced with * in the child's own standard output and standard error. + /// This is a plain substring replacement, so a short value is replaced wherever it appears in that output. member ctx.RunSensitiveCommandCaptureOutput ( commandStr: FormattableString, @@ -233,7 +244,8 @@ module BuiltinCmds = prefix, printOutput = (not disablePrintOutput && not (ctx.GetNoStdRedirectForStep())), captureOutput = true, - cancellationToken = ct + cancellationToken = ct, + maskedValues = maskedCommandValues commandStr ) if ct.IsCancellationRequested then @@ -272,6 +284,8 @@ module BuiltinCmds = /// Same as RunCommandCaptureAll, but encrypt the string for logging + /// The interpolated arguments are also replaced with * in the child's own standard output and standard error. + /// This is a plain substring replacement, so a short value is replaced wherever it appears in that output. member ctx.RunSensitiveCommandCaptureAll ( commandStr: FormattableString, @@ -290,11 +304,14 @@ module BuiltinCmds = ?workingDir = workingDir, ?disablePrintOutput = disablePrintOutput, ?disablePrintCommand = disablePrintCommand, - ?cancellationToken = cancellationToken + ?cancellationToken = cancellationToken, + maskedValues = maskedCommandValues commandStr ) /// Run a command string with current context, and encrypt the string for logging + /// The interpolated arguments are also replaced with * in the child's own standard output and standard error. + /// This is a plain substring replacement, so a short value is replaced wherever it appears in that output. member ctx.RunSensitiveCommand ( commandStr: FormattableString, diff --git a/Fun.Build/CHANGELOG.md b/Fun.Build/CHANGELOG.md index d3df5c5..abd36d0 100644 --- a/Fun.Build/CHANGELOG.md +++ b/Fun.Build/CHANGELOG.md @@ -5,6 +5,8 @@ - Add `RunCommandCaptureAll` and `RunSensitiveCommandCaptureAll` which return a `CommandOutput` record with the exit code, standard output and standard error, whatever the exit code was #93 - Fix: standard error was never read when output is redirected (prefixed, silenced or captured), so it was lost. It is now printed like standard output. - Fix: captured standard output no longer ends with an extra empty line +- Fix: `RunSensitiveCommand`, `RunSensitiveCommandCaptureOutput` and `RunSensitiveCommandCaptureAll` now mask the interpolated values in the child's own standard output and standard error, not only in the logged command #95 +- Fix: the child's standard error is written to standard error instead of standard output #95 ## [1.1.18] - 2026-08-31 diff --git a/Fun.Build/ProcessExtensions.fs b/Fun.Build/ProcessExtensions.fs index 339002e..7589564 100644 --- a/Fun.Build/ProcessExtensions.fs +++ b/Fun.Build/ProcessExtensions.fs @@ -101,75 +101,98 @@ type Process with static member StartAsync - (startInfo: ProcessStartInfo, commandLogString: string, logPrefix: string, ?printOutput, ?captureOutput, ?cancellationToken: CancellationToken) = - async { - let printOutput = defaultArg printOutput true - let captureOutput = defaultArg captureOutput false - let noPrefix = String.IsNullOrEmpty logPrefix - // We want to redirect output if - // 1. Use want to add prefix to the output - // 2. User asked to not print output - // 3. User asked to capture output - let shouldRedirectOutput = not noPrefix || not printOutput || captureOutput - - // By default, we don't redirect output because redirecting the - // output lose the color information. - if shouldRedirectOutput then - // We redirect both standard output and error output - // because some process write mixed output to both... - // This should in theory avoid losing information because of the redirection. - startInfo.RedirectStandardOutput <- true - startInfo.RedirectStandardError <- true - startInfo.StandardOutputEncoding <- Encoding.UTF8 - startInfo.StandardErrorEncoding <- Encoding.UTF8 - - use result = Process.Start startInfo - let standardOutputSb = StringBuilder() - let standardErrorSb = StringBuilder() - - let handleDataReceived (sb: StringBuilder) (ev: DataReceivedEventArgs) = - // Data is null once the stream is closed - if ev.Data <> null then - if captureOutput then sb.AppendLine ev.Data |> ignore - if printOutput && not (String.IsNullOrEmpty ev.Data) then - if noPrefix then - Console.WriteLine(ev.Data) - else - Console.WriteLine(logPrefix + " " + ev.Data) - - if shouldRedirectOutput then - result.OutputDataReceived.Add(handleDataReceived standardOutputSb) - result.ErrorDataReceived.Add(handleDataReceived standardErrorSb) - - use! cd = - Async.OnCancel(fun _ -> - AnsiConsole.Markup $"[yellow]{logPrefix}[/] " - AnsiConsole.WriteLine $"{commandLogString}: is cancelled or timed out and the process will be killed." + ( + startInfo: ProcessStartInfo, + commandLogString: string, + logPrefix: string, + ?printOutput, + ?captureOutput, + ?cancellationToken: CancellationToken, + ?maskedValues: string seq + ) = async { + let printOutput = defaultArg printOutput true + let captureOutput = defaultArg captureOutput false + // Values the caller considers secret. They are masked on the way to the console only: + // captured output is handed back verbatim, because the caller asked for it and already knows them. + let maskedValues = + match maskedValues with + | None -> Array.empty + | Some values -> values |> Seq.filter (String.IsNullOrEmpty >> not) |> Seq.toArray + let noPrefix = String.IsNullOrEmpty logPrefix + // We want to redirect output if + // 1. Use want to add prefix to the output + // 2. User asked to not print output + // 3. User asked to capture output + let shouldRedirectOutput = not noPrefix || not printOutput || captureOutput + + // By default, we don't redirect output because redirecting the + // output lose the color information. + if shouldRedirectOutput then + // We redirect both standard output and error output + // because some process write mixed output to both... + // This should in theory avoid losing information because of the redirection. + startInfo.RedirectStandardOutput <- true + startInfo.RedirectStandardError <- true + startInfo.StandardOutputEncoding <- Encoding.UTF8 + startInfo.StandardErrorEncoding <- Encoding.UTF8 + + use result = Process.Start startInfo + let standardOutputSb = StringBuilder() + let standardErrorSb = StringBuilder() + + let mask (line: string) = Array.fold (fun (line: string) (value: string) -> line.Replace(value, "*")) line maskedValues + + // Both pumps used to call Console.WriteLine, a single writer, so writes were serialized for + // free. Routing stderr to Console.Error gives that up: the two are separate SyncTextWriter + // instances, each locking on itself, so one line can be cut in half by the other. This lock + // puts it back. It does not make the order between the two streams deterministic; they are + // pumped by separate threads and the OS decides who wins. + let consoleLock = obj () + + let handleDataReceived (sb: StringBuilder) (out: TextWriter) (ev: DataReceivedEventArgs) = + // Data is null once the stream is closed + if ev.Data <> null then + if captureOutput then sb.AppendLine ev.Data |> ignore + if printOutput && not (String.IsNullOrEmpty ev.Data) then + let line = mask ev.Data + lock consoleLock (fun () -> if noPrefix then out.WriteLine(line) else out.WriteLine(logPrefix + " " + line)) + + if shouldRedirectOutput then + result.OutputDataReceived.Add(handleDataReceived standardOutputSb Console.Out) + // The child's stderr goes to our stderr. Sending it to stdout, as this did before, + // empties err.txt for `... >out.txt 2>err.txt` and hides failures from any CI wrapper + // that classifies a run by whether stderr is non-empty. + result.ErrorDataReceived.Add(handleDataReceived standardErrorSb Console.Error) + + use! cd = + Async.OnCancel(fun _ -> + AnsiConsole.Markup $"[yellow]{logPrefix}[/] " + AnsiConsole.WriteLine $"{commandLogString}: is cancelled or timed out and the process will be killed." + Native.kill result + ) + + use _ = + match cancellationToken with + | Some ct -> + ct.Register(fun () -> + AnsiConsole.MarkupLine("[yellow]Command is cancelled by your token[/]") Native.kill result ) - - use _ = - match cancellationToken with - | Some ct -> - ct.Register(fun () -> - AnsiConsole.MarkupLine("[yellow]Command is cancelled by your token[/]") - Native.kill result - ) - :> IDisposable - | _ -> { - new IDisposable with - member _.Dispose() = () - } - - if shouldRedirectOutput then - result.BeginOutputReadLine() - result.BeginErrorReadLine() - - result.WaitForExit() - - return { - ExitCode = result.ExitCode - StandardOutput = standardOutputSb.ToString() - StandardError = standardErrorSb.ToString() - } + :> IDisposable + | _ -> { + new IDisposable with + member _.Dispose() = () + } + + if shouldRedirectOutput then + result.BeginOutputReadLine() + result.BeginErrorReadLine() + + result.WaitForExit() + + return { + ExitCode = result.ExitCode + StandardOutput = standardOutputSb.ToString() + StandardError = standardErrorSb.ToString() } + } From c6524d17c48d55939d9228b6c7a62247b521ece8 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Fri, 11 Sep 2026 15:14:01 +0200 Subject: [PATCH 5/5] Share the command preamble and add CommandOutput.IsCancelled #96 #97 Follow ups on #94, all of them corrections to that PR rather than to anything older. RunCommand, RunCommandCaptureOutput and RunSensitiveCommandCaptureOutput each rebuilt the same step prefix, printed the command and called Process.StartAsync with argument for argument identical parameters. The stderr fix in #94 only landed everywhere because StartAsync is shared; the preamble was not, so the next change to prefix rendering or print suppression would have reached some copies and not others. All three now sit on RunCommandCaptureAllInternal, which gained a captureOutput parameter for the one thing they genuinely disagree on: RunCommand deliberately does not capture, because StartAsync only redirects when it has to and redirecting loses the child's colours. The masking idiom and the exit code to Result mapping are likewise one helper each instead of two copies. CommandOutput gains IsCancelled. A command killed by a cancellation token came back as exit code 143 on Linux or -1 on Windows with a truncated output prefix, which is indistinguishable from a genuine failure. IsAcceptableExitCode and MapExitCodeToResult move from StageContextExtensionsInternal to the auto opened StageContextExtensions. A caller holding a CommandOutput had no supported way to honour acceptExitCodes and would hand roll `if ExitCode <> 0`. This is also the answer to an async { } block that captures output and always reports success: return ctx.MapExitCodeToResult output.ExitCode. No run overload for Async is added, since `run "cmd"` already covers the case where the output is not wanted, and going through CaptureAll to get there would force a redirect and lose colour for no benefit. Two of the three tests added in #94 put their assertions inside platform guarded stages without wrapping them in shouldBeCalled. A pipeline whose stages are all skipped completes successfully, so a condition regression would have left them green while the code under test never ran. RunSensitiveCommandCaptureAll also only asserted exit code, stdout and stderr, which all go through the same path as the non sensitive member, so nothing covered the masking that is the one thing it does differently. That is why the leak fixed in #95 went unnoticed. README and demo.fsx document the capture then decide shape. demo.fsx is the runnable twin of that README block and the only copy type checked against the built dll, and it had been missing the example entirely. --- Fun.Build.Tests/StageContextExtensions.fs | 180 ++++++++++++++++------ Fun.Build/BuiltinCmds.fs | 138 +++++++---------- Fun.Build/CHANGELOG.md | 7 +- Fun.Build/ProcessExtensions.fs | 4 + Fun.Build/StageContextExtensions.fs | 34 ++-- Fun.Build/Types.fs | 4 + README.md | 2 + demo.fsx | 9 ++ 8 files changed, 227 insertions(+), 151 deletions(-) diff --git a/Fun.Build.Tests/StageContextExtensions.fs b/Fun.Build.Tests/StageContextExtensions.fs index 94d09d2..7f4f23a 100644 --- a/Fun.Build.Tests/StageContextExtensions.fs +++ b/Fun.Build.Tests/StageContextExtensions.fs @@ -1,5 +1,6 @@ module Fun.Build.Tests.StageContextExtensions +open System.Threading open Xunit open Fun.Build open Fun.Build.StageContextExtensionsInternal @@ -156,30 +157,33 @@ let ``RunCommandCaptureOutput should return an error if command failed`` () = [] -let ``RunCommandCaptureAll should return exit code, stdout and stderr`` () = pipeline "" { - stage "" { - whenAny { - platformOSX - platformLinux +let ``RunCommandCaptureAll should return exit code, stdout and stderr`` () = + shouldBeCalled (fun call -> pipeline "" { + stage "" { + whenAny { + platformOSX + platformLinux + } + run (fun ctx -> async { + let! result = ctx.RunCommandCaptureAll "sh -c \"echo out; echo err >&2; exit 3\"" + Assert.Equal(3, result.ExitCode) + Assert.Equal("out\n", result.StandardOutput) + Assert.Equal("err\n", result.StandardError) + call () + }) } - run (fun ctx -> async { - let! result = ctx.RunCommandCaptureAll "sh -c \"echo out; echo err >&2; exit 3\"" - Assert.Equal(3, result.ExitCode) - Assert.Equal("out\n", result.StandardOutput) - Assert.Equal("err\n", result.StandardError) - }) - } - stage "" { - whenWindows - run (fun ctx -> async { - let! result = ctx.RunCommandCaptureAll "powershell -Command \"echo out; [Console]::Error.WriteLine('err'); exit 3\"" - Assert.Equal(3, result.ExitCode) - Assert.Equal("out\r\n", result.StandardOutput) - Assert.Equal("err\r\n", result.StandardError) - }) - } - runImmediate -} + stage "" { + whenWindows + run (fun ctx -> async { + let! result = ctx.RunCommandCaptureAll "powershell -Command \"echo out; [Console]::Error.WriteLine('err'); exit 3\"" + Assert.Equal(3, result.ExitCode) + Assert.Equal("out\r\n", result.StandardOutput) + Assert.Equal("err\r\n", result.StandardError) + call () + }) + } + runImmediate + }) [] let ``RunCommandCaptureAll should not fail the stage on a non zero exit code`` () = @@ -207,30 +211,45 @@ let ``RunCommandCaptureAll should not fail the stage on a non zero exit code`` ( }) [] -let ``RunSensitiveCommandCaptureAll should work`` () = pipeline "" { - stage "" { - whenAny { - platformOSX - platformLinux - } - run (fun ctx -> async { - let! result = ctx.RunSensitiveCommandCaptureAll $"""echo {"42"}""" - Assert.Equal(0, result.ExitCode) - Assert.Equal("42\n", result.StandardOutput) - Assert.Equal("", result.StandardError) - }) - } - stage "" { - whenWindows - run (fun ctx -> async { - let! result = ctx.RunSensitiveCommandCaptureAll $"""powershell echo {"42"}""" - Assert.Equal(0, result.ExitCode) - Assert.Equal("42\r\n", result.StandardOutput) - Assert.Equal("", result.StandardError) - }) - } - runImmediate -} +let ``RunSensitiveCommandCaptureAll should work`` () = + let secret = "SENSITIVE97VALUE" + let mutable stdout = "" + + shouldBeCalled (fun call -> + let out, _ = + captureConsole (fun () -> pipeline "" { + stage "" { + whenAny { + platformOSX + platformLinux + } + run (fun ctx -> async { + let! result = ctx.RunSensitiveCommandCaptureAll $"""echo {secret}""" + Assert.Equal(0, result.ExitCode) + // Captured output is the caller's own business, so it comes back unmasked. + Assert.Equal(secret + "\n", result.StandardOutput) + Assert.Equal("", result.StandardError) + call () + }) + } + stage "" { + whenWindows + run (fun ctx -> async { + let! result = ctx.RunSensitiveCommandCaptureAll $"""powershell echo {secret}""" + Assert.Equal(0, result.ExitCode) + Assert.Equal(secret + "\r\n", result.StandardOutput) + Assert.Equal("", result.StandardError) + call () + }) + } + runImmediate + }) + stdout <- out + ) + + // Masking is the only thing this member does differently from RunCommandCaptureAll, and it was + // the one thing the original test never checked. + Assert.DoesNotContain(secret, stdout) // The markers below wrap the secret without any whitespace so that the whole thing survives as a @@ -325,6 +344,73 @@ let ``The child's stderr should be written to stderr and not to stdout`` () = Assert.DoesNotContain(marker, stdout) +[] +let ``A cancelled command should be distinguishable from a failed one`` () = + shouldBeCalled (fun call -> pipeline "" { + stage "" { + whenAny { + platformOSX + platformLinux + } + run (fun ctx -> async { + use cts = new CancellationTokenSource(500) + let! result = ctx.RunCommandCaptureAll("sh -c \"sleep 30\"", cancellationToken = cts.Token) + // Without IsCancelled this is just exit code 143, the same shape as a real failure. + Assert.True(result.IsCancelled) + call () + }) + } + stage "" { + whenWindows + run (fun ctx -> async { + use cts = new CancellationTokenSource(500) + let! result = ctx.RunCommandCaptureAll("powershell -Command \"Start-Sleep -Seconds 30\"", cancellationToken = cts.Token) + Assert.True(result.IsCancelled) + call () + }) + } + runImmediate + }) + + +[] +let ``Every line of the child's output should still get the step prefix`` () = + // RunCommand no longer builds its own preamble, so pin the prefixing it used to own. line96b is + // written without a trailing newline, which exercises the last line of the stream. + let mutable stdout = "" + + shouldBeCalled (fun call -> + let out, _ = + captureConsole (fun () -> pipeline "" { + noPrefixForStep false + stage "prefixed" { + whenAny { + platformOSX + platformLinux + } + run (fun ctx -> async { + do! ctx.RunCommand("sh -c \"echo line96a; printf line96b\"", disablePrintCommand = true) |> Async.Ignore + call () + }) + } + stage "prefixed" { + whenWindows + run (fun ctx -> async { + do! + ctx.RunCommand("powershell -Command \"echo line96a; [Console]::Out.Write('line96b')\"", disablePrintCommand = true) + |> Async.Ignore + call () + }) + } + runImmediate + }) + stdout <- out + ) + + Assert.Contains("prefixed line96a", stdout) + Assert.Contains("prefixed line96b", stdout) + + [] let ``Soft cancel should work`` () = let mutable i = 0 diff --git a/Fun.Build/BuiltinCmds.fs b/Fun.Build/BuiltinCmds.fs index a3b9906..15502f9 100644 --- a/Fun.Build/BuiltinCmds.fs +++ b/Fun.Build/BuiltinCmds.fs @@ -18,6 +18,20 @@ module BuiltinCmdsInternal = let maskedCommandValues (commandStr: FormattableString) = commandStr.GetArguments() |> Array.map string |> Array.filter (String.IsNullOrEmpty >> not) + /// The command as it should be logged: every interpolated argument replaced by a *. + let maskedCommandString (commandStr: FormattableString) = + let args: obj[] = Array.create commandStr.ArgumentCount "*" + String.Format(commandStr.Format, args) + + /// Turn a finished command into the result the CaptureOutput members hand back. A cancelled + /// command counts as successful: StageBuilder documents the token as a way to stop a command and + /// still mark it as success. + let toCapturedOutputResult (ctx: StageContext) (ct: CancellationToken) (result: CommandOutput) = + if ct.IsCancellationRequested || ctx.IsAcceptableExitCode result.ExitCode then + Ok result.StandardOutput + else + Error "Exit code is not indicating as successful." + type StageContext with @@ -57,10 +71,12 @@ module BuiltinCmdsInternal = ?disablePrintOutput: bool, ?disablePrintCommand: bool, ?cancellationToken: CancellationToken, - ?maskedValues: string seq + ?maskedValues: string seq, + ?captureOutput: bool ) : Async = async { let disablePrintOutput = defaultArg disablePrintOutput false let disablePrintCommand = defaultArg disablePrintCommand false + let captureOutput = defaultArg captureOutput true let command = ctx.BuildCommand(commandStr, ?workingDir = workingDir) let noPrefixForStep = ctx.GetNoPrefixForStep() let prefix = @@ -82,7 +98,7 @@ module BuiltinCmdsInternal = commandLogString, prefix, printOutput = (not disablePrintOutput && not (ctx.GetNoStdRedirectForStep())), - captureOutput = true, + captureOutput = captureOutput, cancellationToken = ct, ?maskedValues = maskedValues ) @@ -111,7 +127,12 @@ module BuiltinCmds = type StageContext with - /// Run a command string with current context + /// + /// Run a command string with current context. + /// + /// Command to run + /// Current step rank + /// Working directory for command member ctx.RunCommand ( commandStr: string, @@ -120,31 +141,22 @@ module BuiltinCmds = ?disablePrintOutput: bool, ?disablePrintCommand: bool, ?cancellationToken: CancellationToken - ) = async { - let disablePrintOutput = defaultArg disablePrintOutput false - let disablePrintCommand = defaultArg disablePrintCommand false - let command = ctx.BuildCommand(commandStr, ?workingDir = workingDir) - let noPrefixForStep = ctx.GetNoPrefixForStep() - let prefix = - if noPrefixForStep then - "" - else - match step with - | Some i -> ctx.BuildStepPrefix i - | None -> ctx.GetNamePath() - - if not noPrefixForStep then AnsiConsole.Markup $"[green]{prefix}[/] " - if not disablePrintCommand then AnsiConsole.WriteLine commandStr - + ) : Async> = async { let ct = defaultArg cancellationToken CancellationToken.None + // Nothing is captured here, and that is deliberate: StartAsync only redirects when it has + // to, and redirecting loses the child's colours. Capturing would quietly turn every plain + // `run "..."` monochrome. let! result = - Process.StartAsync( - command, + ctx.RunCommandCaptureAllInternal( commandStr, - prefix, - printOutput = (not disablePrintOutput && not (ctx.GetNoStdRedirectForStep())), - cancellationToken = ct + commandStr, + ?step = step, + ?workingDir = workingDir, + ?disablePrintOutput = disablePrintOutput, + ?disablePrintCommand = disablePrintCommand, + cancellationToken = ct, + captureOutput = false ) return @@ -168,40 +180,21 @@ module BuiltinCmds = ?disablePrintOutput: bool, ?disablePrintCommand: bool, ?cancellationToken: CancellationToken - ) = async { - let disablePrintOutput = defaultArg disablePrintOutput false - let disablePrintCommand = defaultArg disablePrintCommand false - let command = ctx.BuildCommand(commandStr, ?workingDir = workingDir) - let noPrefixForStep = ctx.GetNoPrefixForStep() - let prefix = - if noPrefixForStep then - "" - else - match step with - | Some i -> ctx.BuildStepPrefix i - | None -> ctx.GetNamePath() - - if not noPrefixForStep then AnsiConsole.Markup $"[green]{prefix}[/] " - if not disablePrintCommand then AnsiConsole.WriteLine commandStr - + ) : Async> = async { let ct = defaultArg cancellationToken CancellationToken.None let! result = - Process.StartAsync( - command, + ctx.RunCommandCaptureAllInternal( commandStr, - prefix, - printOutput = (not disablePrintOutput && not (ctx.GetNoStdRedirectForStep())), - captureOutput = true, + commandStr, + ?step = step, + ?workingDir = workingDir, + ?disablePrintOutput = disablePrintOutput, + ?disablePrintCommand = disablePrintCommand, cancellationToken = ct ) - if ct.IsCancellationRequested then - return Ok result.StandardOutput - else if ctx.IsAcceptableExitCode result.ExitCode then - return Ok result.StandardOutput - else - return Error "Exit code is not indicating as successful." + return toCapturedOutputResult ctx ct result } @@ -217,45 +210,24 @@ module BuiltinCmds = ?disablePrintCommand: bool, ?cancellationToken: CancellationToken ) : Async> = async { - let disablePrintOutput = defaultArg disablePrintOutput false - let disablePrintCommand = defaultArg disablePrintCommand false - let command = ctx.BuildCommand(commandStr.ToString(), ?workingDir = workingDir) - let noPrefixForStep = ctx.GetNoPrefixForStep() - let args: obj[] = Array.create commandStr.ArgumentCount "*" - let encryptiedStr = String.Format(commandStr.Format, args) - - let prefix = - if noPrefixForStep then - "" - else - match step with - | Some i -> ctx.BuildStepPrefix i - | None -> ctx.GetNamePath() - - if not noPrefixForStep then AnsiConsole.Markup $"[green]{prefix}[/] " - if not disablePrintCommand then AnsiConsole.WriteLine encryptiedStr - let ct = defaultArg cancellationToken CancellationToken.None let! result = - Process.StartAsync( - command, - encryptiedStr, - prefix, - printOutput = (not disablePrintOutput && not (ctx.GetNoStdRedirectForStep())), - captureOutput = true, + ctx.RunCommandCaptureAllInternal( + commandStr.ToString(), + maskedCommandString commandStr, + ?step = step, + ?workingDir = workingDir, + ?disablePrintOutput = disablePrintOutput, + ?disablePrintCommand = disablePrintCommand, cancellationToken = ct, maskedValues = maskedCommandValues commandStr ) - if ct.IsCancellationRequested then - return Ok result.StandardOutput - else if ctx.IsAcceptableExitCode result.ExitCode then - return Ok result.StandardOutput - else - return Error "Exit code is not indicating as successful." + return toCapturedOutputResult ctx ct result } + /// /// Run a command string with current context, and return the exit code, standard output and standard error /// whatever the exit code was. Unlike RunCommandCaptureOutput, this does not decide whether the run succeeded. @@ -295,11 +267,9 @@ module BuiltinCmds = ?disablePrintCommand: bool, ?cancellationToken: CancellationToken ) : Async = - let args: obj[] = Array.create commandStr.ArgumentCount "*" - let encryptiedStr = String.Format(commandStr.Format, args) ctx.RunCommandCaptureAllInternal( commandStr.ToString(), - encryptiedStr, + maskedCommandString commandStr, ?step = step, ?workingDir = workingDir, ?disablePrintOutput = disablePrintOutput, diff --git a/Fun.Build/CHANGELOG.md b/Fun.Build/CHANGELOG.md index abd36d0..69a2e5f 100644 --- a/Fun.Build/CHANGELOG.md +++ b/Fun.Build/CHANGELOG.md @@ -2,11 +2,12 @@ ## [Unreleased] -- Add `RunCommandCaptureAll` and `RunSensitiveCommandCaptureAll` which return a `CommandOutput` record with the exit code, standard output and standard error, whatever the exit code was #93 -- Fix: standard error was never read when output is redirected (prefixed, silenced or captured), so it was lost. It is now printed like standard output. +- Add `RunCommandCaptureAll` and `RunSensitiveCommandCaptureAll`, which return a `CommandOutput` record with the exit code, standard output, standard error and whether your cancellation token stopped the run, whatever the exit code was #93 #96 +- Breaking: `Process.StartAsync` now returns the `CommandOutput` record instead of the anonymous struct it returned in 1.1.18. Source and binary breaking if you call it directly #93 +- Fix: standard error was never read when output is redirected (prefixed, silenced or captured), so it was lost. It is now read, and written to standard error #95 - Fix: captured standard output no longer ends with an extra empty line - Fix: `RunSensitiveCommand`, `RunSensitiveCommandCaptureOutput` and `RunSensitiveCommandCaptureAll` now mask the interpolated values in the child's own standard output and standard error, not only in the logged command #95 -- Fix: the child's standard error is written to standard error instead of standard output #95 +- `IsAcceptableExitCode` and `MapExitCodeToResult` moved to the auto opened `StageContextExtensions`, so a caller holding a `CommandOutput` can honour `acceptExitCodes` #96 ## [1.1.18] - 2026-08-31 diff --git a/Fun.Build/ProcessExtensions.fs b/Fun.Build/ProcessExtensions.fs index 7589564..f9764e2 100644 --- a/Fun.Build/ProcessExtensions.fs +++ b/Fun.Build/ProcessExtensions.fs @@ -194,5 +194,9 @@ type Process with ExitCode = result.ExitCode StandardOutput = standardOutputSb.ToString() StandardError = standardErrorSb.ToString() + IsCancelled = + match cancellationToken with + | None -> false + | Some ct -> ct.IsCancellationRequested } } diff --git a/Fun.Build/StageContextExtensions.fs b/Fun.Build/StageContextExtensions.fs index 7d7d50c..559d9bf 100644 --- a/Fun.Build/StageContextExtensions.fs +++ b/Fun.Build/StageContextExtensions.fs @@ -138,23 +138,6 @@ module StageContextExtensionsInternal = member ctx.BuildIndent(?margin) = String(' ', ctx.GetNamePath().Length - ctx.Name.Length + defaultArg margin 4) - /// Verify if the exit code is allowed. - member stage.IsAcceptableExitCode(exitCode: int) : bool = - let parentAcceptableExitCodes = - match stage.ParentContext with - | ValueNone -> Set.empty - | ValueSome(StageParent.Pipeline pipeline) -> pipeline.AcceptableExitCodes - | ValueSome(StageParent.Stage parentStage) -> parentStage.AcceptableExitCodes - - Set.contains exitCode stage.AcceptableExitCodes || Set.contains exitCode parentAcceptableExitCodes - - member stage.MapExitCodeToResult(exitCode: int) = - if stage.IsAcceptableExitCode exitCode then - Ok() - else - Error "Exit code is not indicating as successful." - - /// Run the stage. If index is not provided then it will be treated as sub-stage. member stage.Run(index: StageIndex, cancellationToken: CancellationToken) = let mutable isSuccess = true @@ -424,6 +407,23 @@ module StageContextExtensions = type StageContext with + /// Verify if the exit code is allowed. + member stage.IsAcceptableExitCode(exitCode: int) : bool = + let parentAcceptableExitCodes = + match stage.ParentContext with + | ValueNone -> Set.empty + | ValueSome(StageParent.Pipeline pipeline) -> pipeline.AcceptableExitCodes + | ValueSome(StageParent.Stage parentStage) -> parentStage.AcceptableExitCodes + + Set.contains exitCode stage.AcceptableExitCodes || Set.contains exitCode parentAcceptableExitCodes + + member stage.MapExitCodeToResult(exitCode: int) = + if stage.IsAcceptableExitCode exitCode then + Ok() + else + Error "Exit code is not indicating as successful." + + /// Stage under pipeline should be level 0, the level will get increased for nested stages member ctx.GetStageLevel() = match ctx.ParentContext with diff --git a/Fun.Build/Types.fs b/Fun.Build/Types.fs index d5cb1d1..f61e051 100644 --- a/Fun.Build/Types.fs +++ b/Fun.Build/Types.fs @@ -19,6 +19,10 @@ type CommandOutput = { ExitCode: int StandardOutput: string StandardError: string + /// True when the command was killed because your cancellation token fired. Without this a + /// cancelled command is indistinguishable from a genuine failure: it comes back as 143 on Linux + /// or -1 on Windows, with whatever output it had managed to produce. + IsCancelled: bool } diff --git a/README.md b/README.md index 0e1d847..de1d937 100644 --- a/README.md +++ b/README.md @@ -116,9 +116,11 @@ let demo1 = do! ctx.RunCommand "dotnet --version" }) // You can also capture the exit code, standard output and standard error and decide yourself what to do with them + // Note an async { } block returning unit always reports success, so return a Result if the exit code should decide run (fun ctx -> async { let! output = ctx.RunCommandCaptureAll "dotnet --version" printfn "exit code %d, stdout %s, stderr %s" output.ExitCode output.StandardOutput output.StandardError + return ctx.MapExitCodeToResult output.ExitCode }) // You can run async functions run (Async.Sleep 1000) diff --git a/demo.fsx b/demo.fsx index 1cd1a25..147507b 100644 --- a/demo.fsx +++ b/demo.fsx @@ -27,6 +27,15 @@ let demo1 = do! ctx.RunCommand "dotnet --version" } ) + // You can also capture the exit code, standard output and standard error and decide yourself what to do with them + // Note an async { } block returning unit always reports success, so return a Result if the exit code should decide + run (fun ctx -> + async { + let! output = ctx.RunCommandCaptureAll "dotnet --version" + printfn "exit code %d, stdout %s, stderr %s" output.ExitCode output.StandardOutput output.StandardError + return ctx.MapExitCodeToResult output.ExitCode + } + ) // You can run async functions run (Async.Sleep 1000) run (fun _ -> Async.Sleep 1000)