namespace WoofWorkflows open System.IO type Stdout = Stdout of string Comp type PipelineModifier<'Agent> = private { EnvVars : (string * string Comp) list // TODO: does this stack the right way? Does last win or first win? WorkingDir : DirectoryInfo Comp option Agent : 'Agent } override this.ToString () = let envVars = if this.EnvVars.IsEmpty then "" else this.EnvVars |> Seq.map (fun (k, v) -> k + ":" + Comp.force v) |> String.concat ", " |> fun s -> "; Env vars: " + s let workingDir = match this.WorkingDir with | None -> "" | Some dir -> "; Working dir: " + (Comp.force dir).FullName let agent = this.Agent.ToString () agent + envVars + workingDir type NewAgent = private | NewAgent of image : string Comp override this.ToString () = match this with | NewAgent comp -> "Image: " + Comp.force comp type SameAgent = private | SameAgent override this.ToString () = "" type Pipeline<'Agent> = | Empty | ShellScript of script : string * andThen : Pipeline<'Agent> | ShellScriptBind of script : string * consumeStdout : (Stdout -> Pipeline<'Agent>) | WithModifier of Pipeline<'Agent> * PipelineModifier<'Agent> | Sequence of first : Pipeline * second : Pipeline<'Agent> | ChangeAgent of first : Pipeline * second : Pipeline [] module Pipeline = let rec private toStringInner<'A> (bindCount : int) (indentCount : int) (this : Pipeline<'A>) : string = let indent = String.replicate indentCount " " match this with | Pipeline.Empty -> $"%s{indent}" | Pipeline.ShellScript(script, andThen) -> $"%s{indent}Run: %s{script}\n%s{indent}Then\n%s{toStringInner bindCount (indentCount + 2) andThen}" | Pipeline.ShellScriptBind(script, consumeStdout) -> let outputDummy = $"" let dummyPipeline = consumeStdout (Stdout (Comp.make outputDummy)) $"%s{indent}Run: %s{script}\n%s{indent}With its output labelled %s{outputDummy}:\n%s{toStringInner (bindCount + 1) (indentCount + 2) dummyPipeline}" | Pipeline.WithModifier(pipeline, pipelineModifier) -> $"%s{indent}Modified pipeline (%O{pipelineModifier}):\n%s{toStringInner bindCount (indentCount + 2) pipeline}" | Pipeline.Sequence(first, second) -> $"%s{toStringInner bindCount indentCount first}\n%s{toStringInner bindCount indentCount second}" let toString (p : Pipeline<'A>) = toStringInner 0 0 p [] module private PipelineModifier = let empty : PipelineModifier = { EnvVars = [] WorkingDir = None Agent = SameAgent } [] type PipelineBuilder<'plat, 'agentFinal> () = [] member _.WithEnv (mods : PipelineModifier<'Agent>, (key : string, value : string Comp)) : PipelineModifier<'Agent> = { mods with EnvVars = (key, value) :: mods.EnvVars } [] member _.WithEnv (() : unit, (key : string, value : string Comp)) : PipelineModifier = { PipelineModifier.empty with EnvVars = [key, value] } [] member _.WorkingDir (mods : PipelineModifier<'Agent>, dir : DirectoryInfo Comp) : PipelineModifier<'Agent> = { mods with WorkingDir = Some dir } [] member _.WorkingDir (() : unit, dir : DirectoryInfo Comp) : PipelineModifier = { PipelineModifier.empty with WorkingDir = Some dir } [] member _.Remote (() : unit, image : string Comp) : PipelineModifier = { EnvVars = [] WorkingDir = None Agent = NewAgent image } [] member _.Remote (mods : PipelineModifier, image : string Comp) : PipelineModifier = { EnvVars = mods.EnvVars WorkingDir = mods.WorkingDir Agent = NewAgent image } member _.Return<'Agent> (() : unit) = Pipeline<'Agent>.Empty /// For running a script, capturing stdout member _.Bind (toRun : string, cont : Stdout -> Pipeline<'Agent>) : Pipeline<'Agent> = Pipeline.ShellScriptBind (toRun, cont) /// For running a script, without capturing stdout member _.Bind (toRun : string, cont : unit -> Pipeline<'Agent>) : Pipeline<'Agent> = Pipeline.ShellScript (toRun, cont ()) /// We can bind any pipeline which runs on the "same agent". member _.Bind (p : Pipeline, cont : unit -> Pipeline<'Agent>) : Pipeline<'Agent> = Pipeline.Sequence (p, cont ()) /// We can also bind in any pipeline to run on a different agent. member _.Bind (p : Pipeline, cont : unit -> Pipeline) : Pipeline<'Agent> = Pipeline.ChangeAgent (p, cont ()) member _.Yield (() : unit) : unit = () member _.For (expr : PipelineModifier<'Agent>, cont : unit -> Pipeline<'Agent>) : Pipeline<'Agent> = Pipeline.WithModifier (cont (), expr) [] module PipelineBuilder = let pipeline<'agent> = PipelineBuilder () let toStepDag<'Agent> (p : Pipeline<'Agent>) : SealedStepDag = failwith $"TODO\n%s{Pipeline.toString p}" let foo () : SealedStepDag = pipeline { remote (Comp.make "some-image") workingDir (Comp.make (DirectoryInfo "code root here")) withEnv ("foo", Comp.make "bar") withEnv ("hi", Comp.make "bye") let! (Stdout stdout) = "sh script here" // type annotation is not necessary here but makes the location explicit do! pipeline { withEnv ("foo", stdout) do! "git config --foo" return () } // again, type annotation is not necessary here but makes it explicit that this is running somewhere // else do! pipeline { // remote (Comp.make "another-image") return () } do! "a shell script" return () } |> toStepDag