SPFx 1.22.0 Beta 3 Devops Pipeline

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

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.