The branch, environment and org model
The pipeline's behaviour is driven almost entirely by which branch you are on and what a branch maps to. Get this model straight and the rest of the toolchain — which org gets deployed to, whether delta applies, whether validation runs against a scratch org — falls into place.
Branches map to stage orgs
sequenceDiagram
participant feature as feature/* (dev / scratch orgs)
participant develop as develop (SIT)
participant uat as uat (UAT)
participant main as main (PRODUCTION)
feature->>develop: Pull Request = scan secrets, validate (scratch + org)
feature->>develop: Merge = automatic deploy to SIT
develop->>uat: Pull Request = validate against UAT
develop->>uat: Merge = automatic deploy to UAT
uat->>main: Pull Request = validate against PRODUCTION
uat->>main: Merge = manual deploy to PRODUCTION
Each long-lived branch corresponds to a stage and a stage org. A pull request into a
branch validates the change against that branch's org; a merge to a branch deploys to it. The
default chain is develop → uat → main, mapping to SIT → UAT → PRODUCTION, though projects add
or drop a stage as they need.
Casing is a signal here, not decoration: a lowercase name (develop, uat, sit) is a genuine git
branch or Bitbucket deployment-environment name; an upper-case name (SIT, UAT, PRODUCTION) is
the stage — the ENVIRONMENT / SF_DEPLOY_TARGET value the toolchain derives from it. The two
sometimes share a spelling (uat the branch vs. UAT the stage) — the case tells you which one is
meant.
Every merge is a release to an audience
From develop onwards, treat each merge as a release to a specific audience. SIT integrates
everyone's work; UAT is where the client tests; PRODUCTION is live. Release preparation
(scheduling, pre/post steps, data migration) is a separate concern the pipeline only partly
automates.
From branch to environment to target
The mapping is resolved in one place — utils/env.mjs — and nothing else is allowed to re-derive
it. Two values matter:
ENVIRONMENT— the coarse stage the run belongs to (SIT,UAT,PRODUCTION), derived from the Bitbucket deployment environment or, on a pull request, from the target branch.SF_DEPLOY_TARGET— what per-environment value replacements resolve against (a Salesforce DX feature keyed on this variable; see how to replace a value per environment). It falls back toSITso scratch-org and local runs still resolve sensible values.
graph TD
Branch["Branch / PR target"] --> Env["branchToDeploymentEnvironment()"]
Env --> Environment["ENVIRONMENT
(SIT | UAT | PRODUCTION | '')"]
Environment --> Target["SF_DEPLOY_TARGET
(defaults to SIT)"]
ENVIRONMENT is set on pull requests too
A common trap: on a PR targeting a named stage, ENVIRONMENT is already set (the PR is for
that stage) — even though the actual deploy target during scratch validation is a throwaway
scratch org, not the named org. Logic that keys off ENVIRONMENT to mean "deploying to the
named org" will misfire during scratch validation. Prefer an explicit signal for "this is a
scratch deploy".
PRODUCTION deploys on a manual trigger
The Deploy to Production step carries trigger: manual in bitbucket-pipelines.yml, so a merge
to main does not deploy to PRODUCTION automatically — the pipeline stops and waits for someone to
press the button. PRODUCTION deploys frequently need human-timed pre- and post-deployment steps,
which is why the default is set this way; a project can change the trigger if that no longer holds.
Refreshing a sandbox is a deploy, not a sync
There is no separate "sync" mechanism: refreshing a sandbox means deploying a branch to it. A deploy only touches what is in its diff — components added or changed on the branch get pushed, and (for a delta deploy) components deleted on the branch are removed via a computed destructive-changes manifest — but nothing outside that diff is touched. It never reconciles the sandbox to match the branch wholesale.
A delta is computed from git history, not from the org: metadata changed directly in the sandbox (declarative clicks, ad-hoc deploys) is invisible to the diff and left as-is, because the toolchain has no source-tracking mediator keeping a sandbox and its branch convergent. The steps, including how to pick the base ref, are in how to refresh your sandbox from a branch.
Where this lives
- Branch → environment mapping:
branchToDeploymentEnvironment()inutils/env.mjs. - Branch → pipeline behaviour: the
pipelines:section ofbitbucket-pipelines.yml(see the pipeline reference). - Environment → deploy target and toggles: the exports of
utils/env.mjs(see the environment variable reference).