SPFx 1.22.0 Beta 3 Devops Pipeline
Do not Fear the Next Deployment of Your SPFx Project
all sample sopurce can be found at: https://github.com/petkir/session-samples/tree/main/ESPC2025_Webinar Webinar: https://www.sharepointeurope.com/webinars/dont-fear-the-next-deployment-of-your-spfx-project/
1. Single Pipeline Build & Deploy
What it shows: A straightforward CI/CD that builds once and deploys immediately.
Typical stages/steps:
- Install toolchain: use a known Node version;
npm. - Quality gates: lint, unit tests (if present).
- (1.21.1 Build:
gulp bundle --ship. - (1.21.1) Package:
gulp package-solution --ship→ produces.sppkg. - (1.22.0.Beta3) added npm task in
package.jsoncommand"package-solution-prod": "heft build --production && heft package-solution --production". - Publish artifacts: SharePoint package (
*.sppkg) for the app catalog. - Deploy: App Catalog (use Microsoft 365 CLI or PnP PowerShell to add/upgrade the
.sppkg, approve, and optionally install to a site).
name: Deploy Solution BasicWebPartCICD
trigger:
branches:
include:
- main
paths:
include:
- '1_22_0_Beta3/02Pipelines/01BuildDeploy/BasicWebPartCICD22'
pool:
vmImage: ubuntu-latest
variables:
- group: Test
- name: PackageName
value: basic-web-part-cicd-22.sppkg
- name: NodeVersion
value: 22.x
- name: CertificateSecureFileName
value: Test.pfx
- name: WorkingDir
value: '1_22_0_Beta3/02Pipelines/01BuildDeploy/BasicWebPartCICD22'
stages:
- stage: Build_and_Deploy
jobs:
- job: Build_and_Deploy
steps:
- task: NodeTool@0
displayName: Use Node.js
inputs:
versionSpec: $(NodeVersion)
- task: NodeTool@0
displayName: Use Node.js
inputs:
versionSpec: $(NodeVersion)
- task: DownloadSecureFile@1
displayName: Download PFX Certificate
name: certificateFile
inputs:
secureFile: $(CertificateSecureFileName)
- task: Npm@1
displayName: Run npm install
inputs:
command: install
workingDir: '$(WorkingDir)'
- task: Npm@1
displayName: Run npm package-solution-prod
inputs:
command: custom
customCommand: run package-solution-prod
workingDir: '$(WorkingDir)'
- task: Npm@1
displayName: Install CLI for Microsoft 365
inputs:
command: custom
verbose: false
customCommand: install -g @pnp/cli-microsoft365
- script: >
m365 login --authType certificate --certificateFile '$(certificateFile.secureFilePath)' --password '$(CertificatePassword)' --appId '$(CDPipeline_EntraID)' --tenant '$(CDPipeline_TenantID)'
m365 spo set --url '$(SharePointBaseUrl)'
m365 spo app add --filePath '$(Build.SourcesDirectory)/$(WorkingDir)/sharepoint/solution/$(PackageName)' --overwrite
m365 spo app deploy --skipFeatureDeployment --name '$(PackageName)' --appCatalogScope 'tenant'
displayName: CLI for Microsoft 365 Deploy App
When to use it: Small teams, single environment, fast iteration.
2. Multi-Stage Pipeline (Dev/Test/Prod)
What it shows: Classic separation of concerns with approvals and per-environment settings.
Typical stages:
- Build (once): produce
.sppkgas artifacts. - Deploy Dev → Test → Prod: Each stage reuses the same build artifacts.
- Environment-specific variables (site URLs, tenant app catalog flags).
- Manual approvals or gates between stages.
- Same deployment mechanics as above (App Catalog operations).
name: Deploy Solution BasicWebPartCICD
trigger:
branches:
include:
- main
paths:
include:
- '1_22_0_Beta3/02Pipelines/02MultiStage/BasicWebPartMultiStage22'
pool:
vmImage: ubuntu-latest
variables:
- name: PackageName
value: basic-web-part-multi-stage-22.sppkg
- name: NodeVersion
value: 22.x
- name: WorkingDir
value: '1_22_0_Beta3/02Pipelines/02MultiStage/BasicWebPartMultiStage22'
stages:
- stage: Build
displayName: Build and Package
jobs:
- job: Build
displayName: Build SPFx Solution
steps:
- task: NodeTool@0
displayName: Use Node.js $(NodeVersion)
inputs:
versionSpec: $(NodeVersion)
- task: Npm@1
displayName: Run npm install
inputs:
command: install
workingDir: '$(WorkingDir)'
- task: Npm@1
displayName: Run npm package-solution-prod
inputs:
command: custom
customCommand: run package-solution-prod
workingDir: '$(WorkingDir)'
- task: PublishPipelineArtifact@1
displayName: Publish Build Artifact
inputs:
targetPath: '$(Build.SourcesDirectory)/$(WorkingDir)/sharepoint/solution/$(PackageName)'
artifact: 'spfx-package'
publishLocation: 'pipeline'
- stage: Deploy_Test
displayName: Deploy to Test Environment
dependsOn: Build
variables:
- group: Test
jobs:
- deployment: Deploy_Test
displayName: Deploy to Test
environment: 'Test'
strategy:
runOnce:
deploy:
steps:
- task: DownloadSecureFile@1
displayName: Download PFX Certificate
name: certificateFile
inputs:
secureFile: Test.pfx
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
inputs:
artifact: 'spfx-package'
path: '$(Pipeline.Workspace)/spfx-package'
- task: Npm@1
displayName: Install CLI for Microsoft 365
inputs:
command: custom
verbose: false
customCommand: install -g @pnp/cli-microsoft365
- script: |
m365 login --authType certificate --certificateFile '$(certificateFile.secureFilePath)' --password '$(CertificatePassword)' --appId '$(CDPipeline_EntraID)' --tenant '$(CDPipeline_TenantID)'
m365 spo set --url '$(SharePointBaseUrl)'
m365 spo app add --filePath '$(Pipeline.Workspace)/spfx-package/$(PackageName)' --overwrite
m365 spo app deploy --skipFeatureDeployment --name '$(PackageName)' --appCatalogScope 'tenant'
displayName: Deploy to Test Environment
- stage: Deploy_Production
displayName: Deploy to Production Environment
dependsOn: Deploy_Test
variables:
- group: Prod
jobs:
- deployment: Deploy_Production
displayName: Deploy to Production
environment: 'Prod'
strategy:
runOnce:
deploy:
steps:
- task: DownloadSecureFile@1
displayName: Download PFX Certificate
name: certificateFile
inputs:
secureFile: Prod.pfx
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
inputs:
artifact: 'spfx-package'
path: '$(Pipeline.Workspace)/spfx-package'
- task: Npm@1
displayName: Install CLI for Microsoft 365
inputs:
command: custom
verbose: false
customCommand: install -g @pnp/cli-microsoft365
- script: |
m365 login --authType certificate --certificateFile '$(certificateFile.secureFilePath)' --password '$(CertificatePassword)' --appId '$(CDPipeline_EntraID)' --tenant '$(CDPipeline_TenantID)'
m365 spo set --url '$(SharePointBaseUrl)'
m365 spo app add --filePath '$(Pipeline.Workspace)/spfx-package/$(PackageName)' --overwrite
m365 spo app deploy --skipFeatureDeployment --name '$(PackageName)' --appCatalogScope 'tenant'
displayName: Deploy to Production Environment
Why it’s helpful: Predictable promotions, fewer “it works on Dev only” surprises, controlled change flow.
3. Multi-Stage with API (SPFx + Azure Function)
What the folder tells us:
azure-function-sk/– a function app scaffold to serve as the API backend.react-chat-sk.sln– .NET solution wrapper (the function may be C#), though some function samples are JS/TS-this layout is designed to demonstrate a real backend integration.assets/,sharepoint/,src/,teams/– SPFx front-end alongside the API.
Typical pipeline stages:
Backend build/deploy (not in pipeline scope):
- If C#:
dotnet buildand package; if JS/TS:npm ciand function packaging. - Create/update Azure Function App.
- Configure app settings, secrets, and CORS to allow SharePoint/Teams origins.
- Deploy Dev → Test → Prod: Each stage reuses the same build artifacts.
Frontend build abd deploy:
- Each environment needs a build (because of EntraApp ID)
- Update
package-solution.jsonandglobalConfig.tswith"package-solution-prod": "node ./setGlobalConfig.js && node ./setApiPermissions.js && heft build --production && heft package-solution --production" - Add/upgrade
.sppkgin App Catalog; optionally install to environment-specific sites.
Why it’s helpful: Shows a full-stack pattern: SPFx web part talking to a secured, scalable API on Azure Functions.
name: Deploy Solution BasicWebPartCICD
trigger:
branches:
include:
- main
paths:
include:
- '1_22_0_Beta3/02Pipelines/03MultiStageWithAPI/react-chat-sk'
pool:
vmImage: ubuntu-latest
variables:
- name: PackageName
value: react-chat-sk.sppkg
- name: NodeVersion
value: 22.x
- name: WorkingDir
value: '1_22_0_Beta3/02Pipelines/03MultiStageWithAPI/react-chat-sk'
stages:
- stage: Build
displayName: Build and Package
jobs:
- job: Build
displayName: Artifact Src Package
steps:
- task: PublishPipelineArtifact@1
displayName: Publish Build Artifact
inputs:
targetPath: '$(Build.SourcesDirectory)/$(WorkingDir)'
artifact: 'spfx-src-package'
publishLocation: 'pipeline'
- stage: Deploy_Test
displayName: Deploy to Test Environment
dependsOn: Build
variables:
- group: Test
jobs:
- deployment: Deploy_Test
displayName: Deploy to Test
environment: 'Test'
strategy:
runOnce:
deploy:
steps:
- task: DownloadSecureFile@1
displayName: Download PFX Certificate
name: certificateFile
inputs:
secureFile: Test.pfx
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
inputs:
artifact: 'spfx-src-package'
path: '$(Pipeline.Workspace)/spfx-src-package'
- task: NodeTool@0
displayName: Use Node.js $(NodeVersion)
inputs:
versionSpec: $(NodeVersion)
- task: Npm@1
displayName: Run npm install
inputs:
command: install
workingDir: '$(Pipeline.Workspace)/spfx-src-package'
- task: Npm@1
displayName: Run npm package-solution-prod
inputs:
command: custom
customCommand: run package-solution-prod
workingDir: '$(Pipeline.Workspace)/spfx-src-package'
env:
ENTRA_ResourceName: $(ENTRA_ResourceName)
ENTRA_ResourceAppId: $(ENTRA_ResourceAppId)
ENTRA_ResourceScope: $(ENTRA_ResourceScope)
ENTRA_ResourceReplyUrl: $(ENTRA_ResourceReplyUrl)
- task: Npm@1
displayName: Install CLI for Microsoft 365
inputs:
command: custom
verbose: false
customCommand: install -g @pnp/cli-microsoft365
- script: |
m365 login --authType certificate --certificateFile '$(certificateFile.secureFilePath)' --password '$(CertificatePassword)' --appId '$(CDPipeline_EntraID)' --tenant '$(CDPipeline_TenantID)'
m365 spo set --url '$(SharePointBaseUrl)'
m365 spo app add --filePath '$(Pipeline.Workspace)/spfx-src-package//sharepoint/solution/$(PackageName)' --overwrite
m365 spo app deploy --skipFeatureDeployment --name '$(PackageName)' --appCatalogScope 'tenant'
displayName: Deploy to Test Environment
- task: PublishPipelineArtifact@1
displayName: Publish Build Artifact
inputs:
targetPath: '$(Pipeline.Workspace)/spfx-src-package/sharepoint/solution/$(PackageName)'
artifact: 'package-test'
publishLocation: 'pipeline'
- task: PublishPipelineArtifact@1
displayName: Publish Build Artifact
inputs:
targetPath: '$(Pipeline.Workspace)/spfx-src-package/src/globalConfig.ts'
artifact: 'package-config-test'
publishLocation: 'pipeline'
- stage: Deploy_Production
displayName: Deploy to Production Environment
dependsOn: Deploy_Test
variables:
- group: Prod
jobs:
- deployment: Deploy_Production
displayName: Deploy to Production
environment: 'Prod'
strategy:
runOnce:
deploy:
steps:
- task: DownloadSecureFile@1
displayName: Download PFX Certificate
name: certificateFile
inputs:
secureFile: Prod.pfx
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
inputs:
artifact: 'spfx-src-package'
path: '$(Pipeline.Workspace)/spfx-src-package'
- task: NodeTool@0
displayName: Use Node.js $(NodeVersion)
inputs:
versionSpec: $(NodeVersion)
- task: Npm@1
displayName: Run npm install
inputs:
command: install
workingDir: '$(Pipeline.Workspace)/spfx-src-package'
- task: Npm@1
displayName: Run npm package-solution-prod
inputs:
command: custom
customCommand: run package-solution-prod
workingDir: '$(Pipeline.Workspace)/spfx-src-package'
env:
ENTRA_ResourceName: $(ENTRA_ResourceName)
ENTRA_ResourceAppId: $(ENTRA_ResourceAppId)
ENTRA_ResourceScope: $(ENTRA_ResourceScope)
ENTRA_ResourceReplyUrl: $(ENTRA_ResourceReplyUrl)
- task: Npm@1
displayName: Install CLI for Microsoft 365
inputs:
command: custom
verbose: false
customCommand: install -g @pnp/cli-microsoft365
- script: |
m365 login --authType certificate --certificateFile '$(certificateFile.secureFilePath)' --password '$(CertificatePassword)' --appId '$(CDPipeline_EntraID)' --tenant '$(CDPipeline_TenantID)'
m365 spo set --url '$(SharePointBaseUrl)'
m365 spo app add --filePath '$(Pipeline.Workspace)/spfx-src-package//sharepoint/solution/$(PackageName)' --overwrite
m365 spo app deploy --skipFeatureDeployment --name '$(PackageName)' --appCatalogScope 'tenant'
displayName: Deploy to Production Environment
- task: PublishPipelineArtifact@1
displayName: Publish Build Artifact
inputs:
targetPath: '$(Pipeline.Workspace)/spfx-src-package/sharepoint/solution/$(PackageName)'
artifact: 'package-prod'
publishLocation: 'pipeline'
- task: PublishPipelineArtifact@1
displayName: Publish Build Artifact
inputs:
targetPath: '$(Pipeline.Workspace)/spfx-src-package/src/globalConfig.ts'
artifact: 'package-config'
publishLocation: 'pipeline'
Why build per environment here?
- The SPFx package embeds API scopes/URLs and Entra details. Building in each env ensures the correct config is baked in without risky transform tricks on the final .sppkg.
Choosing the right approach
- Go single-pipeline if you’re a small team and just need speed.
- Go multi-stage if you want explicit promotions and approvals.
- Go API pattern when your SPFx talks to a real backend and config differs by environment.
FAQ
- Gulp vs Heft? In 1.22+ use Heft via npm scripts. Older gulp bundle/package-solution still applies to 1.21 or lower projects.
- PnP PowerShell vs CLI for Microsoft 365? Both work. I prefer CLI in Linux agents and YAML because it’s cross-platform and simple.
- Should I deploy to tenant app catalog or site collection? Tenant catalog is common; site collection app catalogs are useful for isolation-adjust the deploy command accordingly.
Credits
- Based on my ESPC 2025 webinar. Samples are published in the repo above for you to copy/adapt.