SfSkills Domains Docs GitHub

Home / devops / bitbucket-pipelines-for-salesforce

bitbucket-pipelines-for-salesforce

Use this skill to set up, review, or troubleshoot Bitbucket Pipelines CI/CD workflows for Salesforce using SFDX JWT Bearer Flow or SFDP URL authentication, Apex test gates, branch-based deployment strategies, and Bitbucket repository variables for secret management. Trigger keywords: bitbucket pipelines, bitbucket CI, atlassian pipe, salesforce-deploy pipe, bitbucket-pipelines.yml, bitbucket repository variables. NOT for GitHub Actions (use devops/github-actions-for-salesforce), Jenkins, Copado, Azure DevOps, or any non-Bitbucket CI platform.

Open the skill folder on GitHub


Bitbucket Pipelines for Salesforce

This skill activates when a practitioner needs to build, review, or fix a Bitbucket Pipelines CI/CD workflow that deploys Salesforce metadata and runs Apex tests using the Salesforce CLI. It covers all three operating modes: initial setup from scratch, pipeline audit and review, and live troubleshooting of CI failures. It is scoped exclusively to Bitbucket Cloud pipelines using bitbucket-pipelines.yml.


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

JWT Bearer Flow Authentication in Bitbucket

The Salesforce CLI's sf org login jwt command authenticates non-interactively using a private RSA key and a Connected App. Per the Salesforce DX Developer Guide, the flow requires:

  1. Generate a self-signed X.509 certificate:
    openssl req -x509 -nodes -sha256 -days 365 -newkey rsa:2048 \
      -keyout server.key -out server.crt \
      -subj "/C=US/ST=CA/L=SF/O=MyOrg/CN=ci-service"
  2. Upload server.crt to the Connected App in Salesforce (Setup > App Manager > Edit > Upload Certificate).
  3. Store the content of server.key in a secured (masked) Bitbucket repository variable (e.g., SF_JWT_SERVER_KEY). Store the Connected App consumer key in a second secured variable (e.g., SF_CONSUMER_KEY).
  4. In the pipeline YAML, write the key content to a temporary file before calling sf org login jwt --jwt-key-file.

The certificate has a finite lifespan (365 days by default). Expiry is the most common cause of sudden, unexplained CI auth failures. There is no platform notification when a cert nears expiry.

sf CLI vs. Legacy sfdx Commands

As of Spring '25, Salesforce has unified the CLI under sf (Salesforce CLI v2). The legacy sfdx force:* namespace still works but is deprecated. All new pipelines must use sf project deploy start (not sfdx force:source:deploy) and sf apex run test (not sfdx force:apex:test:run). Mixing old and new namespaces in a single pipeline causes ambiguous output and will break when deprecated commands are removed.

Bitbucket Repository Variables for Secrets

Bitbucket repository variables come in two types:

Variables are injected as environment variables into every pipeline step. Reference them with $VARIABLE_NAME in YAML scripts. Never echo a secured variable in a run: step without intentional masking — even with masking, this is poor hygiene.

bitbucket-pipelines.yml Branch Pipeline Structure

Bitbucket Pipelines uses a declarative YAML structure. The key sections for Salesforce CI are:


Common Patterns

Mode 1: Set Up Bitbucket Pipelines from Scratch

When to use: No bitbucket-pipelines.yml exists yet, or it is a placeholder. Team wants branch-based push-to-deploy with Apex test gating.

How it works:

Step 1 — Generate and upload the JWT cert (done once, outside CI):

openssl req -x509 -nodes -sha256 -days 365 -newkey rsa:2048 \
  -keyout server.key -out server.crt \
  -subj "/C=US/ST=CA/L=SF/O=MyOrg/CN=ci-service"

Upload server.crt to the Connected App. Store server.key content and the consumer key as secured Bitbucket repository variables. Delete server.key locally after uploading.

Step 2 — Create bitbucket-pipelines.yml in the repository root:

image: node:20

pipelines:
  branches:
    # Feature branches: validate only (no deploy)
    'feature/**':
      - step:
          name: Validate Salesforce Metadata
          script:
            - npm install --global @salesforce/cli@latest
            - echo "$SF_JWT_SERVER_KEY" > /tmp/server.key
            - sf org login jwt
                --client-id "$SF_CONSUMER_KEY"
                --jwt-key-file /tmp/server.key
                --username "$SF_USERNAME"
                --instance-url "$SF_INSTANCE_URL"
                --alias target-org
                --set-default
            - sf project deploy validate
                --target-org target-org
                --source-dir force-app
                --test-level RunLocalTests
                --wait 30
          after-script:
            - rm -f /tmp/server.key

    # Develop branch: deploy to full sandbox
    develop:
      - step:
          name: Run Apex Tests
          script:
            - npm install --global @salesforce/cli@latest
            - echo "$SF_JWT_SERVER_KEY_SANDBOX" > /tmp/server.key
            - sf org login jwt
                --client-id "$SF_CONSUMER_KEY_SANDBOX"
                --jwt-key-file /tmp/server.key
                --username "$SF_USERNAME_SANDBOX"
                --instance-url "https://test.salesforce.com"
                --alias sandbox-org
                --set-default
            - sf apex run test
                --target-org sandbox-org
                --test-level RunLocalTests
                --code-coverage
                --result-format json
                --output-dir test-results
                --wait 20
            - python3 -c "
                import json, sys
                with open('test-results/test-result.json') as f:
                    data = json.load(f)
                pct = data.get('summary', {}).get('orgWideCoverage', '0%').replace('%','')
                assert float(pct) >= 75, f'Coverage {pct}% is below 75% threshold'
                print(f'Coverage OK: {pct}%')
              "
          after-script:
            - rm -f /tmp/server.key
      - step:
          name: Deploy to Sandbox
          script:
            - npm install --global @salesforce/cli@latest
            - echo "$SF_JWT_SERVER_KEY_SANDBOX" > /tmp/server.key
            - sf org login jwt
                --client-id "$SF_CONSUMER_KEY_SANDBOX"
                --jwt-key-file /tmp/server.key
                --username "$SF_USERNAME_SANDBOX"
                --instance-url "https://test.salesforce.com"
                --alias sandbox-org
                --set-default
            - sf project deploy start
                --target-org sandbox-org
                --source-dir force-app
                --test-level NoTestRun
                --wait 30
          after-script:
            - rm -f /tmp/server.key

    # Main branch: deploy to production
    main:
      - step:
          name: Deploy to Production
          deployment: Production
          script:
            - npm install --global @salesforce/cli@latest
            - echo "$SF_JWT_SERVER_KEY_PROD" > /tmp/server.key
            - sf org login jwt
                --client-id "$SF_CONSUMER_KEY_PROD"
                --jwt-key-file /tmp/server.key
                --username "$SF_USERNAME_PROD"
                --instance-url "https://login.salesforce.com"
                --alias prod-org
                --set-default
            - sf project deploy start
                --target-org prod-org
                --source-dir force-app
                --test-level RunLocalTests
                --wait 60
          after-script:
            - rm -f /tmp/server.key

Why not the simpler approach: Using sfdx auth:sfdxurl:store or storing the SFDP URL as an unsecured variable exposes credentials in build logs and violates the Connected App isolation model.

Mode 2: Reviewing/Auditing an Existing Pipeline

When to use: A bitbucket-pipelines.yml exists but the team suspects security gaps, deprecated commands, or flaky test failures.

How it works:

Check for these items in order:

  1. Auth method — Is sf org login jwt used (correct) or sf org login web/sfdx auth:sfdxurl:store (wrong for CI)?
  2. Variable security — Are all credentials referenced from secured (masked) Bitbucket repository variables? Are any credentials echoed to stdout?
  3. CLI namespace — Is sfdx force:* (deprecated) used instead of sf?
  4. Test level — Is --test-level explicitly set? Omitting it means zero tests run on sandbox deploys.
  5. Branch matchers — Do branch patterns match the team's actual branching model? Glob patterns like feature/** must be quoted in YAML.
  6. after-script: cleanup — Is rm -f /tmp/server.key in after-script: (runs on failure too), not just in the main script: block?
  7. Certificate expiry — When was the JWT certificate generated? Check the Connected App in Setup for the expiry date. Certs default to 365-day lifespans.
  8. Per-environment credentials — Are separate secured variables used per environment (sandbox vs. production), not one shared secret?

Mode 3: Troubleshooting CI Failures

When to use: A previously working pipeline starts failing.

SymptomRoot CauseFix
INVALID_SESSION_ID or expired access/refresh tokenJWT cert expired (365-day default)Regenerate cert, re-upload to Connected App, update Bitbucket secured variable
No org found for alias target-orgAuth step failed silently; later steps reference unregistered aliasAdd --set-default to login step; add sf org list in a debug step
Deploy failed: Test coverage of classes is 0%--test-level NoTestRun used on production deployUse RunLocalTests for production; run separate test step first
Error: ENOTFOUND login.salesforce.com or connection timeoutWrong --instance-url for org typeUse https://test.salesforce.com for sandbox, https://login.salesforce.com for production
LIMIT_EXCEEDED: scratch org creation limitDaily scratch org allocation exhaustedSwitch to sandbox-based testing; check limits in Dev Hub
Error: Can't set default org / user not authorizedConnected App OAuth Policies missing pre-authorizationVerify pre-authorized users in Connected App > OAuth Policies
Pipeline build step not triggeredBranch pattern not matchingQuote glob patterns in YAML; test with Bitbucket's pipeline YAML validator

Decision Guidance

SituationRecommended ApproachReason
Pull request validation only (no deploy)sf project deploy validate with --dry-runValidates metadata and runs tests without committing to the org; idempotent
Deploy on merge to main (production)sf project deploy start with --test-level RunLocalTestsEnforces platform 75% threshold at deploy time
Feature branch with ephemeral scratch orgCreate scratch org per step, run tests, delete on after-script:Org-per-PR isolation; watch daily limits
Sandbox promotion (develop → staging)sf project deploy start targeting sandbox alias with separate secured variable setUse separate Bitbucket Deployment environments per environment for credential isolation
Rollback after failed production deployTrigger a revert branch and re-run the pipelineSalesforce does not support automated rollback via CLI; rollback is a new deployment
JWT vs. SFDP URL authJWT Bearer Flow preferredSFDP URL embeds a refresh token that can expire; JWT cert expiry is predictable and rotatable

Step-by-step instructions for an AI agent or practitioner activating this skill:

  1. Gather context — confirm the org edition, relevant objects, and current configuration state
  2. Review official sources — check the references in this skill's well-architected.md before making changes
  3. Implement or advise — apply the patterns from Core Concepts and Common Patterns sections above
  4. Validate — run the skill's checker script and verify against the Review Checklist below
  5. Document — record any deviations from standard patterns and update the template if needed

Review Checklist

Run through these before marking the pipeline complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. after-script: is the only way to guarantee cleanup on failure — Unlike the main script: block, after-script: runs regardless of whether earlier steps succeed or fail. If rm -f /tmp/server.key is placed only in script:, a deploy failure leaves the key file on the runner for the rest of that build's lifetime. Always place key file deletion in after-script:.
  2. JWT certificate expiry has no built-in notification — Salesforce does not send any warning when a JWT certificate on a Connected App is nearing expiry. The pipeline returns INVALID_SESSION_ID on the expiry date with no advance notice. Prevention: record the cert generation date in a team calendar or README; set a reminder 30 days before the 365-day lifespan ends; rotate before expiry.
  3. --test-level NoTestRun silently skips all tests on sandbox deploys — When --test-level is omitted from sf project deploy start, Salesforce defaults to NoTestRun for sandbox targets. Zero tests run and zero coverage is checked. Teams discover this only when a later production deploy fails the 75% threshold. Always specify --test-level explicitly.
  4. Bitbucket YAML branch glob patterns must be quoted — feature/** without quotes is valid YAML but some Bitbucket Pipeline parsers treat * as a YAML alias anchor character and silently misparse the pattern, resulting in the step never triggering. Always quote branch patterns: 'feature/**'.
  5. Connected App OAuth pre-authorization is separate from profile assignment — Adding a user to the profile or permission set assigned to the Connected App is not sufficient for JWT auth. The Connected App's OAuth Policies must have "Permitted Users: Admin approved users are pre-authorized" and the specific user (or their profile) must be listed in the "Manage Connected Apps > Pre-Authorize Users" section. Skipping this step produces a silent auth failure.

Output Artifacts

ArtifactDescription
bitbucket-pipelines.ymlComplete Bitbucket Pipelines YAML with JWT auth, test gate, branch-conditional deploy, and cleanup
test-results/ directoryJSON and JUnit XML Apex test output produced by sf apex run test --output-dir
Pipeline audit findingsOrdered list of security and best-practice gaps found in an existing pipeline YAML
Troubleshooting diagnosisRoot cause and fix for a specific CI failure symptom

Dependencies

Package files