Automate regular tasks with PowerShell scripts
Many development teams use PowerShell scripts to speed up common tasks. Even in cases where the script automates one or two commands, a developer can simply run a file from a folder without the need to look up or memorize several specific commands.
This guide will show you how to create PowerShell scripts to automate common recurring tasks in Kentico Xperience environments.
Repository and folder structure
The examples in this guide use the structure of the Training guides repository when traversing directories.
Each script will assume it is located in a folder called scripts in the root of the repository, and will deal with projects located in the src folder.
The code samples in this guide were developed using Windows PowerShell 5.1. Note that some other versions of PowerShell may not support the same commands, and you may need to make minor adjustments for your environment.
Generate code files
Code generation is a common development function that requires a command. Xperience code generation through the .NET CLI automatically generates strongly typed C# classes and interfaces based on content types, module classes, reusable field schemas and forms that exist in the Xperience database.
The code generation command has many parameters that you can use to control which objects to include and where to save their files.
This guide’s example will demonstrate all object types and save them to the TrainingGuides.Entities project.
Save the current location to a variable before switching to the TrainingGuides.Web directory.
Utilize the PowerShell $PSScriptRoot variable to ensure the correct file paths even if you call the script from other than the scripts folder.Call the
runcommand in the .NET CLI with the--kxp-codegenoption and specify the--typeparameter for each type supported by the tool.Return back to the initial directory, in case the script is called through a command window instead of with a click.
This way developers executing the script through the command line can continue their work without having to change directories.Track errors and use that to determine whether the script returns a nonzero exit code. You can extract this into a function and add some human-friendly messages.
<#
.Synopsis
Generates code for classes, forms, and content types stored in the database.
#>
$exitCode = 0
$originalLocation = Get-Location
Set-Location -Path $PSScriptRoot/../src/TrainingGuides.Web
# https://docs.xperience.io/xp/developers-and-admins/development/content-retrieval/generate-code-files-for-xperience-objects
$contentTypesNamespace = "TrainingGuides"
function Write-Result-Get-Exit-Code {
param(
[string] $type
)
if ($LASTEXITCODE -ne 0) {
Write-Error "$type code generation failed."
return 1;
}
else{
Write-Host "$type code generation succeeded." -ForegroundColor Green
return 0;
}
Write-Host
}
#Reusable content types
dotnet run --no-build -- --kxp-codegen --type "ReusableContentTypes" --namespace $contentTypesNamespace --location "../TrainingGuides.Entities/{type}/{name}"
$exitCode = Write-Result-Get-Exit-Code "Reusable content type"
#Page content types
dotnet run --no-build -- --kxp-codegen --type "PageContentTypes" --namespace $contentTypesNamespace --location "../TrainingGuides.Entities/{type}/{name}" --skip-confirmation
$exitCode = Write-Result-Get-Exit-Code "Page content type"
#Email content types
dotnet run --no-build -- --kxp-codegen --type "EmailContentTypes" --namespace $contentTypesNamespace --location "../TrainingGuides.Entities/{type}/{name}" --skip-confirmation
$exitCode = Write-Result-Get-Exit-Code "Email content type"
#Reusable field schemas
dotnet run --no-build -- --kxp-codegen --type "ReusableFieldSchemas" --namespace $contentTypesNamespace --location "../TrainingGuides.Entities/{type}/{name}" --skip-confirmation
$exitCode = Write-Result-Get-Exit-Code "Reusable field schema"
#Custom module classes
dotnet run --no-build -- --kxp-codegen --type "Classes" --with-provider-class False --location "../TrainingGuides.Entities/{type}/{name}" --skip-confirmation
$exitCode = Write-Result-Get-Exit-Code "Class"
#Forms
dotnet run --no-build -- --kxp-codegen --type "Forms" --location "../TrainingGuides.Entities/{type}/{name}" --skip-confirmation
$exitCode = Write-Result-Get-Exit-Code "Form"
if ($exitCode -ne 0) {
Set-Location -Path $originalLocation
Write-Error "Completed with errors. See above."
Read-Host -Prompt "Press Enter to exit"
exit $exitCode
}
Set-Location -Path $originalLocation
Read-Host -Prompt "Press Enter to exit"
Visit our Code generator documentation to learn more about its parameters and usage. Additionally, you can see a more detailed exploration and examples in this video.
Publish
The .NET CLI allows for projects to be built and published, meaning this process can be automated. Using a Powershell script, you can carry out additional automated steps before and after, and ensure your team doesn’t need to worry about Visual Studio publish profiles.
This example is relatively straightforward, but you can expand it with additional deployment tasks for your specific scenario.
- Take a
switchparameter called$KeepProductVersionto represent whether or not a custom build number should be used as the version suffix. - Save the current location and set up variables to store the path to the output folder and the build number.
- Switch to the TrainingGuides.Web directory.
- Assemble a new string containing the
dotnet publishcommand based on the value of the$KeepProductVersionparameter, and execute it. - Log any errors before returning to the original directory, in case the script is called through a command window instead of by clicking.
<#
.Synopsis
Creates a deployment package.
#>
[CmdletBinding()]
param ([switch]$KeepProductVersion)
$originalLocation = Get-Location
Set-Location -Path $PSScriptRoot
$outputFolderPath = "./bin/Deployment/"
$buildNumber = (Get-Date).ToUniversalTime().ToString("yyyyMMddHHmm")
Set-Location -Path ../src/TrainingGuides.Web
# Publish the application in the 'Release' mode
$publishCommand = "dotnet publish --nologo -c Release --self-contained true --runtime win-x64 -o $OutputFolderPath"
if (!$KeepProductVersion) {
$publishCommand += " --version-suffix $buildNumber"
}
Write-Host $publishCommand
Invoke-Expression $publishCommand
if ($LASTEXITCODE -ne 0) {
Set-Location -Path $originalLocation
Write-Error "Publishing the website failed."
Read-Host -Prompt "Press Enter to exit"
exit 1
}
Set-Location -Path $originalLocation
Read-Host -Prompt "Press Enter to exit"
Continuous integration
Continuous integration (CI) is a feature of Xperience that allows you to easily share database changes with other developers on your team.
Using the --kxp-ci-store option you can serialize your data changes into XML format in your file system (see CI store). These can be shared over a source control with your team members who can then restore the changes to update their database, using --kxp-ci-restore (see CI restore).
The following sections will share tips and best practices on how to work with these commands to help your team be the most effective.
CI store
The following script serializes database data and automatically stores it in the App_Data/CIRepositorydirectory of your current project - in this case, the TrainingGuides.Web.
- Save the current location to a variable, and switch to the TrainingGuides.Web directory.
- Call the
runcommand in the .NET CLI with the--kxp-ci-storeoption.- Optionally, use the
--no-buildparameter to save time, if your team knows to run the command only when the site has been compiled with any necessary updates.
- Optionally, use the
- Log an error if there are any issues.
- Return to the original directory, in case it is being run from a PowerShell window instead of with a click.
<#
.Synopsis
Serializes database data to the continuous integration repository.
#>
$originalLocation = Get-Location
Set-Location -Path $PSScriptRoot/../src/TrainingGuides.Web
Write-Host 'Storing CI files'
dotnet run --no-build --kxp-ci-store
if ($LASTEXITCODE -ne 0) {
Set-Location -Path $originalLocation
Write-Error "CI store failed."
Read-Host -Prompt "Press Enter to exit"
exit 1
}
else{
Write-Host 'CI files stored'
}
Set-Location -Path $originalLocation
Read-Host -Prompt "Press Enter to exit"
Connection string function
Restoring data from the CIRepository requires retrieving a connection string to access the database. Because the CI restore script in this guide will need this utility, let’s create a reusable Get-ConnectionString script before diving into CI restore.
Create a new file called Get-ConnectionString.ps1 in the scripts directory of the repository.
Define a function with the same name as the file, taking two
stringparameters:$Pathand$OriginalLocation.Check if there is a CMSConnectionString saved in the user secrets, and return it if found.
Leave this part out if your team does not use user secrets.
Fall back the appSettings.json file for the CMSConnectionString if one is not found in the user secrets.
<#
.Synopsis
Contains functions for use in other scripts
#>
<#
.DESCRIPTION
Gets the database connection string from the config file
#>
function Get-ConnectionString {
param(
[string] $Path,
[string] $OriginalLocation
)
# Try to get the connection string from user secrets first
$connectionString = dotnet user-secrets list --project $Path `
| Select-String -Pattern "ConnectionStrings:" `
| ForEach-Object { $_.Line -replace '^ConnectionStrings:CMSConnectionString \= ','' }
if (-not [string]::IsNullOrEmpty($connectionString)) {
Write-Host 'Using ConnectionString from user-secrets'
return $connectionString
}
Write-Host 'Unable to find connection string in user secrets.'
$appSettingsFileNames = 'appSettings.json'
foreach ($appSettingFileName in $appSettingsFileNames)
{
$jsonFilePath = Join-Path $Path $appSettingFileName
if (Test-Path $jsonFilePath)
{
$appSettingsJson = Get-Content $jsonFilePath | Out-String | ConvertFrom-Json
$connectionString = $appSettingsJson.ConnectionStrings.CMSConnectionString;
if ($connectionString)
{
Write-Host "Using ConnectionString from $appSettingFileName"
return $connectionString;
}
}
}
Set-Location $OriginalLocation
Write-Error "Connection string not found."
Read-Host -Prompt "Press Enter to exit"
exit 1
}
CI restore
The script below covers restoring data from the App_Data/CIRepository folder of your project and updating the Xperience database to match the version specified by the NuGet packages in your application.
While it may take just a single command to restore objects from the CIRepository to the database, things get more complicated when changes are made to the database structure.
Continuous integration handles some database schema changes on its own, creating and deleting tables for custom content types, forms, and module classes. However, further customizations, such as custom indexes, are not accounted for.
Our documentation shows how to run migration scripts before and after a CI restore. Since certain changes to the database schema may interfere with the restore process, this process automatically executes certain SQL commands before and after the CI restore operation.
This guide’s example closely follows the PowerShell script provided by the documentation. Start by copying the script and changing it in three key ways.
- Assume that the script is stored in the scripts folder of the repository, and remove the path as a parameter.
- Use the
Get-ConnectionStringfunction from the previous section, rather than including the function innately. - Add a
Handle-Errorfunction that sets location back to the original directory, logs an error and returnsexit 1. Use it throughout the script.
<#
.Synopsis
Restores objects serialized in the CI repository into the database.
#>
param (
# Displays time elapsed for the restore operation including migrations
[switch] $DisplayTimeElapsed
)
$originalLocation = Get-Location
Set-Location -Path $PSScriptRoot
. .\Get-ConnectionString.ps1
$beforeList = "Before.txt"
$afterList = "After.txt"
$repositoryPath = "App_data\CIRepository"
$migrationFolder = "@migrations"
Set-Location -Path ../src/TrainingGuides.Web
$path = Get-Location
<#
.DESCRIPTION
Handles errors by displaying a message and exiting the script.
#>
function Handle-Error {
param(
[string] $Message
)
Set-Location -Path $originalLocation
Write-Error $Message
Read-Host -Prompt "Press Enter to exit"
exit 1
}
<#
.DESCRIPTION
Runs a database migration with the given name
#>
function Run-Migration {
param(
[System.Data.SqlClient.SqlConnection] $Connection,
[System.Data.SqlClient.SqlTransaction] $Transaction,
[string] $MigrationName
)
$migrationPath = "$path\$repositoryPath\$migrationFolder\$MigrationName.sql"
if (!(Test-Path $migrationPath)) {
Write-Error "The file $migrationPath does not exist."
return $FALSE
}
$sourceScript = Get-Content $migrationPath
$sqlCommand = ""
$sqlList = @()
foreach ($line in $sourceScript) {
if ($line -imatch "^\s*GO\s*$") {
$sqlList += $sqlCommand
$sqlCommand = ""
}
else {
$sqlCommand += $line + "`r`n"
}
}
$sqlList += $sqlCommand
$rowsAffected = 0
foreach ($sql in $sqlList) {
if ([bool]$sql.Trim()) {
$command = New-Object System.Data.SqlClient.SqlCommand($sql, $Connection)
$command.Transaction = $Transaction
try {
$rowsAffectedInBatch = $command.ExecuteNonQuery()
if ($rowsAffectedInBatch -gt 0) {
$rowsAffected += $rowsAffectedInBatch
}
}
catch {
Write-Error $_.Exception.Message
return $FALSE
}
}
}
Log-RowsAffected -Connection $Connection -Transaction $Transaction -MigrationName $MigrationName -RowsAffected $rowsAffected
return $TRUE
}
<#
.DESCRIPTION
Logs rows affected by the migration.
#>
function Log-RowsAffected {
param(
[System.Data.SqlClient.SqlConnection] $Connection,
[System.Data.SqlClient.SqlTransaction] $Transaction,
[string] $MigrationName,
[int] $RowsAffected
)
$logRowsAffectedQuery = "UPDATE CI_Migration SET RowsAffected = $RowsAffected WHERE MigrationName = '$MigrationName'"
$logRowsAffectedCommand = New-Object System.Data.SqlClient.SqlCommand($logRowsAffectedQuery, $Connection)
$logRowsAffectedCommand.Transaction = $Transaction
try {
$logRowsAffectedCommand.ExecuteNonQuery()
}
catch {
Write-Host "Can't log rows affected: $_.Exception.Message"
}
}
<#
.DESCRIPTION
Checks if a migration with the given name was already applied. If not, the method returns false and the migration is marked as applied.
#>
function Check-Migration {
param(
[System.data.SqlClient.SQLConnection] $Connection,
[System.Data.SqlClient.SqlTransaction] $Transaction,
[string] $MigrationName
)
$sql = "DECLARE @migrate INT
EXEC @migrate = Proc_CI_CheckMigration '$MigrationName'
SELECT @migrate"
$command = New-Object system.data.sqlclient.sqlcommand($sql, $Connection)
$command.Transaction = $Transaction
return $command.ExecuteScalar()
}
<#
.DESCRIPTION
Runs all migrations in the migration list
#>
function Run-MigrationList {
param(
[string] $ConnectionString,
[string] $MigrationList
)
$migrations = Get-Content "$path\$repositoryPath\$MigrationList"
$connection = New-Object system.data.SqlClient.SQLConnection($ConnectionString)
$connection.Open()
foreach ($migrationName in $migrations) {
$transaction = $connection.BeginTransaction("MigrationTransaction")
if (Check-Migration -Connection $connection -Transaction $transaction -MigrationName $migrationName) {
Write-Host "Applying migration '$migrationName'."
if (!(Run-Migration -Connection $Connection -Transaction $transaction -MigrationName $migrationName)) {
$transaction.Rollback()
$connection.Close()
return $FALSE
}
}
$transaction.Commit()
}
$connection.Close()
return $TRUE
}
<#
.DESCRIPTION
Restores the repository to the database and executes migrations before and after the restore.
#>
function Run-Restore {
param(
[string] $Path
)
$connectionString = Get-ConnectionString -Path $Path -OriginalLocation $originalLocation
# Creates an 'App_Offline.htm' file to stop the website
"<html><head></head><body>Continuous Integration restore in progress...</body></html>" > "$Path\App_Offline.htm"
# Executes migration scripts before the restore
if (!(Run-MigrationList $connectionString $beforeList)) {
Handle-Error "Database migrations before the restore failed."
}
$configuration = "Release";
if (Test-Path (Join-Path $Path "bin\Debug"))
{
$configuration = "Debug";
}
# Runs the restore CLI command
dotnet run --project $Path --no-build -c "$configuration" -- --kxp-ci-restore
if ($LASTEXITCODE -ne 0) {
Handle-Error "Restore failed."
}
# Executes migration scripts after the restore
if (!(Run-MigrationList $connectionString $afterList)) {
Handle-Error "Database migrations after the restore failed."
}
# Removes the 'App_Offline.htm' file to bring the site back online
Remove-Item "$Path\App_Offline.htm"
Write-Host "Done"
}
$sw = [System.Diagnostics.Stopwatch]::StartNew()
Run-Restore -Path $path
$sw.Stop()
if ($DisplayTimeElapsed) {
Write-Host "Time Elapsed: $($sw.Elapsed)"
}
if ($LASTEXITCODE -ne 0) {
Handle-Error "Completed with errors. See above."
}
Set-Location -Path $originalLocation
Read-Host -Prompt "Press Enter to exit"
While most of the script is copied from the documentation, it is still worth understanding its primary components, and the structures it relies on.
Essentially, the script needs the App_Data/CIRepository folder to contain files called Before.txt and After.txt. These text files can hold lists of the names of .sql files in the @migrations subfolder (not including the extension). The lists in the text files determine which of these SQL files, called migrations, are executed, and in what order.
Information about the migrations is logged into the CI_Migration table of the database, which you can check to ensure that the same migration does not run multiple times.
Run-Migration- Finds the SQL file that corresponds to the provided migration name and executes the commands within it, using the provided connection and transaction.
- Returns
$FALSEif it throws an exception,$TRUEotherwise. - Is called by
Run-MigrationList.
Log-RowsAffected- Logs how many rows were affected by a migration to the row corresponding to it in the
CI_Migrationtable of the database. - Is called by
Run-Migration.
- Logs how many rows were affected by a migration to the row corresponding to it in the
Check-Migration- Checks the
CI_Migrationtable to see if a migration with the given name was already applied. - Returns
$TRUEif the migration already exists,$FALSEotherwise. - Is called by
Run-MigrationList.
- Checks the
Run-MigrationList- Establishes a database connection, then creates and executes a new transaction for each migration name in the provided list.
- Commits the transactions after they are executed.
- Rolls back transaction and returns
$FALSEif an exception is encountered, returns$TRUEotherwise. - Is called by
Run-Restore.
Run-RestoreTakes the application offline and runs the Before migration list.
Runs a CI restore.
Just like the CI store script above, CI restore also uses the optional--no-buildparameter. Consider your team’s procedures when deciding whether to include it.Runs the After migration list and brings the application back online.
Writes any errors it encounters along the way.
To provide this script with the conditions it needs, create new text files named Before.txt and After.txt in the App_Data/CIRepository folder of the TrainingGuides.Web project, along with an empty folder named @migrations.
You can find an example of the type of migration that can be run here on this documentation page.
Update
The last script updates your project to a specific version of Xperience by Kentico. It sets the Xperience NuGet packages of the project to the version you pass in, and then updates the database to match.
To prevent continuous integration operations from interfering and causing errors, the script disables CI before it changes anything, and re-enables it once the update is finished.
Xperience provides .NET CLI commands for managing the continuous integration state without starting the administration: --kxp-ci-disable and --kxp-ci-enable. Both support the --format json option, which makes them print their result as a line of JSON that a script can parse.
Note that the .NET CLI continuous integration commands require refresh 31.6.0 or newer.
Both the CI state commands and the update itself start the application through dotnet run, so the project needs a working CMSConnectionString connection string - in the appsettings.json file, user secrets, or an environment variable.
- Add a mandatory
Versionparameter that specifies the version to update the packages to, and validate its format.- An explicit version keeps runs reproducible, and works for projects that use Central Package Management, where the versions live in a shared Directory.Packages.props file.
- You can find the latest available version by running
dotnet list <project> package --outdated.
- Add a
SkipConfirmationswitch parameter. Without it, the update command prompts for confirmation of the database backup, which prevents the script from running unattended. - Group the values that differ between projects - the project file, launch profiles, and the list of Xperience packages - into a configuration section at the top of the script, so that other teams only need to edit one place.
- Add helper functions for the repeated tasks:
Invoke-ExpressionWithExceptionruns a command and throws if it returns a nonzero exit code.Write-Status,Write-Notification, andWrite-Errorprint color-coded progress messages.Write-CommandOutputprints the output of a failed command. The CLI commands report problems such as an unreachable database through their output rather than the exit code, so the output needs to be visible for the failure to be actionable.
- Resolve the path to the project file relative to
$PSScriptRootand verify that the file exists. - Add an
Invoke-CIStateCommandfunction that runs one of the CI state commands with the--format jsonoption and returns the parsed result.- Because
dotnet runalso prints build output and log messages, the function selects the output line that holds the JSON result rather than parsing the whole output.
- Because
- Add a
Restore-CIStatefunction that re-enables CI only if it was enabled before, and reports a problem instead of throwing, so that it does not mask an earlier failure. - Choose the launch profile and build configuration according to the
ASPNETCORE_ENVIRONMENTvariable. - Disable continuous integration with the
--kxp-ci-disableoption and save thechangedvalue of the result for future reference. It isfalseif continuous integration was already disabled, which means the script must leave it disabled after the update. - Set every configured Xperience package to the version from the
Versionparameter. - Call the
runcommand in the .NET CLI and use the--kxp-updateoption to trigger the update, adding--skip-confirmationif the switch was used. If the update fails, re-enable continuous integration when it was enabled before, so that a failed update does not leave the instance with CI turned off. - If continuous integration was initially enabled, re-enable it with the
--kxp-ci-enableoption.
Disable CI before updating the packages
The order of the last steps matters. The CI state commands run through dotnet run, which starts the application, and the application refuses to start once the package version and the database version differ.
If you update the packages first, --kxp-ci-disable fails with a message about the database version not matching the project version, and the CI state can only be changed again after the database is updated.
The example below leaves out the documentation comments of the script for brevity. See the full script in the Training guides repository for the complete version, including its setup instructions and troubleshooting tips.
param (
# Version to update the Xperience by Kentico packages to, for example '31.7.3'
[Parameter(Mandatory = $true)]
[ValidatePattern('^\d+(\.\d+){1,3}(-[A-Za-z0-9.]+)?$')]
[string] $Version,
# Skips the database backup prompt of the update command, so that the script can run unattended
[switch] $SkipConfirmation
)
#region Configuration - UPDATE THESE VALUES FOR YOUR PROJECT
# 1. PROJECT CONFIGURATION
# Update these paths to match your project structure
$SCRIPT_CONFIG_PROJECT_FOLDER = "src/YourProject.Web" # Folder containing your .csproj file (relative to the script parent directory, including the parent directory name)
$SCRIPT_CONFIG_PROJECT_FILE = "YourProject.Web.csproj" # Name of your .csproj file
# 2. LAUNCH PROFILES
# Update these profile names to match your project's launchSettings.json
$SCRIPT_CONFIG_CI_LAUNCH_PROFILE = "YourProject.WebCI" # Launch profile for CI environment
$SCRIPT_CONFIG_DEV_LAUNCH_PROFILE = "YourProject.Web" # Launch profile for development environment
# 3. NUGET PACKAGES
# The list must cover every Xperience package referenced anywhere in your solution
$SCRIPT_CONFIG_XPERIENCE_PACKAGES = @(
"kentico.xperience.admin", # Administration interface
"kentico.xperience.azurestorage", # Azure Blob Storage integration
"kentico.xperience.core", # Core API, often referenced directly by class library projects
"kentico.xperience.imageprocessing", # Image processing capabilities
"kentico.xperience.mjml", # MJML email template support
"kentico.xperience.webapp" # Core web application functionality
)
#endregion
#region Helper Functions
#Executes a command and throws an exception if it fails (non-zero exit code)
function Invoke-ExpressionWithException {
param([string]$Command)
Write-Host "Executing: $Command" -ForegroundColor Yellow
Invoke-Expression $Command
if ($LASTEXITCODE -ne 0) {
throw "Command failed with exit code $LASTEXITCODE"
}
}
#Displays a status message in green color for major operation updates
function Write-Status {
param([string]$Message)
Write-Host $Message -ForegroundColor Green
}
#Displays a notification message in cyan color for successful operations
function Write-Notification {
param([string]$Message)
Write-Host $Message -ForegroundColor Cyan
}
#Displays an error message in red color for failed operations
function Write-Error {
param([string]$Message)
Write-Host $Message -ForegroundColor Red
}
#Displays the output of a failed command, so that its cause is visible
function Write-CommandOutput {
param([object[]]$Output)
if (-not $Output) {
return
}
Write-Error "Output of the failed command:"
$Output | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray }
}
#endregion
#region Project Path Configuration
Write-Status "Configuring project paths..."
# Set project path using configuration variables
$projectPath = Join-Path (Split-Path $PSScriptRoot -Parent) $SCRIPT_CONFIG_PROJECT_FOLDER
$projectFile = Join-Path $projectPath $SCRIPT_CONFIG_PROJECT_FILE
# Validate that the project file exists
if (!(Test-Path $projectFile)) {
throw "Project file not found at: $projectFile"
}
Write-Notification "Project file validated: $projectFile"
#endregion
#region Continuous Integration Functions
#Runs one of the continuous integration state commands of the .NET CLI and returns its result
function Invoke-CIStateCommand {
param(
[string] $Option,
[string] $ProjectFile,
[string] $LaunchProfile,
[string] $Configuration
)
# The '--format json' option makes the command print its result as a single line of JSON
$command = "dotnet run " + `
"--project `"$ProjectFile`" " + `
"--launch-profile $LaunchProfile " + `
"-c $Configuration " + `
"-- $Option --format json"
Write-Host "Executing: $command" -ForegroundColor Yellow
# '2>&1' keeps the error output, which holds the reason when the command fails to start
$output = Invoke-Expression "$command 2>&1"
if ($LASTEXITCODE -ne 0) {
Write-CommandOutput $output
throw "Command '$Option' failed with exit code $LASTEXITCODE"
}
# 'dotnet run' also prints build output, so pick out the line that holds the JSON result
$json = $output | Where-Object { "$_".Trim().StartsWith('{') } | Select-Object -Last 1
if ([string]::IsNullOrWhiteSpace($json)) {
Write-CommandOutput $output
throw "Command '$Option' did not return a JSON result."
}
$result = $json | ConvertFrom-Json
if (-not $result.success) {
throw "Command '$Option' did not succeed: $($result.message)"
}
Write-Notification $result.message
return $result
}
#Re-enables CI mode if it was enabled before the update, without masking an earlier failure
function Restore-CIState {
param(
[bool] $WasEnabled,
[string] $ProjectFile,
[string] $LaunchProfile,
[string] $Configuration
)
if (-not $WasEnabled) {
return
}
try {
Invoke-CIStateCommand '--kxp-ci-enable' $ProjectFile $LaunchProfile $Configuration | Out-Null
}
catch {
Write-Error "Could not re-enable CI mode: $($_.Exception.Message)"
Write-Error "Re-enable it manually once the database version matches the package version again."
}
}
#endregion
#region Main Update Process
# Determine launch profile and configuration based on environment
# Uses if-else approach to support PowerShell 5.1+.
if ($Env:ASPNETCORE_ENVIRONMENT -eq "CI") {
$launchProfile = $SCRIPT_CONFIG_CI_LAUNCH_PROFILE
$configuration = "Release"
}
else {
$launchProfile = $SCRIPT_CONFIG_DEV_LAUNCH_PROFILE
$configuration = "Debug"
}
Write-Status "Using launch profile: $launchProfile with configuration: $configuration"
Write-Status "Begin Xperience Update"
Write-Host "`n"
# Step 1: Disable CI mode before anything changes the package version.
# The CI state commands start the application, which refuses to start once the package version and
# the database version differ, so this must happen before the NuGet packages are updated.
Write-Status "Disabling CI mode for update process..."
$result = Invoke-CIStateCommand '--kxp-ci-disable' $projectFile $launchProfile $configuration
# The 'changed' value is false when CI mode was already disabled, in which case it must stay disabled after the update
$isUsingCI = $result.changed
# Step 2: Update the Xperience by Kentico NuGet packages
Write-Status "Updating Xperience by Kentico NuGet packages to version $Version..."
# Use the configured package list
$xperiencePackages = $SCRIPT_CONFIG_XPERIENCE_PACKAGES
foreach ($pkg in $xperiencePackages) {
Write-Status "Updating NuGet package: $pkg"
$updateCmd = "dotnet add `"$projectFile`" package $pkg --version $Version"
try {
Invoke-ExpressionWithException $updateCmd
Write-Notification "Updated $pkg to $Version."
}
catch {
Write-Error "Failed to update NuGet package ${pkg}: $($_.Exception.Message)"
}
}
# Step 3: Execute the Xperience update command
Write-Status "Running Xperience update process..."
$command = "dotnet run " + `
"--project `"$projectFile`" " + `
"--launch-profile $launchProfile " + `
"-c $configuration " + `
"-- --kxp-update"
# Without the '--skip-confirmation' option, the update command waits for a keypress to confirm the database backup prompt
if ($SkipConfirmation) {
$command += " --skip-confirmation"
}
try {
Invoke-ExpressionWithException $command
}
catch {
# Leave CI mode in the state it was in before the update
if ($isUsingCI) {
Write-Status "Re-enabling CI mode after the failed update..."
Restore-CIState $isUsingCI $projectFile $launchProfile $configuration
}
throw
}
# Step 4: Re-enable CI mode if it was enabled before the update
if ($isUsingCI) {
Write-Status "Re-enabling CI mode..."
Invoke-CIStateCommand '--kxp-ci-enable' $projectFile $launchProfile $configuration | Out-Null
}
Write-Host "`n"
Write-Status "Update Complete"
#endregion
Adjust the configuration section to match your project structure, launch profiles, and packages, then run the script from the scripts folder:
.\Update-XperienceProjectWithDatabase.ps1 -Version 31.7.3
Add the SkipConfirmation switch to run the update unattended, for example in a pipeline:
.\Update-XperienceProjectWithDatabase.ps1 -Version 31.7.3 -SkipConfirmation
Automation
Because the script doesn’t rely on database credentials to toggle continuous integration, it works well in automated pipelines and agent-driven workflows. Combined with the -SkipConfirmation switch, it can run fully unattended.
Data consistency
The script only brings the database up to the version of the packages - it does not serialize objects to the CI repository. After the update finishes, we highly recommend taking the following steps:
- Building your application
- Running CI store
This ensures that updates to the schema of objects in the database are not in conflict with the data serialized in the CI repository.
Make sure your solution builds without errors before running CI store - otherwise, the serialized data may not be in the correct format.
What’s next?
Scripts like these will save your developers time and uncertainty in recurring tasks. Most of them need nothing more than a right-click and Run with PowerShell, or a PowerShell command line opened in the scripts folder - only the update script requires a parameter, so run it from the command line. You may want to customize these scripts to fit your team’s procedures or look into any other tasks that could potentially be automated in similar ways.