Preview Environments with Azure DevOps: Automated Per-PR Deployments with Bunnyshell
GuideMarch 20, 202611 min read

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:

ApproachPipeline changesControl levelBest for
Approach A: Webhook-OnlyNoneBunnyshell manages everythingTeams that want the fastest setup
Approach B: Pipeline IntegrationAdd stages to azure-pipelines.ymlFull control over lifecycleTeams 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

Text
1PR created → Azure DevOps webhook → Bunnyshell API
23                                  Create environment
4                                  Build images
5                                  Deploy to K8s
67                                  Post comment on PR
8                                  with preview URL

Bunnyshell 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

Text
1PR created → Azure Pipeline triggers (pr: trigger)
23          Pipeline stage installs Bunnyshell CLI
4          CLI creates/deploys environment
5          CLI retrieves preview URL
67          Post-deploy stages (migrations, seeds, tests)
89          Pipeline posts preview URL as PR comment

Your 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

  1. Log into Bunnyshell
  2. Navigate to your Project and select your primary environment
  3. Go to Settings > Integrations
  4. 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) and Service Hooks (Read & Write) scopes

Bunnyshell uses the PAT to read repository data and register service hooks.

Step 2: Enable Automatic Preview Environments

  1. In your environment, go to Settings
  2. Find the Ephemeral environments section
  3. Toggle "Create ephemeral environments on pull request" to ON
  4. Toggle "Destroy environment after merge or close pull request" to ON
  5. 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:

  1. In Azure DevOps, go to Project Settings > Service hooks
  2. You should see a Bunnyshell webhook subscribed to:
    • Pull request created
    • Pull request updated
    • Pull request merge attempted

Step 4: Test It

  1. Create a new branch and make a change
  2. Open a pull request in Azure DevOps
  3. Bunnyshell creates an ephemeral environment cloned from your primary, using the PR branch
  4. Within a few minutes, Bunnyshell posts a comment on the PR with the preview URL
  5. 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

  1. In Azure DevOps, go to Pipelines > Library > Variable groups
  2. Create a new variable group called bunnyshell-config
  3. Add these variables:
VariableValueSecret
BUNNYSHELL_TOKENYour Bunnyshell API tokenYes (lock icon)
BUNNYSHELL_ORG_IDYour organization IDNo
BUNNYSHELL_PROJECT_IDYour project IDNo
BUNNYSHELL_CLUSTER_IDTarget K8s cluster IDNo
BUNNYSHELL_ENV_IDPrimary environment ID to clone fromNo
AZURE_DEVOPS_PATPAT for posting PR commentsYes (lock icon)
  1. 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:

  1. Go to Repos > Branches
  2. Click the three dots next to your target branch (e.g., main) > Branch policies
  3. Under Build validation, click Add build policy
  4. 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:

YAML
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:

YAML
1pr:
2  branches:
3    include:
4      - main
5      - master

Azure 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:

YAML
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:

YAML
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:

YAML
1jobs:
2  - deployment: DeployPreviewEnv
3    displayName: 'Deploy Preview'
4    environment: 'preview-environments'
5    strategy:
6      runOnce:
7        deploy:
8          steps:
9            # ... same steps as above

This registers each deployment in Azure DevOps Environments, giving you a history of deployments and the ability to add manual approval gates.


Secrets and Variables

Store all Bunnyshell configuration in a variable group:

  1. Go to Pipelines > Library > Variable groups
  2. Create a group named bunnyshell-config
  3. Add all variables (mark secrets with the lock icon)
  4. Reference in your pipeline:
YAML
variables:
  - group: bunnyshell-config

Pipeline Variables

For simpler setups, use pipeline-level variables:

  1. Edit your pipeline in Azure DevOps
  2. Click Variables > New variable
  3. Add each variable and check Keep this value secret for tokens

Key Vault Integration

For enterprise teams, link variables to Azure Key Vault:

  1. Create a Key Vault in Azure
  2. Add BUNNYSHELL-TOKEN and AZURE-DEVOPS-PAT as secrets
  3. In the variable group, click Link secrets from an Azure Key Vault
  4. 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:

  1. Go to Project Settings > Service connections
  2. Create a Generic service connection
  3. Set the server URL to https://api.bunnyshell.com and add your token
  4. Reference it in pipelines using the connectedServiceName parameter

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:

Bash
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 (:PAT base64-encoded)
  • api-version=7.0 is 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:

YAML
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

YAML
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):

YAML
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

IssueSolution
Pipeline doesn't trigger on PRCheck 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 emptyThis 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 matchesAzure DevOps uses PullRequest (exact case). Make sure your condition uses eq(variables['Build.Reason'], 'PullRequest') with the correct casing.
Variable group not foundEnsure the variable group is authorized for the pipeline. Go to Library > variable group > Pipeline permissions and add your pipeline.
Secret variables are empty in scriptsSecret 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 postedVerify 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 timeoutIncrease the --timeout value beyond 900 seconds. Check Bunnyshell build logs for stuck image builds or failing health checks.
Output variables not passing between stagesUse 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 PRIf 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

Ship faster starting today.

14-day full-feature trial. No credit card required. Pay-as-you-go from $0.007/min per environment.