Org Skills

A complete Salesforce pipeline

Supporting material for salesforce-devops. Agents load it on demand; it ships inside the skill folder.

RawSource

Target: Spring '26, sf CLI. Sources: sf project deploy commands, salesforcecli/plugin-deploy-retrieve, Authorization for CI.

1. Authentication in CI

JWT bearer against an External Client App, with the certificate's private key in the CI secret store:

echo "$SF_JWT_KEY" > server.key
sf org login jwt --client-id "$SF_CLIENT_ID" --jwt-key-file server.key \
  --username "$SF_USERNAME" --instance-url "$SF_INSTANCE_URL" --alias prod

One app and one integration user per target org, each with its own permission set. Never a username-password login and never a personal user's token in CI.

2. Pull-request job — cheap gates, then a validation

name: pr
on: pull_request
jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }          # delta deploys need history
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: npm }
      - run: npm ci
      - run: npm run lint
      - run: npm run test:unit -- --coverage
      - run: npx @salesforce-ux/slds-linter@latest lint force-app/main/default/lwc

      - run: npm install -g @salesforce/cli && sf plugins install code-analyzer
      - run: sf code-analyzer run --workspace force-app --severity-threshold 3 --output-file code-analyzer.csv
      - if: always()
        uses: actions/upload-artifact@v4
        with: { name: code-analyzer, path: code-analyzer.csv }

      - name: Authenticate
        run: |
          echo "$SF_JWT_KEY" > server.key
          sf org login jwt --client-id "$SF_CLIENT_ID" --jwt-key-file server.key \
            --username "$SF_USERNAME" --instance-url "$SF_INSTANCE_URL" --alias ci
        env: { SF_JWT_KEY: "${{ secrets.SF_JWT_KEY }}", SF_CLIENT_ID: "${{ secrets.SF_CLIENT_ID }}", SF_USERNAME: "${{ secrets.SF_USERNAME }}", SF_INSTANCE_URL: "${{ secrets.SF_INSTANCE_URL }}" }

      - name: Scratch org build (proves the repo builds from nothing)
        run: |
          sf org create scratch --definition-file config/project-scratch-def.json \
            --alias scratch --duration-days 1 --wait 20
          sf project deploy start --target-org scratch --wait 60
          sf apex run test --target-org scratch --test-level RunLocalTests --code-coverage \
            --result-format junit --output-dir test-results --wait 30
          sf flow run test --target-org scratch --test-level RunLocalTests \
            --result-format junit --output-dir test-results
      - if: always()
        run: sf org delete scratch --target-org scratch --no-prompt

3. Release job — validate, then quick deploy

name: release
on:
  push: { branches: [main] }
jobs:
  validate:
    runs-on: ubuntu-latest
    outputs: { job-id: "${{ steps.validate.outputs.job-id }}" }
    steps:
      - uses: actions/checkout@v4
      - name: Validate against production
        id: validate
        run: |
          sf project deploy validate --target-org prod --source-dir force-app \
            --test-level RunLocalTests --coverage-formatters cobertura --junit \
            --results-dir test-results --wait 90 --json > validate.json
          echo "job-id=$(jq -r '.result.id' validate.json)" >> "$GITHUB_OUTPUT"

  deploy:
    needs: validate
    environment: production          # manual approval gate
    runs-on: ubuntu-latest
    steps:
      - run: sf project deploy quick --target-org prod --job-id "${{ needs.validate.outputs.job-id }}" --wait 30

A validated deployment can be quick-deployed for 10 days, so the approval can wait for the release window without re-running the test suite. If the branch moves, validate again — a quick deploy applies the metadata that was validated, not what is on the branch now.

4. Delta deploys

On a large repo a full-source validation is slow. Two supported ways to narrow it:

# 1. Deploy only what changed, generated from git
sf project generate manifest --source-dir $(git diff --name-only origin/main...HEAD -- force-app | tr '\n' ',') \
  --output-dir manifest
sf project deploy validate --target-org prod --manifest manifest/package.xml --test-level RunLocalTests

# 2. Let Salesforce pick the tests (Beta)
sf project deploy validate --target-org prod --source-dir force-app --test-level RunRelevantTests

Delta deploys are an optimisation with a cost: a component that depends on something outside the delta can validate and still fail at runtime. Run a full validation on the release branch at least once before the window, whatever the pull requests do.

5. Destructive changes

<!-- manifest/destructiveChangesPost.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
    <types>
        <members>OrderLegacyController</members>
        <name>ApexClass</name>
    </types>
</Package>
sf project deploy start --target-org prod --source-dir force-app \
  --post-destructive-changes manifest/destructiveChangesPost.xml \
  --test-level RunLocalTests --wait 60

pre deletes before the new metadata lands, post after — post is the safe default because the replacement exists first. The manifest still needs an (often empty) package.xml alongside it in metadata-format deploys. Deletion and its replacement belong in one deploy, so a rollback restores a consistent state.

6. Rollback

There is no "undo deploy". Plan one of these before the release:

Strategy How Cost
Reverse deploy Deploy the previous commit's metadata, plus destructive changes for anything new Minutes, but data created under the new version may not fit the old schema
Package version rollback Install the previous promoted package version Clean for packaged code; not available for unpackaged metadata
Feature switch Ship dark behind a custom permission or Custom Metadata flag, then turn it on The only true instant rollback — prefer it for anything risky

Fields and objects are not deleted in a rollback: deletion is a separate, deliberate release. Rehearse the rollback in a full sandbox once per major release; a plan nobody has run is a wish.

7. Artefacts a pipeline should publish

  • JUnit results from sf apex run test and sf flow run test (--result-format junit --output-dir test-results).
  • Coverage from --coverage-formatters cobertura on the validation, plus Jest's LCOV.
  • The Code Analyzer output file, kept per build so trends are visible.
  • The deploy job id and the validated commit SHA, so the release record answers "what exactly is in production".