Skip to content
ConsoleMonster

Module build pipeline

Follow all sixteen build steps with phase timings, compiler lanes, module facts and captured output.

Demos Dashboards Showcase

Use it when a multistage build needs progress, worker status and tool output in the same terminal.

  • Live
  • 140×40
  • PowerShell 7.4+
  • .NET 8.0+
Module build pipeline 140×4024-bitFull

How it's built

The whole program in one file, with its sample data at the top. It drew the recording above; both languages draw the same cells.

PowerShellpipeline.ps1View on GitHub
# Module build pipeline: phases, sixteen steps, compiler lanes and captured output.
param(
    [switch]$StaticSnapshot,
    [ValidateRange(0, 9)][int]$Step = 0
)

Import-Module ConsoleMonster

# Data: fictional tool output and timings. No builds, signing, publishing or file changes occur.
$steps = @(
    @{ Phase = 'Prepare'; Key = 'config'; Label = 'Read Build/project.build.json'; Seconds = 0.4; Result = 'Pass'; Output = @('Loaded project.build.json (3 projects, version 2.0.X)', 'Resolved version 2.0.0 from PowerShell Gallery (current 0.0.4)') }
    @{ Phase = 'Prepare'; Key = 'manifest'; Label = 'Update ConsoleMonster.psd1'; Seconds = 0.5; Result = 'Pass'; Output = @('ModuleVersion 0.0.4 -> 2.0.0', 'CmdletsToExport: 38 binary cmdlets') }
    @{ Phase = 'Restore'; Key = 'restore'; Label = 'Restore NuGet packages'; Seconds = 1.6; Result = 'Pass'; Output = @('Restored src\Orion\Orion.csproj (in 412 ms)', 'Restored src\Orion.Shell\Orion.Shell.csproj (in 380 ms)') }
    @{ Phase = 'Build'; Key = 'core-ns'; Label = 'Compile ConsoleMonster (netstandard2.0)'; Seconds = 1.8; Result = 'Pass'; Output = @('ConsoleMonster -> bin\Release\netstandard2.0\ConsoleMonster.dll') }
    @{ Phase = 'Build'; Key = 'core-fx'; Label = 'Compile ConsoleMonster (net472)'; Seconds = 1.5; Result = 'Pass'; Output = @('ConsoleMonster -> bin\Release\net472\ConsoleMonster.dll') }
    @{ Phase = 'Build'; Key = 'core-net'; Label = 'Compile ConsoleMonster (net8.0)'; Seconds = 1.4; Result = 'Pass'; Output = @('ConsoleMonster -> bin\Release\net8.0\ConsoleMonster.dll') }
    @{ Phase = 'Build'; Key = 'ps'; Label = 'Compile ConsoleMonster.PowerShell'; Seconds = 1.9; Result = 'Pass'; Output = @('ConsoleMonster.PowerShell -> bin\Release\net472\ConsoleMonster.PowerShell.dll', 'ConsoleMonster.PowerShell -> bin\Release\net8.0\ConsoleMonster.PowerShell.dll') }
    @{ Phase = 'Test'; Key = 'xunit'; Label = 'Run xUnit tests (net472, net8.0)'; Seconds = 3.2; Result = 'Pass'; Output = @('Passed!  - Failed: 0, Passed: 286, Skipped: 0 - ConsoleMonster.Tests.dll (net472)', 'Passed!  - Failed: 0, Passed: 286, Skipped: 0 - ConsoleMonster.Tests.dll (net8.0)') }
    @{ Phase = 'Test'; Key = 'compatibility'; Label = 'Run compatibility tests'; Seconds = 2.4; Result = 'Pass'; Output = @('Tests completed in 2.1s', 'Tests Passed: 41, Failed: 0, Skipped: 0') }
    @{ Phase = 'Test'; Key = 'pester7'; Label = 'Run Pester tests (PowerShell 7.5)'; Seconds = 2.1; Result = 'Warn'; Output = @('Tests completed in 1.8s', 'Tests Passed: 40, Failed: 0, Skipped: 1 (requires conhost)') }
    @{ Phase = 'Docs'; Key = 'md'; Label = 'Generate markdown help (38 commands)'; Seconds = 1.7; Result = 'Pass'; Output = @('Docs\Readme.md updated', 'Docs\Write-ConsoleTable.md ... Docs\Start-ConsoleDashboard.md') }
    @{ Phase = 'Docs'; Key = 'maml'; Label = 'Generate en-US MAML help'; Seconds = 0.9; Result = 'Pass'; Output = @('en-US\ConsoleMonster.PowerShell.dll-Help.xml (412 KB)') }
    @{ Phase = 'Sign'; Key = 'sign'; Label = 'Sign module files'; Seconds = 0.3; Result = 'Skip'; Output = @('SignModule is $false for this build; skipping Authenticode signing') }
    @{ Phase = 'Package'; Key = 'nupkg'; Label = 'Pack ConsoleMonster.2.0.0.nupkg'; Seconds = 1.2; Result = 'Pass'; Output = @('Successfully created package ''Artefacts\ProjectBuild\ConsoleMonster.2.0.0.nupkg''.') }
    @{ Phase = 'Package'; Key = 'zip'; Label = 'Create module archive'; Seconds = 0.8; Result = 'Pass'; Output = @('Artefacts\Packed\ConsoleMonster-v2.0.0.zip (1.1 MB)') }
    @{ Phase = 'Publish'; Key = 'publish'; Label = 'Publish to PowerShell Gallery'; Seconds = 0.3; Result = 'Skip'; Output = @('RunMode is Build; publishing is disabled') }
 )
$phaseNames = 'Prepare', 'Restore', 'Build', 'Test', 'Docs', 'Sign', 'Package', 'Publish'
$compilerNames = 'netstandard2.0', 'net472', 'net8.0', 'PowerShell'
$checkpoints = 0, 2, 3, 5, 7, 9, 10, 12, 14, 16
$culture = [cultureinfo]::InvariantCulture
$snapshot = $StaticSnapshot -or [Console]::IsInputRedirected
$completed = if ($snapshot) { $checkpoints[$Step] } else { 0 }
function Format-PipelinePath($Text) { $Text.Replace([char]92, [IO.Path]::DirectorySeparatorChar) }
function Format-PipelineDuration($Seconds) { $Seconds.ToString('0.0', $culture) + 's' }

# Layout: preserve the complete checklist, phase strip, module facts, progress, compiler lanes and log.
$dashboard = New-ConsoleDashboard -Title '[bold]Module build[/]' -Subtitle 'Build-Module ConsoleMonster' -Footer '[muted]Prepare · Restore · Build · Test · Docs · Sign · Package · Publish    Ctrl+C stop[/]' {
    New-ConsolePadding -Padding 1, 0, 1, 1 {
        New-ConsolePhases {
            $offset = 0
            foreach ($phase in $phaseNames) {
                $group = @($steps | Where-Object Phase -EQ $phase)
                $last = $offset + $group.Count
                $state = if ($last -le $completed) {
                    if ($group.Result -contains 'Warn') { 'Warn' } elseif (@($group | Where-Object Result -NE Skip).Count -eq 0) { 'Skip' } else { 'Pass' }
                } elseif ($offset -le $completed -and $completed -lt $steps.Count) { 'Running' } else { 'Pending' }
                $detail = if ($last -le $completed) { Format-PipelineDuration ($group | Measure-Object Seconds -Sum).Sum } else { '' }
                New-ConsoleStatusItem $phase -Status $state -Detail $detail
                $offset = $last
            }
        }
    }
    New-ConsoleColumns {
        New-ConsolePanel -Title Steps -Weight 1.5 {
            New-ConsoleStatusList -Name steps {
                for ($i = 0; $i -lt $steps.Count; $i++) {
                    $item = $steps[$i]
                    $state = if ($i -lt $completed) { $item.Result } elseif ($i -eq $completed) { 'Running' } else { 'Pending' }
                    $detail = if ($i -lt $completed) { Format-PipelineDuration $item.Seconds } else { '' }
                    New-ConsoleStatusItem (Format-PipelinePath $item.Label) -Key $item.Key -Status $state -Detail $detail
                }
            }
        }
        New-ConsolePanel -Title Module {
            New-ConsoleKeyValue -Name module -Markup -Data ([ordered]@{
                Module = '[bold]ConsoleMonster[/]'
                Version = '0.0.4 [muted]→[/] [bold]2.0.0[/]'
                Frameworks = 'netstandard2.0 · net472 · net8.0'
                PowerShell = '5.1 Desktop · 7.x Core'
                'Run mode' = 'Build'
                Elapsed = if ($completed) { Format-PipelineDuration ($steps | Select-Object -First $completed | Measure-Object Seconds -Sum).Sum } else { '-' }
            })
            New-ConsoleRule
            $progress = New-ConsoleProgress -Name overall -Label Overall -Total $steps.Count -Value $completed
            $progress.LabelWidth = 8
            $progress
            New-ConsoleRule 'Compiler workers'
            New-ConsoleLanes -Name compilers {
                for ($i = 0; $i -lt $compilerNames.Count; $i++) {
                    $done = if ($completed -gt $i + 3) { 1 } else { 0 }
                    $state = if ($done) { 'Pass' } elseif ($completed -eq $i + 3) { 'Running' } else { 'Pending' }
                    $detail = if ($done) { 'compiled' } elseif ($state -eq 'Running') { 'compiling' } else { 'queued' }
                    New-ConsoleLane "c$i" -Label $compilerNames[$i] -Total 1 -Done $done -Passed $done -Status $state -Detail $detail
                }
            }
        }
    }
    New-ConsolePanel -Title Output -Fill {
        New-ConsoleLog -Height 8 -NoTime {
            New-ConsoleLogEntry 'Build engine · release pipeline'
            foreach ($item in ($steps | Select-Object -First $completed)) {
                $level = if ($item.Result -eq 'Warn') { 'warn' } elseif ($item.Result -eq 'Skip') { 'skip' } else { 'info' }
                foreach ($line in $item.Output) { New-ConsoleLogEntry (Format-PipelinePath $line) -Level $level }
            }
            if ($completed -lt $steps.Count) { New-ConsoleLogEntry ((Format-PipelinePath $steps[$completed].Label) + '...') }
        }
    }
}
$dashboard.ShowClock = $false

# Behaviour: native cmdlets post progress; Write-Host and Write-Warning land in the output region.
if ($snapshot) {
    $dashboard | Write-ConsoleWidget
    return
}
Start-ConsoleDashboard $dashboard -ArgumentList (, $steps) {
    param($Steps)
    $culture = [cultureinfo]::InvariantCulture
    $elapsed = 0.0
    for ($i = 0; $i -lt $Steps.Count; $i++) {
        if ($ConsoleSession.Cancellation.IsCancellationRequested) { break }
        $item = $Steps[$i]
        Update-ConsolePhase -Key $item.Phase -Status Running
        Update-ConsoleStatus -Name steps -Key $item.Key -Status Running
        Update-ConsoleProgress -Name overall -Label $item.Phase
        if ($i -ge 3 -and $i -le 6) { Update-ConsoleLane -Name compilers -Key ('c' + ($i - 3)) -Status Running -Detail compiling }
        Write-Host ($item.Label.Replace([char]92, [IO.Path]::DirectorySeparatorChar) + '...')
        Start-Sleep -Milliseconds ([int]($item.Seconds * 400))
        foreach ($line in $item.Output) {
            $text = $line.Replace([char]92, [IO.Path]::DirectorySeparatorChar)
            if ($item.Result -eq 'Warn') { Write-Warning $text } else { Write-Host $text }
        }
        $elapsed += $item.Seconds
        $detail = $item.Seconds.ToString('0.0', $culture) + 's'
        Update-ConsoleStatus -Name steps -Key $item.Key -Status $item.Result -Detail $detail
        Set-ConsoleValue -Name module -Key Elapsed -Value ($elapsed.ToString('0.0', $culture) + 's')
        Update-ConsoleProgress -Name overall -Value ($i + 1)
        if ($i -ge 3 -and $i -le 6) { Update-ConsoleLane -Name compilers -Key ('c' + ($i - 3)) -Result Pass -Detail compiled -Complete }
        if ($i -eq $Steps.Count - 1 -or $Steps[$i + 1].Phase -ne $item.Phase) {
            $group = @($Steps | Where-Object Phase -EQ $item.Phase)
            $state = if ($group.Result -contains 'Warn') { 'Warn' } elseif (@($group | Where-Object Result -NE Skip).Count -eq 0) { 'Skip' } else { 'Pass' }
            $detail = ($group | Measure-Object Seconds -Sum).Sum.ToString('0.0', $culture) + 's'
            Update-ConsolePhase -Key $item.Phase -Status $state -Detail $detail
        }
    }
}
C#Program.csView on GitHub
// Module build pipeline: phases, sixteen steps, compiler lanes and captured output.
using System.Globalization;
using ConsoleMonster;
using ConsoleMonster.Layout;
using ConsoleMonster.Live;
using ConsoleMonster.Styling;
using ConsoleMonster.Widgets;

// Data: all tool messages and outcomes are fictional. No build, signing, publishing or file writes occur.
BuildStep[] steps = {
    new("Prepare", "config", "Read Build/project.build.json", 0.4, StatusKind.Pass, "Loaded project.build.json (3 projects, version 2.0.X)", "Resolved version 2.0.0 from PowerShell Gallery (current 0.0.4)"),
    new("Prepare", "manifest", "Update ConsoleMonster.psd1", 0.5, StatusKind.Pass, "ModuleVersion 0.0.4 -> 2.0.0", "CmdletsToExport: 38 binary cmdlets"),
    new("Restore", "restore", "Restore NuGet packages", 1.6, StatusKind.Pass, "Restored src\\Orion\\Orion.csproj (in 412 ms)", "Restored src\\Orion.Shell\\Orion.Shell.csproj (in 380 ms)"),
    new("Build", "core-ns", "Compile ConsoleMonster (netstandard2.0)", 1.8, StatusKind.Pass, "ConsoleMonster -> bin\\Release\\netstandard2.0\\ConsoleMonster.dll"),
    new("Build", "core-fx", "Compile ConsoleMonster (net472)", 1.5, StatusKind.Pass, "ConsoleMonster -> bin\\Release\\net472\\ConsoleMonster.dll"),
    new("Build", "core-net", "Compile ConsoleMonster (net8.0)", 1.4, StatusKind.Pass, "ConsoleMonster -> bin\\Release\\net8.0\\ConsoleMonster.dll"),
    new("Build", "ps", "Compile ConsoleMonster.PowerShell", 1.9, StatusKind.Pass, "ConsoleMonster.PowerShell -> bin\\Release\\net472\\ConsoleMonster.PowerShell.dll", "ConsoleMonster.PowerShell -> bin\\Release\\net8.0\\ConsoleMonster.PowerShell.dll"),
    new("Test", "xunit", "Run xUnit tests (net472, net8.0)", 3.2, StatusKind.Pass, "Passed!  - Failed: 0, Passed: 286, Skipped: 0 - ConsoleMonster.Tests.dll (net472)", "Passed!  - Failed: 0, Passed: 286, Skipped: 0 - ConsoleMonster.Tests.dll (net8.0)"),
    new("Test", "compatibility", "Run compatibility tests", 2.4, StatusKind.Pass, "Tests completed in 2.1s", "Tests Passed: 41, Failed: 0, Skipped: 0"),
    new("Test", "pester7", "Run Pester tests (PowerShell 7.5)", 2.1, StatusKind.Warn, "Tests completed in 1.8s", "Tests Passed: 40, Failed: 0, Skipped: 1 (requires conhost)"),
    new("Docs", "md", "Generate markdown help (38 commands)", 1.7, StatusKind.Pass, "Docs\\Readme.md updated", "Docs\\Write-ConsoleTable.md ... Docs\\Start-ConsoleDashboard.md"),
    new("Docs", "maml", "Generate en-US MAML help", 0.9, StatusKind.Pass, "en-US\\ConsoleMonster.PowerShell.dll-Help.xml (412 KB)"),
    new("Sign", "sign", "Sign module files", 0.3, StatusKind.Skip, "SignModule is $false for this build; skipping Authenticode signing"),
    new("Package", "nupkg", "Pack ConsoleMonster.2.0.0.nupkg", 1.2, StatusKind.Pass, "Successfully created package 'Artefacts\\ProjectBuild\\ConsoleMonster.2.0.0.nupkg'."),
    new("Package", "zip", "Create module archive", 0.8, StatusKind.Pass, "Artefacts\\Packed\\ConsoleMonster-v2.0.0.zip (1.1 MB)"),
    new("Publish", "publish", "Publish to PowerShell Gallery", 0.3, StatusKind.Skip, "RunMode is Build; publishing is disabled"),
};
string[] phaseNames = { "Prepare", "Restore", "Build", "Test", "Docs", "Sign", "Package", "Publish" };
string[] compilerNames = { "netstandard2.0", "net472", "net8.0", "PowerShell" };
int[] checkpoints = { 0, 2, 3, 5, 7, 9, 10, 12, 14, 16 };
int stepAt = Array.IndexOf(args, "--step");
int step = stepAt < 0 ? 0 : int.Parse(args[stepAt + 1], CultureInfo.InvariantCulture);
if (step < 0 || step >= checkpoints.Length) throw new ArgumentOutOfRangeException(nameof(step));
bool snapshot = args.Contains("--snapshot") || (!args.Contains("--interactive") && Console.IsInputRedirected);
int completed = snapshot ? checkpoints[step] : 0;
string PathText(string text) => text.Replace('\\', Path.DirectorySeparatorChar);
string Duration(double seconds) => seconds.ToString("0.0", CultureInfo.InvariantCulture) + "s";
StatusKind Worst(IEnumerable<BuildStep> items) => items.Any(item => item.Result == StatusKind.Warn) ? StatusKind.Warn
    : items.All(item => item.Result == StatusKind.Skip) ? StatusKind.Skip : StatusKind.Pass;

// Layout: a phase strip and detailed checklist beside module facts, overall progress and compiler lanes.
Phases phases = new Phases();
foreach (string phase in phaseNames) {
    BuildStep[] group = steps.Where(item => item.Phase == phase).ToArray();
    int first = Array.IndexOf(steps, group[0]);
    int last = first + group.Length;
    StatusKind state = last <= completed ? Worst(group) : first <= completed && completed < steps.Length ? StatusKind.Running : StatusKind.Pending;
    phases.Set(phase, state, detail: last <= completed ? Duration(group.Sum(item => item.Seconds)) : null);
}
StatusList status = new StatusList("steps");
for (int i = 0; i < steps.Length; i++) status.Set(steps[i].Key, PathText(steps[i].Label),
    i < completed ? steps[i].Result : i == completed ? StatusKind.Running : StatusKind.Pending,
    i < completed ? Duration(steps[i].Seconds) : null);
KeyValueList module = new KeyValueList("module").Add("Module", "[bold]ConsoleMonster[/]")
    .Add("Version", "0.0.4 [muted]→[/] [bold]2.0.0[/]").Add("Frameworks", "netstandard2.0 · net472 · net8.0")
    .Add("PowerShell", "5.1 Desktop · 7.x Core").Add("Run mode", "Build")
    .Add("Elapsed", completed == 0 ? "-" : Duration(steps.Take(completed).Sum(item => item.Seconds)));
ProgressBar progress = new ProgressBar("Overall", "overall") { Total = steps.Length, Value = completed, LabelWidth = 8 };
Lanes compilers = new Lanes("compilers");
for (int i = 0; i < compilerNames.Length; i++) {
    Lane lane = compilers.Add("c" + i, compilerNames[i], 1);
    lane.Status = completed > i + 3 ? StatusKind.Pass : completed == i + 3 ? StatusKind.Running : StatusKind.Pending;
    lane.Done = completed > i + 3 ? 1 : 0;
    lane.Passed = (int)lane.Done;
    lane.Detail = lane.Done > 0 ? "compiled" : lane.Status == StatusKind.Running ? "compiling" : "queued";
}
LogRegion log = new LogRegion { PreferredHeight = 8, ShowTime = false };
log.Write("Build engine · release pipeline");
foreach (BuildStep item in steps.Take(completed)) foreach (string line in item.Output) log.Write(PathText(line), item.Result == StatusKind.Warn ? "warn" : item.Result == StatusKind.Skip ? "skip" : "info");
if (completed < steps.Length) log.Write(PathText(steps[completed].Label) + "...");
Rows body = new Rows(new Padding(phases, 1, 0, 1, 1),
    new Columns().Add(new Panel(status, "Steps"), LayoutSize.Fraction(1.5))
        .Add(new Panel(new Rows(module, new Rule(), progress, new Rule("Compiler workers"), compilers), "Module")))
    .Add(new Panel(log, "Output"), LayoutSize.Fraction());
Dashboard dashboard = new Dashboard("[bold]Module build[/]", body) {
    Subtitle = "Build-Module ConsoleMonster", ShowClock = false,
    Footer = "[muted]Prepare · Restore · Build · Test · Docs · Sign · Package · Publish    Ctrl+C stop[/]"
};

// Behaviour: native handles update each region while ordinary tool output is captured by the live session.
if (snapshot) { Terminal.Write(dashboard); return; }
using LiveSession session = new LiveSession(dashboard);
session.Run(live => {
    var phaseHandle = phases.Handle(live);
    var stepHandle = status.Handle(live);
    var moduleHandle = module.Handle(live);
    var progressHandle = progress.Handle(live);
    var compilerHandle = compilers.Handle(live);
    double elapsed = 0;
    for (int i = 0; i < steps.Length; i++) {
        if (live.Cancellation.IsCancellationRequested) break;
        BuildStep item = steps[i];
        phaseHandle.Start(item.Phase);
        stepHandle.Set(item.Key, StatusKind.Running);
        progressHandle.Label(item.Phase);
        if (i >= 3 && i <= 6) compilerHandle.Start("c" + (i - 3), "compiling");
        Console.WriteLine(PathText(item.Label) + "...");
        if (live.Cancellation.WaitHandle.WaitOne((int)(item.Seconds * 400))) break;
        foreach (string line in item.Output) {
            if (item.Result == StatusKind.Warn) Console.Error.WriteLine(PathText(line)); else Console.WriteLine(PathText(line));
        }
        elapsed += item.Seconds;
        stepHandle.Set(item.Key, item.Result);
        stepHandle.Detail(item.Key, Duration(item.Seconds));
        moduleHandle.Set("Elapsed", Duration(elapsed));
        progressHandle.Set(i + 1);
        if (i >= 3 && i <= 6) {
            compilerHandle.Advance("c" + (i - 3), StatusKind.Pass, "compiled");
            compilerHandle.Complete("c" + (i - 3), StatusKind.Pass);
        }
        if (i == steps.Length - 1 || steps[i + 1].Phase != item.Phase) {
            var group = steps.Where(value => value.Phase == item.Phase);
            live.Post(new LiveEvent("phases", "state", item.Phase, Duration(group.Sum(value => value.Seconds)), Worst(group).ToString()));
        }
    }
});

record BuildStep(string Phase, string Key, string Label, double Seconds, StatusKind Result, params string[] Output);

Run it yourself

Three steps in PowerShell, four in C#. Every command copies with one click.

  1. Install the module

    Once. Windows PowerShell 5.1 or PowerShell 7 on Windows, macOS or Linux.

    Install-Module ConsoleMonster -Scope CurrentUser
  2. Save the program

    Download it, or copy it from above into pipeline.ps1.

  3. Run it

    From the folder you saved it in.

    ./pipeline.ps1
  1. Create a project

    A plain console app; .NET 8 or later.

    dotnet new console -n Pipeline; cd Pipeline
  2. Add ConsoleMonster

    The only package the program needs.

    dotnet add package ConsoleMonster
  3. Replace Program.cs

    Download it, or copy the program from above.

  4. Run it

    dotnet run

Or skip the copying. Every demo ships with the module and the project template.

Show-ConsoleDemo pipeline
Get-ConsoleDemo pipeline -Path ./pipeline.ps1
dotnet new install ConsoleMonster.Templates
dotnet new consolemonster --demo pipeline -n Pipeline

Make it yours

Start with these, then read the rest of the file.

  1. Replace the sixteen steps with your build phases and expected outcomes.

  2. Stream your tool output into the log while updating compiler lanes.

  3. Change module facts and artifact paths for your package.

More dashboards

All demos