April 17, 2025

Automating Azure Service App Deployment with Azure DevOps Pipelines

Introduction

In modern software development, Continuous Integration and Continuous Deployment (CI/CD) are crucial in ensuring smooth, automated deployments. Azure DevOps provides robust pipeline capabilities that enable developers to automate the deployment of their applications, reducing manual effort and minimizing errors.

In this blog, we’ll walk through setting up an Azure DevOps pipeline for an Azure Service App, ensuring seamless deployment whenever changes are pushed to the repository.

Setting Up the Azure DevOps Pipeline


Before diving into the pipeline configuration, ensure you have:
  • An Azure Service App created in the Azure Portal.
  • service connection in Azure DevOps is linked to your Azure subscription.
  • Your Service App code is stored in a repository like Azure Repos, GitHub, or Bitbucket.

Pipeline Configuration

Below is a YAML-based Azure DevOps pipeline that automates the build and deployment of an Azure Service App.


trigger:
  branches:
    include:
    - main # Change this to your branch if needed
  paths:
    include:
    - ServiceAppCode/*

variables:
  azureSubscription: 'ServiceAppDeployment' # Azure service connection name
  serviceAppName: 'TestingApp' # Azure Service App name
  serviceAppPath: 'ServiceAppCode' # Path to Service App source code
  buildConfiguration: 'Release' # Build service app source code in release
  publishDirectory: '$(Build.ArtifactStagingDirectory)/publish'

pool:
  vmImage: 'ubuntu-22.04' # Also use ubuntu-latest

stages:
- stage: Build
  displayName: 'Build Stage'
  jobs:
  - job: Build
    displayName: 'Build Job'
    steps:
    - task: UseDotNet@2
      displayName: 'Install .NET SDK'
      inputs:
        packageType: 'sdk'
        version: '8.0.x'
        includePreviewVersions: false

    - script: |
        echo "Cleaning up existing publish directory..."
        rm -rf $(publishDirectory)
        mkdir -p $(publishDirectory)
      displayName: 'Ensure Clean Publish Directory'

    - task: DotNetCoreCLI@2
      displayName: 'Restore Dependencies'
      inputs:
        command: 'restore'
        projects: '$(serviceAppPath)/*.csproj'

    - task: DotNetCoreCLI@2
      displayName: 'Build Service App'
      inputs:
        command: 'build'
        projects: '$(serviceAppPath)/*.csproj'
        arguments: '--configuration $(buildConfiguration) /p:WarningLevel=0' # /p:WarningLevel=0 Remove the warning at the time of build service app

    - task: DotNetCoreCLI@2
      displayName: 'Publish Service App'
      inputs:
        command: 'publish'
        projects: '$(serviceAppPath)/*.csproj'
        publishWebProjects: false
        arguments: '--configuration $(buildConfiguration) --output $(publishDirectory)'
        zipAfterPublish: true

    - task: PublishBuildArtifacts@1
      displayName: 'Publish Artifacts'
      inputs:
        pathToPublish: '$(publishDirectory)'
        artifactName: 'drop'

- stage: Deploy
  displayName: 'Deploy Stage'
  dependsOn: Build
  condition: succeeded()
  jobs:
  - job: Deploy
    displayName: 'Deploy to Azure Service App'
    steps:
    - download: current
      displayName: 'Download Build Artifacts'
      artifact: 'drop'

    - task: AzureServiceApp@1
      displayName: 'Deploy to Azure Service App'
      inputs:
        azureSubscription: '$(azureSubscription)'
        appType: 'webApp' # functionApp for function app OR webApp for web app
        appName: '$(serviceAppName)'
        package: '$(Pipeline.Workspace)/drop/*.zip'

Understanding the Pipeline

1. Trigger Configuration

  • The pipeline is triggered when changes are pushed to the main branch.
  • It monitors specific folders where Service App updates are made.

2. Defining Variables

  • azureSubscription: The name of the Azure DevOps service connection.
  • serviceAppName: The name of the Service App in Azure.
  • serviceAppPath: The directory containing the Service App source code.
  • publishDirectory: The folder where the build output is stored.

3. Choosing the Right Agent Pool

  • The pipeline uses the Ubuntu 22.04 VM image for the build process

4. Build Stage

  • Installing .NET SDK: Ensures that the required version is available.
  • Cleaning the Publish Directory: Prevents old files from interfering with new builds.
  • Restoring Dependencies: Ensures that all NuGet dependencies are downloaded.
  • Building the Service App: Compiles the service app with the specified configuration.
  • Publishing the Service App: Packages the compiled app for deployment.
  • Publishing Artifacts: Stores the published files as build artifacts for the deployment stage.

5. Deploy Stage

  • Downloading Build Artifacts: Retrieves the published application files.
  • Deploying to Azure: Uses the AzureServiceApp@1 task to deploy the service app to Azure.

Conclusion

By setting up this Azure DevOps pipeline, you can automate the deployment of your Azure Service App, ensuring quick and error-free releases.

With automation in place, you can focus on developing new features while Azure DevOps handles the heavy lifting of deployments!


If you have any questions you can reach out our SharePoint Consulting team here.

Custom PowerShell Script to Copy Site Groups, Permissions, and Settings Without Migration Tool

Introduction 

Migrating permissions, site groups, and settings from a SharePoint Hub Site to an associated site is a common task, especially in large organizations. While available migration tools can make this easy, they often come at a cost. 

In this blog post, we’ll walk through a custom PowerShell script that automates this process without using any migration tool.  

What This Script Does 

  • Connects to both Hub Site and Associated Site 
  • Copies custom permission levels 
  • Copies site groups with users 
  • Applies appropriate role assignments 
  • Updates sharing capabilities 
  • Streamlines migration process for SharePoint Online environments 

Script Parameters 

Param ( 
    [string] $ClientId = $(Throw "Please provide ClientId"), 
    [string] $HubSiteURL = $(Throw "Please provide HubSiteURL"), 
    [string] $AdminSiteURL = $(Throw "Please provide AdminSiteURL"), 
    [string] $ClientSecret = $(Throw "Please provide ClientSecret"), 
    [string] $AssociatedSiteURL = $(Throw "Please provide Associated Site URL") 
) 

You’ll need to register an Azure AD App with appropriate SharePoint API permissions and provide its ClientId and ClientSecret. This ensures secure authentication without using stored credentials. 

Authentication and Setup 

$HubSiteConnection = Connect-PnPOnline -Url $HubSiteURL -ClientId $ClientId -ClientSecret $ClientSecret -ReturnConnection 
$AssociatedSiteConnection = Connect-PnPOnline -Url $AssociatedSiteURL -ClientId $ClientId -ClientSecret $ClientSecret -ReturnConnection

This part of the script connects to both the Hub Site and the Associated Site using PnP PowerShell, returning secure connections for use in the following functions. 

Updating Sharing Capability 

function Update-ExternalSharing { 
    param ( 
        [string] $AssociatedSiteURL, 
        [string] $AdminSiteURL 
    ) 
 
    Connect-PnPOnline -Url $AdminSiteURL -ClientId $ClientId -ClientSecret $ClientSecret 
    Set-PnPTenantSite -Url $AssociatedSiteURL -SharingCapability ExternalUserSharingOnly 
}
This function sets the sharing capability of the associated site to allow external users to access the site — but only if they are authenticated. This is done by updating the SharingCapability property of the site to ExternalUserSharingOnly. 

Copying Permission Levels 

function Copy-PermissionLevels { 
    param ( 
        $HubSiteConnection, 
        $AssociatedSiteConnection 
    ) 
 
    $AllPermissionLevels = Get-PnPRoleDefinition -Connection $HubSiteConnection 
 
    foreach ($PermissionLevel in $AllPermissionLevels) { 
        if (-not $PermissionLevel.Hidden) { 
            $ExistingPermission = Get-PnPRoleDefinition -Identity $PermissionLevel.Name -Connection $AssociatedSiteConnection -ErrorAction SilentlyContinue 
            if (!$ExistingPermission) { 
                $selectedPermissions = New-Object Microsoft.SharePoint.Client.BasePermissions 
                [Enum]::GetValues([Microsoft.SharePoint.Client.PermissionKind]) | ForEach-Object { 
                    if ($PermissionLevel.BasePermissions.Has($_)) { 
                        $selectedPermissions.Set($_) 
                    } 
                } 
 
                $newRole = Add-PnPRoleDefinition -RoleName $PermissionLevel.Name -Description $PermissionLevel.Description -Connection $AssociatedSiteConnection 
                $newRole.BasePermissions = $selectedPermissions 
                $newRole.Update() 
                Invoke-PnPQuery -Connection $AssociatedSiteConnection 
            } 
        } 
    } 
} 
This function copies non-hidden permission levels from the hub site to the associated site. It checks if the permission already exists to avoid duplication and applies the same Base Permissions.  Copying Groups and Users 
function Copy-HubSiteGroups { 
    param ( 
        $HubSiteName, 
        $AssociatedSiteName, 
        $HubSiteConnection, 
        $AssociatedSiteConnection 
    ) 
 
    $GroupMappings = @( 
        @{ Source = "$HubSiteName Owners"; Destination = "$AssociatedSiteName Owners" }, 
        @{ Source = "$HubSiteName Members"; Destination = "$AssociatedSiteName Members" }, 
        @{ Source = "$HubSiteName Visitors"; Destination = "$AssociatedSiteName Visitors" } 
    ) 
 
    foreach ($Mapping in $GroupMappings) { 
        $SourceGroup = Get-PnPGroup -Identity $Mapping.Source -Connection $HubSiteConnection 
        $DestinationGroup = Get-PnPGroup -Identity $Mapping.Destination -Connection $AssociatedSiteConnection -ErrorAction SilentlyContinue 
 
        if (-not $DestinationGroup) { 
            New-PnPGroup -Title $Mapping.Destination -Connection $AssociatedSiteConnection 
        } 
 
        $SourceGroup.Users | ForEach-Object { 
            $LoginName = if ($_ -like "*#ext#*") { $_.Email } else { $_.LoginName } 
            Add-PnPGroupMember -Group $Mapping.Destination -LoginName $LoginName -Connection $AssociatedSiteConnection 
        } 
    } 
} 

This function replicates SharePoint default groups (Owners, Members, Visitors) from the hub to the associated site. It handles both internal and external users and ensures group membership is preserved. 

Main Function

To simplify script execution, we can wrap all functional calls inside a Main function:

function Main {
    # Connect to the Hub Site and Associated Site using PnP PowerShell
    $HubSiteConnection = Connect-PnPOnline -Url $HubSiteURL -ClientId $ClientId -ClientSecret $ClientSecret -ReturnConnection
    $AssociatedSiteConnection = Connect-PnPOnline -Url $AssociatedSiteURL -ClientId $ClientId -ClientSecret $ClientSecret -ReturnConnection

    # Update external sharing settings before proceeding
    Update-ExternalSharing -AdminSiteURL $AdminSiteURL -AssociatedSiteURL $AssociatedSiteURL

    # Copy custom permission levels from Hub Site to Associated Site
    Copy-PermissionLevels -HubSiteConnection $HubSiteConnection -AssociatedSiteConnection $AssociatedSiteConnection

    # Extract names of sites to match default group names
    $HubSiteName = ($HubSiteURL -split "/")[-1]
    $AssociatedSiteName = ($AssociatedSiteURL -split "/")[-1]

    # Copy site groups and their users
    Copy-HubSiteGroups -HubSiteName $HubSiteName -AssociatedSiteName $AssociatedSiteName -HubSiteConnection $HubSiteConnection -AssociatedSiteConnection $AssociatedSiteConnection
}

Main
Running the Script To run this PowerShell script, first make sure you’ve saved it with a .ps1 extension — for example, name it CopyGroupsAndPermissions.ps1. 

Once saved, navigate to the directory where the script is stored using PowerShell and run the following command (make sure to replace the placeholder values with your actual credentials and URLs): 

./CopyGroupsAndPermissions.ps1 ` 
    -ClientId "xxxx-xxxx-xxxx-xxxx" ` 
    -HubSiteURL "https://yourtenant.sharepoint.com/sites/hubsite" ` 
    -ClientSecret "your-client-secret" ` 
    -AdminSiteURL "https://yourtenant-admin.sharepoint.com" ` 
    -AssociatedSiteURL "https://yourtenant.sharepoint.com/sites/associatedsite" 
  

Final Thoughts 

This script is ideal for scenarios where: 

  • You need to replicate security and structure across multiple sites 
  • You want to avoid third-party tools like ShareGate 

If you're working on managing large SharePoint environments with many associated sites, this can save time and reduce manual effort.

If you have any questions you can reach out our SharePoint Consulting team here.

How to Integrate Azure OpenAI with SharePoint Library Data

Azure Open AI with SharePoint

We can index documents from a SharePoint library with Azure Cognitive Search, and use an Azure OpenAI model to query the data. Using a custom connector, we can bring this power into Power Automate.

Prerequisites

  • Have documents (*.docx, *.pdf) in a SharePoint library
  • Azure Cognitive Search service (Basic or Standard tier)
  • Azure OpenAI

Preparations

  • Note down the URL of the Site your library.
    • Example: https://***nexus.sharepoint.com/sites/Manish/AI_Documents
  • Note down the URL and the Admin Key of your Azure Cognitive Search Service











  • Turn on System-Assigned Managed Identity in your Azure Cognitive Search Service
  • Create an Entra Id app registration with the following parameters:

Connect your SharePoint library with Azure Cognitive Search

  • Create a Data Source with the Azure Cognitive Search Preview REST API
    • We have use the Automate flow to create a new data source for Azure Cognitive search using the REST API.
      • POST to https://(name-of-your Azure Cognitive Search service).search.windows.net/datasources?api-version=2024-06-01-Preview
      • Params:
        • api-version: 2024-06-01-Preview
      • Headers:
        • Content-Type: application/json
        • api-key: (the Admin key of your Azure Cognitive Search Service)
      • Body:
        					  
          {
              "name": "sharepoint-datasource",
              "type": "sharepoint",
              "credentials": {
                  "connectionString": "SharePointOnlineEndpoint=
                  (your SharePoint Site URL0);ApplicationId=(your_App_Id)"
              },
              "container": {
              "name": "defaultSiteLibrary",
              "query": null
              }
          }   
                         
        				





















































      • Below is the HTTP Action we have used for create a source.









































      • This will return a 201 response, indicating that your data source was created. You can check this in the Azure portal.

Create your Index

  • Let’s now leverage metadata of your document to enhance your search experience. This as well is done by using the REST API. We will again do this in automate flow.
  • POST to https://(name-of-your Azure Cognitive Search service).search.windows.net/indexes?api-version=2024-06-01-Preview.
  • Params:
    • api-version: 2024-06-01-Preview
  • Headers:
    • Content-Type: application/json
    • api-key: (the Admin key of your Azure Cognitive Search Service)






























































  • Body:
  • 					  
      {
      "name": "sharepoint-index",
      "fields": [
        {
          "name": "id",
          "type": "Edm.String",
          "key": true,
          "searchable": false
        },
        {
          "name": "metadata_spo_item_name",
          "type": "Edm.String",
          "key": false,
          "searchable": true,
          "filterable": false,
          "sortable": false,
          "facetable": false
        },
        {
          "name": "metadata_spo_item_path",
          "type": "Edm.String",
          "key": false,
          "searchable": false,
          "filterable": false,
          "sortable": false,
          "facetable": false
        },
        {
          "name": "metadata_spo_item_content_type",
          "type": "Edm.String",
          "key": false,
          "searchable": false,
          "filterable": true,
          "sortable": false,
          "facetable": true
        },
        {
          "name": "metadata_spo_item_last_modified",
          "type": "Edm.DateTimeOffset",
          "key": false,
          "searchable": false,
          "filterable": false,
          "sortable": true,
          "facetable": false
        },
        {
          "name": "metadata_spo_item_size",
          "type": "Edm.Int64",
          "key": false,
          "searchable": false,
          "filterable": false,
          "sortable": false,
          "facetable": false
        },
        {
          "name": "content",
          "type": "Edm.String",
          "searchable": true,
          "filterable": false,
          "sortable": false,
          "facetable": false
        }
      ]
    }
                     
    				
  • This will return a 201 response, indicating that your data source was created. You can check this in the Azure portal.

  • Create your indexer

    • We want to create an indexer. It will later automate the indexing process from your SharePoint library to the Azure Cognitive Search service.
    • Once again, we do this in automate flow. This is a two-step process as we first need to POST a Create an indexer request - which will run and run and run as it is waiting for us to log in. So we will run a second call, which to GET the indexer status. This will return a device code with which we can sign in - Once we did that we can see that the call returns a 200. After that, the POST will succeed and return a 201 as well.

    Create an indexer request

    • POST to https://(name-of-your Azure Cognitive Search service).search.windows.net/indexers?api-version=2024-06-01-Preview
    • Params:
      • api-version: 2024-06-01-Preview
    • Headers:
      • Content-Type: application/json
      • api-key: (the Admin key of your Azure Cognitive Search Service)
    • * Image *

    • Body:
    • 					  
      {
        "name": "sharepoint-indexer",
        "dataSourceName": "aidocumentssource",
        "targetIndexName": "sharepoint-index",
        "parameters": {
      	"batchSize": null,
      	"maxFailedItems": null,
      	"maxFailedItemsPerBatch": null,
      	"base64EncodeKeys": null,
      	"configuration": {
      	"indexedFileNameExtensions": ".pdf, .docx",
      	"excludedFileNameExtensions": ".png, .jpg",
      	"dataToExtract": "contentAndMetadata"
      	}
      },
      "schedule": {},
      "fieldMappings": [{
      	"sourceFieldName": "metadata_spo_site_library_item_id",
      	"targetFieldName": "id",
      	"mappingFunction": {
      	"name": "base64Encode"}
      }]
      }
                       
      				

    Get indexer status

    • Now, we have create a new flow for get a device login code.
      • GET to https://(name-of-your Azure Cognitive Search service).search.windows.net/indexers/sharepoint-indexer/status?api-version=2024-06-01-Preview
      • Params:
        • api-version: 2024-06-01-Preview
      • Headers:
        • Content-Type: application/json
        • api-key: (the Admin key of your Azure Cognitive Search Service)
      • This will return a response that contains an errormessage:
        • "errorMessage": "To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code LFXXXXXP to authenticate.\r\nTo sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code LFXXXXXP to authenticate."
      • Copy the code and open the link, then paste the code into the device login popup. Once you are logged in, you can close that browser tab again.












































      • Check now in the Azure portal that you do not only have an index, but also an indexer and documents indexed.


    Test your app in the Playground

    The Azure Open AI playground is a fabulous way to test and try out - so let’s do this.

    • In the Playground, create a new deployment
    • Select Add your data and then Add a data source
    • Select the Azure Cognitive Search service, your Subscription its running in, and the index we just created. All of these will automagically appear in the respective dropdown fields.
    • Now proceed with the index data field mapping - where you select all fields to be content
    • Save and close

    You can now chat against your documents and ask the bot questions about it. By check/uncheck of the Limit responses to your data content you can determine whether you want the bot only to consider content from your documents or not. You can now deploy this as a web app - Or you can walk with me some more steps and have that power in Power Apps or Power Automate

    If you have any questions you can reach out our SharePoint Consulting team here.