Azure DevOps Pipelines¶
This guide explains the Azure DevOps starter pipeline generated by haute init. It runs validation and deployment commands. Its production deployment runs under an Azure DevOps environment, whose approval checks decide who may release (Who can start production). Databricks is the end-to-end service-update adapter; generic container builds/pushes only, and Azure Container Apps, AWS ECS, and GCP Cloud Run build and push the image and leave the service update to you.
New to Git, pull requests, or CI/CD?
This page assumes you understand basic Git concepts like branches, commits, and pull requests. If these are unfamiliar, read Before You Start first - it explains everything in plain English.
Not sure if this is the right page?
If you access your project on dev.azure.com, this is the right page. If it's on github.com, go to GitHub Actions. If it's on gitlab.com (or a company GitLab server), go to GitLab CI/CD. If you're not sure, ask your IT team or tech lead: "Where is our code repository hosted?" They'll tell you GitHub, GitLab, or Azure DevOps.
What is Azure DevOps?
Azure DevOps is Microsoft's platform for managing code, builds, and releases. Pipelines is the CI/CD feature - it runs scripts automatically when you push code or open a pull request. If your organisation uses Azure DevOps for source control (instead of GitHub or GitLab), this is your CI/CD provider.
Who does what on this page?
This page has two parts. As a pricing analyst, you only need to do Step 4 (test it). Steps 1-3 (creating the variable group, environment, and pipeline) are usually done by your IT team or tech lead - they have the credentials and know the Azure DevOps UI. If you're setting this up yourself, the steps walk you through it, but it's perfectly fine to hand it to IT and say: "Can you set up CI/CD for our Haute project?", with the credential names haute init printed (Step 1 lists them by target).
What Haute generates¶
When you run haute init --ci azure-devops, Haute creates an azure-pipelines.yml file in the root of your repository. It defines five stages with ordering dependencies (three for Azure Container Apps, AWS ECS and GCP Cloud Run, where DeployProduction follows DeployStaging directly); whether a stage can create or update a live endpoint depends on the selected target:
| Stage | When it runs | What it does |
|---|---|---|
Validate |
On PRs and pushes to main | Lints, type-checks, tests, validates pipeline, dry-run deploy |
DeployStaging |
On push to main only | Runs the target-dependent staging deploy command |
SmokeTest |
After a successful staging deploy | Scores test quotes against an existing live staging endpoint |
ImpactAnalysis |
After smoke succeeds | Compares existing staging and production endpoints |
DeployProduction |
After ImpactAnalysis (after DeployStaging for the platform-container targets) |
Runs production deployment. It runs automatically unless the production environment has an approval check (Step 2) |
This gives you the same command sequence as GitHub Actions and GitLab; a live release still depends on the selected target and hosting process:
Pull request → Validation → Merge → Staging → Smoke test → Impact analysis → [Approval] → Production
Workbench projects¶
A workbench project hosts no API: what it releases is
the underwriter app. Its
azure-pipelines.yml has three stages instead: Validate, which lints, type-checks and
runs tests, then builds the app's release, which checks your pipeline and schema and scores
your test quotes, on every pull request and on main; Publish, on main only, a deployment
to the staging environment that builds the app's image once, pushes it to the registry
[deploy.container] names, rolls the staging app out to it and compares staging with
production on saved quotes, publishing impact_report.md as the impact-report
artifact; and DeployProduction, a
deployment to the production environment that you run by hand, which rolls the production
app out to the release staging runs. Each rollout checks the app and puts the previous
release back when the new one fails
(The underwriter app). Add an
Exclusive lock check to both environments so one rollout of each runs at a time. The
stages read your host's variables from the haute-credentials group,
AZURE_SUBSCRIPTION_ID, AZURE_TENANT_ID, AZURE_CLIENT_ID and AZURE_CLIENT_SECRET.
How it works, step by step¶
1. You make a change and open a pull request¶
You edit your pipeline, update a model, or change a configuration. You push your changes to a branch and open a pull request (PR) in Azure DevOps - which is a way of asking your teammate to review your changes before they go live.
2. Validation runs automatically¶
The Validate stage runs immediately. It has four parallel jobs:
- Lint & Format - checks your code style with
ruff - Type Check - runs
mypyto catch type errors - Test - runs
pytest - Pipeline Validation - runs
haute lintandhaute deploy --dry-run
If any of these fail, the PR gets a red ❌ and you can see what went wrong in the pipeline logs.
3. You merge to main¶
Once validation passes and your teammate has reviewed the PR, you merge it - which applies your changes to the main version of the project. This triggers the remaining stages.
4. Staging deployment (automatic)¶
The DeployStaging stage runs haute deploy --endpoint-suffix "-staging". On Databricks it waits until the staging endpoint serves the new version, and fails with Databricks' build log if the model cannot be served; the generic container target does not host an endpoint; platform-container targets push the image and leave the service update to you, so their pipeline has no smoke or impact stage and DeployProduction follows DeployStaging directly.
5. Smoke test (automatic)¶
If deploy succeeds, the SmokeTest stage scores configured test quotes against an existing live staging endpoint. On Databricks the endpoint is already serving when it starts, since the deploy waited for it. On the generic container target it calls the staging address in haute.toml.
6. Impact analysis (automatic)¶
If smoke succeeds, the ImpactAnalysis stage compares existing staging and production endpoints on the portfolio sample in the repository, or on your test quotes when no sample is named. The report is uploaded as a pipeline artifact only when this command runs.
7. You review and approve¶
The DeployProduction stage targets an Environment called production. Azure DevOps environments can enforce approval checks when you configure them; Haute's min_approvers configuration does not create or enforce those checks. Without an approval check on that environment, production deploys straight after the impact analysis, on every merge to main.
The impact report shows exactly what changed, for each output column:
Output: technical_price
────────────────────────────────────────────────────────────
Staging mean: 548.57 Production mean: 536.12
Rows changed: 8,420 / 10,000 (84.2%)
Mean change: +2.3%
Median change: +1.9%
Premium impact: +2.3%
Max increase: +18.7%
Max decrease: -4.2%
Distribution: P5=-0.8% P25=+0.6% P50=+1.9% P75=+3.4% P95=+7.1%
Segment tables follow, one for each text column of the portfolio sample with 2 to 50 values, listing the segments whose prices move most. The report is advisory: it never fails the stage.
8. Production deploy¶
Once approved, the pipeline runs haute deploy and then tags the release in Git. Databricks can update its serving endpoint; the generic container target only packages an image, and the Azure, AWS, and GCP platform-container targets build and push the image for a manual service update.
Setup¶
Step 1: Create a Variable Group for credentials¶
This step is usually done by IT
If you're not comfortable navigating Azure DevOps, ask your IT team or tech lead to do Steps 1-3. Give them the credential names haute init printed; the tables below list them by target.
Azure DevOps stores shared secrets in Variable Groups. Haute's generated pipeline expects a group called haute-credentials.
- Go to your project page in Azure DevOps (e.g.
dev.azure.com/yourcompany/motor-pricing) - In the left sidebar, click Pipelines → Library (the 📚 icon)
- Click + Variable group
- Name it
haute-credentials - Add each credential as a variable:
For Databricks:
| Name | Value | Secret? |
|---|---|---|
DATABRICKS_MLFLOW_HOST |
The workspace URL for MLflow and the model registry | ✓ Lock |
DATABRICKS_MLFLOW_TOKEN |
A personal access token scoped for MLflow and the model registry | ✓ Lock |
DATABRICKS_RATING_HOST |
Your workspace URL | ✓ Lock |
DATABRICKS_RATING_TOKEN |
Your personal access token scoped for Model Serving | ✓ Lock |
For Docker/Container targets:
| Name | Value | Secret? |
|---|---|---|
DOCKER_USERNAME |
Your registry username | ✓ Lock |
DOCKER_PASSWORD |
Your registry password | ✓ Lock |
For AWS ECS:
| Name | Value | Secret? |
|---|---|---|
AWS_ACCESS_KEY_ID |
Your AWS access key | ✓ Lock |
AWS_SECRET_ACCESS_KEY |
Your AWS secret key | ✓ Lock |
AWS_DEFAULT_REGION |
e.g. eu-west-1 |
- |
For Azure Container Apps:
| Name | Value | Secret? |
|---|---|---|
AZURE_SUBSCRIPTION_ID |
Your subscription ID | ✓ Lock |
AZURE_TENANT_ID |
Your tenant ID | ✓ Lock |
AZURE_CLIENT_ID |
Your service principal app ID | ✓ Lock |
AZURE_CLIENT_SECRET |
Your service principal secret | ✓ Lock |
For GCP Cloud Run:
| Name | Value | Secret? |
|---|---|---|
GCP_PROJECT_ID |
The Google Cloud project holding the registry and the service | - |
GCP_SERVICE_ACCOUNT_KEY |
The JSON key of the service account CI deploys as, on one line | ✓ Lock |
The deploy jobs log in to Artifact Registry with the service account's key.
Click Save when done.
Lock secrets
Click the 🔒 lock icon next to each secret value. This encrypts it and prevents it from appearing in pipeline logs.
Step 2: Create the production Environment¶
The production deploy stage runs under an Azure DevOps Environment: production, unless [ci.production] environment in haute.toml names another (run haute ci update after changing it). Add approval checks in Azure DevOps if you need them; this is not configured or enforced by Haute. Without one, every merge to main that passes the earlier stages deploys production.
- Go to Pipelines → Environments
- Click New environment
- Name it
production - Resource: None
- Click Create
Now add an approval check:
- Click the
productionenvironment you just created - Click the ⋮ menu (top right) → Approvals and checks
- Click + Add check → Approvals
- Add the people who should approve production deploys
- Set Minimum number of approvers (e.g. 1)
- Click Create
When the pipeline reaches the production stage, it will pause and send a notification to the approvers. They review the impact report, then approve or reject.
Step 3: Create the Pipeline¶
First commit azure-pipelines.yml with pyproject.toml and uv.lock: every stage installs with uv sync --frozen, which needs the lock file (uv add creates it; run uv sync once if it is missing).
- Go to Pipelines → Pipelines
- Click New pipeline
- Select your repository source (Azure Repos Git, GitHub, etc.)
- Select your repository
- Choose Existing Azure Pipelines YAML file
- Select
/azure-pipelines.ymlfrom the branch dropdown - Click Run (or Save if you want to run it later)
Two more settings, if your code is in Azure Repos Git:
- Pull request checks. Azure Repos ignores the file's
pr:trigger, so theValidatestage runs on pull requests only once you add a Build validation branch policy onmainthat runs this pipeline (Repos → Branches →main→ Branch policies). - Release tags. The production stage pushes a Git tag, so allow the project's build service Contribute and Create tag on the repository, or the tag push fails.
Step 4: Test it¶
- Create a branch, make a small change, and open a pull request
- The validation stage should run automatically on the PR
- If it passes, merge the PR
- Watch the target-dependent pipeline; smoke and impact need a ready staging endpoint and do not run after a failed deploy
- If the impact job ran, check its report artifact before approving production
The generated pipeline in detail¶
You don't need to read or understand the YAML below
The pipeline file was generated by haute init and works out of the box. The sections below are reference only - for troubleshooting or if your IT team wants to understand what’s happening.
azure-pipelines.yml structure
trigger:
branches:
include: [main]
paths:
include:
- rating/
- tests/quotes/
- haute.toml
- pyproject.toml
- uv.lock
pr:
branches:
include: [main]
stages:
- stage: Validate # 4 parallel jobs
- stage: DeployStaging # depends on Validate, main branch only
- stage: SmokeTest # depends on DeployStaging
- stage: ImpactAnalysis # depends on SmokeTest
- stage: DeployProduction # depends on ImpactAnalysis, uses 'production' environment
Key features
- Path filters - the pipeline only triggers when relevant files change (the
rating/pipeline package, config, data, models, test quotes, and the dependency filespyproject.tomlanduv.lock; a workbench's files are in the pipeline package, so a change to them deploys too), not on README changes - PR validation - the
Validatestage runs on pull requests too (through a branch policy on Azure Repos), giving you feedback before merge - Variable group - credentials come from the
haute-credentialsvariable group, not hardcoded - Environment approvals - configure approval checks on the
productionenvironment if you need manual approval before production deploy - Deployment job - the production stage uses a
deploymentjob type, which gives you deployment history and tracking in Azure DevOps - Artifacts - the impact report is published as a pipeline artifact you can download from the build summary
- Git tagging - after production deploy, the pipeline tags the release:
deploy/v<version>(the registered model version) for Databricks,deploy/<commit>for container targets, whose image tag carries the same commit. On Databricks the tag step reads the version withhaute status, with the same variables as the deploy step, and fails rather than tagging a version it could not read. The production checkout keeps its credentials (persistCredentials: true) so the tag can be pushed; the pipeline's build service needs permission to create tags in the repository
Stage details
| Stage | Jobs | Timeout |
|---|---|---|
| Validate | Lint, Type Check, Test, Pipeline Validation (parallel) | 10 min each |
| DeployStaging | Deploy to staging | 60 min on Databricks, which waits for its endpoint; 15 min on the container targets |
| SmokeTest | Score test quotes against staging | 10 min |
| ImpactAnalysis | Compare staging vs production | 10 min |
| DeployProduction | Deploy to production + tag | 60 min on Databricks; 15 min on the container targets |
Azure DevOps vs GitHub/GitLab - key differences¶
| Concept | GitHub | GitLab | Azure DevOps |
|---|---|---|---|
| Config file | .github/workflows/*.yml |
.gitlab-ci.yml |
azure-pipelines.yml |
| Credentials | Repository Secrets | CI/CD Variables | Variable Groups |
| Manual approval | Separate workflow + button | when: manual + ▶ button |
Environment approval checks |
| Artifacts | Upload/download actions | artifacts: keyword |
publish: keyword |
| PR checks | Workflow on pull_request |
Job with merge_request_event rule |
pr: trigger, or a build validation branch policy on Azure Repos |
The biggest difference is how manual approval works:
- GitHub: you manually run a separate workflow
- GitLab: you click a play button on a manual job
- Azure DevOps: once the
productionenvironment has an approval check, the pipeline pauses automatically and sends a notification to approvers - they approve directly from their email or the Azure DevOps UI
This makes Azure DevOps the most "enterprise-friendly" option for approval workflows.
Working solo?¶
If you're the only person on the team:
min_approversinhaute.tomlis metadata, not an enforced Azure DevOps approval setting- In the
productionenvironment, either remove the approval check (then every merge to main deploys production) or set yourself as the sole approver - The generated stages still attempt validation → staging → smoke test → impact analysis; later stages do not run after a failed deployment
- Approve your own production deploys after reviewing the impact report
Troubleshooting¶
Pipeline doesn't trigger¶
- On PR: Make sure the PR targets the
mainbranch - On merge: Check the trigger paths - if you only changed a markdown file, the pipeline won't trigger (by design)
- Check Pipelines: Go to Pipelines → Pipelines and look for recent runs
"Variable not found" or "Authentication failed"¶
Your variable group is missing or misconfigured:
- Go to Pipelines → Library
- Check the variable group is named exactly
haute-credentials - Check variable names are correct (case-sensitive)
- Make sure secrets are saved (not just typed - click Save)
Pipeline authorisation error on variable group¶
The first time a pipeline uses a variable group, Azure DevOps may ask you to authorise it:
- Go to the failed pipeline run
- Click View on the authorisation prompt
- Click Permit
This only happens once per variable group per pipeline.
"Environment not found"¶
Make sure you created the environment the production stage runs under in Pipelines → Environments: production, or the name [ci.production] environment gives. The name must match exactly.
Impact report not appearing¶
The impact report is published as a pipeline artifact. To find it:
- Go to the pipeline run → click the Summary tab
- Under Published artifacts, look for
impact-report - Click to download
impact_report.md
Approval notification not received¶
Check that:
- The production environment has an approval check configured
- Your email notifications are enabled in Azure DevOps
- You're listed as an approver on the environment
Keeping the pipeline file up to date¶
After upgrading Haute, or changing [ci.staging] endpoint_suffix or [ci.production] environment in haute.toml, run haute ci update and commit azure-pipelines.yml. It overwrites changes you made to the file by hand: see Keeping the generated files up to date for what it rewrites and removes.