Public/Publish-IntuneAppPackage.ps1

function Publish-IntuneAppPackage {
    <#
    .SYNOPSIS
        Publishes an Intune Win32 application package (.intunewin and its JSON configuration) to Microsoft Intune.

    .DESCRIPTION
        The Publish-IntuneAppPackage function reads the application configuration JSON created by
        New-IntuneApplication and publishes the .intunewin package with Microsoft Graph:

          - When no app with the same display name exists, it creates the Win32 app and uploads the
            package (see New-IntuneWin32Application).
          - When a Win32 app with the same display name exists, it stops unless -Force is used; with
            -Force it uploads the package as a new content version of the existing app.

        The detection and requirement rules come from -Rules or, when omitted, from
        DetectionRuleConfig and RequirementRuleConfig in the JSON file. They must be rules created with
        New-IntuneWin32Rule (hashtables with an '@odata.type').

        Requires the Microsoft.Graph.Authentication module and a connection made with
        Connect-MgGraph -Scopes DeviceManagementApps.ReadWrite.All.

    .PARAMETER IntuneAppJSONPath
        The path to the application configuration JSON file created by New-IntuneApplication.

    .PARAMETER IntuneWinPath
        The path to the .intunewin package file.

    .PARAMETER Force
        Upload the package as a new content version when an app with the same display name exists.

    .PARAMETER NoTenantDetails
        Do not show the tenant connection details.

    .PARAMETER Rules
        Detection and requirement rules created with New-IntuneWin32Rule. Overrides the rules in the JSON file.

    .PARAMETER PollIntervalSeconds
        How often to check the upload and commit state. Default is 5 seconds.

    .PARAMETER TimeoutSeconds
        How long to wait for each upload or commit state. Default is 600 seconds.

    .OUTPUTS
        System.Management.Automation.PSCustomObject
        The Win32 app (id, displayName and committedContentVersion).

    .EXAMPLE
        Publish-IntuneAppPackage -IntuneAppJSONPath "C:\Packages\MyApp.1.0.json" -IntuneWinPath "C:\Packages\setup.intunewin"

        Creates the MyApp Win32 app in Intune and uploads setup.intunewin.

    .EXAMPLE
        Publish-IntuneAppPackage -IntuneAppJSONPath "C:\Packages\MyApp.1.1.json" -IntuneWinPath "C:\Packages\setup.intunewin" -Force -NoTenantDetails

        Uploads setup.intunewin as a new content version of the existing MyApp app.

    .NOTES
        Requires connection to Microsoft Graph using Connect-MgGraph before running this function.

    .LINK
        New-IntuneWin32Application
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript({ Test-Path -Path $_ -PathType Leaf })]
        [string]$IntuneAppJSONPath,

        [Parameter(Mandatory = $true)]
        [ValidateScript({ Test-Path -Path $_ -PathType Leaf })]
        [string]$IntuneWinPath,

        [switch]$Force,

        [switch]$NoTenantDetails,

        [hashtable[]]$Rules,

        [ValidateRange(0, 300)]
        [int]$PollIntervalSeconds = 5,

        [ValidateRange(1, 86400)]
        [int]$TimeoutSeconds = 600
    )
    process {
        $TelemetryArgs = @{
            ModuleName    = $MyInvocation.MyCommand.Module.Name
            ModuleVersion = [string]$MyInvocation.MyCommand.Module.Version
            CommandName   = $MyInvocation.MyCommand.Name
            ExecutionID   = [guid]::NewGuid().ToString()
        }
        Invoke-TelemetryCollection @TelemetryArgs -Stage Start -ClearTimer
        try {
            $GraphContext = Assert-MgGraphConnection
            $AppParameters = (Get-Content -Path $IntuneAppJSONPath -Raw -ErrorAction Stop | ConvertFrom-Json).ApplicationParameters
            if (-not $AppParameters -or [string]::IsNullOrEmpty($AppParameters.ApplicationName)) {
                throw "'$IntuneAppJSONPath' does not contain ApplicationParameters.ApplicationName."
            }
            $DisplayName = [string]$AppParameters.ApplicationName

            if (-not $NoTenantDetails) {
                Write-Information -MessageData ("You're connected to Microsoft Graph as:`n`tUserName: $($GraphContext.Account)`n`t" +
                    "Context scope: $($GraphContext.ContextScope)`n`tTenantId: $($GraphContext.TenantId)") -InformationAction Continue
            }

            # Check if a Win32 app with the same display name already exists
            $Filter = [System.Uri]::EscapeDataString("displayName eq '$($DisplayName.Replace("'", "''"))'")
            $Response = Invoke-MgGraphRequest -Method GET -Uri "v1.0/deviceAppManagement/mobileApps?`$filter=$Filter" -OutputType PSObject -ErrorAction Stop
            $Existing = @($Response.value | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.win32LobApp' }) | Select-Object -First 1

            if ($Existing) {
                if (-not $Force) {
                    throw "The application '$DisplayName' already exists in Intune ($($Existing.id)). Use -Force to upload the package as a new content version."
                }
                if (-not $PSCmdlet.ShouldProcess("$DisplayName ($($Existing.id))", "Upload '$IntuneWinPath' as a new content version")) {
                    Invoke-TelemetryCollection @TelemetryArgs -Stage End
                    return
                }
                $ContentVersion = Publish-IntuneWin32AppContent -AppId $Existing.id -IntuneWinPath $IntuneWinPath -PollIntervalSeconds $PollIntervalSeconds -TimeoutSeconds $TimeoutSeconds -ErrorAction Stop
                [PSCustomObject]@{
                    id                      = $Existing.id
                    displayName             = $Existing.displayName
                    committedContentVersion = $ContentVersion
                }
                Invoke-TelemetryCollection @TelemetryArgs -Stage End
                return
            }

            # Convert the rules in the JSON file (PSCustomObjects) to hashtables
            if (-not $PSBoundParameters.ContainsKey('Rules')) {
                $Rules = foreach ($Config in @($AppParameters.DetectionRuleConfig, $AppParameters.RequirementRuleConfig)) {
                    foreach ($Rule in @($Config | Where-Object { $_ })) {
                        $Hashtable = @{}
                        foreach ($Property in $Rule.PSObject.Properties) {
                            $Hashtable[$Property.Name] = $Property.Value
                        }
                        if (-not $Hashtable.ContainsKey('@odata.type')) {
                            throw "The rule configuration in '$IntuneAppJSONPath' is not a Win32 app rule. Create the rules with New-IntuneWin32Rule, or pass them with -Rules."
                        }
                        $Hashtable
                    }
                }
            }

            $Splat = @{
                Name                 = $DisplayName
                Description          = [string]$AppParameters.Description
                Publisher            = [string]$AppParameters.Publisher
                InstallCommandLine   = [string]$AppParameters.InstallCommand
                UninstallCommandLine = [string]$AppParameters.UninstallCommand
                Rules                = @($Rules)
                IntuneWinFilePath    = $IntuneWinPath
                PollIntervalSeconds  = $PollIntervalSeconds
                TimeoutSeconds       = $TimeoutSeconds
                ErrorAction          = 'Stop'
            }
            foreach ($Pair in @(
                    @('Developer', 'Developer'), @('Owner', 'Owner'), @('Notes', 'Notes'), @('IsFeatured', 'IsFeatured'),
                    @('InstallFor', 'InstallExperienceRunAsAccount'), @('RestartBehavior', 'InstallExperienceDeviceRestartBehavior'), @('LogoPath', 'IconFilePath'))) {
                $Value = $AppParameters.($Pair[0])
                if ($null -ne $Value -and "$Value" -ne '') {
                    if ($Pair[1] -eq 'InstallExperienceRunAsAccount') {
                        $Value = ([string]$Value).ToLowerInvariant()
                    }
                    $Splat[$Pair[1]] = $Value
                }
            }
            $ParsedVersion = $null
            if ([version]::TryParse([string]$AppParameters.Version, [ref]$ParsedVersion)) {
                $Splat['Version'] = $ParsedVersion
            }

            New-IntuneWin32Application @Splat
            Invoke-TelemetryCollection @TelemetryArgs -Stage End
        }
        catch {
            Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $_
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}