Preview Environments with Azure DevOps: Automated Per-PR Deployments with Bunnyshell
Why Integrate Bunnyshell with Azure DevOps?
Azure DevOps is the backbone of many enterprise engineering teams — Repos for source control, Boards for project management, Pipelines for CI/CD, and Artifacts for package management. Pull requests are where code review happens. But reviewing a diff is not the same as seeing the actual running application.
Bunnyshell creates isolated preview environments for every pull request — a full-stack deployment with your app, databases, and services — running in Kubernetes with a unique HTTPS URL. Reviewers click a link, see the live app, and give feedback on the actual behavior, not just the code.
Two options:
| Approach | Pipeline changes | Control level | Best for |
|---|---|---|---|
| Approach A: Webhook-Only | None | Bunnyshell manages everything | Teams that want the fastest setup |
| Approach B: Pipeline Integration | Add stages to azure-pipelines.yml | Full control over lifecycle | Teams needing custom steps (migrations, seeds, tests, Azure Environments) |
Both approaches produce the same result: an isolated environment per PR with automatic cleanup on merge.
How It Works
Webhook-Only Flow
1PR created → Azure DevOps webhook → Bunnyshell API
2 ↓
3 Create environment
4 Build images
5 Deploy to K8s
6 ↓
7 Post comment on PR
8 with preview URLBunnyshell registers a service hook on your Azure DevOps project. When a PR is created, updated, or completed, Azure DevOps sends events directly to Bunnyshell. No pipeline involvement.
Pipeline-Triggered Flow
1PR created → Azure Pipeline triggers (pr: trigger)
2 ↓
3 Pipeline stage installs Bunnyshell CLI
4 CLI creates/deploys environment
5 CLI retrieves preview URL
6 ↓
7 Post-deploy stages (migrations, seeds, tests)
8 ↓
9 Pipeline posts preview URL as PR commentYour azure-pipelines.yml controls when and how Bunnyshell environments are created, giving you the ability to run custom commands before or after deployment.
Prerequisites
Before you begin, ensure you have:
- A Bunnyshell account — Sign up at bunnyshell.com
- A Bunnyshell project and primary environment — At least one environment deployed and in Running or Stopped state
- An Azure DevOps project with Pipelines enabled
- A Bunnyshell API token — Generate one in Bunnyshell under Settings > API Tokens
- Your Bunnyshell IDs — Organization ID, Project ID, Environment ID, and Cluster ID
- Azure DevOps Personal Access Token (PAT) — If using Approach B, you'll need a PAT with
Code (Read & Write)scope to post PR comments
If you haven't set up a Bunnyshell environment yet, follow one of the framework-specific guides first (e.g., Laravel, Django, Express) to create your primary environment. This guide focuses on the CI/CD integration layer.
Approach A: Webhook-Only (Zero Pipeline Config)
This is the fastest path. Bunnyshell handles everything — no azure-pipelines.yml changes required.
Step 1: Connect Your Azure DevOps Repository
- Log into Bunnyshell
- Navigate to your Project and select your primary environment
- Go to Settings > Integrations
- Add your Azure DevOps integration:
- Provide your Azure DevOps organization URL (e.g.,
https://dev.azure.com/your-org) - Authorize Bunnyshell with a PAT that has
Code (Read)andService Hooks (Read & Write)scopes
- Provide your Azure DevOps organization URL (e.g.,
Bunnyshell uses the PAT to read repository data and register service hooks.
Step 2: Enable Automatic Preview Environments
- In your environment, go to Settings
- Find the Ephemeral environments section
- Toggle "Create ephemeral environments on pull request" to ON
- Toggle "Destroy environment after merge or close pull request" to ON
- Select the target Kubernetes cluster for ephemeral environments
Step 3: Verify the Service Hook
After enabling, Bunnyshell registers a service hook on your Azure DevOps project:
- In Azure DevOps, go to Project Settings > Service hooks
- You should see a Bunnyshell webhook subscribed to:
Pull request createdPull request updatedPull request merge attempted
Step 4: Test It
- Create a new branch and make a change
- Open a pull request in Azure DevOps
- Bunnyshell creates an ephemeral environment cloned from your primary, using the PR branch
- Within a few minutes, Bunnyshell posts a comment on the PR with the preview URL
- Complete or abandon the PR — the environment is automatically destroyed
The primary environment must be in Running or Stopped status. If it's still deploying or in an error state, ephemeral environment creation will fail.
That's it. No pipeline config, no YAML, no tokens to manage in pipelines. Bunnyshell handles the full lifecycle.
Approach B: Pipeline Integration with Bunnyshell CLI
For teams that need custom pre- or post-deploy steps — database migrations, data seeding, integration tests, or Azure Environments for deployment tracking — you can drive Bunnyshell from your azure-pipelines.yml.
Step 1: Store the Bunnyshell Token
- In Azure DevOps, go to Pipelines > Library > Variable groups
- Create a new variable group called
bunnyshell-config - Add these variables:
| Variable | Value | Secret |
|---|---|---|
BUNNYSHELL_TOKEN | Your Bunnyshell API token | Yes (lock icon) |
BUNNYSHELL_ORG_ID | Your organization ID | No |
BUNNYSHELL_PROJECT_ID | Your project ID | No |
BUNNYSHELL_CLUSTER_ID | Target K8s cluster ID | No |
BUNNYSHELL_ENV_ID | Primary environment ID to clone from | No |
AZURE_DEVOPS_PAT | PAT for posting PR comments | Yes (lock icon) |
- Click Save
Always mark BUNNYSHELL_TOKEN and AZURE_DEVOPS_PAT as secret variables (click the lock icon). Secret variables are encrypted at rest and masked in pipeline logs. Never hardcode tokens in azure-pipelines.yml.
Step 2: Set Up PR Build Validation
To ensure the pipeline triggers on every PR:
- Go to Repos > Branches
- Click the three dots next to your target branch (e.g.,
main) > Branch policies - Under Build validation, click Add build policy
- Select your pipeline and configure:
- Trigger: Automatic
- Policy requirement: Optional (so the PR isn't blocked during deploy)
- Build expiration: After 12 hours
This ensures the pipeline triggers on every PR targeting main, even if the pr: trigger in YAML isn't sufficient.
Step 3: Create the Pipeline Configuration
Create or update azure-pipelines.yml in your repository root:
1# azure-pipelines.yml
2trigger:
3 branches:
4 include:
5 - main
6 - master
7
8pr:
9 branches:
10 include:
11 - main
12 - master
13
14variables:
15 - group: bunnyshell-config
16
17pool:
18 vmImage: 'ubuntu-latest'
19
20stages:
21 # ── Stage 1: Deploy Preview Environment ──
22 - stage: DeployPreview
23 displayName: 'Deploy Preview Environment'
24 condition: eq(variables['Build.Reason'], 'PullRequest')
25 jobs:
26 - job: DeployPreviewEnv
27 displayName: 'Create and Deploy Preview'
28 steps:
29 # Install Bunnyshell CLI
30 - script: |
31 curl -sL https://raw.githubusercontent.com/bunnyshell/cli/main/install.sh | bash
32 echo "##vso[task.prependpath]$HOME/.bunnyshell/bin"
33 displayName: 'Install Bunnyshell CLI'
34
35 # Derive environment name from PR
36 - script: |
37 PR_ID=$(echo "$(System.PullRequest.PullRequestId)")
38 ENV_NAME="pr-${PR_ID}"
39 echo "##vso[task.setvariable variable=ENV_NAME;isoutput=true]$ENV_NAME"
40 echo "##vso[task.setvariable variable=PR_ID;isoutput=true]$PR_ID"
41 echo "Environment name: $ENV_NAME"
42 displayName: 'Set Environment Name'
43 name: SetEnvName
44
45 # Check if environment already exists
46 - script: |
47 export BUNNYSHELL_TOKEN=$(BUNNYSHELL_TOKEN)
48 ENV_NAME="$(SetEnvName.ENV_NAME)"
49
50 EXISTING_ENV=$(bns environments list \
51 --organization "$(BUNNYSHELL_ORG_ID)" \
52 --project "$(BUNNYSHELL_PROJECT_ID)" \
53 --search "$ENV_NAME" \
54 --output json 2>/dev/null | \
55 python3 -c "import sys,json; items=json.load(sys.stdin).get('_embedded',{}).get('item',[]); print(items[0]['id'] if items else '')" \
56 ) || true
57
58 echo "##vso[task.setvariable variable=EXISTING_ENV;isoutput=true]$EXISTING_ENV"
59 echo "Existing environment ID: $EXISTING_ENV"
60 displayName: 'Check Existing Environment'
61 name: CheckEnv
62
63 # Create or redeploy environment
64 - script: |
65 export BUNNYSHELL_TOKEN=$(BUNNYSHELL_TOKEN)
66 ENV_NAME="$(SetEnvName.ENV_NAME)"
67 EXISTING_ENV="$(CheckEnv.EXISTING_ENV)"
68
69 if [ -n "$EXISTING_ENV" ]; then
70 echo "Environment $ENV_NAME already exists (ID: $EXISTING_ENV). Redeploying..."
71 bns environments deploy \
72 --id "$EXISTING_ENV" \
73 --wait \
74 --timeout 900
75 DEPLOY_ENV_ID="$EXISTING_ENV"
76 else
77 echo "Creating new environment: $ENV_NAME"
78 CREATE_OUTPUT=$(bns environments create \
79 --from-environment "$(BUNNYSHELL_ENV_ID)" \
80 --name "$ENV_NAME" \
81 --k8s "$(BUNNYSHELL_CLUSTER_ID)" \
82 --output json)
83 DEPLOY_ENV_ID=$(echo "$CREATE_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
84 echo "Created environment ID: $DEPLOY_ENV_ID"
85
86 bns environments deploy \
87 --id "$DEPLOY_ENV_ID" \
88 --wait \
89 --timeout 900
90 fi
91
92 echo "##vso[task.setvariable variable=DEPLOY_ENV_ID;isoutput=true]$DEPLOY_ENV_ID"
93 displayName: 'Create or Redeploy Environment'
94 name: DeployEnv
95
96 # Retrieve preview URL
97 - script: |
98 export BUNNYSHELL_TOKEN=$(BUNNYSHELL_TOKEN)
99 DEPLOY_ENV_ID="$(DeployEnv.DEPLOY_ENV_ID)"
100
101 PREVIEW_URL=$(bns environments show \
102 --id "$DEPLOY_ENV_ID" \
103 --output json | \
104 python3 -c "
105 import sys, json
106 data = json.load(sys.stdin)
107 endpoints = data.get('endpoints', [])
108 if endpoints:
109 print(endpoints[0].get('url', 'N/A'))
110 else:
111 print('N/A')
112 ")
113
114 echo "##vso[task.setvariable variable=PREVIEW_URL;isoutput=true]$PREVIEW_URL"
115 echo "Preview URL: $PREVIEW_URL"
116 displayName: 'Get Preview URL'
117 name: GetURL
118
119 # Post PR comment with preview URL
120 - script: |
121 PREVIEW_URL="$(GetURL.PREVIEW_URL)"
122 DEPLOY_ENV_ID="$(DeployEnv.DEPLOY_ENV_ID)"
123 PR_ID="$(SetEnvName.PR_ID)"
124 REPO_ID="$(Build.Repository.ID)"
125 PROJECT="$(System.TeamProject)"
126 ORG_URL="$(System.CollectionUri)"
127
128 if [ "$PREVIEW_URL" != "N/A" ]; then
129 COMMENT_BODY=$(cat <<COMMENTEOF
130 {
131 "comments": [
132 {
133 "parentCommentId": 0,
134 "content": "## Preview Environment Ready\n\nYour preview environment is live:\n\n**URL:** ${PREVIEW_URL}\n\nEnvironment ID: \`${DEPLOY_ENV_ID}\`\n\n---\n_Deployed by Bunnyshell via Azure Pipelines_",
135 "commentType": 1
136 }
137 ],
138 "status": 1
139 }
140 COMMENTEOF
141 )
142
143 curl -s -X POST \
144 -H "Content-Type: application/json" \
145 -H "Authorization: Basic $(echo -n ":$(AZURE_DEVOPS_PAT)" | base64)" \
146 "${ORG_URL}${PROJECT}/_apis/git/repositories/${REPO_ID}/pullRequests/${PR_ID}/threads?api-version=7.0" \
147 -d "$COMMENT_BODY"
148
149 echo "Posted preview URL comment on PR #${PR_ID}"
150 fi
151 displayName: 'Post PR Comment'
152
153 # ── Stage 2: Post-Deploy Tasks ──
154 - stage: PostDeploy
155 displayName: 'Post-Deploy Tasks'
156 condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest'))
157 dependsOn: DeployPreview
158 variables:
159 DEPLOY_ENV_ID: $[ stageDependencies.DeployPreview.DeployPreviewEnv.outputs['DeployEnv.DEPLOY_ENV_ID'] ]
160 jobs:
161 - job: RunMigrations
162 displayName: 'Run Migrations and Seeds'
163 steps:
164 - script: |
165 curl -sL https://raw.githubusercontent.com/bunnyshell/cli/main/install.sh | bash
166 echo "##vso[task.prependpath]$HOME/.bunnyshell/bin"
167 displayName: 'Install Bunnyshell CLI'
168
169 - script: |
170 export BUNNYSHELL_TOKEN=$(BUNNYSHELL_TOKEN)
171 DEPLOY_ENV_ID="$(DEPLOY_ENV_ID)"
172
173 sleep 30
174
175 COMPONENT_ID=$(bns components list \
176 --environment "$DEPLOY_ENV_ID" \
177 --output json | \
178 python3 -c "
179 import sys, json
180 items = json.load(sys.stdin).get('_embedded', {}).get('item', [])
181 for item in items:
182 if 'app' in item.get('name', '').lower():
183 print(item['id'])
184 break
185 ")
186
187 if [ -n "$COMPONENT_ID" ]; then
188 echo "Running migrations on component $COMPONENT_ID..."
189 bns exec "$COMPONENT_ID" -- php artisan migrate --force || true
190 bns exec "$COMPONENT_ID" -- php artisan db:seed --force || true
191 fi
192 displayName: 'Run Migrations'
193
194 # ── Stage 3: Destroy Preview Environment on Merge ──
195 - stage: DestroyPreview
196 displayName: 'Destroy Preview Environment'
197 condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
198 jobs:
199 - job: CleanupEnvs
200 displayName: 'Cleanup Merged PR Environments'
201 steps:
202 - script: |
203 curl -sL https://raw.githubusercontent.com/bunnyshell/cli/main/install.sh | bash
204 echo "##vso[task.prependpath]$HOME/.bunnyshell/bin"
205 displayName: 'Install Bunnyshell CLI'
206
207 - script: |
208 export BUNNYSHELL_TOKEN=$(BUNNYSHELL_TOKEN)
209
210 ENVS=$(bns environments list \
211 --project "$(BUNNYSHELL_PROJECT_ID)" \
212 --output json | \
213 python3 -c "
214 import sys, json
215 items = json.load(sys.stdin).get('_embedded', {}).get('item', [])
216 for item in items:
217 name = item.get('name', '')
218 if name.startswith('pr-'):
219 print(item['id'])
220 ")
221
222 for ENV_ID in $ENVS; do
223 echo "Destroying environment $ENV_ID..."
224 bns environments delete --id "$ENV_ID" --force || true
225 done
226 displayName: 'Destroy PR Environments'Understanding the Pipeline Configuration
PR trigger:
1pr:
2 branches:
3 include:
4 - main
5 - masterAzure DevOps Pipelines use the pr: section to define which PRs trigger the pipeline. The include list specifies target branches. The pipeline only runs for PRs targeting main or master.
Condition-based stages:
condition: eq(variables['Build.Reason'], 'PullRequest')The DeployPreview stage only runs when the build was triggered by a pull request. The DestroyPreview stage only runs on direct pushes to main (i.e., merges).
Azure DevOps sets Build.Reason to PullRequest for PR-triggered builds and IndividualCI or BatchedCI for push-triggered builds. Use these to control which stages run.
Output variables across stages:
variables:
DEPLOY_ENV_ID: $[ stageDependencies.DeployPreview.DeployPreviewEnv.outputs['DeployEnv.DEPLOY_ENV_ID'] ]Azure Pipelines uses output variables to pass data between jobs and stages. The syntax $[ stageDependencies.StageName.JobName.outputs['StepName.VariableName'] ] references an output variable from a previous stage.
Azure Environments (optional):
For deployment tracking and approvals, wrap the deploy job in an environment:
1jobs:
2 - deployment: DeployPreviewEnv
3 displayName: 'Deploy Preview'
4 environment: 'preview-environments'
5 strategy:
6 runOnce:
7 deploy:
8 steps:
9 # ... same steps as aboveThis registers each deployment in Azure DevOps Environments, giving you a history of deployments and the ability to add manual approval gates.
Secrets and Variables
Variable Groups (Recommended)
Store all Bunnyshell configuration in a variable group:
- Go to Pipelines > Library > Variable groups
- Create a group named
bunnyshell-config - Add all variables (mark secrets with the lock icon)
- Reference in your pipeline:
variables:
- group: bunnyshell-configPipeline Variables
For simpler setups, use pipeline-level variables:
- Edit your pipeline in Azure DevOps
- Click Variables > New variable
- Add each variable and check Keep this value secret for tokens
Key Vault Integration
For enterprise teams, link variables to Azure Key Vault:
- Create a Key Vault in Azure
- Add
BUNNYSHELL-TOKENandAZURE-DEVOPS-PATas secrets - In the variable group, click Link secrets from an Azure Key Vault
- Select the Key Vault and authorize access
Azure Key Vault secret names cannot contain underscores. Use hyphens (e.g., BUNNYSHELL-TOKEN) and reference them with underscores in the pipeline (Azure DevOps automatically converts hyphens to underscores).
Service Connections
For Approach A (webhook-only), you can also use an Azure DevOps Service Connection to manage the Bunnyshell integration:
- Go to Project Settings > Service connections
- Create a Generic service connection
- Set the server URL to
https://api.bunnyshell.comand add your token - Reference it in pipelines using the
connectedServiceNameparameter
PR Comments with Preview URL
In Approach A (webhook-only), Bunnyshell automatically posts a comment on the PR. No configuration needed.
In Approach B (pipeline integration), you post the comment using the Azure DevOps REST API. The pipeline example includes this, but here's the standalone version:
1# Post a PR thread comment with the preview URL
2ORG_URL="$(System.CollectionUri)"
3PROJECT="$(System.TeamProject)"
4REPO_ID="$(Build.Repository.ID)"
5PR_ID="$(System.PullRequest.PullRequestId)"
6
7curl -s -X POST \
8 -H "Content-Type: application/json" \
9 -H "Authorization: Basic $(echo -n ":${AZURE_DEVOPS_PAT}" | base64)" \
10 "${ORG_URL}${PROJECT}/_apis/git/repositories/${REPO_ID}/pullRequests/${PR_ID}/threads?api-version=7.0" \
11 -d '{
12 "comments": [
13 {
14 "parentCommentId": 0,
15 "content": "## Preview Environment Ready\n\n**URL:** '"${PREVIEW_URL}"'\n\nEnvironment ID: `'"${DEPLOY_ENV_ID}"'`\n\n---\n_Deployed by Bunnyshell via Azure Pipelines_",
16 "commentType": 1
17 }
18 ],
19 "status": 1
20 }'Key details:
- The Azure DevOps REST API uses threads for PR comments, not individual comments
commentType: 1= text comment,status: 1= active thread- Authentication uses a PAT encoded as Basic auth with an empty username (
:PATbase64-encoded) api-version=7.0is the latest stable version
To update an existing comment thread instead of creating a new one on each push, list existing threads with a GET request, find the Bunnyshell thread by content, and update it with a PATCH request.
Pipeline Examples
Deploy with Build Validation Policy
When combined with a branch policy, you can require the preview environment to be healthy before the PR can be merged:
1# azure-pipelines.yml (simplified)
2trigger: none
3
4pr:
5 branches:
6 include:
7 - main
8
9pool:
10 vmImage: 'ubuntu-latest'
11
12variables:
13 - group: bunnyshell-config
14
15steps:
16 - script: |
17 curl -sL https://raw.githubusercontent.com/bunnyshell/cli/main/install.sh | bash
18 echo "##vso[task.prependpath]$HOME/.bunnyshell/bin"
19 displayName: 'Install Bunnyshell CLI'
20
21 - script: |
22 export BUNNYSHELL_TOKEN=$(BUNNYSHELL_TOKEN)
23 ENV_NAME="pr-$(System.PullRequest.PullRequestId)"
24
25 bns environments create \
26 --from-environment "$(BUNNYSHELL_ENV_ID)" \
27 --name "$ENV_NAME" \
28 --k8s "$(BUNNYSHELL_CLUSTER_ID)" || true
29
30 bns environments deploy \
31 --name "$ENV_NAME" \
32 --project "$(BUNNYSHELL_PROJECT_ID)" \
33 --wait \
34 --timeout 900
35 displayName: 'Deploy Preview Environment'Set the build validation policy to Required — the PR cannot be completed until the pipeline succeeds, which means the preview environment is healthy.
Multi-Stage with Tests
1stages:
2 - stage: Build
3 jobs:
4 - job: BuildAndTest
5 steps:
6 - script: npm install && npm test
7 displayName: 'Run Tests'
8
9 - stage: Preview
10 dependsOn: Build
11 condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest'))
12 jobs:
13 - job: DeployPreview
14 steps:
15 - script: |
16 curl -sL https://raw.githubusercontent.com/bunnyshell/cli/main/install.sh | bash
17 echo "##vso[task.prependpath]$HOME/.bunnyshell/bin"
18 displayName: 'Install CLI'
19 - script: |
20 export BUNNYSHELL_TOKEN=$(BUNNYSHELL_TOKEN)
21 ENV_NAME="pr-$(System.PullRequest.PullRequestId)"
22 bns environments create \
23 --from-environment "$(BUNNYSHELL_ENV_ID)" \
24 --name "$ENV_NAME" \
25 --k8s "$(BUNNYSHELL_CLUSTER_ID)" || true
26 bns environments deploy \
27 --name "$ENV_NAME" \
28 --project "$(BUNNYSHELL_PROJECT_ID)" \
29 --wait --timeout 900
30 displayName: 'Deploy Preview'Cleanup with Scheduled Pipeline
For environments that might get orphaned (e.g., if a branch is deleted without merging):
1# azure-pipelines-cleanup.yml
2trigger: none
3
4schedules:
5 - cron: '0 2 * * *'
6 displayName: 'Nightly cleanup'
7 branches:
8 include:
9 - main
10
11pool:
12 vmImage: 'ubuntu-latest'
13
14variables:
15 - group: bunnyshell-config
16
17steps:
18 - script: |
19 curl -sL https://raw.githubusercontent.com/bunnyshell/cli/main/install.sh | bash
20 echo "##vso[task.prependpath]$HOME/.bunnyshell/bin"
21 displayName: 'Install CLI'
22
23 - script: |
24 export BUNNYSHELL_TOKEN=$(BUNNYSHELL_TOKEN)
25
26 ENVS=$(bns environments list \
27 --project "$(BUNNYSHELL_PROJECT_ID)" \
28 --output json | \
29 python3 -c "
30 import sys, json
31 items = json.load(sys.stdin).get('_embedded', {}).get('item', [])
32 for item in items:
33 name = item.get('name', '')
34 if name.startswith('pr-'):
35 print(item['id'] + ' ' + name)
36 ")
37
38 echo "$ENVS" | while read ENV_ID ENV_NAME; do
39 if [ -n "$ENV_ID" ]; then
40 echo "Checking if $ENV_NAME still has an open PR..."
41 # Add your logic to check if the PR is still open
42 # If not, destroy the environment
43 bns environments delete --id "$ENV_ID" --force || true
44 fi
45 done
46 displayName: 'Cleanup Orphaned Environments'Troubleshooting
| Issue | Solution |
|---|---|
| Pipeline doesn't trigger on PR | Check the pr: section in azure-pipelines.yml. Ensure the target branch is listed in include. Also verify the pipeline is linked to the repository in Azure DevOps. |
System.PullRequest.PullRequestId is empty | This variable is only available when Build.Reason is PullRequest. If testing manually, trigger via a real PR, not a manual run. |
Stage condition PullRequest never matches | Azure DevOps uses PullRequest (exact case). Make sure your condition uses eq(variables['Build.Reason'], 'PullRequest') with the correct casing. |
| Variable group not found | Ensure the variable group is authorized for the pipeline. Go to Library > variable group > Pipeline permissions and add your pipeline. |
| Secret variables are empty in scripts | Secret variables must be explicitly mapped to script tasks using env: mapping or $(VariableName) syntax. They're not available as environment variables by default. |
| PR comment not posted | Verify AZURE_DEVOPS_PAT has Code (Read & Write) scope. Check the API URL format — it should include the project name and use api-version=7.0. |
| Environment creation timeout | Increase the --timeout value beyond 900 seconds. Check Bunnyshell build logs for stuck image builds or failing health checks. |
| Output variables not passing between stages | Use the full syntax: $[ stageDependencies.StageName.JobName.outputs['StepName.VariableName'] ]. Ensure the source step uses name: and sets isoutput=true. |
| Service hook not firing (Approach A) | Go to Project Settings > Service hooks. Verify the Bunnyshell hook is active and check the history for delivery failures. Test with a manual trigger. |
| Pipeline runs twice on PR | If you have both a pr: trigger and a Build Validation policy, the pipeline may trigger twice. Remove the pr: trigger from YAML and rely solely on the build validation policy, or vice versa. |
What's Next?
- Add Azure Environments — Use deployment environments for approval gates and deployment history
- Integrate with Azure Boards — Link work items to preview environment deployments for full traceability
- Add smoke tests — Run API health checks or Playwright tests against the preview URL
- Azure Key Vault integration — Store all secrets in Azure Key Vault and link to your variable group
- YAML templates — Extract the Bunnyshell steps into a reusable YAML template for multiple repositories
- Self-hosted agents — If your cluster is behind a firewall, use self-hosted Azure Pipelines agents with network access to Bunnyshell
Related Resources
- Bunnyshell CLI Reference
- Bunnyshell API Documentation
- Azure Pipelines YAML Reference
- Azure DevOps REST API - Pull Request Threads
- Ephemeral Environments — Learn more about the concept
- All Guides — More technical guides
Ship faster starting today.
14-day full-feature trial. No credit card required. Pay-as-you-go from $0.007/min per environment.