Headless Auth: Salesforce CLI + GitHub Actions (No Browser Login)

Reading Time: 13 min
Author: William Watson Published: February 13, 2026
Jump to section Current: Top of article

If your GitHub Actions job asks for interactive login, your CI auth model is wrong for automation. Use JWT-based headless auth so pipelines can authenticate and deploy without browser prompts.

This is a focused deep dive from the Salesforce CLI DevOps Playbook.

What is Salesforce CLI headless auth in CI/CD?

Headless auth means the pipeline logs into Salesforce with a connected app, username, and signed JWT, not a browser flow. It is the standard pattern for repeatable CI deploys.

Core command:

sf org login jwt \
  --client-id "$SF_CLIENT_ID" \
  --jwt-key-file "$SF_JWT_KEY_PATH" \
  --username "$SF_USERNAME" \
  --instance-url https://login.salesforce.com

What do I need before configuring GitHub Actions?

You need a connected app configured for JWT bearer flow, an integration user, and an RSA key pair. Missing any one of these causes invalid_grant or auth failures.

Preflight requirements:

  • Connected app consumer key (SF_CLIENT_ID).
  • Integration username (SF_USERNAME).
  • Private key file available to the workflow (server.key).
  • Correct login domain (https://login.salesforce.com or sandbox URL).

How do I generate a key pair for JWT auth?

Generate the private/public key pair once, upload the certificate to the connected app, and keep the private key out of git.

openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr
openssl x509 -req -sha256 -days 365 -in server.csr -signkey server.key -out server.crt

Upload server.crt to your connected app. Store server.key as an encrypted GitHub secret or encrypted file in the workflow.

Which GitHub secrets should I create?

Store credentials as repository or environment secrets and reconstruct the key during workflow runtime. Never commit private key material to source control.

Recommended secrets:

  • SF_CLIENT_ID
  • SF_USERNAME
  • SF_JWT_KEY_BASE64 (base64-encoded contents of server.key)
  • SF_INSTANCE_URL (https://login.salesforce.com or sandbox endpoint)

What does a minimal GitHub Actions workflow look like?

Install Node and Salesforce CLI, restore the JWT key, authenticate with sf org login jwt, then validate deploy. Keep auth and deploy in the same job unless you intentionally split with artifacts.

name: Salesforce Validate Deploy

on:
  pull_request:
    branches: [main]

jobs:
  validate:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install Salesforce CLI
        run: npm install @salesforce/[email protected] --global

      - name: Restore JWT key
        run: |
          echo "$SF_JWT_KEY_BASE64" | base64 --decode > server.key
          chmod 600 server.key
        env:
          SF_JWT_KEY_BASE64: ${{ secrets.SF_JWT_KEY_BASE64 }}

      - name: Authenticate to Salesforce
        run: |
          sf org login jwt \
            --client-id "$SF_CLIENT_ID" \
            --jwt-key-file server.key \
            --username "$SF_USERNAME" \
            --instance-url "$SF_INSTANCE_URL"
          sf org display --target-org "$SF_USERNAME"
        env:
          SF_CLIENT_ID: ${{ secrets.SF_CLIENT_ID }}
          SF_USERNAME: ${{ secrets.SF_USERNAME }}
          SF_INSTANCE_URL: ${{ secrets.SF_INSTANCE_URL }}

      - name: Validate deploy
        run: |
          sf project deploy validate \
            --source-dir force-app \
            --target-org "$SF_USERNAME" \
            --test-level RunLocalTests \
            --wait 60
        env:
          SF_USERNAME: ${{ secrets.SF_USERNAME }}

How do I fix common JWT auth failures in Actions?

Most failures are configuration mismatches, not Salesforce CLI bugs. Verify the username, connected app consumer key, certificate, and instance URL first.

Fast checks:

  • invalid_grant: wrong username, cert mismatch, or user not approved for connected app.
  • audience is invalid: wrong --instance-url for target org type.
  • Intermittent failures: key formatting issues from secret decoding.
  • “No auth found”: auth step ran in a different job or key file was not created.

How do I keep this secure in production pipelines?

Use a dedicated integration user with least privilege, isolate secrets in GitHub environments, and rotate certificates on a schedule. Treat JWT key handling as production credential management.

Security baseline:

  • Restrict connected app policies to approved users.
  • Use protected environments for production deployment secrets.
  • Rotate cert/key pair periodically and after team changes.
  • Add a pre-deploy sf org display check so failures happen before metadata operations.

For deployment command strategy after auth, continue with sf project deploy start vs sf project deploy validate: when to use each.