Sometimes you want your build/release system to execute some Sitecore process. I’ve found that this method works pretty well. To date I’ve used this same model to:
- Sitecore Publish
- Index rebuild
- Package install
Note: this is an adaptation of the strategy pioneered in this blog post
There are three separate concerns in this technique
- Web service to perform the operation
- Kudu for dynamic service installation
- Powershell to execute the operation
Step 1 – create the service
This code will run a publish
using System.Collections.Generic; using System.Linq; using System.Web.Services; using Sitecore.Jobs; [WebService(Namespace = "http://tempuri.org/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] [System.ComponentModel.ToolboxItem(false)] public class PublishManager : System.Web.Services.WebService { [WebMethod(Description = "Publishes all content")] public bool PublishAll(string token) { if (string.IsNullOrEmpty(token)) return false; if (token != "[TOKEN]") return false; var db = Sitecore.Configuration.Factory.GetDatabase("master"); var item = db.GetRootItem(); var publishingTargets = Sitecore.Publishing.PublishManager.GetPublishingTargets(item.Database); foreach (var publishingTarget in publishingTargets) { var targetDatabaseName = publishingTarget["Target database"]; if (string.IsNullOrEmpty(targetDatabaseName)) continue; var targetDatabase = Sitecore.Configuration.Factory.GetDatabase(targetDatabaseName); if (targetDatabase == null) continue; var publishOptions = new Sitecore.Publishing.PublishOptions( item.Database, targetDatabase, Sitecore.Publishing.PublishMode.Smart, item.Language, System.DateTime.Now); var publisher = new Sitecore.Publishing.Publisher(publishOptions); publisher.Options.RootItem = item; publisher.Options.Deep = true; publisher.PublishAsync(); } return true; } [WebMethod(Description = "Checks publish status")] public string[] PublishStatus() { return JobManager.GetJobs().Where(x => !x.IsDone && x.Name.StartsWith("Publish")).Select(x => x.Status.Processed + " -> " + x.Name).ToArray(); } }
This asmx service has two methods. The first method initiates the publish to all publishing targets using the root Sitecore item as starting point. The second method checks the status. It’s important to do it this way because Azure has an shortish forced timeout that’s something like 3-5 minutes, which a publish can easilly surpass. To avoid this, we trigger the publish asynchronously then use the next method to check the status until the publish is completed.
Step 2 – Kudu powershell scripts
Note: This powershell code requires that you have an authenticated azure session to the appropriate Azure subscription.
function Get-AzureRmWebAppPublishingCredentials($resourceGroupName, $webAppName, $slotName = $null){ if ([string]::IsNullOrWhiteSpace($slotName) -or $slotName.ToLower() -eq "production"){ $resourceType = "Microsoft.Web/sites/config" $resourceName = "$webAppName/publishingcredentials" } else{ $resourceType = "Microsoft.Web/sites/slots/config" $resourceName = "$webAppName/$slotName/publishingcredentials" } $publishingCredentials = Invoke-AzureRmResourceAction -ResourceGroupName $resourceGroupName -ResourceType $resourceType -ResourceName $resourceName -Action list -ApiVersion 2015-08-01 -Force return $publishingCredentials } function Get-KuduApiAuthorisationHeaderValue($resourceGroupName, $webAppName, $slotName = $null){ $publishingCredentials = Get-AzureRmWebAppPublishingCredentials $resourceGroupName $webAppName $slotName $ret = @{} $ret.header = ("Basic {0}" -f [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $publishingCredentials.Properties.PublishingUserName, $publishingCredentials.Properties.PublishingPassword)))) $ret.url = $publishingCredentials.Properties.scmUri return $ret } function Get-FileFromWebApp($resourceGroupName, $webAppName, $slotName = "", $kuduPath){ $KuduAuth = Get-KuduApiAuthorisationHeaderValue $resourceGroupName $webAppName $slotName $kuduApiAuthorisationToken = $KuduAuth.header $kuduApiUrl = $KuduAuth.url + "/api/vfs/site/wwwroot/$kuduPath" Write-Host " Downloading File from WebApp. Source: '$kuduApiUrl'." -ForegroundColor DarkGray $tmpPath = "$($env:TEMP)\$([guid]::NewGuid()).xml" $null = Invoke-RestMethod -Uri $kuduApiUrl ` -Headers @{"Authorization"=$kuduApiAuthorisationToken;"If-Match"="*"} ` -Method GET ` -ContentType "multipart/form-data" ` -OutFile $tmpPath $ret = Get-Content $tmpPath | Out-String Remove-Item $tmpPath -Force return $ret } function Write-FileToWebApp($resourceGroupName, $webAppName, $slotName = "", $fileContent, $kuduPath){ $KuduAuth = Get-KuduApiAuthorisationHeaderValue $resourceGroupName $webAppName $slotName $kuduApiAuthorisationToken = $KuduAuth.header $kuduApiUrl = $KuduAuth.url + "/api/vfs/site/wwwroot/$kuduPath" Write-Host " Writing File to WebApp. Destination: '$kuduApiUrl'." -ForegroundColor DarkGray Invoke-RestMethod -Uri $kuduApiUrl ` -Headers @{"Authorization"=$kuduApiAuthorisationToken;"If-Match"="*"} ` -Method Put ` -ContentType "multipart/form-data"` -Body $fileContent } function Write-FileFromPathToWebApp($resourceGroupName, $webAppName, $slotName = "", $filePath, $kuduPath){ $KuduAuth = Get-KuduApiAuthorisationHeaderValue $resourceGroupName $webAppName $slotName $kuduApiAuthorisationToken = $KuduAuth.header $kuduApiUrl = $KuduAuth.url + "/api/vfs/site/wwwroot/$kuduPath" Write-Host " Writing File to WebApp. Destination: '$kuduApiUrl'." -ForegroundColor DarkGray Invoke-RestMethod -Uri $kuduApiUrl ` -Headers @{"Authorization"=$kuduApiAuthorisationToken;"If-Match"="*"} ` -Method Put ` -ContentType "multipart/form-data"` -InFile $filePath } function Write-ZipToWebApp($resourceGroupName, $webAppName, $slotName = "", $zipFile, $kuduPath){ $KuduAuth = Get-KuduApiAuthorisationHeaderValue $resourceGroupName $webAppName $slotName $kuduApiAuthorisationToken = $KuduAuth.header $kuduApiUrl = $KuduAuth.url + "/api/zip/site/wwwroot/$kuduPath" Write-Host " Writing Zip to WebApp. Destination: '$kuduApiUrl'." -ForegroundColor DarkGray Invoke-RestMethod -Uri $kuduApiUrl ` -Headers @{"Authorization"=$kuduApiAuthorisationToken;"If-Match"="*"} ` -Method Put ` -ContentType "multipart/form-data"` -InFile $zipFile } function Remove-FileFromWebApp($resourceGroupName, $webAppName, $slotName = "", $kuduPath){ $KuduAuth = Get-KuduApiAuthorisationHeaderValue $resourceGroupName $webAppName $slotName $kuduApiAuthorisationToken = $KuduAuth.header $kuduApiUrl = $KuduAuth.url + "/api/vfs/site/wwwroot/$kuduPath" Write-Host " Writing File to WebApp. Destination: '$kuduApiUrl'." -ForegroundColor DarkGray Invoke-RestMethod -Uri $kuduApiUrl ` -Headers @{"Authorization"=$kuduApiAuthorisationToken;"If-Match"="*"} ` -Method Delete ` -ContentType "multipart/form-data" }
This is a collection of kudu utilities to manage files on an app service, which is important for what we’re going to do next.
Step 3 – Manage the publish
param( [Parameter(Mandatory=$true)] [string]$ResourceGroupName, [Parameter(Mandatory=$true)] [string]$AppServiceName ) . "$PSScriptRoot\Get-KuduUtility.ps1" $folderKey = -join ((97..122) | Get-Random -Count 10 | ForEach-Object {[char]$_}) $accessKey = -join ((97..122) | Get-Random -Count 10 | ForEach-Object {[char]$_}) try{ (Get-Content "$PSScriptRoot\PublishManager.asmx").Replace("[TOKEN]", $accessKey) | Set-Content "$PSScriptRoot\tmp.asmx" Write-FileFromPathToWebApp -resourceGroupName $ResourceGroupName -webAppName $AppServiceName -slotName "" -filePath "$PSScriptRoot\tmp.asmx" -kuduPath "PublishManager/$folderKey/PublishManager.asmx" Remove-Item "$PSScriptRoot\tmp.asmx" -Force $site = Get-AzureRmWebApp -ResourceGroupName $ResourceGroupName -Name $AppServiceName $webURI= "https://$($site.HostNames | Select-Object -Last 1)/PublishManager/$folderKey/PublishManager.asmx?WSDL" try{ $null = Invoke-WebRequest -Uri $webURI -UseBasicParsing }catch{ $null = Invoke-WebRequest -Uri $webURI -UseBasicParsing } $proxy = New-WebServiceProxy -uri $webURI $proxy.Timeout = 1800000 $ready = $proxy.PublishAll($accessKey) if (-not $ready){ throw "Unable to publish, check server logs for details." } Write-Host "Starting publish process and scanning for progress." for ($i = 0; $i -lt 180; $i++) { $done = $true $proxy.PublishStatus() | ForEach-Object { $done = $false write-host $_ } write-host "*********** $($i * 20) Seconds **********" if ($done){ Write-Host "Publish Completed." break } Start-Sleep -Seconds 20 if ($i -eq 179){ write-host "Sitecore Publish Timeout." } } }finally{ Write-Host "Removing Sitecore Publish service" Remove-FileFromWebApp -resourceGroupName $ResourceGroupName -webAppName $AppServiceName -slotName "" -kuduPath "PublishManager/$folderKey/PublishManager.asmx" Remove-FileFromWebApp -resourceGroupName $ResourceGroupName -webAppName $AppServiceName -slotName "" -kuduPath "PublishManager/$folderKey" }
There’re a few important components to this script:
- The script generates a couple guids, first obfuscates the service path, the second is used as a security key that must be passed into the service. The service is modified to write this security key into the asmx file prior to uploading to Azure.
- Uploads the modified asmx file to be dynamically compiled and utilized
- Executes the initialize publish service method
- Calls the status method every 20 seconds and outputs the current status
- Removal of the service for security purposes