Private/Wait-Win32ToolkitUploadState.ps1

function Wait-Win32ToolkitUploadState {
    <#
    .SYNOPSIS
        Polls an Intune mobileAppContentFile until it reaches a target uploadState, with bounded
        exponential back-off and a caller-supplied timeout.
    .DESCRIPTION
        Two steps of the Win32 LOB upload sequence are asynchronous on Intune's side: the Azure Storage
        SAS URI request, and the file commit. Both are observed the same way — GET the file entry and
        look at its `uploadState` — so both go through this single helper and cannot drift apart.
 
        Previously each step ran its own `for ($i = 0; $i -lt 20; $i++) { Start-Sleep -Seconds 3 ... }`
        loop, i.e. a hard 60-second ceiling with no way to raise it. A slow tenant, or the commit of a
        large (200 MB+) package, routinely needs longer than that — and the failure landed AFTER the
        blob had already been uploaded, throwing the whole upload away.
 
        Behaviour:
        - Sleeps BEFORE the first poll (Intune never answers instantly).
        - Back-off: InitialDelaySeconds, doubling, capped at MaxDelaySeconds. A slow tenant is polled
          patiently instead of 20 times in a minute.
        - The final delay is clamped so the total time slept never exceeds TimeoutSeconds.
        - Any uploadState containing 'Error' / 'Fail' throws immediately (no point waiting it out).
        - On timeout the message quotes the ACTUAL elapsed seconds and the configured timeout.
    .PARAMETER FileUri
        Graph URI of the mobileAppContentFile entry (…/contentVersions/{v}/files/{id}).
    .PARAMETER TargetState
        The uploadState that means success, e.g. 'azureStorageUriRequestSuccess' or 'commitFileSuccess'.
    .PARAMETER Activity
        Human-readable name of what is being waited for, used in the messages. Phrase it so it reads
        after "waiting for" — e.g. 'the Azure Storage SAS URI'.
    .PARAMETER TimeoutSeconds
        Total seconds to wait before giving up.
    .PARAMETER InitialDelaySeconds
        First back-off delay (default 2 s).
    .PARAMETER MaxDelaySeconds
        Upper bound the doubling back-off saturates at (default 15 s).
    .OUTPUTS
        [pscustomobject] the Graph file entry as returned by the poll that reached TargetState.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$FileUri,

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

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

        [ValidateRange(5, 7200)]
        [int]$TimeoutSeconds = 300,

        [ValidateRange(1, 60)]
        [int]$InitialDelaySeconds = 2,

        [ValidateRange(1, 300)]
        [int]$MaxDelaySeconds = 15
    )

    $delay     = [math]::Min($InitialDelaySeconds, $MaxDelaySeconds)
    $elapsed   = 0
    $lastState = '(none)'

    while ($true) {
        # Clamp so the total slept time can never overshoot the configured timeout.
        if (($elapsed + $delay) -gt $TimeoutSeconds) { $delay = $TimeoutSeconds - $elapsed }

        Start-Sleep -Seconds $delay
        $elapsed += $delay

        $poll = Invoke-MgGraphRequest -Method GET -Uri $FileUri -OutputType PSObject
        if ($poll -and $poll.uploadState) { $lastState = [string]$poll.uploadState } else { $lastState = '(unknown)' }

        if ($lastState -eq $TargetState) { return $poll }

        if ($lastState -like '*Error*' -or $lastState -like '*Fail*') {
            throw "Intune reported a failure while waiting for $Activity. Upload state: $lastState"
        }

        if ($elapsed -ge $TimeoutSeconds) { break }

        Write-Verbose " Waiting... (state: $lastState, ${elapsed}s / ${TimeoutSeconds}s)"
        $delay = [math]::Min($delay * 2, $MaxDelaySeconds)
    }

    throw ("Timed out after $elapsed s waiting for $Activity (configured timeout: $TimeoutSeconds s; " +
           "last upload state: $lastState). The content may still be uploading on Intune's side — " +
           'raise -TimeoutSeconds for large packages or a slow tenant.')
}