Private/Invoke-Win32ToolkitFinalize.ps1

function Invoke-Win32ToolkitFinalize {
    <#
    .SYNOPSIS
        Runs the back half of the packaging pipeline for a scaffolded project.
    .DESCRIPTION
        Shared "finalize" tail used by both Invoke-Win32Toolkit (winget flow) and
        Complete-Win32ToolkitManualApp (manual flow), so the two cannot drift:
 
          1. New-TargetedDocumentation — launch the documentation sandbox (installs via the
             project's Invoke-AppDeployToolkit.ps1 and captures the install changes). Returns the
             expected capture path (truthy string) on success, $false on failure.
          2. Wait-ForDocumentationAndProcess -ExpectedJsonPath — waits for exactly this run's
             capture (never a stale one), then writes AppConfig.Uninstall, the requirement script,
             and processes-to-close from it.
          3. Optional -RunTest scenarios, then -PackageIntune / -PublishIntune.
 
        See knowledge-base/designs/manual-app-packaging.md.
    .PARAMETER ProjectPath
        Full path to the scaffolded PSADT project.
    .PARAMETER ProjectName
        Project (folder) name — used for the sandbox documentation config.
    .PARAMETER AppInfo
        App metadata object (passed through to New-TargetedDocumentation).
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$ProjectPath,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$ProjectName,

        [object]$AppInfo,

        [ValidateSet('InstallUninstall', 'Update')]
        [string[]]$RunTest,

        # Update-test baseline selection, passed through to Test-Win32ToolkitProject so a scripted
        # pipeline never blocks on the interactive version picker mid-run. Omit both to keep the
        # interactive picker (current behavior). SpecificVersion wins when both are supplied.
        [ValidateRange(1, 1000)]
        [int]$UpdateVersionsBack,

        [string]$UpdateSpecificVersion,

        [switch]$PackageIntune,

        [switch]$PublishIntune,

        # Also publish the update app (2nd app, same version, requirement-gated to installed devices).
        [switch]$PublishUpdate
    )

    $filesPath = Join-Path $ProjectPath 'Files'

    # 1-2. Sandbox documentation capture → uninstall / requirement / processes.
    # New-TargetedDocumentation returns the EXPECTED capture path (truthy) on success, $false on
    # failure; passing it to the waiter makes the wait immune to stale captures from earlier runs.
    # Documentation capture follows the configured test backend (Sandbox default; HyperV opt-in, falls
    # back to Sandbox if the VM isn't ready). Under Sandbox the .wsb launches inside
    # New-TargetedDocumentation; under HyperV the script is prepared here and executed in the VM below
    # (revert → copy-in → run over PS Direct → copy-out), then the SAME waiter/consumer processes it.
    $backend = Get-Win32ToolkitTestBackend
    Write-Host "`nGenerating targeted installation documentation ($backend)..." -ForegroundColor Yellow
    $docSuccess = New-TargetedDocumentation -ProjectPath $ProjectPath -ProjectName $ProjectName -AppInfo $AppInfo -Backend $backend

    $captureReady = [bool]$docSuccess
    if ($docSuccess -and $backend -eq 'HyperV') {
        Write-Host 'Running documentation capture inside the Hyper-V VM...' -ForegroundColor Cyan
        $ran = Invoke-Win32ToolkitHyperVRun -ProjectPath $ProjectPath -Phase @(
            @{ Label = 'Document install changes'; Command = "& 'C:\PSADT\SupportFiles\TargetedDocumentationScript.ps1'" }
        ) -Output @('Documentation\InstallationChanges_*.json', 'Documentation\Targeted_Documentation_Log_*.txt', 'Sandbox\Logs\*')
        if (-not $ran) {
            $captureReady = $false
            Write-Warning 'Hyper-V documentation capture did not complete — skipping processing. Review the VM and its logs.'
        }
    }

    if ($captureReady) {
        Write-Host '✓ Targeted documentation setup completed!' -ForegroundColor Green
        Write-Host "`nWaiting for documentation completion..." -ForegroundColor Yellow
        $fileInfo      = Get-InstallerFileInfo -FilesPath $filesPath
        $jsonProcessed = Wait-ForDocumentationAndProcess -ProjectPath $ProjectPath -InstallerType $fileInfo.Type -ExpectedJsonPath $docSuccess
        if ($jsonProcessed) {
            Write-Host '✓ Documentation processing completed successfully!' -ForegroundColor Green
        }
        else {
            Write-Warning 'Documentation processing had issues - please review manually'
        }

        # Promote the icon extracted during the install run to Assets\AppIcon.png. A winget IconUrl or a
        # manual -IconPath already applied takes precedence (winget-primary); otherwise this is how manual
        # and iconless apps finally get a real Intune tile. Best-effort — never let it derail packaging.
        try { $null = Import-Win32ToolkitCapturedIcon -ProjectPath $ProjectPath }
        catch { Write-Warning "Captured-icon promotion skipped: $($_.Exception.Message)" }
    }
    else {
        Write-Warning 'Documentation generation had issues - please review manually'
    }

    # 3. Optional test scenarios. The Update scenario returns a verdict ($true/$false/$null) —
    # surface a failure loudly before any packaging/publishing continues.
    if ($RunTest) {
        foreach ($scenario in $RunTest) {
            $testArgs = @{ ProjectPath = $ProjectPath; Scenario = $scenario }
            if ($scenario -eq 'Update') {
                # Plumb the baseline choice through so automation never lands on the interactive picker.
                if ($UpdateSpecificVersion)   { $testArgs['SpecificVersion'] = $UpdateSpecificVersion }
                elseif ($UpdateVersionsBack)  { $testArgs['VersionsBack']    = $UpdateVersionsBack }
            }
            $verdict = Test-Win32ToolkitProject @testArgs
            if ($verdict -eq $false) {
                $assertLog = if ($scenario -eq 'InstallUninstall') { 'InstallAssertions.log' } else { 'UpdateAssertions.log' }
                Write-Warning "The $scenario test FAILED its assertions — review $ProjectPath\Sandbox\Logs\$assertLog before publishing this package."
            }
            elseif ($null -eq $verdict) {
                # $null means NO verdict — the test errored before running (e.g. a sandbox that never
                # freed), timed out, or produced no assertions. Packaging continues (matching the
                # $false policy of warn-don't-block), but a run that never happened must never read
                # as a pass.
                Write-Warning "The $scenario test produced NO verdict — it may never have run (see the errors above). The package is about to be processed UNTESTED for this scenario."
            }
        }
    }

    # 3. Optional package / publish (-PublishIntune / -PublishUpdate imply packaging).
    if ($PackageIntune -or $PublishIntune -or $PublishUpdate) {
        Export-Win32ToolkitIntuneWin -ProjectPath $ProjectPath -PublishIntune:$PublishIntune -PublishUpdate:$PublishUpdate
    }
}