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:

RouteActions / toolsBest 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 scriptClean, minimal YAML, cross-platform, tenant or site-collection catalogs
Direct CLI in a stepnpm 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 PowerShellAdd-PnPApp, Publish-PnPApp, Install-PnPApp, Update-PnPApp, Remove-PnPApp; community action anoopt/action-pnp-powershell-with-oidc for OIDC authTeams already living in PnP PowerShell; Windows-centric admin scripts
Avoid generic "upload to SharePoint" marketplace actions for package deployment: they upload files but don't understand the app catalog lifecycle (deploy vs install vs upgrade), so they silently produce half-deployed apps. Use lifecycle-aware tooling.

Setting up auth (once per repo)

  1. Register an app in Microsoft Entra ID (App registrations โ†’ New registration). Note the application (client) ID and tenant ID.
  2. Upload a certificate to that app registration (Certificates & secrets โ†’ Certificates). Keep the private key (.pfx) locally โ€” it's what your runner will present.
  3. Grant the SharePoint permission: API permissions โ†’ add Microsoft Graph or SharePoint โ†’ application permission Sites.FullControl.All โ†’ Grant admin consent. (For catalog-only work, scoping the principal to App Catalog admin also works.)
  4. 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 optionally CERTIFICATE_PASSWORD.
Even better: OIDC federated credentials. Instead of a long-lived certificate secret, you can configure the Entra app with a federated credential that trusts GitHub's OIDC token (scoped to a repo/branch/environment). The workflow requests 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 ci from the lockfile, then Heft's production build and package commands emit sharepoint/solution/<name>.sppkg.
  • action-cli-login installs 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-deploy wraps add + deploy. OVERWRITE: true lets repeat runs of the same solution version replace the package; SKIP_FEATURE_DEPLOYMENT: true makes it available tenant-wide. Its APP_ID output 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.version in config/package-solution.json per release, then re-run add/deploy and m365 spo app upgrade --id โ€ฆ (or Update-PnPApp) against installed sites so they move to the new version.
  • Rollback: redeploy the previous .sppkg version, or m365 spo app retract --id โ€ฆ to pull the app from availability (keeps it in the catalog), then spo app remove once nobody uses it.
You now own the whole loop โ€” Lesson 1's diagram is real: push โ†’ Actions builds โ†’ login + deploy actions publish to the catalog โ†’ sites pick it up. Every subsequent change is a pull request and a green pipeline, exactly like any other software you ship.

๐Ÿง  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?

Further Reading