sfpm
Guides

Validate pull requests

Run local or org-backed validation for pull requests with the validate-pr action.

The validate-pr action checks a pull request before it merges. It runs in one of two modes:

ModeWhat it doesNeeds
local (default)Compile and dependency checks for changed packages, using the local nimbus validator. No org is used.nimbus on the runner
orgDeploys changed packages to a scratch org taken from a pool, and runs the org checks.DevHub authentication, a pool tag, and a pool filled by fill-pool

Local mode

Local mode needs no Salesforce access. It runs nimbus compile and dependency checks against the packages the pull request changes. The DevHub does not need to be authenticated.

Local checks are best-effort. The org is the authoritative check. In local mode, compile and dependency findings are reported as warnings.

If nimbus is not on the runner, validate-pr reports that nimbus is missing instead of running the local checks. The compile check is marked skipped and the dependency check reports an error. The run may still pass, so a green run is not proof that local validation ran. Check the job log for the nimbus message, or install nimbus through a channel your security review approves.

The boundary checks use nimbus graph. The actions do not start a nimbus daemon.

Org mode

Org mode deploys the changed packages to a scratch org from a pool. Each pull request reuses its own org for a set time, so repeated pushes to the same PR do not consume a new org each time.

  • The action fetches an org from the pool named by pool-tag. It caches the org for the pull request, keyed by PR number, for cache-ttl-hours. The default is 4 hours. Set it to 0 to disable the cache.
  • Unlocked packages are always deployed as source in org mode. A pull request never creates a package version. This guide's workflows create package versions only in build on pushes to main, so concurrent pull requests do not compete for build numbers. That split is a recommended workflow convention; sfpm does not enforce it.
  • The pool must have orgs in it. Schedule the fill-pool action to keep it stocked, and use clean-pool to remove stale orgs.

Filling the pool

This job keeps a scratch org pool stocked on a weekday schedule.

on:
  schedule:
    - cron: '0 6 * * 1-5'
  workflow_dispatch:

permissions:
  contents: read

jobs:
  fill:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '22'

      - uses: pnpm/action-setup@v4
        with:
          version: 10

      - name: Install Salesforce CLI
        run: pnpm add -g @salesforce/cli

      - name: Authenticate DevHub
        env:
          SFDX_CONSUMER_KEY: ${{ secrets.SFDX_CONSUMER_KEY }}
          SFDX_JWT_KEY: ${{ secrets.SFDX_JWT_KEY }}
          DEVHUB_USERNAME: ${{ secrets.DEVHUB_USERNAME }}
        run: |
          printf '%s' "$SFDX_JWT_KEY" > server.key
          sf org login jwt \
            --client-id "$SFDX_CONSUMER_KEY" \
            --jwt-key-file server.key \
            --username "$DEVHUB_USERNAME" \
            --set-default-dev-hub \
            --alias devhub
          rm server.key

      - name: Fill pool
        uses: b64hub/sfpm/packages/actions/fill-pool@vX.Y.Z
        with:
          devhub-username: devhub
          pool-tag: ci-pool
          max-allocation: '10'

The DevHub authentication step is the same as in the validation workflow below.

Changed-package detection

When you do not set packages, validate-pr validates only the packages that changed between the pull request's base commit and its head. The action runs git diff for this. The checkout must include the base commit, so set fetch-depth: 0 on actions/checkout.

Without full history, the diff fails. The action then logs a warning and validates all packages. Full history keeps validation limited to the changed packages.

  • packages overrides detection. The action validates exactly the packages you list.
  • base-ref overrides the base commit used for the diff. It applies only when packages is not set.

Workflow: local mode

name: SFPM Validate PR

on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: read

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Validate PR
        id: validate
        uses: b64hub/sfpm/packages/actions/validate-pr@vX.Y.Z

Install nimbus on the runner before the validate step, through a channel your security review approves. The action does not install it.

Workflow: org mode

name: SFPM Validate PR

on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: read

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v4
        with:
          node-version: '22'

      - uses: pnpm/action-setup@v4
        with:
          version: 10

      - name: Install Salesforce CLI
        run: pnpm add -g @salesforce/cli

      - name: Authenticate DevHub
        env:
          SFDX_CONSUMER_KEY: ${{ secrets.SFDX_CONSUMER_KEY }}
          SFDX_JWT_KEY: ${{ secrets.SFDX_JWT_KEY }}
          DEVHUB_USERNAME: ${{ secrets.DEVHUB_USERNAME }}
        run: |
          printf '%s' "$SFDX_JWT_KEY" > server.key
          sf org login jwt \
            --client-id "$SFDX_CONSUMER_KEY" \
            --jwt-key-file server.key \
            --username "$DEVHUB_USERNAME" \
            --set-default-dev-hub \
            --alias devhub
          rm server.key

      - name: Validate PR
        id: validate
        uses: b64hub/sfpm/packages/actions/validate-pr@vX.Y.Z
        with:
          mode: org
          devhub-username: devhub
          pool-tag: ci-pool
          cache-ttl-hours: '6'

      - name: Report outcome
        if: always()
        run: |
          echo "Success: ${{ steps.validate.outputs.success }}"
          echo "Org: ${{ steps.validate.outputs.org-username }}"
          echo "Cache hit: ${{ steps.validate.outputs.cache-hit }}"

Replace vX.Y.Z with the release tag you pin. See Versioning and pinning.

Repository secrets are not passed to workflows triggered by pull requests from forks. Org mode fails for those pull requests. Use local mode for fork pull requests, or review fork pull requests before they run with secrets.

Outputs

OutputMeaning
successWhether all validations passed.
org-usernameUsername of the scratch org used. Empty in local mode.
org-idOrg ID of the scratch org used. Empty in local mode.
cache-hitWhether a cached org for this PR was reused.
cache-age-minutesAge of the reused org in minutes. Empty unless cache-hit is true.
pr-numberNumber of the validated pull request.
resultFull JSON result with per-package outcomes, including coverage.

For the full list of inputs and outputs, see each action's action.yml. For the permissions and network access the action needs, see Security and network.

On this page