Skip to content

Instantly share code, notes, and snippets.

@michaelmaillot
Created April 19, 2022 06:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save michaelmaillot/ceeca60b9c3f604b431afff7190036e7 to your computer and use it in GitHub Desktop.
Save michaelmaillot/ceeca60b9c3f604b431afff7190036e7 to your computer and use it in GitHub Desktop.
Build & Deploy SPFx package in an ephemeral site before production (Azure DevOps & PnP PowerShell)
name: main
trigger:
branches:
include:
- main
stages:
- template: pipelines/stages/build-deploy-spfx-pnp-powershell.yml
parameters:
include_tests: false
variable_group_uat: 'contoso-UAT'
variable_group_prd: 'contoso-PRD'
deploy_uat: true
deploy_prd: true
parameters:
- name: include_tests
type: boolean
default: true
- name: variable_group_uat
type: string
- name: variable_group_prd
type: string
- name: deploy_uat
type: boolean
default: true
- name: deploy_prd
type: boolean
default: true
stages:
- stage: 'build'
jobs:
- template: ../jobs/build-spfx.yml
parameters:
include_tests: false
- ${{ if eq(parameters.deploy_uat, true) }}:
- stage: 'deploy_uat'
displayName: Deploy on UAT
dependsOn: [ build ]
jobs:
- template: ../jobs/deploy-spfx-pnp-powershell.yml
parameters:
target_environment: 'UAT'
variable_group_name: ${{ parameters.variable_group_uat }}
- ${{ if eq(parameters.deploy_prd, true) }}:
- stage: 'deploy_prd'
displayName: Deploy on PRD
dependsOn: [ deploy_uat ]
variables:
siteToRemove: $[stageDependencies.deploy_uat.deploy.outputs['deploy.CreateSiteCopy.siteCopy']]
# This syntax gives us the ephemeral site URL, in order to remove it once the deployment on PRD environment is done
# $[stageDependencies.STAGE_NAME.JOB_NAME.VARIABLE_NAME]
jobs:
- template: ../jobs/deploy-spfx-pnp-powershell.yml
parameters:
target_environment: 'PRD'
variable_group_name: ${{ parameters.variable_group_prd }}
uat_site_to_remove: $(siteToRemove)
# No triggers here, which makes the pipeline not callable except from another pipeline
parameters:
- name: target_environment
type: string
- name: variable_group_name
type: string
- name: uat_site_to_remove
type: string
default: ''
jobs:
- deployment: deploy
displayName: 'Upload & deploy *.sppkg to SharePoint app catalog'
pool:
vmImage: 'ubuntu-latest'
environment: ${{ parameters.target_environment }}
variables:
- group: ${{ parameters.variable_group_name }}
# This is specific to AzDO: it allows to use variables / secrets stored in a "library"
# It's very useful with multi-stage pipelines
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: drop
patterns: '**/*.sppkg'
- pwsh: Install-Module -Name "PnP.PowerShell" -Force
displayName: Installing PnP.PowerShell Module
- task: DownloadSecureFile@1
inputs:
secureFile: PnP.SharePoint.AppOnly.pfx
displayName: 'Download authentication certificate'
name: AppCertificate
# Thanks to the AzDO pipeline files management, we can store secure files
# Such as a certificate file
- pwsh: |
$package = Get-ChildItem -Path $(Pipeline.Workspace)/drop -Recurse -Filter '*.sppkg' | Select Name | Select-Object -First 1
Write-Host "##vso[task.setvariable variable=SpPkgFileName;isOutput=true]$($package.Name)"
name: GetSharePointPackage
displayName: Get generated *.sppkg filename
- ${{ if ne(parameters.target_environment, 'PRD') }}:
- pwsh: |
$securePassword = ConvertTo-SecureString "$(aad_app_password)" -AsPlainText -Force
Connect-PnPOnline -Url $(site_url_prd) -ClientId $(aad_app_id) -Tenant $(aad_tenant_id) -CertificatePath "$(AppCertificate.secureFilePath)" -CertificatePassword $securePassword
Get-PnPSiteTemplate -Out $(Pipeline.Workspace)/template.pnp
Add-PnPDataRowsToSiteTemplate -Path $(Pipeline.Workspace)/template.pnp -List "$(site_list_prd)"
displayName: Connecting to Production site and get site template (with list data)
- pwsh: |
$securePassword = ConvertTo-SecureString "$(aad_app_password)" -AsPlainText -Force
Connect-PnPOnline -Url $(site_url_prd) -ClientId $(aad_app_id) -Tenant $(aad_tenant_id) -CertificatePath "$(AppCertificate.secureFilePath)" -CertificatePassword $securePassword
$web = Get-PnPWeb
$siteName = "$($web.Title)-${{ parameters.target_environment }}" + [guid]::NewGuid().Guid
$uri = [System.Uri]"$(site_url_prd)"
$siteUrl = $uri.Scheme + "://" + $uri.Authority + "/sites/" + $siteName
$site = New-PnPSite -Type CommunicationSite -Title $siteName -Url $siteUrl -Owner "$(site_copy_owner)" -Wait
Write-Host "##vso[task.setvariable variable=siteCopy;isOutput=true]$site"
Write-Host $site
name: CreateSiteCopy
displayName: Create ${{ parameters.target_environment }} site copy
- pwsh: |
$securePassword = ConvertTo-SecureString "$(aad_app_password)" -AsPlainText -Force
Connect-PnPOnline -Url $(CreateSiteCopy.siteCopy) -ClientId $(aad_app_id) -Tenant $(aad_tenant_id) -CertificatePath "$(AppCertificate.secureFilePath)" -CertificatePassword $securePassword
Invoke-PnPSiteTemplate -Path $(Pipeline.Workspace)/template.pnp -ClearNavigation
displayName: Apply PRD site template to ${{ parameters.target_environment }} site copy
- pwsh: |
$uploadHasFailed = $false
try {
$securePassword = ConvertTo-SecureString "$(aad_app_password)" -AsPlainText -Force
Connect-PnPOnline -Url $(CreateSiteCopy.siteCopy) -ClientId $(aad_app_id) -Tenant $(aad_tenant_id) -CertificatePath "$(AppCertificate.secureFilePath)" -CertificatePassword $securePassword
Add-PnPSiteCollectionAppCatalog
$packageId = Add-PnPApp -Path "$(Pipeline.Workspace)/drop/sharepoint/solution/$(GetSharePointPackage.SpPkgFileName)" -Scope Site -Publish
}
catch {
Write-Host $_.Exception.Message -ForegroundColor Yellow
Write-Host "Retrying by removing and re-enabling the site collection app catalog, then the package (after 30 seconds delay)"
$uploadHasFailed = $true
}
finally {
if ($uploadHasFailed -eq $true) {
Remove-PnPSiteCollectionAppCatalog -Site $(CreateSiteCopy.siteCopy)
Add-PnPSiteCollectionAppCatalog
Start-Sleep -Seconds 30
$packageId = Add-PnPApp -Path "$(Pipeline.Workspace)/drop/sharepoint/solution/$(GetSharePointPackage.SpPkgFileName)" -Scope Site -Publish -Overwrite
}
}
displayName: Enable Site Collection App Catalog, upload SharePoint package, deploy it and add it to the site
# Enabling the site collection app catalog or deploying a solution on it can fail, as the site has just been created
# We prevent this failure, by waiting and retrying those actions
- pwsh: |
$webPartHasFailed = $false
if ('$(webpart_name)' -ne '') {
try {
$securePassword = ConvertTo-SecureString "$(aad_app_password)" -AsPlainText -Force
Connect-PnPOnline -Url $(CreateSiteCopy.siteCopy) -ClientId $(aad_app_id) -Tenant $(aad_tenant_id) -CertificatePath "$(AppCertificate.secureFilePath)" -CertificatePassword $securePassword
Add-PnPPage -Name "TestSPFx"
Add-PnPPageWebPart -Page "TestSPFx" -Component "$(webpart_name)"
}
catch {
Write-Host $_.Exception.Message -ForegroundColor Yellow
Write-Host "Retrying the WebPart addition after 30 seconds delay"
$webPartHasFailed = $true
Start-Sleep -Seconds 30
}
finally {
if ($webPartHasFailed -eq $true) {
Add-PnPPageWebPart -Page "TestSPFx" -Component "$(webpart_name)"
}
}
}
displayName: Add an article page and the WebPart (if exists)
# Here again, adding a page with the WebPart ont it can fail, as the site has just been created
# We prevent this failure, by waiting and retrying those actions
- ${{ if eq(parameters.target_environment, 'PRD') }}:
- pwsh: |
$securePassword = ConvertTo-SecureString "$(aad_app_password)" -AsPlainText -Force
Connect-PnPOnline -Url $(site_url_prd) -ClientId $(aad_app_id) -Tenant $(aad_tenant_id) -CertificatePath "$(AppCertificate.secureFilePath)" -CertificatePassword $securePassword
Add-PnPApp -Path "$(Pipeline.Workspace)/drop/sharepoint/solution/$(GetSharePointPackage.SpPkgFileName)" -Scope $(app_catalog_scope) -Publish -Overwrite
displayName: Upload & deploy SharePoint package
- ${{ if ne(parameters.uat_site_to_remove, '') }}:
- pwsh: |
$securePassword = ConvertTo-SecureString "$(aad_app_password)" -AsPlainText -Force
Connect-PnPOnline -Url $(site_url_prd) -ClientId $(aad_app_id) -Tenant $(aad_tenant_id) -CertificatePath "$(AppCertificate.secureFilePath)" -CertificatePassword $securePassword
Remove-PnPTenantSite -Url ${{ parameters.uat_site_to_remove }} -SkipRecyclebin -Force
displayName: Remove site copy
@michaelmaillot
Copy link
Author

Abstract

This sample provides a ready-to-use Azure DevOps pipeline that will build an SPFx package, then create an testing site, based on a template from the production site, on which the package will be deployed (in a site collection app catalog) dedicated to test out new features / bug fixes, with a testing page. Then the pipeline will be deployed on the tenant app catalog (or a production site collection app catalog), and finally remove the testing site. This sample will use PnP PowerShell in order to connect to SharePoint Online and deploy the SPFx package, using the Provisioning Engine. Below the different files description:

  • azure-pipelines.yml: the main pipeline that will be triggered as the initiator
  • build-deploy-spfx-pnp-powershell.yml: a global CI / CD template, that will call subsequently
    • build-spfx.yml: CI pipeline
    • deploy-spfx-pnp-powershell.yml: CD pipeline

Configuration

SharePoint site

For this sample, we'll need one site for production stage, this can use a tenant app catalog or a site collection one (in this case, it will have to be enabled first).

For this sample, the production site has to be a Communication one.

The app will have to be already installed on PRD site.

Solution

The SPFx solution can be ordered like this:

│   .gitignore
│   .npmignore
│   .yo-rc.json
│   azure-pipelines.yml  --------> main pipeline
│   gulpfile.js
│   package-lock.json
│   package.json
│   README.md
│   tsconfig.json
│   tslint.json
│
├───.vscode
│
├───config
│
├───pipelines
│   ├───jobs
│   │       build-spfx.yml  --------> CI pipeline
│   │       deploy-spfx-pnp-powershell.yml  --------> CD pipeline
│   │
│   └───stages
│           build-deploy-spfx-pnp-powershell.yml  --------> global CI / CD template
│
├───src
│   │   index.ts
│   │
│   └───webparts
│       └───helloWorld
│           │   HelloWorldWebPart.manifest.json
│           │   HelloWorldWebPart.ts
│           │
│           ├───components
│           │       HelloWorld.module.scss
│           │       HelloWorld.module.scss.ts
│           │       HelloWorld.tsx
│           │       IHelloWorldProps.ts
│           │
│           └───loc
│                   en-us.js
│                   mystrings.d.ts
│
├───teams

Environments

The environments will be necessary for approving a pipeline before being run. We'll have to add two environments:

  • UAT (testing)
  • PRD (production)

Environments can be configured here: https://dev.azure.com/[ORGANIZATION]/[PROJECT]/_environment.

Approvers

For each environment, we can add an approver by clicking on the three dots located in the upper right of it, then "Approvals and checks" and select "Approvals".

Variable Groups

Variable groups allow to store specific values that can be called when running a pipeline. We'll have to add two variable groups:

  • contoso-UAT (testing)
  • contoso-PRD (production)

Variable groups can be configured here: https://dev.azure.com/[ORGANIZATION]/[PROJECT]/_library?itemType=VariableGroups.

Below the variables used in the pipelines, depending on the context:

Name UAT PRD Secret value? Definition
aad_app_id YES YES NO The Azure AD application ID with which the authentication to SharePoint will be done
aad_app_password YES YES YES The certificate password necessary for the authentication
aad_tenant_id YES YES NO ID or domain (for example "contoso.onmicrosoft.com") of the tenant from which accounts should be able to authenticate
site_url_prd YES YES NO SharePoint PRD site URL where the SPFx package will be deployed
site_list_prd YES NO NO SharePoint list relative URL ("Lists/MyList") to export data to the template
webpart_name YES NO NO SPFx WebPart name to add to the testing site page
app_catalog_scope NO YES NO Production app catalog scope (can be "tenant" or "site collection")
app_catalog_site_url NO YES NO Production app catalog site URL (can be a tenant or a site collection one)

Secure files

Especially for AzDO, we will use the Secure files feature to store a PFX file in order to login to SharePoint in Application context. In this example, we'll use one PFX file for both testing and production environments: PnP.SharePoint.AppOnly.pfx.

Secure files can be configured here: https://dev.azure.com/[ORGANIZATION]/[PROJECT]/_library?itemType=SecureFiles.

Stage permissions

During the pipeline run, both CI and CD parts are considered as "Stages". As the UAT and PRD deployments will have to access to environments, secure files and variable groups, we will have to give permission for those stages once.

For each of those, we will have to permit the pipeline to access to them only on the first run. This will be seen on the pipeline run page, as we'll see that the UAT / PR deployments will be pending for approving permissions. More info here.

Azure AD application

As the authentication will be done in app context (not delegated), to be sure to authenticate correctly, the AAD application will have to be configured with the following info:

  • Certificates & secrets
    • Add a certificate file (.pem, crt or .cer), for which we'll have the thumbprint info and encoded file or .pfx file (in that case, the PnP.SharePoint.AppOnly.pfx one)
  • API permission
    • Add SharePoint application permission "Sites.FullControl.All"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment