Convert-BuildStep.ps1

function Convert-BuildStep
{
    <#
    .Synopsis
        Converts Build Steps into build system input
    .Description
        Converts Build Steps defined in a PowerShell script into build steps in a build system
    .Example
        Get-Command Convert-BuildStep | Convert-BuildStep
    .Link
        Import-BuildStep
    .Link
        Expand-BuildStep
    #>

    param(
    # The name of the build step
    [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
    [string]
    $Name,

    # The Script Block that will be converted into a build step
    [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='ScriptBlock',ValueFromPipeline)]
    [ScriptBlock]
    $ScriptBlock,

    # The module that -ScriptBlock is declared in. If piping in a command, this will be bound automatically
    [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='ScriptBlock')]
    [Management.Automation.PSModuleInfo]
    $Module,

    # The path to the file
    [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='ScriptBlock')]
    [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='PathAndExtension')]
    [Alias('Fullname')]
    [string]
    $Path,

    # The extension of the file
    [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='PathAndExtension')]
    [string]
    $Extension,


    # The name of parameters that should be supplied from build variables.
    # Wildcards accepted.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string[]]
    $VariableParameter,

    # The name of parameters that should be supplied from the environment.
    # Wildcards accepted.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string[]]
    $EnvironmentParameter,

    # The name of parameters that should be referred to uniquely.
    # For instance, if converting function foo($bar) {} and -UniqueParameter is 'bar'
    # The build parameter would be foo_bar.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string[]]
    $UniqueParameter,

    # The name of parameters that should be excluded.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string[]]
    $ExcludeParameter,

    # Default parameters for a build step
    [Parameter(ValueFromPipelineByPropertyName)]
    [Collections.IDictionary]
    $DefaultParameter = @{},

    # The build system. Currently supported options, ADO and GitHubActions. Defaulting to ADO.
    [ValidateSet('ADO', 'GitHubActions')]
    [string]
    $BuildSystem = 'ado'


    )

    begin {
        $MatchesAnyWildcard = {
            param([string[]]$text, [string[]]$Wildcard)
            foreach ($t in $text) {
                foreach ($wc in $Wildcard) {
                    if ($t -like $wc) {return $t }
                }
            }
            return $false
        }
    }

    process {
        if ($PSCmdlet.ParameterSetName -eq 'PathAndExtension') {
            if ($Extension -eq '.ps1')
            {
                $splatMe=  @{} + $PSBoundParameters
                $splatMe.Remove('Path')
                $splatMe.Remove('Extension')
                Get-Item -Path $path |
                    Get-Command { $_.FullName } |
                    Convert-BuildStep @splatMe
            }
            elseif ($Extension -eq '.sh')
            {
                [Ordered]@{bash="$ft";displayName=$metaData.Name}
            }
            return
        }
        $innerScript = "$ScriptBlock"

        $sbParams =
            if ($ScriptBlock.Ast.ParamBlock) {
                $ScriptBlock.Ast.ParamBlock
            } elseif ($ScriptBlock.ast.Body.ParamBlock) {
                $ScriptBlock.Ast.Body.ParamBlock
            }
        $definedParameters = @()
        if ($sbParams) {
            $function:_TempFunction = $ScriptBlock
            $tempCmd =
                $ExecutionContext.SessionState.InvokeCommand.GetCommand("_TempFunction",'Function')
            $tempCmdMd = [Management.Automation.CommandMetadata]$tempCmd

            $collectParameters = @(
                '$Parameters = @{}'

                foreach ($parameterName in $tempCmdMd.Parameters.Keys) {
                    $parameterAttributes = $tempCmdMd.Parameters[$parameterName].Attributes
                    $isMandatory =
                        foreach ($attr in $parameterAttributes) {
                            if ($attr.IsMandatory) { $true; break }
                        }


                    $disambiguatedParameter = $Name + '_' + $parameterName
                    $makeUnique = & $MatchesAnyWildcard $parameterName,$disambiguatedParameter $UniqueParameter
                    $shouldExclude =
                        & $MatchesAnyWildCard $disambiguatedParameter, $parameterName $ExcludeParameter
                    if ($shouldExclude) { continue }
                    $VariableName =
                        & $MatchesAnyWildcard $disambiguatedParameter, $parameterName $VariableParameter

                    $EnvVariableName =
                        & $MatchesAnyWildcard $disambiguatedParameter, $parameterName $EnvironmentParameter
                    $paramType = $tempCmdMd.Parameters[$parameterName].ParameterType

                    $defaultValue =
                        if ($DefaultParameter[$disambiguatedParameter]) # If we provided a default value for the disambiguated parameter,
                        {
                            $DefaultParameter[$disambiguatedParameter]  # use that as the default value.
                        } elseif ($DefaultParameter[$parameterName])    # Otherwise, if we have provided a default by name,
                        {
                            $DefaultParameter[$parameterName]           # use that as the default.
                        } else
                        {
                            foreach ($param in $sbParams.Parameters) {
                                if ($parameterName -eq $param.Name.VariablePath) {
                                    if ($param.DefaultValue.SubExpression) { # If the default value was a subexpression
                                        break # then break, which will actually have a blank default.
                                        # This is desirable, because otherwise, we have to allow string expansion on _any_ incoming parameter
                                        # Doing that would allow generic code injection into a pipeline, which we do not want.
                                    }
                                    "$($param.DefaultValue)"
                                    break
                                }
                            }
                        }
                    if ($BuildSystem -eq 'ado') {

                        $stepParamName = if ($makeUnique) { $disambiguatedParameter } else {$ParameterName}

                        if ($variableName)
                        {
                            "`$Parameters.$ParameterName = '`$($stepParamName)'"
                        }
                        elseif ($envVariableName)
                        {
                            "`$Parameters.$ParameterName = `$env:$($stepParamName)"
                        }
                        else
                        {
                            $thisParameter = [Ordered]@{
                                name = $stepParamName
                                type =
                                    $(if ([switch], [bool] -contains $paramType)
                                    {
                                        'boolean'
                                    }
                                    elseif ([int],[float],[double],[uint32],[byte], [long] -contains $paramType)
                                    {
                                        'number'
                                    }
                                    elseif ([string],
                                        [Version],
                                        [ScriptBlock],[ScriptBlock[]],
                                        [string[]],
                                        [int[]],
                                        [float[]] -contains $paramType -or
                                        $paramType.IsSubclassOf([Enum])) {
                                        'string'
                                    } else {
                                        'object'
                                    })
                            }
                            if ($paramType.IsSubclassOf([Enum])) {
                                $thisParameter.values = [Enum]::GetValues($paramType)
                            } else {
                                foreach ($attr in $parameterAttributes) {
                                    if ($attr -is [Management.Automation.ValidateSetAttribute]) {
                                        $thisParameter.values = $attr.ValidValues
                                        break
                                    }
                                }
                            }

                            if (-not $isMandatory) {
                                $thisParameter.default = ''
                                if ($thisParameter.Contains('values')) {
                                    $thisParameter.values = @('') + $thisParameter.values
                                }
                            }

                            if ($null -ne $defaultValue) {
                                $thisParameter.default = $defaultValue
                            }

                            $definedParameters += $thisParameter
                            "`$Parameters.$ParameterName = `${{parameters.$stepParamName}};"
                        }

                        if ([int[]], [string[]],[float[]] -contains $paramType) {
                            "`$Parameters.$ParameterName = `$parameters.$ParameterName -split ';'"
                        }
                        if ([ScriptBlock], [ScriptBlock[]] -contains $paramType) {
                            "`$Parameters.$ParameterName = foreach (`$p in `$parameters.$ParameterName){ [ScriptBlock]::Create(`$p) }"
                        }
                    }
                }

                if ($tempCmdmd.SupportsShouldProcess) {
                    '$Parameters.Confirm = $false'
                }
                @'
foreach ($k in @($parameters.Keys)) {
    if ([String]::IsNullOrEmpty($parameters[$k])) {
        $parameters.Remove($k)
    }
}
'@

            )
            $collectParameters =
                    $collectParameters -join [Environment]::NewLine -replace '\$\{','`${'

            if ($Name -and $Module) {
                $modulePathVariable = "${Module}Path"
                $sb = [ScriptBlock]::Create(@"
$collectParameters
Import-Module `$($modulePathVariable) -Force -PassThru
$Name `@Parameters
"@
) -replace "`\$\{\{parameters\.(?<Name>[^\}]+?)}};", '${{coalesce(format(''"{0}"'',parameters.${Name}), ''$null'')}};'
                $innerScript = $sb
            } else {
                $sb = [scriptBlock]::Create(@"
$CollectParameters
& {$ScriptBlock} `@Parameters
"@
) -replace "`\$\{\{parameters\.(?<Name>[^\}]+?)}};", '${{coalesce(format(''"{0}"'',parameters.${Name}), ''$null'')}};'
                $innerScript = $sb
            }
            Remove-Item -Force function:_TempFunction
        }
        $out = [Ordered]@{}
        if ($BuildSystem -eq 'ADO') {
            if ($outObject.pool -and $outObject.pool.vmimage -notlike '*win*') {
                $out.pwsh = "$innerScript" -replace '`\$\{','${'
            } else {
                $out.powershell = "$innerScript" -replace '`\$\{','${'
            }
            $out.displayName = $Name
            if ($definedParameters) {
                $out.parameters = $definedParameters
            }
            if ($UseSystemAccessToken) {
                $out.env = @{"SYSTEM_ACCESSTOKEN"='$(System.AccessToken)'}
            }
        } elseif ($BuildSystem -eq 'GitHubActions') {
            $out.name = $Name
            $out.runs = "$innerScript"
            $out.shell = 'pwsh'
        }
        $out
    }
}