Public/Invoke-WebAppUpdate.ps1

function Invoke-WebAppUpdate {
    <#
        .SYNOPSIS
            Update a web application by pulling the latest source and running build pipeline steps.
        .DESCRIPTION
            Core deployment pipeline. Fetches the latest code from a git branch, optionally resets
            and pulls, then executes an ordered set of build script blocks. Supports mutex-based
            single-execution guard, progress reporting, logging, and email notifications.
 
            Ported from Generic-Updater in helpers.ps1, with all paths and recipients parameterized.
        .PARAMETER Directory
            Path to the application working directory.
        .PARAMETER Branch
            Name of the git branch to use.
        .PARAMETER Alias
            Display name for the application (used in progress and notifications).
        .PARAMETER Url
            The application URL (used in notifications for display purposes).
        .PARAMETER ScriptBlock
            An ordered hashtable of named script blocks defining the build pipeline steps.
        .PARAMETER MutexName
            Mutex name for single-execution guard. Default: 'WebAppUpdateMutex'.
        .PARAMETER Force
            If set, always reset and pull. Otherwise, only pull if the branch is behind remote.
        .PARAMETER LogFile
            Path to the log file. If not specified, no file logging is performed.
        .PARAMETER SmtpServer
            SMTP server for email notifications. If not specified, email is skipped.
        .PARAMETER SmtpFrom
            Sender address for email notifications.
        .PARAMETER To
            Recipient(s) for email notifications.
        .PARAMETER Cc
            CC recipient(s) for email notifications.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Directory,

        [Parameter(Mandatory = $true)]
        [string]$Branch,

        [Parameter(Mandatory = $true)]
        [string]$Alias,

        [Parameter(Mandatory = $true)]
        [string]$Url,

        [Parameter(Mandatory = $true)]
        [System.Collections.IDictionary]$ScriptBlock,

        [Parameter(Mandatory = $false)]
        [string]$MutexName = 'WebAppUpdateMutex',

        [Parameter(Mandatory = $false)]
        [switch]$Force,

        [Parameter(Mandatory = $false)]
        [string]$LogFile,

        [Parameter(Mandatory = $false)]
        [string]$SmtpServer,

        [Parameter(Mandatory = $false)]
        [string]$SmtpFrom,

        [Parameter(Mandatory = $false)]
        [string[]]$To,

        [Parameter(Mandatory = $false)]
        [string[]]$Cc
    )
    Begin {
        Write-Verbose "[$($MyInvocation.MyCommand.Name)] Function started"
        $OldDirectory = Get-Location

        $PullScriptBlock = [ordered]@{
            Cleaning = {
                Write-Verbose '> Cleaning up!'
                Reset-GitRepository
                Set-GitCurrentBranch -Branch $Branch
                git clean -fd 2>&1
            }
            Fetching = {
                Write-Verbose '> Fetching!'
                git fetch origin $Branch 2>&1
            }
            Pulling = {
                Write-Verbose '> Updating!'
                git reset --hard "origin/$Branch" 2>&1
            }
            Reporting = {
                Write-Verbose '> Reporting!'
                git log --abbrev-commit --decorate --format=format:'%h - (%ar) %s - %an%d' -10
            }
        }

        $Progress = @{
            Activity         = "$Alias ($Url)"
            Status           = 'Updating'
            CurrentOperation = 'Initializing'
            PercentComplete  = 0
        }
        Write-Progress @Progress
    }
    End {
        $Progress.Completed = $true
        Write-Progress @Progress
        Write-Verbose "[$($MyInvocation.MyCommand.Name)] Done"
        Set-Location $OldDirectory
        Write-Verbose "[$($MyInvocation.MyCommand.Name)] Function ended"
    }
    Process {
        if ($LogFile) {
            Write-Output "`n$((Get-Date -UFormat '%d/%m/%Y %T'))" | Out-File $LogFile
        }

        $Mutex = New-Object System.Threading.Mutex($false, $MutexName)
        try {
            Write-Verbose "[$($MyInvocation.MyCommand.Name)] Updating $Alias ($Url)"

            # Wait for mutex
            Write-Verbose "[$($MyInvocation.MyCommand.Name)] Waiting for mutex..."
            $Progress.Status = 'Waiting'
            Write-Progress @Progress
            $Mutex.WaitOne() | Out-Null

            # Set working directory
            Write-Verbose "[$($MyInvocation.MyCommand.Name)] Entering directory $Directory"
            Set-Location $Directory -ErrorAction Stop

            # Check if update is needed
            $updateNeeded = $false
            if ($Force) {
                $updateNeeded = $true
            } else {
                Sync-GitRepository
                $status = git status 2>$null
                $updateNeeded = ($status | Select-String -Pattern "^Your branch is up to date with 'origin/" | Measure-Object).Count -eq 0
            }

            $Progress.Status = 'Updating'
            Write-Progress @Progress

            # Send start notification
            if ($SmtpServer -and $SmtpFrom -and $To) {
                $mailParams = @{
                    Subject    = "Upgrade started ($Alias)"
                    Body       = "Upgrade of $Url has started"
                    SmtpServer = $SmtpServer
                    From       = $SmtpFrom
                    To         = $To
                    Priority   = 'Low'
                    BodyAsHtml = $true
                }
                Send-MailMessage @mailParams -ErrorAction SilentlyContinue
            }

            $HowManySteps = $ScriptBlock.Count
            if ($updateNeeded) {
                $HowManySteps += $PullScriptBlock.Count
                Write-Verbose "[$($MyInvocation.MyCommand.Name)] Updating code"
                $PullScriptBlock.GetEnumerator() | ForEach-Object {
                    $Progress.CurrentOperation = "Task: $($_.Key)"
                    Write-Progress @Progress

                    $scriptLog = & $_.Value

                    if ($LogFile) {
                        $scriptLog | Out-File -Append $LogFile
                    }

                    $Progress.PercentComplete += [int](100 / $HowManySteps)
                    if ($Progress.PercentComplete -gt 100) { $Progress.PercentComplete = 100 }
                    Write-Progress @Progress
                }
            } else {
                Write-Verbose "[$($MyInvocation.MyCommand.Name)] Code already up-to-date, skipping!"
            }

            # Run build pipeline
            Write-Verbose "[$($MyInvocation.MyCommand.Name)] Running pipeline"
            $ScriptBlock.GetEnumerator() | ForEach-Object {
                $Progress.CurrentOperation = "Task: $($_.Key)"
                Write-Progress @Progress

                $scriptLog = & $_.Value

                if ($LogFile) {
                    $scriptLog | Out-File -Append $LogFile
                }

                $Progress.PercentComplete += [int](100 / $HowManySteps)
                if ($Progress.PercentComplete -gt 100) { $Progress.PercentComplete = 100 }
                Write-Progress @Progress
            }

            # Send completion notification
            if ($SmtpServer -and $SmtpFrom -and $To) {
                $mailParams = @{
                    Subject    = "Upgrade complete ($Alias)"
                    Body       = "Upgrade of $Url is complete"
                    SmtpServer = $SmtpServer
                    From       = $SmtpFrom
                    To         = $To
                    Priority   = 'Low'
                    BodyAsHtml = $true
                }
                if ($Cc) { $mailParams['Cc'] = $Cc }
                if ($LogFile -and (Test-Path $LogFile)) {
                    $mailParams.Body += "`n`n" + (Get-Content $LogFile -Raw)
                }
                Send-MailMessage @mailParams -ErrorAction SilentlyContinue
            }
        } catch {
            Write-Verbose "[$($MyInvocation.MyCommand.Name)] Error occurred!"

            if ($LogFile) {
                "[$($_.Exception.GetType().FullName)]" | Out-File -Append $LogFile
                $_.Exception.Message | Out-File -Append $LogFile
            }

            # Send error notification
            if ($SmtpServer -and $SmtpFrom -and $To) {
                $mailParams = @{
                    Subject    = "Error during upgrade ($Alias)"
                    Body       = "An error occurred during upgrade of $Url`n`nERROR: $($_.Exception.Message)"
                    SmtpServer = $SmtpServer
                    From       = $SmtpFrom
                    To         = $To
                    Priority   = 'High'
                    BodyAsHtml = $true
                }
                Send-MailMessage @mailParams -ErrorAction SilentlyContinue
            }
            throw
        } finally {
            $Mutex.ReleaseMutex()
        }
    }
}