Sets up Salesforce CI/CD pipelines using GitHub Actions, JWT auth, SF CLI v2 scratch org workflows, sandbox promotion, deployments, and Apex testing.
npx claudepluginhub jiten-singh-shahi/salesforce-claude-code --plugin salesforce-claude-codeThis skill uses the workspace's default tool permissions.
Reference: @../_reference/DEPLOYMENT_CHECKLIST.md, @../_reference/DOCKER_CI_PATTERNS.md
Set up Salesforce CI/CD pipelines with GitHub Actions, SFDX deployments, JWT auth, and Apex testing. For automating metadata validation and tests in Salesforce repos.
Provides patterns for Salesforce platform development: Lightning Web Components (LWC), Apex triggers/classes, REST/Bulk APIs, Connected Apps, Salesforce DX with scratch orgs and 2GP.
Scaffolds test + deploy CI/CD pipelines for GitHub Actions, GitLab CI, Jenkins, and targets like Vercel, Netlify, Docker after assessing user's git host and deploy setup. Teaches basics to beginners.
Share bugs, ideas, or general feedback.
Reference: @../_reference/DEPLOYMENT_CHECKLIST.md, @../_reference/DOCKER_CI_PATTERNS.md
sf org login web --alias myOrg # Browser-based login
sf org login jwt --client-id <id> --jwt-key-file server.key --username user@org.com --alias ci-org
sf org list # List all authenticated orgs
sf org open --target-org myOrg # Open org in browser
sf org create scratch --definition-file config/project-scratch-def.json --alias myScratch --duration-days 7
sf org delete scratch --target-org myScratch --no-prompt
sf org display --target-org myOrg # Show org details including access token
sf project deploy start --source-dir force-app --target-org myOrg
sf project deploy start --manifest manifest/package.xml --target-org myOrg
sf project deploy validate --manifest manifest/package.xml --target-org myOrg
sf project deploy quick --job-id <id> --target-org myOrg
sf project retrieve start --source-dir force-app --target-org myOrg
sf project deploy start --source-dir force-app --test-level RunLocalTests --target-org myOrg
sf apex run --file scripts/apex/setup.apex --target-org myOrg
sf apex run test --test-level RunLocalTests --result-format human --target-org myOrg
sf apex run test --class-names AccountServiceTest --result-format json --target-org myOrg
sf apex run test --test-level RunAllTestsInOrg --code-coverage --result-format json --output-dir results/
sf apex tail log --target-org myOrg # Stream live debug logs
JWT auth enables non-interactive CI/CD authentication without browser prompts.
http://localhost:1717/OauthRedirectapi, refresh_token, offline_accessopenssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr
openssl x509 -req -days 3650 -in server.csr -signkey server.key -out server.crt
base64 -i server.key | tr -d '\n' # Encode for GitHub Secrets storage
SALESFORCE_JWT_SECRET_KEY -- base64-encoded server.key contentSALESFORCE_CONSUMER_KEY -- Connected App Consumer KeySALESFORCE_USERNAME -- target org username# .github/workflows/ci.yml
name: Salesforce CI/CD
on:
push:
branches: [develop, staging, main]
pull_request:
branches: [develop, staging, main]
jobs:
validate-pr:
name: Validate Pull Request
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install SF CLI
run: npm install -g @salesforce/cli
- name: Authenticate to sandbox
env:
JWT_SECRET_KEY: ${{ secrets.SALESFORCE_JWT_SECRET_KEY }}
CONSUMER_KEY: ${{ secrets.SALESFORCE_CONSUMER_KEY }}
USERNAME: ${{ secrets.SALESFORCE_USERNAME_SANDBOX }}
run: |
echo "$JWT_SECRET_KEY" | base64 --decode > server.key
sf org login jwt \
--client-id "$CONSUMER_KEY" \
--jwt-key-file server.key \
--username "$USERNAME" \
--alias validation-org \
--set-default
rm server.key
- name: Validate deployment
run: |
sf project deploy validate \
--source-dir force-app \
--test-level RunLocalTests \
--target-org validation-org \
--wait 30
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production
steps:
- uses: actions/checkout@v4
- name: Install SF CLI
run: npm install -g @salesforce/cli
- name: Authenticate to Production
env:
JWT_SECRET_KEY: ${{ secrets.SALESFORCE_PROD_JWT_SECRET_KEY }}
CONSUMER_KEY: ${{ secrets.SALESFORCE_PROD_CONSUMER_KEY }}
USERNAME: ${{ secrets.SALESFORCE_PROD_USERNAME }}
run: |
echo "$JWT_SECRET_KEY" | base64 --decode > server.key
sf org login jwt \
--client-id "$CONSUMER_KEY" \
--jwt-key-file server.key \
--username "$USERNAME" \
--instance-url https://login.salesforce.com \
--alias prod \
--set-default
rm server.key
- name: Validate deployment
id: validate
run: |
VALIDATION_RESULT=$(sf project deploy validate \
--source-dir force-app \
--test-level RunLocalTests \
--target-org prod \
--wait 60 \
--json)
echo "VALIDATION_JOB_ID=$(echo "$VALIDATION_RESULT" | jq -r '.result.id')" >> "$GITHUB_ENV"
- name: Quick deploy
run: |
sf project deploy quick \
--job-id "$VALIDATION_JOB_ID" \
--target-org prod \
--wait 10
Do not use
--use-most-recentfor quick deploy in multi-team orgs. Another team's deployment between validate and quick-deploy invalidates the job. Always pass--job-idexplicitly.
feature/ABC-123-account-service
|
v
develop ---- CI: validate + deploy to dev sandbox
|
v
staging ---- CI: validate + deploy to staging sandbox (RunLocalTests)
|
v
main ---- CI: deploy to production (RunLocalTests)
feature/* -- individual work, scratch org per developerdevelop -- integration branch, auto-deploys to dev sandboxstaging -- pre-production, mirrors production as closely as possiblemain -- production. Requires pull request review + CI greentest-in-scratch-org:
name: Test in Scratch Org
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/heads/feature/')
steps:
- uses: actions/checkout@v4
- name: Install SF CLI and authenticate Dev Hub
env:
JWT_SECRET_KEY: ${{ secrets.DEVHUB_JWT_SECRET_KEY }}
CONSUMER_KEY: ${{ secrets.DEVHUB_CONSUMER_KEY }}
USERNAME: ${{ secrets.DEVHUB_USERNAME }}
run: |
npm install -g @salesforce/cli
echo "$JWT_SECRET_KEY" | base64 --decode > server.key
sf org login jwt \
--client-id "$CONSUMER_KEY" \
--jwt-key-file server.key \
--username "$USERNAME" \
--alias devhub \
--set-default-dev-hub
rm server.key
- name: Create scratch org
run: |
sf org create scratch \
--definition-file config/project-scratch-def.json \
--alias ci-scratch \
--set-default \
--duration-days 1 \
--no-ancestors
- name: Push source and run tests
run: |
sf project deploy start --source-dir force-app --target-org ci-scratch
sf apex run test \
--test-level RunLocalTests \
--result-format human \
--code-coverage \
--target-org ci-scratch
- name: Delete scratch org
if: always()
run: sf org delete scratch --target-org ci-scratch --no-prompt
| Environment | Test Level | Rationale |
|---|---|---|
| Feature CI | RunLocalTests | Fast feedback, catches regressions |
| Dev Sandbox | RunLocalTests | Full local test suite |
| Staging | RunLocalTests | Near-production confidence |
| Production | RunLocalTests | Required by Salesforce (75% min) |
| Full release | RunAllTestsInOrg | Complete org-wide regression |
#!/bin/bash
# scripts/get-changed-metadata.sh
BASE_BRANCH=${1:-main}
CHANGED_FILES=$(git diff --name-only origin/$BASE_BRANCH...HEAD)
SF_CHANGED=$(echo "$CHANGED_FILES" | grep "^force-app/")
if [ -z "$SF_CHANGED" ]; then
echo "No Salesforce metadata changes detected"
exit 0
fi
# Use sfdx-git-delta plugin
# Verify command syntax with: sf sgd --help
sf sgd:source:delta \
--to HEAD \
--from origin/$BASE_BRANCH \
--output-dir changed-sources \
--generate-delta
# Deploy only changed sources
TEST_CLASSES=$(cat changed-sources/test-classes.txt 2>/dev/null | tr '\n' ',' | sed 's/,$//')
if [ -n "$TEST_CLASSES" ]; then
sf project deploy start \
--source-dir changed-sources/force-app \
--test-level RunSpecifiedTests \
--tests "$TEST_CLASSES" \
--target-org $TARGET_ORG
else
sf project deploy start \
--source-dir changed-sources/force-app \
--test-level RunLocalTests \
--target-org $TARGET_ORG
fi
Install sfdx-git-delta plugin:
sf plugins install sfdx-git-delta
sf-deployment-constraints -- deployment safety rules