Azure DevOps Pipelines – How to migrate Calendar Rules using PowerShell

I successfully migrated Bookable Resource and Work Hour Template Calendar Rules between environments using the following approach:

Firstly, I inserted the connection details for both the source and target environments. Then, I utilized Microsoft functions, starting with ‘GetCalendar,’ which retrieved the Calendar ID from the Bookable Resource and Work Hour Template. Subsequently, I passed this Calendar ID into the ‘GetCalendarRules’ function, fetching the Calendar Rules associated with it. The final steps involved two functions that I created, namely ‘FormatCalendarJSON_WorkHourTemplate’ and ‘FormatCalendarJSON_BookableResource.’ These functions formatted the JSON data to be passed into the Microsoft ‘CalendarAction’ function, responsible for creating the Calendar Rules in the target environment.

The recommended way to implement the provided code is by migrating the ‘Work Hour Template’ and ‘Bookable Resource’ tables using the ‘Power Platform Export Data’ and ‘Power Platform Import Data’ build tools in Azure DevOps Pipelines. However, it is crucial to exclude the ‘calendarid’ field from the schema to avoid potential issues. The rationale behind omitting the ‘calendarid’ field is that the code generates a new Calendar and CalendarRule in the target environment.

The only drawback is the necessity to input static Start and End times into the ‘CalendarAction,’ which may require creating separate PowerShell scripts for each ‘Bookable Resource’ and ‘Work Hour Template.’ In the provided example, I exported the working hours of Israel from my Dev environment to the target environment.

I hope the code example below proves helpful in your migration efforts.

$DevUrl = (DEVURL)
$DevClientId = (DEVCLIENT)
$DevClientsecret = (DEVCLIENTSECRET)
$DevTenantId = (DEVTENANTID)
$DevConnectionString = "AuthType=ClientSecret;url=$DevUrl;ClientId=$DevClientId;ClientSecret=$DevClientsecret"

$TargetUrl = (TARGETURL)
$TargetClientId = (TARGETCLIENT)
$TargetClientsecret = (TARGETCLIENTSECRET)
$TargetTenantId = (TARGETTENANTID)
$TargetConnectionString = "AuthType=ClientSecret;url=$TargetUrl;ClientId=$TargetClientId;ClientSecret=$TargetClientsecret"

# Login to PowerApps for the Admin commands
Write-Host "Login to PowerApps for the Admin commands"
Write-Host " "
Install-Module Microsoft.PowerApps.Administration.PowerShell -RequiredVersion "2.0.105" -Force -Scope CurrentUser
 
# Login to PowerApps for the Xrm.Data commands
Write-Host "Login to PowerApps for the Xrm.Data commands"
Write-Host " "
Install-Module Microsoft.Xrm.Data.PowerShell -RequiredVersion "2.8.14" -Force -Scope CurrentUser


# Connect to DEV CRM
$DevConn = Get-CrmConnection -ConnectionString $DevConnectionString
Write-Host 'Connected to' $DevConn.ConnectedOrgFriendlyName
Write-Host " "

# Connect to TARGET CRM
$TargetConn = Get-CrmConnection -ConnectionString $TargetConnectionString
Write-Host 'Connected to' $TargetConn.ConnectedOrgFriendlyName
Write-Host " "



function GetCalendar {
    param (
        $PluralEntityLogicalName,
        $EntityId
    )

    Write-Host "Entered GetCalendar"
    Write-Host " "

        # Define the URL for the GET request
        $url = $DevUrl
        $apiVersion = "v9.1"

        $apiEndpoint = "/api/data/$apiVersion/$PluralEntityLogicalName($EntityId)"

        # Define headers if needed (e.g., for authentication)
        $headers = @{
            "Authorization" = "Bearer " + $DevConn.CurrentAccessToken
            "OData-MaxVersion" = "4.0"
            "OData-Version" = "4.0"
            "Accept" = "application/json"
        }

        # Make the GET request
        $response = Invoke-RestMethod -Uri ($url + $apiEndpoint) -Method Get -Headers $headers

        # Output the response (you can handle the data as needed)
        return $response
}


function GetCalendarRules {
    param (
        $CalendarId
    )

     Write-Host "Entered GetCalendarRules"
     Write-Host " "

# Define the URL for the GET request
$url = $DevUrl
$apiVersion = "v9.1"

$apiEndpoint = "/api/data/$apiVersion/calendars("+$CalendarId+")?"+"$"+"expand=calendar_calendar_rules"

# Define headers if needed (e.g., for authentication)
$headers = @{
    "Authorization" = "Bearer " + $DevConn.CurrentAccessToken
    "OData-MaxVersion" = "4.0"
    "OData-Version" = "4.0"
    "Accept" = "application/json"
}

     Write-Host $apiEndpoint
     Write-Host " "

# Make the GET request
$response = Invoke-RestMethod -Uri ($url + $apiEndpoint) -Method Get -Headers $headers

return $response
}


function FormatCalendarJSON_WorkHourTemplate {
    param (
        $Calendar,
        [string]$EntityLogicalName,
        $CalendarRules
    )

     Write-Host "Entered FormatCalendarJSON_WorkHourTemplate"
     Write-Host " "

   $data = @{
                    "CalendarEventInfo" = @"
                    {
                         "CalendarId": "$($Calendar.msdyn_calendarid)",
            "EntityLogicalName": "$($EntityLogicalName)",
            "TimeZoneCode": $($CalendarRules.calendar_calendar_rules[0].timezonecode),
            "IsVaried": false,
            "RulesAndRecurrences": [
                {
                    "Rules": [
                        {
                            "StartTime": "2023-10-12T08:00:00Z",
                            "EndTime": "2023-10-12T18:00:00Z",
                            "WorkHourType": 0,
                            "Effort": 1
                        }
                    ],
                    "RecurrencePattern": "$($CalendarRules.calendar_calendar_rules[0].pattern)"
                }
            ]
                    }
"@
                } 

                $jsonString = ($data | ConvertTo-Json -Compress -Depth 10).Replace('\r\n','')
                $jsonString2 = $jsonString -replace " ",""

                Write-Host "JSON String - " + $jsonString2

                # Call the CalendarAction function with your action and data
                CalendarAction "msdyn_SaveCalendar" $jsonString2
}



function FormatCalendarJSON_BookableResource {
    param (
        $Calendar,
        [string]$EntityLogicalName,
        $CalendarRules
    )

                $data = @{
                    "CalendarEventInfo" = @"
                    {
                         "CalendarId": "$($Calendar.calendarid_Property.value[0].Id)",
            "EntityLogicalName": "$($EntityLogicalName)",
            "TimeZoneCode": $($CalendarRules.calendar_calendar_rules[0].timezonecode),
            "IsVaried": false,
            "RulesAndRecurrences": [
                {
                    "Rules": [
                        {
                            "StartTime": "2023-10-12T08:00:00Z",
                            "EndTime": "2023-10-12T18:00:00Z",
                            "WorkHourType": 0,
                            "Effort": 1
                        }
                    ],
                    "RecurrencePattern": "$($CalendarRules.calendar_calendar_rules[0].pattern)"
                }
            ]
                    }
"@
                } 

                $jsonString = ($data | ConvertTo-Json -Compress -Depth 10).Replace('\r\n','')
                $jsonString2 = $jsonString -replace " ",""

                <#Write-Host $($response2.calendar_calendar_rules[0].pattern)#>
                Write-Host "JSON String - " + $jsonString2

                # Call the CalendarAction function with your action and data
                CalendarAction "msdyn_SaveCalendar" $jsonString2
}

function CalendarAction {
    param (
        [string]$action,
        [object]$data
    )

    $url = $TargetUrl+"api/data/v9.0/$action"

    $headers = @{
        "Authorization" = "Bearer " + $TargetConn.CurrentAccessToken
        "OData-MaxVersion" = "4.0"
        "OData-Version" = "4.0"
        "Accept" = "application/json"
        "Content-Type" = "application/json; charset=utf-8"
        "Prefer" = 'odata.include-annotations="*"'
    }

    <#$jsonBody = "'",$data,"'"#>

    $jsonBody = $data | ConvertTo-Json -Depth 10

    try {
        $response = Invoke-RestMethod -Uri $url -Method Post -Headers $headers -Body $jsonBody

        if ($response) {
            Write-Host "Response:"
            Write-Host $response
            Write-Host " "
        } else {
            Write-Error "Error occurred."
        }
    }
    catch {
        Write-Error $_.Exception.Message
    }
}


## WORK HOUR TEMPLATE ###

Write-Host "Get Work Hour Templates Calendar IDs in Dev"
Write-Host " "
$fetchWorkHourTemplatesDev = @"
<fetch>
  <entity name="msdyn_workhourtemplate">
    <attribute name="msdyn_workhourtemplateid" />
    <attribute name="msdyn_calendarid" />
    <attribute name="msdyn_name" />
    <filter>
      <condition attribute="msdyn_name" operator="eq" value="Israel" />
    </filter>
  </entity>
</fetch>
"@

$workHourTemplatesDev = (Get-CrmRecordsByFetch -conn $DevConn -Fetch $fetchWorkHourTemplatesDev -Verbose).CrmRecords

if ($workHourTemplatesDev.Count -eq 0)
{
    Write-Host "No workHourTemplates found."
}
else
{
    foreach ($workHourTemplateDev in $workHourTemplatesDev) {

        $WorkHourTemplate_Calendar = GetCalendar "msdyn_workhourtemplates" $workHourTemplateDev.msdyn_workhourtemplateid

        Write-Host "WorkHourTemplate_Calendar " + $WorkHourTemplate_Calendar
        Write-Host " "

        $WorkHourTemplate_CalendarRules = GetCalendarRules $WorkHourTemplate_Calendar.msdyn_calendarid

        Write-Host "calendar_calendar_rules" + $WorkHourTemplate_CalendarRules.calendar_calendar_rules
        Write-Host " "


Write-Host "Get Work Hour Template Calendar IDs in Target"
Write-Host " "
$fetchWorkHourTemplatesTarget= @"
<fetch>
  <entity name="msdyn_workhourtemplate">
    <attribute name="msdyn_workhourtemplateid" />
    <attribute name="msdyn_calendarid" />
    <attribute name="msdyn_name" />
    <filter>
      <condition attribute="msdyn_name" operator="eq" value="Israel" />
    </filter>
  </entity>
</fetch>
"@

$workHourTemplatesTarget = (Get-CrmRecordsByFetch -conn $TargetConn -Fetch $fetchWorkHourTemplatesTarget -Verbose).CrmRecords

if ($workHourTemplatesTarget.Count -eq 0)
{
    Write-Host "No Calendar IDs found."
}
else
{
    foreach ($WorkHourTemplateTarget in $workHourTemplatesTarget) {

        FormatCalendarJSON_WorkHourTemplate $workHourTemplatesTarget "msdyn_workhourtemplate" $WorkHourTemplate_CalendarRules

    }
   }
        
 }
}


## BOOKABLE RESOURCE ###


Write-Host "Get Bookable Resource Calendar IDs in Dev"
$fetchBookableResourcesDev = @"
<fetch>
  <entity name="bookableresource">
    <attribute name="bookableresourceid" />
    <attribute name="calendarid" />
    <filter>
      <condition attribute="resourcetype" operator="eq" value="1" />
      <condition attribute="name" operator="eq" value="Calendar Template - Israel" />
    </filter>
  </entity>
</fetch>
"@

$bookableResourcesDev = (Get-CrmRecordsByFetch -conn $DevConn -Fetch $fetchBookableResourcesDev -Verbose).CrmRecords

if ($bookableResourcesDev.Count -eq 0)
{
    Write-Host "No Calendar IDs found."
}
else
{
    foreach ($bookableResourceDev in $bookableResourcesDev) {

        $bookableResource_Calendar = GetCalendar "bookableresources" $bookableResourceDev.bookableresourceid

        Write-Host "bookableResource_Calendar " + $bookableResource_Calendar
        Write-Host " "

        $bookableResource_CalendarRules = GetCalendarRules $bookableResource_Calendar._calendarid_value

        Write-Host "calendar_calendar_rules" + $bookableResource_CalendarRules.calendar_calendar_rules
        Write-Host " "


Write-Host "Get Bookable Resource Calendar IDs in Target"
$fetchBookableResourcesTarget = @"
<fetch>
  <entity name="bookableresource">
    <attribute name="bookableresourceid" />
    <attribute name="calendarid" />
    <filter>
      <condition attribute="resourcetype" operator="eq" value="1" />
      <condition attribute="name" operator="eq" value="Calendar Template - Israel" />
    </filter>
  </entity>
</fetch>
"@

$bookableResourcesTarget = (Get-CrmRecordsByFetch -conn $TargetConn -Fetch $fetchBookableResourcesTarget -Verbose).CrmRecords

if ($bookableResourcesTarget.Count -eq 0)
{
    Write-Host "No Calendar IDs found."
}
else
{
    foreach ($bookableResourceTarget in $bookableResourcesTarget) {

        FormatCalendarJSON_BookableResource $bookableResourceTarget "bookableresource" $bookableResource_CalendarRules
      }
    }
  }
}

Leave a Reply

Your email address will not be published. Required fields are marked *