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.