0

Working with Sitecore XC, we need to reconnect the commerce catalog every time we do an initialize of the Commerce Engine. We can do this initialize optionally during development. We need to execute the following steps in the Sitecore XP UI:

  • Unselect Catalog
  • Select Catalog
  • Set Catalog Templates
  • Delete Data Templates
  • Update Data Templates
  • Rebuild Search Index

How can these steps be automated in a way that can be used on the local development machine, but also in a DevOps release pipeline?

1

The following PowerShell script can be made available as a Web API endpoint within Sitecore. Instructions on how to do this are documented in the script:

# Reconnect Sitecore Commerce catalog
# https://www.sergevandenoever.nl, https://www.linkedin.com/in/sergevandenoever/
#
# Execute the manual steps to correctly reconnect a catelog when a Sitecore XC Initialize is executed
# These steps are:
# - Unselect Catalog
# - Select Catalog
# - Set Catalog Templates
# - Delete Data Templates
# - Update Data Templates
# - Rebuild Search Index
# 
# Deployment of script as Web API endpoint:
# In Content Editor, navigate to /sitecore/system/Modules/PowerShell/Script Library
# Right-click on "Script Library" and select: Insert > Module Wizard
# - Name the module (e.g. name of customer - for example customer)
# - For integration points to create select "Web API"
# Right-click on the folder /sitecore/system/Modules/PowerShell/Script Library/<customer name>/Web API
# and select: Insert > PowerShell Script and name the script customer_ReconnectCatalog.ps1
#
# This will result in a Web API end-point on the Content Management server.
#
# Usage:
# https://customer_cm.com/-/script/v2/master/customer_ReconnectCatalog
#
# Querystring parameters:
# - sitepath: Sitecore path to site where to reconnect catalog
# - catalogname: Name of the catalog to reconnect
# - wait: 0,false,1,true - if 1 or true, wait for the index job to finish
#
# Example:
# https://customer_cm.com/-/script/v2/master/customerc_ReconnectCatalog?sitepath=/sitecore/content/MyTenant/MySite&catalogname=MyCatalog&wait=1
#

$global:debug = $false
$global:log = @()
$global:stopwatch = [system.diagnostics.stopwatch]::startNew()

function Log {
    param (
        [string]$message
    )

    if ($debug) {
        Write-Output $message
    }
    $global:log += $message
}

function SelectCatalog {
    param (
        [string]$SitePath,
        [string]$CatalogName
    )

    $catalogsItemPath = "$SitePath/Home/Catalogs"

    Log "Select catalog $CatalogName"
    $catalogsItem = Get-Item -Path $catalogsItemPath
    $catalogsItem.Editing.BeginEdit() | Out-Null
    $catalogsItem['Selected Catalogs'] = $CatalogName
    $catalogsItem.Editing.EndEdit() | Out-Null
}

function UnselectCatalog {
    param (
        [string]$SitePath,
        [string]$CatalogName
    )

    $catalogsItemPath = "$SitePath/Home/Catalogs"

    Log "Unselect catalog $CatalogName"
    $catalogsItem = Get-Item -Path $catalogsItemPath
    $catalogsItem.Editing.BeginEdit() | Out-Null
    $catalogsItem['Selected Catalogs'] = ''
    $catalogsItem.Editing.EndEdit() | Out-Null
}

function SetCatalogTemplates {
    param (
        [string]$SitePath,
        [string]$CatalogName
    )

    Log "Set catalog templates for catalog $CatalogName"
    Import-Function Get-TenantItem
    Import-Function Get-TenantTemplate

    $Site = Get-Item -Path $SitePath
    $CommerceBundleTemplateName = "Commerce Bundle"
    $CommerceCategoryTemplateName = "Commerce Category"
    $CommerceProductTemplateName = "Commerce Product"
    $CommerceProductVariantTemplateName = "Commerce Product Variant"
    $CommerceDynamicBundleTemplateName = "Commerce Dynamic Bundle"
    $SiteCatalogRelativePath = "/Home/Catalogs/$CatalogName"

    $Tenant = Get-TenantItem $Site
    $TenantTemplatesRootID = $Tenant['Templates']
    
    $TenantTemplatesRoot = Get-Item -Path master: -ID $TenantTemplatesRootID
    $TenantTemplates = Get-TenantTemplate $TenantTemplatesRoot
    $CommerceBundleTemplate = $TenantTemplates | Where-Object {$_.Name -eq $CommerceBundleTemplateName} | Select-Object -First 1
    $CommerceCategoryTemplate = $TenantTemplates | Where-Object {$_.Name -eq $CommerceCategoryTemplateName} | Select-Object -First 1
    $CommerceProductTemplate = $TenantTemplates | Where-Object {$_.Name -eq $CommerceProductTemplateName} | Select-Object -First 1
    $CommerceProductVariantTemplate = $TenantTemplates | Where-Object {$_.Name -eq $CommerceProductVariantTemplateName} | Select-Object -First 1
    $CommerceDynamicBundleTemplate = $TenantTemplates | Where-Object {$_.Name -eq $CommerceDynamicBundleTemplateName} | Select-Object -First 1
    
    $CatalogItemPath = $Site.Paths.Path + $SiteCatalogRelativePath
    $CatalogItem = Get-Item -Path $CatalogItemPath
    
    if ($CatalogItem) {
        $CatalogItem.Editing.BeginEdit() | Out-Null
        $CatalogItem["CategoryTemplate"] = $CommerceCategoryTemplate.ID
        $CatalogItem["ProductTemplate"] = $CommerceProductTemplate.ID
        $CatalogItem["ProductVariantTemplate"] = $CommerceProductVariantTemplate.ID
        $CatalogItem["StaticBundleTemplate"] = $CommerceBundleTemplate.ID
        $CatalogItem["DynamicBundleTemplate"] = $CommerceDynamicBundleTemplate.ID
        $CatalogItem.Editing.EndEdit() | Out-Null
    }
    else {
        Throw "SetCatalogTemplates: Catalog '$CatalogName' not found"
    }
    
    $serviceLocatorInstance = [Sitecore.DependencyInjection.ServiceLocator]::ServiceProvider
    $dataProviderService = $serviceLocatorInstance.GetType().GetMethod('GetService').Invoke($serviceLocatorInstance, [Sitecore.Commerce.XA.Foundation.Common.Services.CEConnect.IDataProviderService])
    $dataProviderService.ClearCommerceCache() | Out-Null
}

# This function sometimes fails on first call, so using retry
function DeleteDataTemplates {
    $maxRetryCount = 5
    $retryCount = 0
    $done = $false
    
    Log "Delete Data Templates - Begin"
    Do {
        try {

            $masterDatabase = Get-Database -Name "master"
            [System.Reflection.Assembly]::LoadWithPartialName("Sitecore.Commerce.Engine.Connect") | Out-Null
            $catalogTemplateGenerator = New-Object -TypeName Sitecore.Commerce.Engine.Connect.DataProvider.Templates.CatalogTemplateGenerator
            $catalogTemplateGenerator.DeleteAllGeneratedTemplates($masterDatabase)
            $done = $true
        } catch {
            Log "Exception: $_"
            $retryCount++
            Log "Retry $retryCount of $maxRetryCount"
        }
    } While ((-not $done) -and $retryCount -lt $maxRetryCount)
    
    if (-not $done) {
        Log "===== FAILED ====="
    } else {
        Log "===== SUCCESS ====="
    }
    Log "Delete Data Templates - End"
}

function UpdateDataTemplates {
    Log "Update Data Templates - Begin"
    $masterDatabase = Get-Database -Name "master"
    [System.Reflection.Assembly]::LoadWithPartialName("Sitecore.Commerce.Engine.Connect") | Out-Null
    $catalogTemplateGenerator = New-Object -TypeName Sitecore.Commerce.Engine.Connect.DataProvider.Templates.CatalogTemplateGenerator
    $catalogTemplateGenerator.BuildCatalogTemplates($masterDatabase) | Out-Null
    Log "Update Data Templates - End"
}

function IndexCatalog {
    param (
        [string]$IndexName = 'sitecore_master_index'
    )

    Log "Rebuild search index $IndexName"
    $jobId = Rebuild-SearchIndex -Name sitecore_master_index -AsJob |
        ForEach-Object { $_.Handle.ToString() }
    $jobId
}

function GetSitecoreJobStatus {
    param (
        [string]$JobId
    )
    
    $job = [Sitecore.Jobs.JobManager]::GetJob([Sitecore.Handle]::Parse($JobId))
    $isDone = $job -eq $null -or $job.IsDone -or $job.Status.Failed
    $status = "No longer exists"
    $jobName = $JobId
    if($job) {
        $jobName = $job.Name.Replace("Index_Update_IndexName=", "")
        $state = $job.Status.State
        $processed = $job.Status.Processed
        if($job.Options -and $job.Options.CustomData -is [Sitecore.Publishing.PublishStatus]) {
            $publishStatus = $job.Options.CustomData -as [Sitecore.Publishing.PublishStatus]
            if($publishStatus.Processed -gt 0) {
                $state = $publishStatus.State
                $processed = $publishStatus.Processed
            }
        }
        $status = "$($state) and processed $($processed)"
    }
    [PSCustomObject]@{
        "Name" = $jobName
        "IsDone" = $isDone
        "Status" = $status
    }
}

function WaitForSitecoreJob {
    <#
    .SYNOPSIS
        Polls for the specified job until it has completed.

    .DESCRIPTON
        The WaitForSitecoreJob command waits for a Sitecore.Jobs.Job to complete processing.
 
    .PARAMETER JobId
        The Sitecore JobId to poll.

    .PARAMETER Delay
        The polling interval in seconds.
#>

    [CmdletBinding()]
    param(
        [ValidateNotNull()]
        [string]$JobId,

        [int]$Delay = 1
    )

    $keepRunning = $true
    while($keepRunning) {
        $response = GetSitecoreJobStatus -JobId $JobId
        if($response -and $response.IsDone) {
            $keepRunning = $false
            Log "Polling job $($response.Name). Status : $($response.Status)."
            Log "Finished polling job $($id)."
        } else {
            Log "Polling job $($response.Name). Status : $($response.Status)."
            Start-Sleep -Seconds $Delay
        }
    }
}

# $VerbosePreference = "Continue"
[System.Web.HttpContext]::Current.Response.ContentType = "application/json"

try {
    if ($debug) {
        [string]$SitePath = "/sitecore/content/MyTenant/MySite"
        [string]$CatalogName = "MyCatalog"
        [boolean]$WaitForJob = $true
    } else { # use query string parameters
        [string]$SitePath = [System.Web.HttpContext]::Current.Request.QueryString['sitepath']
        [string]$CatalogName = [System.Web.HttpContext]::Current.Request.QueryString['catalogname']
        [string]$wait = [System.Web.HttpContext]::Current.Request.QueryString['wait']
        if ([String]::IsNullOrWhiteSpace($SitePath)) {
            Throw "Url parameter sitepath is missing"
        }
        if ([String]::IsNullOrWhiteSpace($CatalogName)) {
            Throw "Url parameter catalogname is missing"
        }
        [boolean]$WaitForJob = $false 
        if (![String]::IsNullOrWhiteSpace($wait) -and ($wait.ToLower() -eq 'true') -or ($wait -eq '1')) {
            $WaitForJob = $true
        }
    }

    Log "========= BEGIN RECONNECTING CATALOG $CatalogName ========="
    UnselectCatalog -SitePath $SitePath -CatalogName $CatalogName
    SelectCatalog -SitePath $SitePath -CatalogName $CatalogName
    SetCatalogTemplates -SitePath $SitePath -CatalogName $CatalogName
    DeleteDataTemplates
    UpdateDataTemplates

    $jobId = IndexCatalog -IndexName 'sitecore_master_index'
    if ($WaitForJob) {
        WaitForSitecoreJob -JobId $jobId -Delay 10
        $jobId = '' 
    }
    
    Log "=========  END RECONNECTING CATALOG $CatalogName  ========="
    
    $global:stopwatch.Stop()
    
    @{
        process = "ReconnectingCatalog"
        sitepath = $SitePath
        catalogname = $CatalogName
        success = $true
        jobId = $jobId
        log = $log
        duration = $global:stopwatch.Elapsed.ToString()
    } | ConvertTo-Json  
} catch {
    Log "Exception: $_"
    $global:stopwatch.Stop()

    @{
        process = "ReconnectingCatalog"
        sitepath = $SitePath
        catalogname = $CatalogName
        success = $false
        exception = $_.ToString()
        log = $log
        duration = $global:stopwatch.Elapsed.ToString()        
    } | ConvertTo-Json
    
    [System.Web.HttpContext]::Current.Response.StatusCode = 500
    [System.Web.HttpContext]::Current.Response.TrySkipIisCustomErrors = $true
}

The Web API call returns a JSON object with status and log information. In case of success the HTTP status code is 200, in case of error the HTTP status code is 500.

Example of the JSON result:

{
    "success":  true,
    "jobId":  "",
    "sitepath":  "/sitecore/content/MyTenant/MySite",
    "log":  [
        "========= BEGIN RECONNECTING CATALOG MyCatalog =========",
        "Unselect catalog MyCatalog",
        "Select catalog MyCatalog",
        "Set catalog templates for catalog MyCatalog",
        "Delete Data Templates - Begin",
        "===== SUCCESS =====",
        "Delete Data Templates - End",
        "Update Data Templates - Begin",
        "Update Data Templates - End",
        "Rebuild search index sitecore_master_index",
        "Polling job sitecore_master_index. Status : Running and processed 0.",
        "Polling job sitecore_master_index. Status : Running and processed 2105.",
        "Polling job sitecore_master_index. Status : Running and processed 6191.",
        "Polling job sitecore_master_index. Status : Running and processed 10345.",
        "Polling job sitecore_master_index. Status : Running and processed 13075.",
        "Polling job sitecore_master_index. Status : Running and processed 18163.",
        "Polling job sitecore_master_index. Status : Finished and processed 20593.",
        "Finished polling job .",
        "=========  END RECONNECTING CATALOG MyCatalog  ========="
    ],
    "duration":  "00:01:26.8856534",
    "catalogname":  "MyCatalog",
    "process":  "ReconnectingCatalog"
}

To call this in your DevOps release pipeline, you can create a step in your task group of type Azure PowerShell with an inline script like below:

# Only need to reconnect catalog if the Sitecore XC system has got a bootstrap, initialize, clean
if ($(xc-initialClean)) {
  $result = Invoke-WebRequest -Uri "https://customer_cm.azurewebsites.net/-/script/v2/master/customer_ReconnectCatalog?sitepath=/sitecore/content/MyTenant/MySite&catalogname=MyCatalog&wait=1&user=admin&password=$(sitecoreAdminPassword)"
  if ($result.StatusCode -ne 200) {
    Throw "Request to url '$Url' failed with status code $($result.StatusCode)"
    [Environment]::Exit(1)
  }
}

Note the $(sitecoreAdminPassword) variable used in the Url.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.