Skip to content
Petkir Blog
XLinkedinBluesky

SPFx 1.22.0 Beta 3 Devops Pipeline

Code, Azure, SPFx8 min read

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).

https://github.com/petkir/session-samples/blob/main/ESPC2025_Webinar/1_22_0_Beta3/02Pipelines/01BuildDeploy/BasicWebPartCICD22/.azuredevops/pipelines/deploy-spfx-solution.yml

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 .sppkg as 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).

https://github.com/petkir/session-samples/blob/main/ESPC2025_Webinar/1_22_0_Beta3/02Pipelines/02MultiStage/BasicWebPartMultiStage22/.azuredevops/pipelines/deploy-spfx-solution.yml

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 build and package; if JS/TS: npm ci and 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.json and globalConfig.ts with "package-solution-prod": "node ./setGlobalConfig.js && node ./setApiPermissions.js && heft build --production && heft package-solution --production"
  • Add/upgrade .sppkg in 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.

https://github.com/petkir/session-samples/blob/main/ESPC2025_Webinar/1_22_0_Beta3/02Pipelines/03MultiStageWithAPI/react-chat-sk/.azuredevops/pipelines/deploy-spfx-solution.yml

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.