Controlling deployments into XM Cloud and Vercel
Posted 27 March 2024, 22:30 | by Ben Duguid | Perma-link
I recently had to put together an Azure DevOps Pipeline to manage deployments of a new Sitecore XM Cloud project with Vercel hosted front-end. Much of the quick start documentation for both XM Cloud and Vercel basically say "Connect to your source control (ideally GitHub) and you're good to go". Clearly great for standing something up quickly but misses a number of important steps:
- Is the code suitable? Does it compile, do the tests pass, have we introduced issues through new dependencies - all of this should be checked before we even think about deploying it somewhere.
- Do things need to happen in sequence? Does the model that the front-end headless app uses need to exist on the API before it can run? In the quick start recommendations both the Sitecore and Vercel deployments would kick off simultaneously, and sometimes that would cause problems if core models hadn't been published to the Experience Edge before the pages were being built.
- Is the team ready for it? If deployments happen whenever new code is available, there's a high likelihood that the environment will restart or change while teams are working on it.
- Do we want a single source of truth for what's deployed where? If deployments are triggered as branches are updated, pulled directly through the Sitecore Deploy App or triggered in Vercel, how can we easily see what's been deployed where, tag releases, etc.?
There are always going to be a number of different solutions to these possible issues, but as we're running the project through Azure DevOps, Pipelines was the option I chose.
The Pipeline Templates
I've built the pipeline from a set of reusable modules to reduce the repetition within the pipeline, and also allow us to have a dedicated "PR Checks" pipeline that just focuses on building and testing PRs, but not have to worry about conditions to limit deployments, etc. - I'm just calling out the templates specific to deploying your application to Sitecore XM Cloud and Vercel here, assuming you'll want to ensure your build and test stages meet your requirements.
Setup Sitecore CLI
Takes parameters XmCloudClientId
, XmCloudClientSecret
Ensures the build agent is running dotnet 6.0, installs the Sitecore CLI, confirms the version number and plugins, and then logs into Sitecore Cloud and confirms the available projects (always good to confirm that you have what you're expecting!).
parameters:
- name: XmCloudClientId
type: string
- name: XmCloudClientSecret
type: string
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core sdk 6.0.x'
inputs:
packageType: sdk
version: 6.0.x
- task: Bash@3
displayName: 'Install Sitecore CLI'
inputs:
targetType: inline
script: |
dotnet tool restore
dotnet sitecore --version
dotnet sitecore plugin list
- task: Bash@3
displayName: 'Login to Sitecore CLI'
inputs:
targetType: inline
script: |
dotnet sitecore cloud login --client-credentials --client-id ${{ parameters.XmCloudClientId }} --client-secret ${{ parameters.XmCloudClientSecret }} --allow-write
dotnet sitecore cloud project list
Setup Vercel CLI
Takes optional parameter: VercelCliVersion
Ensures the build agent is using a recent version of node, and then installs the Vercel CLI and confirms the version number.
parameters:
- name: 'VercelCliVersion'
type: 'string'
default: ''
steps:
- task: UseNode@1
displayName: 'Setup Node.js'
inputs:
version: 18.x
- task: bash@3
displayName: 'Install Vercel CLI'
inputs:
targetType: 'inline'
script: |
npm install -g vercel${{ parameters.VercelCliVersion }}
vercel --version
Deploy XM Cloud
Takes parameters: XmCloudEnvironmentId
Deploys the current working directory to XM Cloud via the Sitecore CLI cloud plugin. Once it's succeeded, store the deployment ID in an output variable for later use.
parameters:
- name: XmCloudEnvironmentId
type: string
steps:
- task: Bash@3
displayName: 'Deploy Project to XM Cloud'
name: deployXmCloud
inputs:
targetType: inline
script: |
echo Deploying to XM Cloud
result=$(dotnet sitecore cloud deployment create --environment-id ${{ parameters.XmCloudEnvironmentId }} --upload --json)
echo $result
isTimedOut=$(echo $result | jq ' .IsTimedOut')
isCompleted=$(echo $result | jq ' .IsCompleted')
deploymentId=$(echo $result | jq ' .DeploymentId')
echo "##vso[task.setvariable variable=deploymentId;isOutput=true]$deploymentId"
if [ $isTimedOut = true ]
then
echo "Operation Timed Out."
exit -1
fi
if ! [ $isCompleted = true ]
then
echo "Operation Failed."
exit -1
fi
echo "Deployment Completed"
Deploy Vercel
Takes parameters: VercelToken
, VercelProjectId
, VercelTeamId
When working with an account that's a member of multiple teams, your common token needs to used with a Team Id to ensure correct scoping.
Triggers a deployment to Vercel from the current working directory.
parameters:
- name: VercelToken
type: string
- name: VercelProjectId
type: string
- name: VercelTeamId
type: string
steps:
- task: bash@3
displayName: 'Deploy Front End to Vercel Production'
inputs:
targetType: 'inline'
script: |
vercel deploy --prod --token ${{ parameters.VercelToken }}
env:
VERCEL_ORG_ID: ${{ parameters.VercelTeamId }}
VERCEL_PROJECT_ID: ${{ parameters.VercelProjectId }}
Deploy XM Cloud Promote
Takes parameters: XmCloudEnvironmentId
, XmCloudDeploymentId
Promotes an existing Sitecore XM Cloud deployment to a new environment. On success, stores the deployment ID as an output parameter for later use.
parameters:
- name: XmCloudEnvironmentId
type: string
- name: XmCloudDeploymentId
type: string
steps:
- task: Bash@3
displayName: 'Promote Project to Next Environment in XM Cloud'
name: promoteXmCloud
inputs:
targetType: inline
script: |
echo Promoting environment in XM Cloud
result=$(dotnet sitecore cloud environment promote --environment-id ${{ parameters.XmCloudEnvironmentId }} --source-id ${{ parameters.XmCloudDeploymentId }} --json)
echo $result
isTimedOut=$(echo $result | jq ' .IsTimedOut')
isCompleted=$(echo $result | jq ' .IsCompleted')
deploymentId=$(echo $result | jq ' .DeploymentId')
echo "##vso[task.setvariable variable=deploymentId;isOutput=true]$deploymentId"
echo "Deployment Completed"
The Finished Pipeline
By setting the pipeline up with these discreet stages, we're able to easily extend or adjust the pipeline, as well as add other stages as needed (for example deploying a Design System version of the front-end application to document components and styles), as well as running a manual release that only deploys the Sitecore components, or the headless Vercel application.
Because both Sitecore XM Cloud and Vercel really want to perform the build process for you (indeed, next.js seems to require a build per environment, with environment variables often baked in to the application in addition to the static page generation requiring this), we aren't able to create a single build package and store that with the release, however XM Cloud does at least allow us to "promote" a deployment to a new environment after the initial deployment, which is close enough for now - you'll also notice that we need to supply a large set of environment variables to the build-node
template - these are typically the environment variables that you'd set that allow the Sitecore JSS next.js application to communicate with a Sitecore instance and generate the various parts that are required to build the headless application.
We have libraries set up to hold the various parameters and secrets used throughout - if you ensure you set your secrets as secrets in the library, the Pipeline runtime takes care of not exposing those values in logs, etc. which is nice.
Environments can be configured with approvals and checks, allowing us to gate deployments pending team readiness - no more uncontrolled deployments into QA as soon as a PR is changed, meaning they can release code when they've finished testing the current crop of features in a stable environment.
trigger:
branches:
include:
- develop
- release/*
paths:
exclude:
- .github/**
- .vscode/**
- docker/**
- poc/**
parameters:
- name: VercelCliVersion
type: string
default: '@latest'
variables:
- group: XMC-Build
name: $(BuildPrefix)$(Rev:r)
- stage: ScanAndBuild
jobs:
- job: XmCloudBuild
displayName: 'Scan and Build the XM Cloud Application'
pool:
vmImage: 'windows-latest'
steps:
- checkout: self
clean: true
- template: /.azuredevops/templates/build/build-dotnet.yml
parameters:
BuildConfiguration: 'Release'
- job: FrontendBuild
displayName: 'Scan and Build the Frontend applications'
pool:
vmImage: 'ubuntu-latest'
variables:
- group: XMC-Dev
steps:
- checkout: self
clean: true
- template: /.azuredevops/templates/build/build-node.yml
parameters:
GraphQlEndpoint: $(GraphQlEndpoint)
JSSAppName: $(JSSAppName)
NextPublicCdpKey: $(NextPublicCdpKey)
NextPublicCdpPos: $(NextPublicCdpPos)
NextPublicCdpTargetUrl: $(NextPublicCdpTargetUrl)
NextPublicUrl: $(NextPublicUrl)
SitecoreApiKey: $(SitecoreApiKey)
SitecoreApiHost: $(SitecoreApiHost)
SitecoreEdgeContextId: $(SitecoreEdgeContextId)
SitecoreSiteName: $(SitecoreSiteName)
XmCloudEnvId: $(XmCloudEnvironmentId)
- template: /.azuredevops/templates/build/build-storybook.yml
- stage: QaSitecore
dependsOn:
- ScanAndBuild
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/develop'), startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/hotfix/')))
variables:
- group: XMC-QA
jobs:
- deployment: QaSitecore
displayName: 'Deploy Sitecore to QA'
environment: 'XMC-QA'
pool:
vmImage: 'ubuntu-latest'
strategy:
runOnce:
deploy:
steps:
- checkout: self
clean: true
- template: /.azuredevops/templates/utils/setup-sitecore-cli.yml
parameters:
XmCloudClientId: $(XmCloudClientId)
XmCloudClientSecret: $(XmCloudClientSecret)
- template: /.azuredevops/templates/deploy/deploy-xmcloud.yml
parameters:
XmCloudEnvironmentId: $(XmCloudEnvironmentId)
- stage: QaVercel
dependsOn:
- QaSitecore
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/develop'), startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/hotfix/')))
variables:
- group: XMC-QA
jobs:
- deployment: QaVercel
displayName: 'Deploy Vercel to QA'
environment: 'XMC-QA'
pool:
vmImage: 'ubuntu-latest'
strategy:
runOnce:
deploy:
steps:
- checkout: self
clean: true
- template: /.azuredevops/templates/utils/setup-vercel-cli.yml
parameters:
VercelCliVersion: ${{ parameters.VercelCliVersion }}
- template: /.azuredevops/templates/deploy/deploy-vercel.yml
parameters:
VercelProjectId: $(VercelProjectId)
VercelToken: $(VercelToken)
VercelTeamId: $(VercelTeamId)
- stage: UatSitecore
dependsOn:
- ScanAndBuild
- QaSitecore
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/hotfix/')))
variables:
- name: QaDeploymentId
value: $[stageDependencies.QaSitecore.QaSitecore.outputs['QaSitecore.deployXmCloud.deploymentId']]
- group: XMC-UAT
jobs:
- deployment: UatSitecore
displayName: 'Promote Sitecore to UAT'
environment: 'XMC-UAT'
pool:
vmImage: 'ubuntu-latest'
strategy:
runOnce:
deploy:
steps:
- checkout: self
clean: true
- template: /.azuredevops/templates/utils/setup-sitecore-cli.yml
parameters:
XmCloudClientId: $(XmCloudClientId)
XmCloudClientSecret: $(XmCloudClientSecret)
- template: /.azuredevops/templates/deploy/deploy-xmcloud-promote.yml
parameters:
XmCloudEnvironmentId: $(XmCloudEnvironmentId)
XmCloudDeploymentId: $(QaDeploymentId)
- stage: UatVercel
dependsOn:
- UatSitecore
- QaVercel
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/hotfix/')))
variables:
- group: XMC-UAT
jobs:
- deployment: UatVercel
displayName: 'Deploy Vercel to UAT'
environment: 'XMC-UAT'
pool:
vmImage: 'ubuntu-latest'
strategy:
runOnce:
deploy:
steps:
- checkout: self
clean: true
- template: /.azuredevops/templates/utils/setup-vercel-cli.yml
parameters:
VercelCliVersion: ${{ parameters.VercelCliVersion }}
- template: /.azuredevops/templates/deploy/deploy-vercel.yml
parameters:
VercelProjectId: $(VercelProjectId)
VercelToken: $(VercelToken)
VercelTeamId: $(VercelTeamId)
- stage: ProdSitecore
dependsOn:
- ScanAndBuild
- QaSitecore
- UatSitecore
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/hotfix/')))
variables:
- name: UatDeploymentId
value: $[stageDependencies.UatSitecore.UatSitecore.outputs['UatSitecore.promoteXmCloud.deploymentId']]
- group: XMC-Prod
jobs:
- deployment: ProdSitecore
displayName: 'Promote Sitecore to Prod'
environment: 'XMC-Prod'
pool:
vmImage: 'ubuntu-latest'
strategy:
runOnce:
deploy:
steps:
- checkout: self
clean: true
- template: /.azuredevops/templates/utils/setup-sitecore-cli.yml
parameters:
XmCloudClientId: $(XmCloudClientId)
XmCloudClientSecret: $(XmCloudClientSecret)
- template: /.azuredevops/templates/deploy/deploy-xmcloud-promote.yml
parameters:
XmCloudEnvironmentId: $(XmCloudEnvironmentId)
XmCloudDeploymentId: $(UatDeploymentId)
- stage: ProdVercel
dependsOn:
- ProdSitecore
- UatVercel
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/hotfix/')))
variables:
- group: XMC-Prod
jobs:
- deployment: ProdVercel
displayName: 'Deploy Vercel to Prod'
environment: 'XMC-Prod'
pool:
vmImage: 'ubuntu-latest'
strategy:
runOnce:
deploy:
steps:
- checkout: self
clean: true
- template: /.azuredevops/templates/utils/setup-vercel-cli.yml
parameters:
VercelCliVersion: ${{ parameters.VercelCliVersion }}
- template: /.azuredevops/templates/deploy/deploy-vercel.yml
parameters:
VercelProjectId: $(VercelProjectId)
VercelToken: $(VercelToken)
VercelTeamId: $(VercelTeamId)
Filed under: Azure, DevOps, Next.js, Sitecore, Vercel