Private/Invoke-M365AppShellAppBuild.ps1

#Requires -Version 5.1
<#
.SYNOPSIS
    Downloads setup.exe for Microsoft 365 Apps via Evergreen, updates the
    configuration XML, and creates a zip archive for Nerdio Shell App import.
 
.DESCRIPTION
    Uses Get-EvergreenApp -Name Microsoft365Apps to retrieve the latest setup.exe
    for the specified channel. Copies the configuration XML (renamed to
    Install-Microsoft365Apps.xml) and Uninstall-Microsoft365Apps.xml to a working
    source folder, updates the XML with Channel, TenantId, and CompanyName, then
    calls Compress-Archive to produce a zip containing setup.exe,
    Install-Microsoft365Apps.xml, and Uninstall-Microsoft365Apps.xml.
 
    No IntuneWin32App module dependency.
 
.PARAMETER ConfigRow
    A configuration row object returned by Get-M365AppConfigurations, containing
    FileName, FilePath, DisplayName, and ConfigId.
 
.PARAMETER ConfigDirectoryPath
    Path to the directory containing the M365 XML configuration files. Must
    include Uninstall-Microsoft365Apps.xml.
 
.PARAMETER Channel
    The update channel to set in the XML (e.g. MonthlyEnterprise, Current).
 
.PARAMETER CompanyName
    The company name written to the AppSettings/Setup element in the XML.
 
.PARAMETER TenantId
    The Entra ID tenant ID written to the TenantId Property element in the XML.
 
.PARAMETER WorkingPath
    Root working directory. A subdirectory named after the package is created here.
 
.PARAMETER SyncHash
    Synchronized hashtable used by Write-UILog for thread-safe UI log output.
 
.OUTPUTS
    PSCustomObject with properties: Succeeded, ZipPath, Version, SourcePath, Error.
#>

function Invoke-M365AppShellAppBuild {
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)]
        [PSCustomObject]$ConfigRow,

        [Parameter(Mandatory)]
        [string]$ConfigDirectoryPath,

        [Parameter(Mandatory)]
        [string]$Channel,

        [Parameter(Mandatory)]
        [string]$CompanyName,

        [Parameter(Mandatory)]
        [string]$TenantId,

        [Parameter(Mandatory)]
        [string]$WorkingPath,

        [Parameter(Mandatory)]
        [System.Collections.Hashtable]$SyncHash
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    $fail = {
        param([string]$Msg)
        Write-UILog -SyncHash $SyncHash -Message "M365 Shell App build: $Msg" -Level Error
        return [PSCustomObject]@{
            Succeeded  = $false
            ZipPath    = ''
            Version    = ''
            SourcePath = ''
            Error      = $Msg
        }
    }

    try {
        # Resolve latest Evergreen entry for the requested channel
        Write-UILog -SyncHash $SyncHash -Message "M365: Querying Evergreen for Microsoft365Apps channel '$Channel'..." -Level Info
        $evergreenRows = Get-EvergreenApp -Name 'Microsoft365Apps' -ErrorAction Stop
        $channelRow    = $evergreenRows |
            Where-Object { $_.Channel -eq $Channel } |
            Sort-Object -Property { [System.Version]$_.Version } -Descending |
            Select-Object -First 1

        if ($null -eq $channelRow) {
            return (& $fail "No Evergreen entry found for Microsoft365Apps channel '$Channel'.")
        }

        $version = [string]$channelRow.Version
        Write-UILog -SyncHash $SyncHash -Message "M365: Latest version for '$Channel': $version" -Level Info

        # Sanitise display name for use as a folder name
        $safeName   = ($ConfigRow.DisplayName -replace '[\\/:*?"<>|]', '_').Trim('_')
        $sourcePath = Join-Path -Path $WorkingPath -ChildPath ($safeName + '\Source')

        if (-not (Test-Path -LiteralPath $sourcePath -PathType Container)) {
            $null = New-Item -Path $sourcePath -ItemType Directory -Force -ErrorAction Stop
        }

        # Download setup.exe via Evergreen
        Write-UILog -SyncHash $SyncHash -Message 'M365: Downloading setup.exe via Evergreen...' -Level Info
        $null = $channelRow | Save-EvergreenApp -CustomPath $sourcePath -ErrorAction Stop

        $setupFile = Join-Path -Path $sourcePath -ChildPath 'setup.exe'
        if (-not (Test-Path -LiteralPath $setupFile -PathType Leaf)) {
            return (& $fail "setup.exe was not found in '$sourcePath' after download.")
        }
        Write-UILog -SyncHash $SyncHash -Message "M365: setup.exe downloaded to '$sourcePath'." -Level Info

        # Copy and rename the install config XML
        $installXmlDest = Join-Path -Path $sourcePath -ChildPath 'Install-Microsoft365Apps.xml'
        Copy-Item -LiteralPath $ConfigRow.FilePath -Destination $installXmlDest -Force -ErrorAction Stop

        # Copy the uninstall XML -- required for zip
        $uninstallSrc = Join-Path -Path $ConfigDirectoryPath -ChildPath 'Uninstall-Microsoft365Apps.xml'
        if (-not (Test-Path -LiteralPath $uninstallSrc -PathType Leaf)) {
            return (& $fail "Uninstall-Microsoft365Apps.xml not found in '$ConfigDirectoryPath'.")
        }
        $uninstallDest = Join-Path -Path $sourcePath -ChildPath 'Uninstall-Microsoft365Apps.xml'
        Copy-Item -LiteralPath $uninstallSrc -Destination $uninstallDest -Force -ErrorAction Stop

        # Update Install-Microsoft365Apps.xml with caller-supplied values
        Write-UILog -SyncHash $SyncHash -Message 'M365: Updating configuration XML with channel, tenant ID, and company name...' -Level Info
        [xml]$xml = Get-Content -LiteralPath $installXmlDest -Raw -ErrorAction Stop

        $xml.Configuration.Add.Channel = $Channel

        $tenantProp = $xml.Configuration.Property |
            Where-Object { $_.Name -eq 'TenantId' } |
            Select-Object -First 1
        if ($null -ne $tenantProp) {
            $tenantProp.Value = $TenantId
        }

        if ($null -ne $xml.Configuration.AppSettings -and $null -ne $xml.Configuration.AppSettings.Setup) {
            $xml.Configuration.AppSettings.Setup.Value = $CompanyName
        }

        $xml.Save($installXmlDest)
        Write-UILog -SyncHash $SyncHash -Message 'M365: Configuration XML updated.' -Level Info

        # Build zip archive containing setup.exe and both XMLs
        $zipPath = Join-Path -Path $WorkingPath -ChildPath ($safeName + '\Microsoft365Apps.zip')
        $zipDir  = Split-Path -Path $zipPath -Parent
        if (-not (Test-Path -LiteralPath $zipDir -PathType Container)) {
            $null = New-Item -Path $zipDir -ItemType Directory -Force -ErrorAction Stop
        }

        if (Test-Path -LiteralPath $zipPath -PathType Leaf) {
            Remove-Item -LiteralPath $zipPath -Force -ErrorAction Stop
        }

        Write-UILog -SyncHash $SyncHash -Message 'M365: Creating zip archive...' -Level Info

        $filesToZip = @(
            $setupFile
            $installXmlDest
            $uninstallDest
        )
        Compress-Archive -Path $filesToZip -DestinationPath $zipPath -ErrorAction Stop

        if (-not (Test-Path -LiteralPath $zipPath -PathType Leaf)) {
            return (& $fail "Zip archive was not created at '$zipPath'.")
        }

        Write-UILog -SyncHash $SyncHash -Message "M365: Zip archive created: $zipPath" -Level Info

        return [PSCustomObject]@{
            Succeeded  = $true
            ZipPath    = $zipPath
            Version    = $version
            SourcePath = $sourcePath
            Error      = ''
        }
    }
    catch {
        return (& $fail $_.Exception.Message)
    }
}