Ship It: Deploying SPFx from GitHub Actions
Lesson 8: Ship It โ Deploying SPFx from GitHub Actions
This is the payoff lesson: the full path from git push to a web part live on a SharePoint site. Three moving pieces โ build (make the .sppkg), auth (a service principal your runner can use), and deployment actions (the steps that talk to the app catalog). Everything below was verified against current documentation and working action repositories.
The deployment actions available
You don't need to hand-roll SharePoint deployment. There are three credible routes, each with real, maintained tooling:
| Route | Actions / tools | Best for |
|---|---|---|
| Official CLI for Microsoft 365 actions (recommended) | pnp/action-cli-login@v3.0.1 โ installs the CLI and logs in; pnp/action-cli-deploy@v5.0.0 โ adds + deploys an SPFx package to the app catalog; pnp/action-cli-runscript@v3.0.0 โ runs any CLI command or script | Clean, minimal YAML, cross-platform, tenant or site-collection catalogs |
| Direct CLI in a step | npm install -g @pnp/cli-microsoft365, then m365 login --authType certificate โฆ and m365 spo app add / deploy / install โฆ | Full control, no third-party action dependency; great when you already script with CLI M365 |
| PnP PowerShell | Add-PnPApp, Publish-PnPApp, Install-PnPApp, Update-PnPApp, Remove-PnPApp; community action anoopt/action-pnp-powershell-with-oidc for OIDC auth | Teams already living in PnP PowerShell; Windows-centric admin scripts |
Setting up auth (once per repo)
- Register an app in Microsoft Entra ID (App registrations โ New registration). Note the application (client) ID and tenant ID.
- Upload a certificate to that app registration (Certificates & secrets โ Certificates). Keep the private key (
.pfx) locally โ it's what your runner will present. - Grant the SharePoint permission: API permissions โ add
Microsoft GraphorSharePointโ application permissionSites.FullControl.Allโ Grant admin consent. (For catalog-only work, scoping the principal to App Catalog admin also works.) - Store GitHub secrets in the repo:
APP_ID,TENANT_ID,CERTIFICATE_ENCODED(the certificate base64-encoded, e.g.openssl base64 -in app-cert.pfx | tr -d '\n'), and optionallyCERTIFICATE_PASSWORD.
permissions: id-token: write and no certificate secret ever touches the repo. The PnP PowerShell OIDC action is built around this pattern; it's the state of the art for M365 CI/CD.
The canonical workflow
Save this as .github/workflows/deploy-spfx.yml in your SPFx repo. It builds the current (Heft-based, SPFx v1.22+) way and deploys with the official actions:
name: Deploy SPFx to SharePoint
on:
push:
branches: [ main ] # or: workflow_dispatch for manual runs
permissions:
contents: read
env:
NODE_VERSION: '20' # match the LTS your SPFx version requires
SPPKG_FILE: my-solution.sppkg # set to your package name
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
- name: Install dependencies
run: npm ci
- name: Build and bundle (production)
run: npx heft build --clean --production
- name: Package solution (.sppkg)
run: npx heft package-solution --production
# Legacy gulp-based projects (SPFx v1.0-1.21.1) instead run:
# npx gulp bundle --ship
# npx gulp package-solution --ship
- name: Log in to Microsoft 365
uses: pnp/action-cli-login@v3.0.1
with:
APP_ID: ${{ secrets.APP_ID }}
CERTIFICATE_ENCODED: ${{ secrets.CERTIFICATE_ENCODED }}
CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
TENANT: ${{ secrets.TENANT_ID }}
- name: Deploy app to the tenant app catalog
id: deploy
uses: pnp/action-cli-deploy@v5.0.0
with:
APP_FILE_PATH: sharepoint/solution/${{ env.SPPKG_FILE }}
SKIP_FEATURE_DEPLOYMENT: true # tenant-wide, no per-site feature activation
OVERWRITE: true # re-upload newer builds of the same version
- name: Install on a site (only when the app needs per-site install)
uses: pnp/action-cli-runscript@v3.0.0
with:
M365_CLI_SCRIPT: m365 spo app install --id ${{ steps.deploy.outputs.APP_ID }} --siteUrl https://contoso.sharepoint.com/sites/intranet
Reading the pipeline
- Build steps are ordinary Node CI โ checkout, pin Node,
npm cifrom the lockfile, then Heft's production build and package commands emitsharepoint/solution/<name>.sppkg. action-cli-logininstalls CLI for Microsoft 365 into the runner and authenticates with your service principal โ every later CLI action reuses that session. The action accepts delegated admin credentials or, as here, an app ID + base64-encoded certificate.action-cli-deploywraps add + deploy.OVERWRITE: truelets repeat runs of the same solution version replace the package;SKIP_FEATURE_DEPLOYMENT: truemakes it available tenant-wide. ItsAPP_IDoutput feeds later steps.- The optional install step is only needed when the app targets specific sites (e.g. an application customizer for one site collection). Pure tenant-wide web parts don't need it.
Environments, upgrades, rollback
- Dev โ prod: keep a dev app catalog (site-collection catalog or a dev tenant) and point a dev workflow at it. Promote by running the same workflow against the prod catalog โ that's the whole point of the repo being the single source of truth.
- Releases: bump
solution.versioninconfig/package-solution.jsonper release, then re-run add/deploy andm365 spo app upgrade --id โฆ(orUpdate-PnPApp) against installed sites so they move to the new version. - Rollback: redeploy the previous
.sppkgversion, orm365 spo app retract --id โฆto pull the app from availability (keeps it in the catalog), thenspo app removeonce nobody uses it.
๐ง Knowledge Check
1. Which pair of official GitHub Actions logs in and then deploys an SPFx package?
2. What goes into the CERTIFICATE_ENCODED GitHub secret for the login action?
3. What does SKIP_FEATURE_DEPLOYMENT: true do on the deploy action?