SPFx 1.22.0 Beta 3 Devops Pipeline
— Code, Azure, SPFx — 8 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).
name: Deploy Solution BasicWebPartCICDtrigger: branches: include: - main paths: include: - '1_22_0_Beta3/02Pipelines/01BuildDeploy/BasicWebPartCICD22'pool: vmImage: ubuntu-latestvariables: - 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 AppWhen 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 BasicWebPartCICDtrigger: branches: include: - main paths: include: - '1_22_0_Beta3/02Pipelines/02MultiStage/BasicWebPartMultiStage22'pool: vmImage: ubuntu-latestvariables: - 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 EnvironmentWhy 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 BasicWebPartCICDtrigger: branches: include: - main paths: include: - '1_22_0_Beta3/02Pipelines/03MultiStageWithAPI/react-chat-sk'pool: vmImage: ubuntu-latestvariables: - 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.