Resfeber Eudaimonia
Resfeber Eudaimonia

Reputation: 106

How to upload the build on microsoft intune through azure devops pipelines

How to upload the build on Microsoft intune through azure DevOps pipelines. I am a microsoft VS App center user where i'm distributing the non prod apps to testers, as many of you know VS app center is going to retire on March 31, 2025, we are looking for some alternatives and landed on intune. Altough intune is not supportive for non-prod build as it doesnt have store functionality. Intune mainly uses for MDM builds and secondly for uploading the build to Google Play and Apple app store, for which you've upload the build first at intune. here is the solution to upload the build using azure pipelines.

Upvotes: 1

Views: 117

Answers (1)

Ziyang Liu-MSFT
Ziyang Liu-MSFT

Reputation: 5211

To upload your app to Intune, you can refer to this step-by-step guidance Intune Line of Business App Upload. Below is how you can run the same Intune Graph API to upload your app to Intune using Azure DevOps pipeline.

steps:
...
{Your build steps. Ensure your IPA/APK/MSI is in $(System.DefaultWorkingDirectory)}
...
- powershell: |
    # tree $(Pipeline.Workspace) /f /a
    
    # Define variables for Intune app
    $appDisplayName = "$(appDisplayName)"
    $sourceFile = "$(System.DefaultWorkingDirectory)\$(appDisplayName).ipa"
    $publisher = "{publisher}"
    $bundleId = "$(bundleId)"
    $version = "1.0.0"
    $buildNumber = "$(Build.BuildId)"
    
    # Define authentication variables
    $tenantId = "$(tenantId)"
    $applicationId = "$(applicationId)"
    $clientSecret = "$(clientSecret)"
    
    # Get authentication token
    $body = @{
        grant_type    = "client_credentials"
        client_id     = $applicationId
        client_secret = $clientSecret
        scope         = "https://graph.microsoft.com/.default"
    } 
    $tokenResponse = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" -Method Post -Body $Body -ContentType "application/x-www-form-urlencoded"
    $accessToken = $tokenResponse.access_token
    $headers = @{
        Authorization = "Bearer $accessToken"
    }

    # # Install Microsoft Graph module if not already installed
    # if (-not (Get-Module -ListAvailable -Name Microsoft.Graph)) {
    #     Install-Module Microsoft.Graph -Scope CurrentUser -Force -AllowClobber
    # }

    # # Convert access token to SecureString
    # $secureAccessToken = ConvertTo-SecureString -String $accessToken -AsPlainText -Force
    # Connect to Microsoft Graph
    # Connect-MgGraph -AccessToken $secureAccessToken

    ####################################################
    ####################################################

    function UploadAzureStorageChunk($sasUri, $id, $body){

      $uri = "$sasUri&comp=block&blockid=$id";
      $request = "PUT $uri";

      $iso = [System.Text.Encoding]::GetEncoding("iso-8859-1");
      $encodedBody = $iso.GetString($body);
      $headers = @{
        "x-ms-blob-type" = "BlockBlob"
      };

      if ($logRequestUris) { Write-Host $request; }
      if ($logHeaders) { WriteHeaders $headers; }

      try
      {
        $response = Invoke-WebRequest $uri -Method Put -Headers $headers -Body $encodedBody;
      }
      catch
      {
        Write-Host -ForegroundColor Red $request;
        Write-Host -ForegroundColor Red $_.Exception.Message;
        throw;
      }

    }
    ####################################################
    function FinalizeAzureStorageUpload($sasUri, $ids){

      $uri = "$sasUri&comp=blocklist";
      $request = "PUT $uri";

      $xml = '<?xml version="1.0" encoding="utf-8"?><BlockList>';
      foreach ($id in $ids)
      {
        $xml += "<Latest>$id</Latest>";
      }
      $xml += '</BlockList>';

      if ($logRequestUris) { Write-Host $request; }
      if ($logContent) { Write-Host -ForegroundColor Gray $xml; }

      try
      {
        Invoke-RestMethod $uri -Method Put -Body $xml;
      }
      catch
      {
        Write-Host -ForegroundColor Red $request;
        Write-Host -ForegroundColor Red $_.Exception.Message;
        throw;
      }
    }
    ####################################################
    function UploadFileToAzureStorage($sasUri, $filepath){

      # Chunk size = 1 MiB
        $chunkSizeInBytes = 1024 * 1024;

      # Read the whole file and find the total chunks.
      #[byte[]]$bytes = Get-Content $filepath -Encoding byte;
        # Using ReadAllBytes method as the Get-Content used alot of memory on the machine
        [byte[]]$bytes = [System.IO.File]::ReadAllBytes($filepath);
      $chunks = [Math]::Ceiling($bytes.Length / $chunkSizeInBytes);

      # Upload each chunk.
      $ids = @();
        $cc = 1

      for ($chunk = 0; $chunk -lt $chunks; $chunk++)
      {
            $id = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($chunk.ToString("0000")));
        $ids += $id;

        $start = $chunk * $chunkSizeInBytes;
        $end = [Math]::Min($start + $chunkSizeInBytes - 1, $bytes.Length - 1);
        $body = $bytes[$start..$end];

            Write-Progress -Activity "Uploading File to Azure Storage" -status "Uploading chunk $cc of $chunks" `
            -percentComplete ($cc / $chunks*100)
            $cc++

            $uploadResponse = UploadAzureStorageChunk $sasUri $id $body;


      }

        Write-Progress -Completed -Activity "Uploading File to Azure Storage"

        Write-Host

      # Finalize the upload.
      $uploadResponse = FinalizeAzureStorageUpload $sasUri $ids;
    }
    ####################################################
    function GenerateKey{

      try
      {
        $aes = [System.Security.Cryptography.Aes]::Create();
            $aesProvider = New-Object System.Security.Cryptography.AesCryptoServiceProvider;
            $aesProvider.GenerateKey();
            $aesProvider.Key;
      }
      finally
      {
        if ($aesProvider -ne $null) { $aesProvider.Dispose(); }
        if ($aes -ne $null) { $aes.Dispose(); }
      }
    }
    ####################################################
    function GenerateIV{

      try
      {
        $aes = [System.Security.Cryptography.Aes]::Create();
            $aes.IV;
      }
      finally
      {
        if ($aes -ne $null) { $aes.Dispose(); }
      }
    }
    ####################################################
    function EncryptFileWithIV($sourceFile, $targetFile, $encryptionKey, $hmacKey, $initializationVector){

      $bufferBlockSize = 1024 * 4;
      $computedMac = $null;

      try
      {
        $aes = [System.Security.Cryptography.Aes]::Create();
        $hmacSha256 = New-Object System.Security.Cryptography.HMACSHA256;
        $hmacSha256.Key = $hmacKey;
        $hmacLength = $hmacSha256.HashSize / 8;

        $buffer = New-Object byte[] $bufferBlockSize;
        $bytesRead = 0;

        $targetStream = [System.IO.File]::Open($targetFile, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write, [System.IO.FileShare]::Read);
        $targetStream.Write($buffer, 0, $hmacLength + $initializationVector.Length);

        try
        {
          $encryptor = $aes.CreateEncryptor($encryptionKey, $initializationVector);
          $sourceStream = [System.IO.File]::Open($sourceFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::Read);
          $cryptoStream = New-Object System.Security.Cryptography.CryptoStream -ArgumentList @($targetStream, $encryptor, [System.Security.Cryptography.CryptoStreamMode]::Write);

          $targetStream = $null;
          while (($bytesRead = $sourceStream.Read($buffer, 0, $bufferBlockSize)) -gt 0)
          {
            $cryptoStream.Write($buffer, 0, $bytesRead);
            $cryptoStream.Flush();
          }
          $cryptoStream.FlushFinalBlock();
        }
        finally
        {
          if ($cryptoStream -ne $null) { $cryptoStream.Dispose(); }
          if ($sourceStream -ne $null) { $sourceStream.Dispose(); }
          if ($encryptor -ne $null) { $encryptor.Dispose(); }   
        }

        try
        {
          $finalStream = [System.IO.File]::Open($targetFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::Read)

          $finalStream.Seek($hmacLength, [System.IO.SeekOrigin]::Begin) > $null;
          $finalStream.Write($initializationVector, 0, $initializationVector.Length);
          $finalStream.Seek($hmacLength, [System.IO.SeekOrigin]::Begin) > $null;

          $hmac = $hmacSha256.ComputeHash($finalStream);
          $computedMac = $hmac;

          $finalStream.Seek(0, [System.IO.SeekOrigin]::Begin) > $null;
          $finalStream.Write($hmac, 0, $hmac.Length);
        }
        finally
        {
          if ($finalStream -ne $null) { $finalStream.Dispose(); }
        }
      }
      finally
      {
        if ($targetStream -ne $null) { $targetStream.Dispose(); }
            if ($aes -ne $null) { $aes.Dispose(); }
      }

      $computedMac;
    }
    ####################################################
    function EncryptFile($sourceFile, $targetFile){

      $encryptionKey = GenerateKey;
      $hmacKey = GenerateKey;
      $initializationVector = GenerateIV;

      # Create the encrypted target file and compute the HMAC value.
      $mac = EncryptFileWithIV $sourceFile $targetFile $encryptionKey $hmacKey $initializationVector;

      # Compute the SHA256 hash of the source file and convert the result to bytes.
      $fileDigest = (Get-FileHash $sourceFile -Algorithm SHA256).Hash;
      $fileDigestBytes = New-Object byte[] ($fileDigest.Length / 2);
        for ($i = 0; $i -lt $fileDigest.Length; $i += 2)
      {
            $fileDigestBytes[$i / 2] = [System.Convert]::ToByte($fileDigest.Substring($i, 2), 16);
        }
      
      # Return an object that will serialize correctly to the file commit Graph API.
      $encryptionInfo = @{};
      $encryptionInfo.encryptionKey = [System.Convert]::ToBase64String($encryptionKey);
      $encryptionInfo.macKey = [System.Convert]::ToBase64String($hmacKey);
      $encryptionInfo.initializationVector = [System.Convert]::ToBase64String($initializationVector);
      $encryptionInfo.mac = [System.Convert]::ToBase64String($mac);
      $encryptionInfo.profileIdentifier = "ProfileVersion1";
      $encryptionInfo.fileDigest = [System.Convert]::ToBase64String($fileDigestBytes);
      $encryptionInfo.fileDigestAlgorithm = "SHA256";

      $fileEncryptionInfo = @{};
      $fileEncryptionInfo.fileEncryptionInfo = $encryptionInfo;

      $fileEncryptionInfo;

    }
    ####################################################
    function WaitForFileProcessing($fileUri, $stage){

      $attempts= 60;
      $waitTimeInSeconds = 1;

      $successState = "$($stage)Success";
      $pendingState = "$($stage)Pending";
      $failedState = "$($stage)Failed";
      $timedOutState = "$($stage)TimedOut";

      $file = $null;
      while ($attempts -gt 0)
      {
        $file = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/deviceAppManagement/$fileUri" -Method GET -Headers $headers -ContentType "application/json"
            
            # MakeGetRequest $fileUri;

        if ($file.uploadState -eq $successState)
        {
          break;
        }
        elseif ($file.uploadState -ne $pendingState)
        {
          throw "File upload state is not success: $($file.uploadState)";
        }

        Start-Sleep $waitTimeInSeconds;
        $attempts--;
      }

      if ($file -eq $null)
      {
        throw "File request did not complete in the allotted time.";
      }

      $file;

    }
    ####################################################
    Function Test-SourceFile(){

    param
    (
        [parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        $sourceFile
    )

        try {

                if(!(test-path "$sourceFile")){

                Write-Host "Source File '$sourceFile' doesn't exist..." -ForegroundColor Red
                throw

                }

            }

        catch {

        Write-Host -ForegroundColor Red $_.Exception.Message;
            Write-Host
        break;

        }

    }
    ####################################################
    ####################################################
        
    $lobType = "microsoft.graph.iosLOBApp"
    Write-Host "================ 0. Prepare tempfile & test sourceFile ================" -ForegroundColor Yellow
    Write-Host "Testing if SourceFile '$sourceFile' Path is valid..." -ForegroundColor Yellow
    Test-SourceFile "$sourceFile"

    # Creating temp file name from Source File path
    $tempFile = [System.IO.Path]::GetDirectoryName("$sourceFile") + "\" + [System.IO.Path]::GetFileNameWithoutExtension("$sourceFile") + "_temp.bin"
    # Creating filename variable from Source File Path
    $fileName = [System.IO.Path]::GetFileName("$sourceFile")

    Write-Host "================ 1. Create a new iOS LOB app. ================" -ForegroundColor Yellow
    Write-Host "Creating JSON data to pass to the service..." -ForegroundColor Yellow
    $mobileAppBody = @{
      "@odata.type" = "#microsoft.graph.iosLobApp"
      "displayName" = $appDisplayName
      "description" = "My iOS Lob App"
      "publisher" = $publisher
      "bundleId" = $bundleId
      applicableDeviceType = @{
        "@odata.type" = "microsoft.graph.iosDeviceType"
        iPad = $true
        iPhoneAndIPod = $true
      }
      minimumSupportedOperatingSystem = @{
        "@odata.type" = "microsoft.graph.iosMinimumOperatingSystem"
        v11_0 = $true
      }
      versionNumber = $version
      buildNumber = $buildNumber
      "fileName" = $fileName
    }
    Write-Host
    Write-Host "Creating application in Intune..." -ForegroundColor Yellow
    $mobileApp = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/deviceAppManagement/mobileApps" -Method POST -Headers $headers -Body ($mobileAppBody | ConvertTo-Json -Depth 10) -ContentType "application/json"
    $mobileApp | ConvertTo-Json
    $appId = $mobileApp.id

    # Get the content version for the new app (this will always be 1 until the new app is committed).
    Write-Host "================ 2. Creating Content Version in the service for the application... ================" -ForegroundColor Yellow
    Write-Host
    $contentVersionUri = "mobileApps/$appId/$lobType/contentVersions";
    $contentVersion = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/deviceAppManagement/$contentVersionUri" -Method POST -Headers $headers -Body "{}" -ContentType "application/json"
    $contentVersion | ConvertTo-Json -Depth 10


    Write-Host "================ 3. Encrypt file and Get File Information ================" -ForegroundColor Yellow
    Write-Host
    Write-Host "Ecrypting the file '$sourceFile'..." -ForegroundColor Yellow
    $encryptionInfo = EncryptFile $sourceFile $tempFile;
    $Size = (Get-Item "$sourceFile").Length
    $EncrySize = (Get-Item "$tempFile").Length

    Write-Host "Creating the manifest file used to install the application on the device..." -ForegroundColor Yellow
    Write-Host
    [string]$manifestXML = '<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>items</key><array><dict><key>assets</key><array><dict><key>kind</key><string>software-package</string><key>url</key><string>{UrlPlaceHolder}</string></dict></array><key>metadata</key><dict><key>AppRestrictionPolicyTemplate</key> <string>http://management.microsoft.com/PolicyTemplates/AppRestrictions/iOS/v1</string><key>AppRestrictionTechnology</key><string>Windows Intune Application Restrictions Technology for iOS</string><key>IntuneMAMVersion</key><string></string><key>CFBundleSupportedPlatforms</key><array><string>iPhoneOS</string></array><key>MinimumOSVersion</key><string>9.0</string><key>bundle-identifier</key><string>bundleid</string><key>bundle-version</key><string>bundleversion</string><key>kind</key><string>software</string><key>subtitle</key><string>LaunchMeSubtitle</string><key>title</key><string>bundletitle</string></dict></dict></array></dict></plist>'

    $manifestXML = $manifestXML.replace("bundleid","$bundleId")
    $manifestXML = $manifestXML.replace("bundleversion","$identityVersion")
    $manifestXML = $manifestXML.replace("bundletitle","$displayName")

    $Bytes = [System.Text.Encoding]::ASCII.GetBytes($manifestXML)
    $EncodedText =[Convert]::ToBase64String($Bytes)

    Write-Host "================ 4. Create a new file for the app. ================" -ForegroundColor Yellow
    Write-Host
    Write-Host "Creating a new file entry in Azure for the upload..." -ForegroundColor Yellow
    $contentVersionId = $contentVersion.id;
    $fileBody = @{ 
      "@odata.type" = "#microsoft.graph.mobileAppContentFile"
      name = $filename
      size = $Size
      sizeEncrypted = $EncrySize;
      manifest = $EncodedText;
    }
    $filesUri = "mobileApps/$appId/$lobType/contentVersions/$contentVersionId/files";
    $file = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/deviceAppManagement/$filesUri" -Method POST -Headers $headers -Body ($fileBody | ConvertTo-Json -Depth 10) -ContentType "application/json"
    $file | convertTo-Json -Depth 10
        
    Write-Host "================ 5. Wait for File Processing - Application Content File. ================" -ForegroundColor Yellow
    Write-Host
    Write-Host "Waiting for the file entry URI to be created..." -ForegroundColor Yellow
    $fileId = $file.id;
    $fileUri = "mobileApps/$appId/$lobType/contentVersions/$contentVersionId/files/$fileId"
    $file = WaitForFileProcessing $fileUri "AzureStorageUriRequest"
    $file | convertTo-Json -Depth 10

    Write-Host "================ 6. Upload the content to Azure Storage. ================" -ForegroundColor Yellow
    Write-Host
    Write-Host "Uploading file to Azure Storage..." -f Yellow
    $sasUri = $file.azureStorageUri;
    UploadFileToAzureStorage $sasUri $tempFile;

    Write-Host "================ 7. Commit the file. ================" -ForegroundColor Yellow
    Write-Host
    Write-Host "Committing the file into Azure Storage..." -ForegroundColor Yellow
    $commitFileUri = "mobileApps/$appId/$lobType/contentVersions/$contentVersionId/files/$fileId/commit"
    Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/deviceAppManagement/$commitFileUri" -Method POST -Headers $headers -Body ($encryptionInfo | ConvertTo-Json -Depth 10) -ContentType "application/json"

    Write-Host "================ 8. Wait for the service to process the commit file request. ================" -ForegroundColor Yellow
    Write-Host
    Write-Host "Waiting for the service to process the commit file request..." -ForegroundColor Yellow
    $file = WaitForFileProcessing $fileUri "CommitFile"
    $file | convertTo-Json -Depth 10

    Write-Host "================ 9. Commit the app. ================" -ForegroundColor Yellow
    Write-Host
    Write-Host "Committing the file into Azure Storage..." -ForegroundColor Yellow
    $commitAppUri = "mobileApps/$appId";
    $commitAppBody = @{ 
      "@odata.type" = "#$lobType"
      committedContentVersion = $contentVersionId
    }
    Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/deviceAppManagement/$commitAppUri" -Method PATCH -Headers $headers -Body ($commitAppBody | ConvertTo-Json -Depth 10) -ContentType "application/json"
    $commitApp = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/deviceAppManagement/mobileApps/$appId" -Method GET -Headers $headers -ContentType "application/json"
    $commitApp | ConvertTo-Json -Depth 10

    Write-Host "Removing Temporary file '$tempFile'..." -f Gray
    Remove-Item -Path "$tempFile" -Force

Upvotes: 0

Related Questions