Skip to content

Instantly share code, notes, and snippets.

@michaelmaillot
Created April 19, 2022 22:00
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/cc7ca7c710b5fdb984070dfe3a970606 to your computer and use it in GitHub Desktop.
Save michaelmaillot/cc7ca7c710b5fdb984070dfe3a970606 to your computer and use it in GitHub Desktop.
Build & Deploy SPFx package in an ephemeral site before production (GitHub Actions & PnP PowerShell)
name: deploy with pnp powershell
on:
workflow_call:
inputs:
environment_name:
description: 'Target environment to deploy to'
required: true
type: string
site_url_prd:
description: 'SharePoint PRD site URL'
required: true
type: string
site_list_prd:
description: 'SharePoint PRD list relative URL ("Lists/MyList")'
default: ""
required: false
type: string
site_copy_owner:
description: 'SharePoint copy site owner to be'
default: ""
required: false
type: string
webpart_name:
description: 'SPFx WebPart name to add to test page'
default: ""
required: false
type: string
site_to_remove:
description: 'SharePoint copy site url to remove'
default: ""
required: false
type: string
app_catalog_site_url:
description: 'SharePoint app catalog site URL (tenant or site collection)'
default: ""
required: false
type: string
app_catalog_scope:
description: 'Indicates the PRD app catalog scope (tenant or site collection)'
default: ""
required: false
type: string
outputs:
site_to_remove:
description: 'SharePoint copy site url to remove'
value: ${{ jobs.deploy.outputs.site_copy_url }}
secrets:
AAD_APP_ID:
required: true
AAD_APP_PASSWORD:
required: true
AAD_APP_ENCODED_CERTIFICATE:
required: true
AAD_TENANT_ID:
required: true
jobs:
deploy:
runs-on: windows-2022
environment: ${{ inputs.environment_name }}
outputs:
site_copy_url: ${{ steps.step_site_copy.outputs.url }}
steps:
- name: Download artifact from build job
uses: actions/download-artifact@v2
- name: Installing PnP.PowerShell Module
shell: pwsh
run: Install-Module -Name "PnP.PowerShell" -Force
- name: Get generated *.sppkg filename
id: package
shell: pwsh
run: |
$package = Get-ChildItem -Path artifact -Recurse -Filter '*.sppkg' | Select Name | Select-Object -First 1
echo "::set-output name=name::$($package.Name)"
- name: Connecting to Production site and get site template
if: ${{ inputs.environment_name != 'PRD' }}
shell: pwsh
run: |
$securePassword = ConvertTo-SecureString "${{ secrets.AAD_APP_PASSWORD }}" -AsPlainText -Force
Connect-PnPOnline -Url ${{ inputs.site_url_prd }} -CertificateBase64Encoded ${{ secrets.AAD_APP_ENCODED_CERTIFICATE }} -CertificatePassword $securePassword -ClientId ${{ secrets.AAD_APP_ID }} -Tenant ${{ secrets.AAD_TENANT_ID }}
Get-PnPSiteTemplate -Out template.pnp
Add-PnPDataRowsToSiteTemplate -Path template.pnp -List "${{ inputs.site_list_prd }}"
- name: Create ${{ inputs.environment_name }} site copy
if: ${{ inputs.environment_name != 'PRD' }}
id: step_site_copy
shell: pwsh
run: |
$securePassword = ConvertTo-SecureString "${{ secrets.AAD_APP_PASSWORD }}" -AsPlainText -Force
Connect-PnPOnline -Url ${{ inputs.site_url_prd }} -CertificateBase64Encoded ${{ secrets.AAD_APP_ENCODED_CERTIFICATE }} -CertificatePassword $securePassword -ClientId ${{ secrets.AAD_APP_ID }} -Tenant ${{ secrets.AAD_TENANT_ID }}
$web = Get-PnPWeb
$siteName = "$($web.Title)" + [guid]::NewGuid().Guid
$uri = [System.Uri]"${{ inputs.site_url_prd }}"
$siteUrl = $uri.Scheme + "://" + $uri.Authority + "/sites/" + $siteName
$site = New-PnPSite -Type CommunicationSite -Title $siteName -Url $siteUrl -Owner "${{ inputs.site_copy_owner }}" -Wait
Write-Host "::set-output name=url::$site"
Write-Host $site
- name: Apply PRD site template to ${{ inputs.environment_name }} site copy
if: ${{ inputs.environment_name != 'PRD' }}
shell: pwsh
run: |
$securePassword = ConvertTo-SecureString "${{ secrets.AAD_APP_PASSWORD }}" -AsPlainText -Force
Connect-PnPOnline -Url ${{ steps.step_site_copy.outputs.url }} -CertificateBase64Encoded ${{ secrets.AAD_APP_ENCODED_CERTIFICATE }} -CertificatePassword $securePassword -ClientId ${{ secrets.AAD_APP_ID }} -Tenant ${{ secrets.AAD_TENANT_ID }}
$applyTemplateHasFailed = $false
try {
Invoke-PnPSiteTemplate -Path template.pnp -ClearNavigation
}
catch {
Write-Host $_.Exception.Message -ForegroundColor Yellow
$applyTemplateHasFailed = $_.Exception.Message -Match "HttpClient.Timeout"
}
finally {
if ($applyTemplateHasFailed -eq $true) {
Write-Host "Retrying to apply PRD site template (after 30 seconds delay)"
Start-Sleep -Seconds 30
Invoke-PnPSiteTemplate -Path template.pnp -ClearNavigation
}
}
- name: Enable Site Collection App Catalog, upload SharePoint package, deploy it and add it to the site
if: ${{ inputs.environment_name != 'PRD' }}
shell: pwsh
run: |
$uploadHasFailed = $false
try {
$securePassword = ConvertTo-SecureString "${{ secrets.AAD_APP_PASSWORD }}" -AsPlainText -Force
Connect-PnPOnline -Url ${{ steps.step_site_copy.outputs.url }} -CertificateBase64Encoded ${{ secrets.AAD_APP_ENCODED_CERTIFICATE }} -CertificatePassword $securePassword -ClientId ${{ secrets.AAD_APP_ID }} -Tenant ${{ secrets.AAD_TENANT_ID }}
Add-PnPSiteCollectionAppCatalog
Add-PnPApp -Path "artifact/${{ steps.package.outputs.name }}" -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 ${{ steps.step_site_copy.outputs.url }}
Add-PnPSiteCollectionAppCatalog
Start-Sleep -Seconds 30
Add-PnPApp -Path "artifact/${{ steps.package.outputs.name }}" -Scope Site -Publish -Overwrite
}
}
# 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
- name: Add an article page and the WebPart (if exists)
if: ${{ inputs.environment_name != 'PRD' }}
shell: pwsh
run: |
$webPartHasFailed = $false
if ('${{ inputs.webpart_name }}' -ne '') {
try {
$securePassword = ConvertTo-SecureString "${{ secrets.AAD_APP_PASSWORD }}" -AsPlainText -Force
Connect-PnPOnline -Url ${{ steps.step_site_copy.outputs.url }} -CertificateBase64Encoded ${{ secrets.AAD_APP_ENCODED_CERTIFICATE }} -CertificatePassword $securePassword -ClientId ${{ secrets.AAD_APP_ID }} -Tenant ${{ secrets.AAD_TENANT_ID }}
Add-PnPPage -Name "TestSPFx"
Add-PnPPageWebPart -Page "TestSPFx" -Component "${{ inputs.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 "${{ inputs.webpart_name }}"
}
}
}
# 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
- name: Upload SharePoint package to ${{ inputs.app_catalog_scope }} App Catalog (PRD)
if: ${{ inputs.environment_name == 'PRD' }}
run: |
$securePassword = ConvertTo-SecureString "${{ secrets.AAD_APP_PASSWORD }}" -AsPlainText -Force
Connect-PnPOnline -Url ${{ inputs.site_url_prd }} -CertificateBase64Encoded ${{ secrets.AAD_APP_ENCODED_CERTIFICATE }} -CertificatePassword $securePassword -ClientId ${{ secrets.AAD_APP_ID }} -Tenant ${{ secrets.AAD_TENANT_ID }}
Add-PnPApp -Path "artifact/${{ steps.package.outputs.name }}" -Scope ${{ inputs.app_catalog_scope }} -Publish -Overwrite
- name: Remove site copy (PRD)
if: ${{ inputs.environment_name == 'PRD' && inputs.site_to_remove != '' }}
run: |
$securePassword = ConvertTo-SecureString "${{ secrets.AAD_APP_PASSWORD }}" -AsPlainText -Force
Connect-PnPOnline -Url ${{ inputs.site_url_prd }} -CertificateBase64Encoded ${{ secrets.AAD_APP_ENCODED_CERTIFICATE }} -CertificatePassword $securePassword -ClientId ${{ secrets.AAD_APP_ID }} -Tenant ${{ secrets.AAD_TENANT_ID }}
Remove-PnPTenantSite -Url ${{ inputs.site_to_remove }} -SkipRecyclebin -Force
name: main
on:
push:
branches:
- main
workflow_dispatch: # Can be triggered manually
jobs:
build:
uses: michaelmaillot/helloworld-copysite/.github/workflows/build-spfx.yml@main
deploy_uat:
needs: build
uses: michaelmaillot/helloworld-copysite/.github/workflows/deploy-spfx-pnp-powershell-copy.yml@main
with:
environment_name: UAT
site_url_prd: https://onepointdev365.sharepoint.com/sites/CommSite
site_list_prd: "Lists/EmployeeOnboarding"
site_copy_owner: michael.maillot@onepointdev365.onmicrosoft.com
webpart_name: "HelloWorld"
secrets:
AAD_APP_ID: ${{ secrets.AAD_APP_ID }}
AAD_APP_PASSWORD: ${{ secrets.AAD_APP_PASSWORD }}
AAD_APP_ENCODED_CERTIFICATE: ${{ secrets.AAD_APP_ENCODED_CERTIFICATE }}
AAD_TENANT_ID: ${{ secrets.AAD_TENANT_ID }}
deploy_prd:
needs: deploy_uat
uses: michaelmaillot/helloworld-copysite/.github/workflows/deploy-spfx-pnp-powershell-copy.yml@main
with:
environment_name: PRD
site_url_prd: https://onepointdev365.sharepoint.com/sites/CommSite
app_catalog_scope: tenant
site_to_remove: "${{ needs.deploy_uat.outputs.site_to_remove }}"
secrets:
AAD_APP_ID: ${{ secrets.AAD_APP_ID }}
AAD_APP_PASSWORD: ${{ secrets.AAD_APP_PASSWORD }}
AAD_APP_ENCODED_CERTIFICATE: ${{ secrets.AAD_APP_ENCODED_CERTIFICATE }}
AAD_TENANT_ID: ${{ secrets.AAD_TENANT_ID }}
@michaelmaillot
Copy link
Author

Abstract

This sample provides a ready-to-use GitHub Action workflow 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:

  • main.yml: the main pipeline that will be triggered as the initiator
  • 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
│   gulpfile.js
│   package-lock.json
│   package.json
│   README.md
│   tsconfig.json
│   tslint.json
│
├───.github
│   └───workflows
│          build-spfx.yml  --------> CI pipeline
│          deploy-spfx-pnp-powershell.yml  --------> CD pipeline
│          main.yml  --------> main pipeline
│
├───.vscode
│
├───config
│
├───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. You'll have to add two environments:

  • UAT (testing)
  • PRD (production)

Environments can be configured here: https://github.com/[ORGANIZATION]/[PROJECT]/settings/environments.

/!\ Beware that in order to use multiple environments, your repo has to be public or your subscription has to be a Pro one.

Approvers

For each environment, you can add an approver by enabling the checkbox marked "Required reviewers", then adding at least one reviewer account then click on "Save protection rules".

Environment secrets

Environment secrets can be configured here: https://github.com/[ORGANIZATION]/[PROJECT]/settings/environments/[ENVIRONMENT_ID]/edit.

Below the secrets used in the workflows:

Name Description
AAD_APP_ID The Azure AD application ID with which the authentication to SharePoint will be done
AAD_APP_PASSWORD The certificate password necessary for the authentication
AAD_APP_ENCODED_CERTIFICATE Base64 encoded certificate
AAD_TENANT_ID ID or domain (for example "contoso.onmicrosoft.com") of the tenant from which accounts should be able to authenticate

CD Workflow parameters

Below the parameters used in the deploy-spfx-pnp-powershell.yml workflow, depending on the environment:

Name UAT PRD Definition
environment_name YES YES The deployment environment (can be 'UAT' or 'PRD')
site_list_prd YES NO SharePoint list relative URL ("Lists/MyList") to export data to the template
webpart_name YES NO SPFx WebPart name to add to the testing site page
site_url_prd NO YES SharePoint PRD site URL where the SPFx package will be deployed
app_catalog_scope NO YES Production app catalog scope (can be "tenant" or "site collection")
app_catalog_site_url NO YES Production app catalog site URL (can be a tenant or a site collection one)

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