Modules/businessdev.ALbuild.Pipeline/Private/Get-AdoPipelineStepList.ps1

function Get-AdoPipelineStepList {
    <#
    .SYNOPSIS
        Flattens a parsed Azure DevOps pipeline document into an ordered list of executable steps.
    .DESCRIPTION
        Internal helper for the local pipeline runner. Walks stages -> jobs -> steps (and a
        deployment job's strategy.runOnce.deploy.steps), resolving compile-time "${{ if }}"
        conditional insertion and "${{ each x in <list> }}" loops using the supplied parameters.
        Only the constructs used by the ALbuild templates are supported. Each returned item carries
        the raw step mapping and the parameter/loop context to expand it with at run time.
    .PARAMETER Document
        The parsed pipeline (an ordered dictionary from ConvertFrom-Yaml -Ordered).
    .PARAMETER Parameters
        Resolved template parameters (name -> value).
    .OUTPUTS
        PSCustomObject[] each with: Step (IDictionary) and Parameters (hashtable).
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param(
        [Parameter(Mandatory)] [System.Collections.IDictionary] $Document,
        [hashtable] $Parameters = @{}
    )

    $result = [System.Collections.Generic.List[object]]::new()

    function Test-IsMap([object] $o) { return ($o -is [System.Collections.IDictionary]) }
    function Test-IsSeq([object] $o) { return ($o -is [System.Collections.IEnumerable] -and $o -isnot [string] -and -not (Test-IsMap $o)) }

    function Get-Insertion([System.Collections.IDictionary] $map) {
        # A conditional/loop insertion is a single-key map whose key is a "${{ if|each ... }}" directive.
        $keys = @($map.Keys)
        if ($keys.Count -ne 1) { return $null }
        $key = "$($keys[0])"
        $ifm = [regex]::Match($key, '^\$\{\{\s*if\s+(?<cond>.+?)\s*\}\}$')
        if ($ifm.Success) { return [PSCustomObject]@{ Kind = 'if'; Cond = $ifm.Groups['cond'].Value; Body = $map[$keys[0]] } }
        $eachm = [regex]::Match($key, '^\$\{\{\s*each\s+(?<var>[A-Za-z_]\w*)\s+in\s+(?<list>.+?)\s*\}\}$')
        if ($eachm.Success) { return [PSCustomObject]@{ Kind = 'each'; Var = $eachm.Groups['var'].Value; ListExpr = $eachm.Groups['list'].Value; Body = $map[$keys[0]] } }
        return $null
    }

    function Copy-Parameter([hashtable] $p, [string] $name, [object] $value) {
        $clone = @{}; foreach ($k in $p.Keys) { $clone[$k] = $p[$k] }
        if ($name) { $clone[$name] = $value }
        return $clone
    }

    # Generic sequence walker: calls $onItem for each non-insertion item; expands if/each.
    function Expand-Sequence([object] $seq, [hashtable] $p, [scriptblock] $onItem) {
        if (-not (Test-IsSeq $seq)) { return }
        foreach ($item in $seq) {
            if (Test-IsMap $item) {
                $ins = Get-Insertion $item
                if ($ins) {
                    if ($ins.Kind -eq 'if') {
                        if (Test-AdoCondition -Condition $ins.Cond -Parameters $p) { Expand-Sequence $ins.Body $p $onItem }
                    }
                    else {
                        $list = Get-AdoExpressionValue -Expr $ins.ListExpr -Parameters $p
                        foreach ($val in @($list)) { Expand-Sequence $ins.Body (Copy-Parameter $p $ins.Var $val) $onItem }
                    }
                    continue
                }
            }
            & $onItem $item $p
        }
    }

    $addStep = {
        param($step, $p)
        $result.Add([PSCustomObject]@{ Step = $step; Parameters = $p })
    }

    $walkJob = {
        param($job, $p)
        if (-not (Test-IsMap $job)) { return }
        if ($job.Contains('steps')) { Expand-Sequence $job['steps'] $p $addStep }
        elseif ($job.Contains('strategy')) {
            $deploySteps = $null
            try { $deploySteps = $job['strategy']['runOnce']['deploy']['steps'] } catch { $deploySteps = $null }
            if ($deploySteps) { Expand-Sequence $deploySteps $p $addStep }
        }
    }

    $walkStage = {
        param($stage, $p)
        if ((Test-IsMap $stage) -and $stage.Contains('jobs')) { Expand-Sequence $stage['jobs'] $p $walkJob }
    }

    if ($Document.Contains('steps')) { Expand-Sequence $Document['steps'] $Parameters $addStep }
    elseif ($Document.Contains('jobs')) { Expand-Sequence $Document['jobs'] $Parameters $walkJob }
    elseif ($Document.Contains('stages')) { Expand-Sequence $Document['stages'] $Parameters $walkStage }

    return $result.ToArray()
}