Optimizely DXP Deployment API PowerShell Scripts for CI/CD in Azure Devops – Part 1

In late-2019, EpiServer released one of their Beta programs, which would allow partners and developers the ability to control the DXP environment deployments via an API.

There have been a few blog posts (like this one by Anders Wahlqvist) on how to use these APIs in one-off instances, but none in how to make these into reusable generic PowerShell scripts.

What we will be going over in this post is how we use these concepts to create reusable Powershell scripts that can be used for any DXP deployment environment for any client or site.

In the next part of this series, we will cover how to set up a manual Release Pipeline to deploy into Integration.

This will eventually lead us to where we will be able to streamline the creation of a CI/DI Release Pipeline that publish all the way from Integration, to Preproduction, and finally to Production in an automated fashion that uses EpiServers deployment process.

Deployment Workflow

The workflow displayed below is the ideal workflow for setting up an EpiServer release pipeline.

As noted above, the scope of this post is going be the creation of PowerShell scripts that will eventually be used in a release pipeline, so we will be covering what is colored in blue.

PowerShell Scripts

As seen in the EpiServer documentation, there are a bunch of PowerShell commands that can be used to control these deployments.

As seen in the workflow above, there are five major events that are part of this API:

  • Uploading the nupkg file to the Epi deployment staging area
  • Deploying the uploaded nupkg file to an environment
  • Deploying to an environment from a source environment
  • Completing a Deployment
  • Resetting a deployment

Upload Package to Staging Area

This script is going to be using the following of the API calls:

  • Get-EpiDeploymentPackageLocation
  • Add-EpiDeploymentPackage

The flow is going to look something like the following:

    1. Invoke script with 4 mandatory Parameters
      1. Client Key for intended environment
      2. Client Secret for intended environment
      3. Project ID (global for subscription)
      4. Artifact Path (Where is the Nupkg file located)
param
  (
    [Parameter(Position=0, Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string]$ClientKey,
    [Parameter(Position=1, Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string]$ClientSecret,
    [Parameter(Position=2, Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string]$ProjectID,
    [Parameter(Position=3, Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string]$ArtifactPath
  )
    1. Validate the parameter
      1. Not Empty/Null
      2. Not Whitespace
if([string]::IsNullOrWhiteSpace($ClientKey)){
    throw "A Client Key is needed. Please supply one."
}
......
if([string]::IsNullOrWhiteSpace($ArtifactPath)){
    throw "A path for the NUPKG file location is needed. Please supply one."
}
    1. Ensure Nupkg file exists at the Artifact Path
$packagePath = Get-ChildItem -Path $ArtifactPath -Filter *.nupkg

if($packagePath.Length -eq 0){
    throw "No NUPKG files were found. Please ensure you're passing the correct path."
}
    1. Check to see that the EpiCloud powershell module is installed
      1. If not, install it
if (-not (Get-Module -Name EpiCloud -ListAvailable)) {
    Install-Module EpiCloud -Scope CurrentUser -Force
}
    1. Create Object and get the Cloud Package Location
$getEpiDeploymentPackageLocationSplat = @{
    ClientKey = "$ClientKey"
    ClientSecret = "$ClientSecret"
    ProjectId = "$ProjectID"
}

Write-Host "Finding deployment location..."


$packageLocation = Get-EpiDeploymentPackageLocation @getEpiDeploymentPackageLocationSplat
    1. Start the upload into the Cloud Path for the nupkg file
$deploy = Add-EpiDeploymentPackage -SasUrl $packageLocation -Path $packagePath.FullName

Invoking the script looks something like the following:

 
.Perficient_UploadEpiPackage.ps1 -ClientKey "****" 
                                  -ClientSecret "****" 
                                  -ProjectId "****" 
                                  -ArtifactPath "C:PackageLocation"

Publish to Environment via Code Package

This script is going to be using the following of the API calls:

  • Start-EpiDeployment
  • Get-EpiDeployment
The flow is going to look something like the following:
    1. Invoke script with 6 mandatory Parameters
      1. Client Key for intended environment
      2. Client Secret for intended environment
      3. Project ID (global for subscription)
      4. Artifact Path (Where is the Nupkg file located – We need the name)
      5. Target Environment (Integration, Preproduction, Production)
      6. Use Maintenance Page (True/1, False/0)
param
  (
    [Parameter(Position=0, Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string]$ClientKey,
    ......
    [Parameter(Position=3, Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string]$ArtifactPath,
    [Parameter(Position=4, Mandatory)]
    [ValidateNotNullOrEmpty()]
    [ValidateSet("Integration", "Preproduction", "Production")]
    [string]$TargetEnvironment,
    [Parameter(Position=5, Mandatory)]
    [ValidateNotNullOrEmpty()]
    [ValidateSet($true, $false, 0, 1)]
    [bool]$UseMaintenancePage
  )
    1. Validate the parameters
      1. Not Empty/Null
      2. Not Whitespace
      3. Target Environment has a valid environment name
      4. Use Maintenance Page has a valid boolean value
if([string]::IsNullOrWhiteSpace($ClientKey)){
    throw "A Client Key is needed. Please supply one."
}
......
if([string]::IsNullOrWhiteSpace($UseMaintenancePage)){
    throw "Please provide an option for if the maintenance page should be shown. Correct values are true or false."
}
    1. Ensure Nupkg file exists at the Artifact Path
$packagePath = Get-ChildItem -Path $ArtifactPath -Filter *.nupkg

if($packagePath.Length -eq 0){
    throw "No NUPKG files were found. Please ensure you're passing the correct path."
}
    1. Check to see that the EpiCloud powershell module is installed
      1. If not, install it
    2. Set up and Start the deployment
      1. When setting up the object, the Wait param can be set to true or false, but within my scenario, I would like to be able to report on the progress, so I set it to false. If it is set to true, the command will not return any value until it is done.
      2. The reason we set this to a variable is because we want the Deploy ID from the object, which will allow us to efficiently get the Epi Deployment status in the next bit of code.
$startEpiDeploymentSplat = @{
    DeploymentPackage = $packagePath.Name
    ProjectId = "$ProjectID"
    Wait = $false
    TargetEnvironment = "$TargetEnvironment"
    UseMaintenancePage = $UseMaintenancePage
    ClientSecret = "$ClientSecret"
    ClientKey = "$ClientKey"
}

Write-Host "Starting the Deployment to" $TargetEnvironment


$deploy = Start-EpiDeployment @startEpiDeploymentSplat
    1. Set up the object and Get the current deployment
$deployId = $deploy | Select -ExpandProperty "id"

$getEpiDeploymentSplat = @{
    ProjectId = "$ProjectID"
    ClientSecret = "$ClientSecret"
    ClientKey = "$ClientKey"
    Id = "$deployId"
}

$percentComplete = 0
$currDeploy = Get-EpiDeployment @getEpiDeploymentSplat | Select-Object -First 1
$status = $currDeploy | Select -ExpandProperty "status"
$exit = 0
    1. Set up a loop to query and print to the screen the current status level
      1. The Powershell Write-Progress command will not work in the Azure DevOps output screen
while($exit -ne 1){

$currDeploy = Get-EpiDeployment @getEpiDeploymentSplat | Select-Object -First 1

$currPercent = $currDeploy | Select -ExpandProperty "percentComplete"
$status = $currDeploy | Select -ExpandProperty "status"

if($currPercent -ne $percentComplete){
    Write-Host "Percent Complete: $currPercent%"
    $percentComplete = $currPercent
}

if($percentComplete -eq 100){
    $exit = 1    
}

if($status -ne 'InProgress'){
    $exit = 1
}

start-sleep -Milliseconds 1000

}
    1. If the deployment fails, throw an error
      1. This will also fail the Azure DevOps Release Task if this process fails
    2. If the deployment succeeds, set an Azure DevOps Output Variable
      1. This has been demonstrated in previous examples but using the output variables hasn’t been the easiest process to set up and use, as it has changed with different scenarios
      2. In the Reset/Complete scripts, we will look at a way to get around having to use the output variable, which will make it easier for these scripts to run independently
if($status -eq "Failed"){
    throw "Deployment Failed. Errors: n" + $deploy.deploymentErrors
}

Write-Host "##vso[task.setvariable variable=DeploymentId;]'$deployId'"

Invoking the script looks something like the following:

 
.Perficient_DeployToEnvironment.ps1 -ClientKey "****" 
                                     -ClientSecret "****" 
                                     -ProjectId "****" 
                                     -ArtifactPath "C:FilePath" 
                                     -TargetEnvironment "Integration" 
                                     -UseMaintenancePage 0

Publish to Environment via Source Environment

This script is going to be using the following of the API calls:

  • Start-EpiDeployment
  • Get-EpiDeployment
The flow is going to look something like the following:
    1. Invoke script with 8 mandatory Parameters
      1. Client Key for intended environment
      2. Client Secret for intended environment
      3. Project ID (global for subscription)
      4. Source Environment (Integration, Preproduction, Production)
      5. Target Environment (Integration, Preproduction, Production)
      6. Use Maintenance Page (True/1, False/0)
      7. Include Blobs (True/1, False/0)
      8. Include DB (True/1, False/0)
param
  (
    [Parameter(Position=0, Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string]$ClientKey,
    ......
    [Parameter(Position=3, Mandatory)]
    [ValidateNotNullOrEmpty()]
    [ValidateSet("Integration", "Preproduction", "Production")]
    [string]$SourceEnvironment,
    [Parameter(Position=4, Mandatory)]
    [ValidateNotNullOrEmpty()]
    [ValidateSet("Integration", "Preproduction", "Production")]
    [string]$TargetEnvironment,
    [Parameter(Position=5)]
    [ValidateSet($true, $false, 0, 1)]
    [bool]$UseMaintenancePage,
    [Parameter(Position=6)]
    [ValidateSet($true, $false, 0, 1)]
    [bool]$IncludeBlobs,
    [Parameter(Position=7)]
    [ValidateSet($true, $false, 0, 1)]
    [bool]$IncludeDb
    [Parameter(Position=8, Mandatory)]
    [ValidateSet('cms','commerce')]
    [String] $SourceApp
    
  )
    1. Validate the parameters
      1. Not Empty/Null
      2. Not Whitespace
      3. Source and Target Environment has a valid environment name
      4. Source and Target Environment are not the same value
      5. Use Maintenance Page, Include Blobs, and Include DB have a valid boolean value
if([string]::IsNullOrWhiteSpace($ClientKey)){
    throw "A Client Key is needed. Please supply one."
}
......
if($SourceEnvironment -eq $TargetEnvironment){
    throw "The source environment cannot be the same as the target environment."    
}
    1. Ensure Nupkg file exists at the Artifact Path
$packagePath = Get-ChildItem -Path $ArtifactPath -Filter *.nupkg

if($packagePath.Length -eq 0){
    throw "No NUPKG files were found. Please ensure you're passing the correct path."
}
    1. Check to see that the EpiCloud powershell module is installed
      1. If not, install it
    2. Set up and Start the deployment
      1. When setting up the object, the Wait param can be set to true or false, but within my scenario, I would like to be able to report on the progress, so I set it to false. If it is set to true, the command will not return any value until it is done.
      2. The reason we set this to a variable is because we want the Deploy ID from the object, which will allow us to efficiently get the Epi Deployment status in the next bit of code.
$startEpiDeploymentSplat = @{
    DeploymentPackage = $packagePath.Name
    ProjectId = "$ProjectID"
    Wait = $false
    TargetEnvironment = "$TargetEnvironment"
    UseMaintenancePage = $UseMaintenancePage
    ClientSecret = "$ClientSecret"
    ClientKey = "$ClientKey"
    IncludeBlob = $IncludeBlobs
    IncludeDb = $IncludeDb
    SourceApp = "$SourceApp"
}

Write-Host "Starting the Deployment to" $TargetEnvironment

$deploy = Start-EpiDeployment @startEpiDeploymentSplat
    1. Set up the object and Get the current deployment
$deployId = $deploy | Select -ExpandProperty "id"

$getEpiDeploymentSplat = @{
    ProjectId = "$ProjectID"
    ClientSecret = "$ClientSecret"
    ClientKey = "$ClientKey"
    Id = "$deployId"
}

$percentComplete = 0
$currDeploy = Get-EpiDeployment @getEpiDeploymentSplat | Select-Object -First 1
$status = $currDeploy | Select -ExpandProperty "status"
$exit = 0
    1. Set up a loop to query and print to the screen the current status level
      1. The Powershell Write-Progress command will not work in the Azure DevOps output screen
while($exit -ne 1){

$currDeploy = Get-EpiDeployment @getEpiDeploymentSplat | Select-Object -First 1

$currPercent = $currDeploy | Select -ExpandProperty "percentComplete"
$status = $currDeploy | Select -ExpandProperty "status"

if($currPercent -ne $percentComplete){
    Write-Host "Percent Complete: $currPercent%"
    $percentComplete = $currPercent
}

if($percentComplete -eq 100){
    $exit = 1    
}

if($status -ne 'InProgress'){
    $exit = 1
}

start-sleep -Milliseconds 1000

}
    1. If the deployment fails, throw an error
      1. This will also fail the Azure DevOps Release Task if this process fails
    2. If the deployment succeeds, set an Azure DevOps Output Variable
      1. This has been demonstrated in previous examples but using the output variables hasn’t been the easiest process to set up and use, as it has changed with different scenarios
      2. In the Reset/Complete scripts, we will look at a way to get around having to use the output variable, which will make it easier for these scripts to run independently
if($status -eq "Failed"){
    throw "Deployment Failed. Errors: n" + $deploy.deploymentErrors
}

Write-Host "##vso[task.setvariable variable=DeploymentId;]'$deployId'"

Invoking the script looks something like the following:

 
.Perficient_PromoteToEnvironment.ps1 -ClientKey "****" 
                                      -ClientSecret "****"  
                                      -ProjectID "****" 
                                      -SourceEnvironment Integration 
                                      -TargetEnvironment Preproduction 
                                      -UseMaintenancePage 1 
                                      -IncludeBlobs $false 
                                      -IncludeDb 0
                                      -SourceApp cms

Complete Deployment

This script is going to be using the following of the API calls:

  • Get-EpiDeployment
  • Complete-EpiDeployment
The flow is going to look something like the following:

    1. Invoke script with 3 mandatory Parameters and 1 optional Parameter
      1. Client Key for intended environment
      2. Client Secret for intended environment
      3. Project ID (global for subscription)
      4. Deployment ID (Optional)
param
  (
    [Parameter(Position=0, Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string]$ClientKey,
    ......
    [Parameter(Position=3)]
    [string]$DeploymentId
  )
    1. Validate the parameters
      1. Not Empty/Null
      2. Not Whitespace
if([string]::IsNullOrWhiteSpace($ClientKey)){
    throw "A Client Key is needed. Please supply one."
}
......
if([string]::IsNullOrWhiteSpace($ProjectID)){
    throw "A Project ID GUID is needed. Please supply one."
}
    1. Check to see that the EpiCloud powershell module is installed
      1. If not, install it
    2. Set up the object to get the current deployment details
      1. If the Deployment ID is passed in, use that (This can be obtained from a previous deployment via an Output Variable)
      2. Otherwise, I have written logic that will use the Get-EpiDeployment command to look up the most recent release.
if([string]::IsNullOrWhiteSpace($DeploymentId)){
    $getEpiDeploymentSplat = @{
        ProjectId = "$ProjectID"
        ClientSecret = "$ClientSecret"
        ClientKey = "$ClientKey"
}
    }else{
        $getEpiDeploymentSplat = @{
            ProjectId = "$ProjectID"
            ClientSecret = "$ClientSecret"
            ClientKey = "$ClientKey"
            id = "$DeploymentId"
        }
}

$currDeploy = Get-EpiDeployment @getEpiDeploymentSplat | Select-Object -First 1

if([string]::IsNullOrWhiteSpace($DeploymentId)){
    Write-Host "No Deployment ID Supplied. Searching for In-Progress Deployment..."
    $DeploymentId = $currDeploy | Select -ExpandProperty "id"
    Write-Host "Deployment ID Found: $DeploymentId"
}
    1. Set up and run the command to Complete the deployment
$completeEpiDeploymentSplat = @{
    ProjectId = "$ProjectID"
    Id = "$DeploymentId"
    Wait = $false
    ClientSecret = "$ClientSecret"
    ClientKey = "$ClientKey"
}

$deploy = Complete-EpiDeployment @completeEpiDeploymentSplat
    1. Set up a loop to query and print to the screen the current status level
$percentComplete = 0
$status = $currDeploy | Select -ExpandProperty "status"
$exit = 0

Write-Host "Percent Complete: $percentComplete%"

while($exit -ne 1){

$currDeploy = Get-EpiDeployment @getEpiDeploymentSplat | Select-Object -First 1

$currPercent = $currDeploy | Select -ExpandProperty "percentComplete"
$status = $currDeploy | Select -ExpandProperty "status"

if($currPercent -ne $percentComplete){
    Write-Host "Percent Complete: $currPercent%"
    $percentComplete = $currPercent
}

if($percentComplete -eq 100){
    $exit = 1    
}

if($status -ne 'Completing'){
    $exit = 1
}

start-sleep -Milliseconds 1000

}

Invoking the script looks something like the following:

.Perficient_CompleteDeployment.ps1 -ClientKey "****" 
                                    -ClientSecret "****" 
                                    -ProjectId "****" 
                                    [-DeploymentId "****"]

Reset Deployment

This script is going to be using the following of the API calls:

  • Get-EpiDeployment
  • Reset-EpiDeployment
The flow is going to look something like the following:

    1. Invoke script with 3 mandatory Parameters and 1 optional Parameter
      1. Client Key for intended environment
      2. Client Secret for intended environment
      3. Project ID (global for subscription)
      4. Deployment ID (Optional)
param
  (
    [Parameter(Position=0, Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string]$ClientKey,
    ......
    [Parameter(Position=3)]
    [string]$DeploymentId
  )
    1. Validate the parameters
      1. Not Empty/Null
      2. Not Whitespace
if([string]::IsNullOrWhiteSpace($ClientKey)){
    throw "A Client Key is needed. Please supply one."
}
......
if([string]::IsNullOrWhiteSpace($ProjectID)){
    throw "A Project ID GUID is needed. Please supply one."
}
    1. Check to see that the EpiCloud powershell module is installed
      1. If not, install it
    2. Set up the object to get the current deployment details
      1. If the Deployment ID is passed in, use that (This can be obtained from a previous deployment via an Output Variable)
      2. Otherwise, I have written logic that will use the Get-EpiDeployment command to look up the most recent release.
if([string]::IsNullOrWhiteSpace($DeploymentId)){
    $getEpiDeploymentSplat = @{
        ProjectId = "$ProjectID"
        ClientSecret = "$ClientSecret"
        ClientKey = "$ClientKey"
}
    }else{
        $getEpiDeploymentSplat = @{
            ProjectId = "$ProjectID"
            ClientSecret = "$ClientSecret"
            ClientKey = "$ClientKey"
            id = "$DeploymentId"
        }
}

$currDeploy = Get-EpiDeployment @getEpiDeploymentSplat | Select-Object -First 1

if([string]::IsNullOrWhiteSpace($DeploymentId)){
    Write-Host "No Deployment ID Supplied. Searching for In-Progress Deployment..."
    $DeploymentId = $currDeploy | Select -ExpandProperty "id"
    Write-Host "Deployment ID Found: $DeploymentId"
}
    1. Set up and run the command to Reset the deployment
$completeEpiDeploymentSplat = @{
    ProjectId = "$ProjectID"
    Id = "$DeploymentId"
    Wait = $false
    ClientSecret = "$ClientSecret"
    ClientKey = "$ClientKey"
}

$deploy = Reset-EpiDeployment @completeEpiDeploymentSplat

    1. Set up a loop to query and print to the screen the current status level
$percentComplete = 0
$status = $currDeploy | Select -ExpandProperty "status"
$exit = 0

Write-Host "Percent Complete: $percentComplete%"

while($exit -ne 1){

$currDeploy = Get-EpiDeployment @getEpiDeploymentSplat | Select-Object -First 1

$currPercent = $currDeploy | Select -ExpandProperty "percentComplete"
$status = $currDeploy | Select -ExpandProperty "status"

if($currPercent -ne $percentComplete){
    Write-Host "Percent Complete: $currPercent%"
    $percentComplete = $currPercent
}

if($percentComplete -eq 100){
    $exit = 1    
}

if($status -ne 'Resetting'){
    $exit = 1
}

start-sleep -Milliseconds 1000

}

Invoking the script looks something like the following:

 
.Perficient_ResetDeployment.ps1 -ClientKey "****" 
                                 -ClientSecret "****" 
                                 -ProjectId "****" 
                                 [-DeploymentId "****"]

Conclusion

At this point, you should be able to create 5 different powershell scripts, which will allow you to:
  • Upload a nupkg file to the staging location
  • Deploy a previously uploaded nupkg file to an environment
  • Deploy from one environment to another
  • Complete a deployment
  • Reset a deployment
In the next (second) post, we will explore how to put these scripts into a Azure DevOps Release Pipeline, in order to do a manual deploy into a single environment, and how to Reset/Complete the deploy.
Eric Markson
Eric Markson

Lead Solutions Architect @ Optimizely

Articles: 18

One comment

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.