AppHandling/Publish-PerTenantExtensionApps.ps1

<#
 .Synopsis
  Preview function for publishing PTE apps to an online tenant
 .Description
  Preview function for publishing PTE apps to an online tenant
#>

function Publish-PerTenantExtensionApps {
    Param(
        [Parameter(Mandatory=$true)]
        [string] $clientId,
        [Parameter(Mandatory=$true)]
        [string] $clientSecret,
        [Parameter(Mandatory=$true)]
        [string] $tenantId,
        [Parameter(Mandatory=$true)]
        [string] $environment,
        [Parameter(Mandatory=$false)]
        [string] $companyName,
        [Parameter(Mandatory=$true)]
        $appFiles,
        [switch] $useNewLine
    )

    $newLine = @{}
    if (!$useNewLine) {
        $newLine = @{ "NoNewLine" = $true }
    }

    if ($appFiles -is [String]) { $appFiles = @($appFiles.Split(',').Trim() | Where-Object { $_ }) }

    $appFolder = Join-Path $ENV:TEMP ([guid]::NewGuid().ToString())
    New-Item $appFolder -ItemType Directory | Out-Null
    try {
        $appFiles | % {
            $appFile = $_
         
            $tempFile = ""
            if ($appFile -like "http://*" -or $appFile -like "https://*") {
                $tempFile = Join-Path $ENV:TEMP "$([guid]::NewGuid().ToString()).zip"
                (New-Object System.Net.WebClient).DownloadFile($appFile, $tempFile)
                $appFile = $tempFile
            }
    
            if ([string]::new([char[]](Get-Content $appFile -Encoding byte -TotalCount 2)) -eq "PK") {
                $zipFolder = Join-Path $ENV:TEMP ([guid]::NewGuid().ToString())
                Expand-Archive $appFile -DestinationPath $zipFolder -Force
                Get-ChildItem -Path $zipFolder -Filter '*.app' -Recurse | ForEach-Object { Copy-Item $_.FullName $appFolder }
                Remove-Item $zipFolder -Recurse -Force
            }
            else {
                $appName = [System.IO.Path]::GetFileName($appFile)
                if ($appName -notlike '*.app') { $appName += '.app' }
                Copy-Item -Path $appFile -Destination (Join-Path $appFolder $appName)
            }
    
            if ($tempFile) {
                Remove-Item $tempFile -Force
            }
        }
    
        $appFiles = Get-Item -Path (Join-Path $appFolder '*.app') | ForEach-Object { $_.FullName }
    
        $loginURL     = "https://login.microsoftonline.com"
        $scopes       = "https://api.businesscentral.dynamics.com/.default"
        $baseUrl      = "https://api.businesscentral.dynamics.com/v2.0/$environment/api/microsoft/automation/v1.0"
        
        Write-Host "Authenticating to $tenantId using $ClientId"
        $body = @{grant_type="client_credentials";scope=$scopes;client_id=$ClientID;client_secret=$ClientSecret}
        $oauth = Invoke-RestMethod -Method Post -Uri $("$loginURL/$tenantId/oauth2/v2.0/token") -Body $body
        $authHeaders = @{ "Authorization" = "Bearer $($oauth.access_token)" }
        Write-Host "Authenticated"
        
        $companies = Invoke-RestMethod -Headers $authHeaders -Method Get -Uri "$baseurl/companies"
        $company = $companies.value | Where-Object { ($companyName -eq "") -or ($_.name -eq $companyName) } | Select-Object -First 1
        if (!($company)) {
            throw "No company $companyName"
        }
        $companyId = $company.id
        Write-Host "Company $companyName has id $companyId"
        
        $getExtensions = Invoke-WebRequest -Headers $authHeaders -Method Get -Uri "$baseUrl/companies($companyId)/extensions"
        $extensions = (ConvertFrom-Json $getExtensions.Content).value | Sort-Object -Property DisplayName
        
        Write-Host "Extensions before:"
        $extensions | % { Write-Host " - $($_.DisplayName), Version $($_.versionMajor).$($_.versionMinor).$($_.versionBuild).$($_.versionRevision), Installed=$($_.isInstalled)" }
        Write-Host
        
        try {
            Sort-AppFilesByDependencies -appFiles $appFiles | ForEach-Object {
                Write-Host "$([System.IO.Path]::GetFileName($_))"
                $tempFolder = Join-Path $ENV:TEMP ([guid]::NewGuid().ToString())
                Extract-AppFileToFolder -appFilename $_ -appFolder $tempFolder -generateAppJson 6> $null
                $appJsonFile = Join-Path $tempFolder "app.json"
                $appJson = Get-Content $appJsonFile | ConvertFrom-Json
                Remove-Item -Path $tempFolder -Force -Recurse
            
                Write-Host @newLine "Publishing and Installing"
                Invoke-WebRequest -Headers ($authHeaders+(@{"If-Match" = "*"})) `
                    -Method Patch `
                    -Uri "$baseUrl/companies($companyId)/extensionUpload(0)/content" `
                    -ContentType "application/octet-stream" `
                    -InFile $_ | Out-Null
                Write-Host @newLine "."    
                $completed = $false
                $errCount = 0
                while (!$completed)
                {
                    Start-Sleep -Seconds 5
                    try {
                        $extensionDeploymentStatusResponse = Invoke-WebRequest -Headers $authHeaders -Method Get -Uri "$baseUrl/companies($companyId)/extensionDeploymentStatus"
                        $extensionDeploymentStatuses = (ConvertFrom-Json $extensionDeploymentStatusResponse.Content).value
                        $completed = $true
                        $extensionDeploymentStatuses | Where-Object { $_.publisher -eq $appJson.publisher -and $_.name -eq $appJson.name -and $_.appVersion -eq $appJson.version } | % {
                            if ($_.status -eq "InProgress") {
                                Write-Host @newLine "."
                                $completed = $false
                            }
                            elseif ($_.Status -ne "Completed") {
                                $errCount = 5
                                throw $_.status
                            }
                        }
                        $errCount = 0
                    }
                    catch {
                        if ($errCount++ -gt 3) {
                            Write-Host $_.Exception.Message
                            throw "Unable to publish app"
                        }
                        $completed = $false
                    }
                }
                if ($completed) {
                    Write-Host "completed"
                }
            }
        }
        finally {
            $getExtensions = Invoke-WebRequest -Headers $authHeaders -Method Get -Uri "$baseUrl/companies($companyId)/extensions"
            $extensions = (ConvertFrom-Json $getExtensions.Content).value | Sort-Object -Property DisplayName
            
            Write-Host
            Write-Host "Extensions after:"
            $extensions | % { Write-Host " - $($_.DisplayName), Version $($_.versionMajor).$($_.versionMinor).$($_.versionBuild).$($_.versionRevision), Installed=$($_.isInstalled)" }
        }
    }
    finally {
        Remove-Item $appFolder -Recurse -Force
    }
}
Export-ModuleMember -Function Publish-PerTenantExtensionApps