How to deploy a feature branch to staging in Azure DevOps
Deploying a feature branch to staging before it is merged lets testers and product owners see a change in a realistic environment while it is still cheap to fix. Azure Pipelines supports this well, but it does not decide for you how the branch gets chosen, who is allowed to choose it, or which branches should never reach staging.
This guide compares three approaches, from the simplest to the most controlled, with YAML you can adapt.
Before you start: one staging, many branches
A single shared staging environment can only run one branch at a time. Each feature-branch deploy replaces the last one. That is fine when the team agrees who is using staging, but it means the deploy step should always make it obvious which branch and ticket is about to go out. If you need several branches live at once, you need ephemeral or per-branch environments, which is a bigger change than this guide covers.
Approach 1: manual run with the branch selected
The pipeline deploys whatever branch it runs on, and the person deploying picks the branch in the Run pipeline dialog.
- Open the staging deployment pipeline in Azure Pipelines.
- Click Run pipeline.
- Change Branch/tag from the default to the feature branch, for example
feature/1482-invoice-export. - Click Run.
Good for: small teams where developers deploy their own branches. Watch out for: the default branch left selected, look-alike branch names, and anyone with queue permission being able to deploy any branch, including main.
Approach 2: runtime parameters and branch conditions in YAML
Add guard rails to the pipeline itself: a runtime parameter for the target environment, and a stage condition that only deploys feature and bugfix branches.
trigger: none
pr: none
parameters:
- name: targetEnvironment
displayName: Target environment
type: string
default: staging
values:
- staging
- uat
stages:
- stage: Deploy
condition: |
or(
startsWith(variables['Build.SourceBranch'], 'refs/heads/feature/'),
startsWith(variables['Build.SourceBranch'], 'refs/heads/bugfix/')
)
jobs:
- deployment: DeployWeb
environment: ${{ parameters.targetEnvironment }}
pool:
vmImage: ubuntu-latest
strategy:
runOnce:
deploy:
steps:
- checkout: self
- script: ./scripts/deploy.sh ${{ parameters.targetEnvironment }}
displayName: Deploy $(Build.SourceBranchName) parametersgives the Run pipeline dialog a dropdown for the environment, so nobody types it.- The stage
conditionskips the deploy for anything that is notfeature/*orbugfix/*. The run still starts, but nothing is deployed. - The
deploymentjob targets an Azure Pipelines environment, so you can add approvals and checks tostagingin Pipelines → Environments. checkout: selfis required because deployment jobs do not check out source by default.
Good for: teams that want the pipeline to enforce rules no matter who queues it. Watch out for: the branch is still chosen by hand, and a skipped stage can look like a successful run if nobody checks the stage list.
Approach 3: deploy from the work item with BranchDeploy
If feature branches are linked to Azure Boards work items, the branch can come from the ticket instead of a person. BranchDeploy adds a BranchDeploy action to the work item toolbar that:
- Reads the work item's linked branch, or a linked PR's source branch.
- Shows the work item, repository, branch, and environment in a confirmation step.
- Blocks the deploy if the branch does not match the environment's allowed patterns, such as
feature/*andbugfix/*. - Queues the staging pipeline with that branch as the run's source branch, as the signed-in user.
BranchDeploy passes context as queue-time variables (for example environment and workItemId), not YAML runtime parameters. If you use runtime parameters as in Approach 2, they keep their default values when BranchDeploy queues the run. For a pipeline built for BranchDeploy, read a variable instead:
trigger: none
pr: none
# "environment" is defined in the pipeline's Variables panel, not here,
# so it can be set at queue time.
stages:
- stage: Deploy
jobs:
- deployment: DeployWeb
environment: staging
pool:
vmImage: ubuntu-latest
strategy:
runOnce:
deploy:
steps:
- checkout: self
- script: ./scripts/deploy.sh $(environment)
displayName: Deploy $(Build.SourceBranchName) Define environment in the pipeline's Variables panel (Edit → Variables) with Let users override this value when running this pipeline ticked. Variables declared in the YAML file itself cannot be overridden at queue time. See queue-time variables vs runtime parameters for the difference. Environment approvals and checks on staging still apply.
Comparison
| Manual run | YAML parameters + conditions | BranchDeploy | |
|---|---|---|---|
| Who picks the branch | The person deploying | The person deploying | The work item's Development link |
| Unsafe branches | Not blocked | Stage skipped after the run starts | Blocked before any run is queued |
| Confirmation before deploy | No | No (approvals optional) | Yes, with work item and branch shown |
| Link to the ticket | None | None unless passed manually | Work item ID passed as a variable |
| Setup | None | YAML changes | Install extension, enter pipeline ID |
The approaches combine well. Many teams keep the YAML condition from Approach 2 as a final safety net and use BranchDeploy so testers can deploy from the ticket.
Common problems
The run succeeds but staging did not change
The deploy stage was probably skipped by a condition. Open the run and check whether the stage shows as skipped, then compare the branch with the condition.
The wrong environment value was used
If the pipeline reads a runtime parameter but the run was queued with a variable (by BranchDeploy, the CLI --variables flag, or the REST API variables object), the parameter default is used. Read the value from a variable instead, or pass it as a template parameter.
Feature branches stay on staging for days
Agree a simple rule, such as "staging shows the ticket in the In QA column", so people know when it is safe to deploy over someone else's branch. See the UAT deployment workflow for a board flow that works.
Frequently asked questions
Can Azure Pipelines deploy a feature branch automatically on push?
Yes. Add the branch pattern to the pipeline's trigger, for example feature/*. With one shared staging environment this means the last push wins, so most teams prefer on-demand deploys for staging.
How do I stop main being deployed to staging?
Use a stage condition in YAML, an environment check, or a BranchDeploy allowlist that leaves main out. The allowlist stops the run before it is queued; the YAML condition stops it inside the run.
Do I need a separate pipeline for staging and UAT?
Not necessarily. One pipeline can deploy to either based on a parameter or variable. BranchDeploy can point several environments at the same pipeline ID and pass a different environment value for each (multiple environments are part of Pro).