Build/GitHub/Actions/PSA.ps1

<#
.Synopsis
    GitHub Action for PSA
.Description
    GitHub Action for PSA. This will:

    * Import PSA
    * Run all *.PSA.ps1 files beneath the workflow directory
    * Run a .PSAScript parameter

    Any files changed can be outputted by the script, and those changes can be checked back into the repo.
    Make sure to use the "persistCredentials" option with checkout.
#>


param(
# A PowerShell Script that uses PSA.
# Any files outputted from the script will be added to the repository.
# If those files have a .Message attached to them, they will be committed with that message.
[string]
$PSAScript,

# If set, will not process any files named *.PSA.ps1
[switch]
$SkipPSAPS1,

# A list of modules to be installed from the PowerShell gallery before scripts run.
[string[]]
$InstallModule = @(),

# If provided, will commit any remaining changes made to the workspace with this commit message.
# If no commit message is provided, changes will not be committed.
[string]
$CommitMessage,

# The user email associated with a git commit.
[string]
$UserEmail,

# The user name associated with a git commit.
[string]
$UserName,

# If set, will not check in changed files. It will instead leave them as artifacts.
[switch]
$NoPush
)

"::group::Parameters" | Out-Host
[PSCustomObject]$PSBoundParameters | Format-List | Out-Host
"::endgroup::" | Out-Host

$gitHubEvent = if ($env:GITHUB_EVENT_PATH) {
    [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json
} else { $null }

@"
::group::GitHubEvent
$($gitHubEvent | ConvertTo-Json -Depth 100)
::endgroup::
"@
 | Out-Host


# Check to ensure we are on a branch
$branchName = git rev-parse --abrev-ref HEAD    
# If we were not, return.
if ((-not $branchName) -or $LASTEXITCODE) {
    $LASTEXITCODE = 0
    "::warning title=No Branch Found::Not on a Branch. Can not run." | Out-Host
    exit 0
    return
}

$repoRoot = (git rev-parse --show-toplevel *>&1) -replace '/', [IO.Path]::DirectorySeparatorChar

# Use ANSI rendering if available
if ($PSStyle.OutputRendering) {
    $PSStyle.OutputRendering = 'ANSI'
}

#region -InstallModule
if ($InstallModule) {
    "::group::Installing Modules" | Out-Host
    foreach ($moduleToInstall in $InstallModule) {
        $moduleInWorkspace = Get-ChildItem -Path $env:GITHUB_WORKSPACE -Recurse -File |
            Where-Object Name -eq "$($moduleToInstall).psd1" |
            Where-Object { 
                $(Get-Content $_.FullName -Raw) -match 'ModuleVersion'
            }
        if (-not $moduleInWorkspace) {
            Install-Module $moduleToInstall -Scope CurrentUser -Force
            Import-Module $moduleToInstall -Force -PassThru | Out-Host
        }
    }
    "::endgroup::" | Out-Host
}
#endregion -InstallModule

$PSD1Found = Get-ChildItem -Recurse -Filter "*.psd1" |
    Where-Object Name -eq 'PSA.psd1' | 
    Select-Object -First 1

if ($PSD1Found) {
    $PipeScriptModulePath = $PSD1Found
    Import-Module $PSD1Found -Force -PassThru | Out-Host
} elseif ($env:GITHUB_ACTION_PATH) {
    $PSAModulePath = Join-Path $env:GITHUB_ACTION_PATH 'PSA.psd1'
    if (Test-path $PSAModulePath) {
        Import-Module $PSAModulePath -Force -PassThru | Out-Host
    } else {
        throw "PSA not found"
    }
} elseif (-not (Get-Module PSA)) {    
    throw "Action Path not found"
}

"::notice title=ModuleLoaded::PSA Loaded from Path - $($PSAModulePath)" | Out-Host

$anyFilesChanged = $false
$totalFilesOutputted = 0 
$totalFilesChanged   = 0
$filesOutputted      = @()
$filesChanged        = @()
$OtherOutput         = @()

filter ProcessActionOutput {
    $out = $_
    
    $outItem = Get-Item -Path $out -ErrorAction Ignore
    
    $totalFilesOutputted++
    $fullName, $shouldCommit = 
        if ($out -is [IO.FileInfo]) {
            if ($out.FullName -notlike "$repoRoot*") { return }
            $out.FullName, (git status $out.Fullname -s)
            $filesOutputted += $out
        } elseif ($outItem) {
            if ($outItem.FullName -notlike "$repoRoot*") { return }
            $outItem.FullName, (git status $outItem.Fullname -s)
            $filesOutputted += $outItem
        }
    if ($shouldCommit) {
        git add $fullName
        $filesChanged += $fullName
        if ($out.Message) {
            git commit -m "$($out.Message)" | Out-Host
        } elseif ($out.CommitMessage) {
            git commit -m "$($out.CommitMessage)" | Out-Host
        }  elseif ($gitHubEvent.head_commit.message) {
            git commit -m "$($gitHubEvent.head_commit.message)" | Out-Host
        }
        $anyFilesChanged = $true
        $totalFilesChanged++
    }
    if ($outItem -isnot [IO.FileInfo]) {
        $OtherOutput += $outItem
    }
    $out
}

function AutoRequires {
    param(
    [Management.Automation.CommandInfo]
    $CommandInfo
    )

    process {
        if (-not $CommandInfo.ScriptBlock) { return }
        $simpleRequirements = 
            foreach ($requiredModule in $CommandInfo.ScriptBlock.Ast.ScriptRequirements.RequiredModules) {
                if ($requiredModule.Name) {
                    $requiredModule
                }
            }

        if ($simpleRequirements) {
            foreach ($required in $requiredModule) {
                $installSplat = [Ordered]@{Name=$required.Name;Force=$true}
                if ($requiredModule.RequiredVersion) {
                    $installSplat.Version = $requiredModule.RequiredVersion
                }
                if ($requiredModule.Version) {
                    $installSplat.MinimumVersion = $requiredModule.Version
                }
                if ($requiredModule.MaximumVersion) {
                    $installSplat.MaximumVersion = $requiredModule.MaximumVersion
                }
                Install-Module @installSplat
            }
            
        }                
    }
}


if (-not $UserName)  {
    $UserName =  $env:GITHUB_ACTOR
}

if (-not $UserEmail) {    
    $UserEmail = "$UserName@github.com"        
}
git config --global user.email $UserEmail
git config --global user.name  $UserName

if (-not $env:GITHUB_WORKSPACE) { throw "No GitHub workspace" }

$checkDetached = git symbolic-ref -q HEAD
if (-not $LASTEXITCODE) {
    git pull | Out-Host
}


$PSAScriptStart = [DateTime]::Now
if ($PSAScript) {
    Invoke-Expression -Command $PSAScript |
        . ProcessActionOutput |
        Out-Host
}
$PSAScriptTook = [Datetime]::Now - $PSAScriptStart

"::notice title=Runtime::$($PSAScriptTook.TotalMilliseconds)"   | Out-Host

$PSAPS1Start = [DateTime]::Now
$PSAPS1List  = @()
if (-not $SkipPSAPS1) {
    $PSAFiles = @(
    Get-ChildItem -Recurse -Path $env:GITHUB_WORKSPACE |
        Where-Object Name -Match '\.PSA\.ps1$')
        
    if ($PSAFiles) {
        $PSAFiles|        
        ForEach-Object {
            $PSAPS1List += $_.FullName.Replace($env:GITHUB_WORKSPACE, '').TrimStart('/')
            $PSAPS1Count++
            "::notice title=Checking Requirements for::$($_.Fullname)" | Out-Host
            $scriptCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand($_.FullName,'ExternalScript')
            AutoRequires $scriptCmd
            "::notice title=Running::$($_.Fullname)" | Out-Host
            . $_.FullName |            
                . ProcessActionOutput  | 
                Out-Host
        }
    }
}

$PSAPS1EndStart = [DateTime]::Now
$PSAPS1Took = [Datetime]::Now - $PSAPS1Start
"Ran $($PSAPS1List.Length) Files in $($PSAPS1Took.TotalMilliseconds)" | Out-Host
if ($filesChanged) {
    "::group::$($filesOutputted.Length) files generated with $($filesChanged.Length) changes" | Out-Host
    $FilesChanged -join ([Environment]::NewLine) | Out-Host
    "::endgroup::" | Out-Host
} else {
    "$($filesOutputted.Length) files generated with no changes"
}

if (-not $NoPush -and ($CommitMessage -or $anyFilesChanged)) {
    if ($CommitMessage) {
        Get-ChildItem $env:GITHUB_WORKSPACE -Recurse |
            ForEach-Object {
                $gitStatusOutput = git status $_.Fullname -s
                if ($gitStatusOutput) {
                    git add $_.Fullname
                }
            }

        git commit -m $ExecutionContext.SessionState.InvokeCommand.ExpandString($CommitMessage)
    }    

    $checkDetached = git symbolic-ref -q HEAD 2>&1
    if (-not $LASTEXITCODE) {
        "::group::Pulling Changes" | Out-Host
        git pull | Out-Host
        "::endgroup::" | Out-Host
        "::group::Pushing Changes" | Out-Host        
        git push | Out-Host
        "::endgroup::" | Out-Host
    } else {
        "::warning title=Not pushing changes::(on detached head)" | Out-Host
        $LASTEXITCODE = 0
        exit 0
    }
} elseif (-not $NoPush) {
    "Nothing to commit in this build." | Out-Host    
}

exit 0