Azure DevOps
The pipeline the kit generates for Azure DevOps, stage by stage for each hosting model, and how to set up the variables, the service connection and the first run. For developers.
On this page
npx mdlake hosting init --ci azure-devops writes azure-pipelines.yml in the repository root. It does what the GitHub Actions workflow does, with Azure DevOps tasks: check the site, then deploy it to the resources from Azure and Bicep. This page lists the stages for each hosting model and the setup you do in Azure DevOps.
Generate the pipeline
npx mdlake hosting init --ci azure-devops --name acme-siteUse --ci github,azure-devops to write both. The file begins with a marker line and, as a comment, the list of variables you must define:
# mdlake-hosting: tier=split runtime=dotnet (generated by `npx mdlake hosting init`; keep your edits with `npx mdlake adopt <file>`)
# Variables to define in the pipeline (Pipelines > Edit > Variables), secrets marked secret:
# MDLAKE_LAKE_TOKEN (secret) read-only token for the lake repository
# SITE_URL public address of the site
# PUBLIC_TURNSTILE_SITE_KEY only for forms with Turnstile
# AZURE_STATIC_WEB_APPS_API_TOKEN (secret) the deployment token of the Static Web App
# AZURE_SERVICE_CONNECTION name of the Azure Resource Manager service connection (workload identity federation)
# AZURE_API_APP_NAME the API Web App created by infra/main.bicep (<name>-api)The file is in the generated zone. To edit it, run npx mdlake adopt azure-pipelines.yml first. See Azure and Bicep.
What every pipeline has
| Part | Setting |
|---|---|
| Trigger | Pushes to main |
| Pull requests | pr: none: no pipeline run for pull requests |
| Agent | ubuntu-latest |
| Node | The NodeTool@0 task with 22.x |
The check steps are the same on every model, in this order:
NodeTool@0for Node 22. On a split .NET site,UseDotNet@2with10.0.x.git --version, so the log shows that git is present.npm ci.- Three
Cache@2tasks for.mdlake/lake,node_modules/.astro/assetsandnode_modules/.cache/mdlake-assets. The keys aremdlake0 | "$(Agent.OS)" | lake.lock,mdlake1 | ...andmdlake2 | ..., so a new lake commit starts fresh. See The lake in CI, and caches. npx playwright install --with-deps chromium.npm run check, withMDLAKE_LAKE_TOKEN,SITE_URLandPUBLIC_TURNSTILE_SITE_KEYin itsenv.
Azure DevOps does not pass secret variables to scripts automatically. The pipeline sets them in env, which is why the check step lists them.
The static model
A single job. After the check it deploys with:
- task: AzureStaticWebApp@0
inputs:
app_location: dist
skip_app_build: true
azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)There is no Azure service connection on a static site.
The server model
A single job. After the check it assembles the package (dist, package.json, package-lock.json, and vendor when present, then npm ci --omit=dev --ignore-scripts inside the deploy folder) and deploys it with the AzureWebApp@1 task. The task uses azureSubscription: $(AZURE_SERVICE_CONNECTION), appType: webAppLinux, appName: $(AZURE_WEBAPP_NAME), runtimeStack: 'NODE|22-lts' and startUpCommand: node dist/server/entry.mjs.
The split model
Three stages run in sequence:
| Stage | Depends on | What it does |
|---|---|---|
Check | none | The check steps, then PublishPipelineArtifact@1 publishes dist as the pipeline artifact site. |
Api | Check | Builds the backend and deploys it with AzureWebApp@1 (appType: webAppLinux, appName: $(AZURE_API_APP_NAME)). |
Site | Api | DownloadPipelineArtifact@2 brings back site into dist, then AzureStaticWebApp@0 deploys it. |
For a .NET backend the Api stage runs UseDotNet@2 and dotnet publish api/Api.csproj -c Release -o $(Build.ArtifactStagingDirectory)/api, and the web app task has runtimeStack: 'DOTNETCORE|10.0'.
For a Node backend the stage runs NodeTool@0, npm ci, npm run build --workspace api and copies api/dist to $(Build.ArtifactStagingDirectory)/api. The web app task then has runtimeStack: 'NODE|22-lts' and startUpCommand: node server.mjs.
Set up Azure DevOps
Create the pipeline
- In your Azure DevOps project, open Pipelines.
- Choose New pipeline, select where the code is (Azure Repos Git or GitHub), and select the repository.
- Choose Existing Azure Pipelines YAML file and pick
/azure-pipelines.ymlonmain. - Do not run it yet. First add the variables.
Variables
Open the pipeline, choose Edit, then Variables, and add:
| Name | Secret | Static | Server | Split | Meaning |
|---|---|---|---|---|---|
MDLAKE_LAKE_TOKEN | yes | yes | yes | yes | Read-only token for the lake. See The lake token. |
SITE_URL | no | yes | yes | yes | The public address. |
PUBLIC_TURNSTILE_SITE_KEY | no | when used | when used | when used | The public Turnstile site key. |
AZURE_STATIC_WEB_APPS_API_TOKEN | yes | yes | yes | The Static Web App deployment token. See Get the deployment token. | |
AZURE_SERVICE_CONNECTION | no | yes | yes | The name of the service connection. | |
AZURE_WEBAPP_NAME | no | yes | The Web App name. | ||
AZURE_API_APP_NAME | no | yes | The API Web App, <name>-api. |
Mark the token variables as secret with the lock icon. Secrets can still be read by any pipeline step you add, so give edit rights on the pipeline only to people you trust with them.
The service connection
The server and split models deploy to a Web App with an Azure Resource Manager service connection that uses workload identity federation, so there is no secret to rotate.
- Open Project settings, then Service connections, then New service connection.
- Choose Azure Resource Manager and the option Workload identity federation (automatic app registration is the quickest).
- Scope it to the resource group of the site (
rg-acme-sitein the examples). - Give it a name, for example
acme-site-azure, and enter that name in the variableAZURE_SERVICE_CONNECTION. - In Azure, check that the connection’s identity has the Website Contributor role on the Web App (the API Web App on the split model). The Contributor role on the resource group includes it. If the deploy step is refused with an authorization error, add the role to the connection’s identity on the Web App yourself.
The first run asks you to permit the pipeline to use the connection. Choose Permit.
Approve the first run
- Open the pipeline and choose Run pipeline, branch
main. - Approve the resource authorizations when asked.
- Watch the
Checkstage. The first run builds every image and fetches the lake, so it is the slowest. - When the last stage is green, open the site.
After this, every push to main runs the pipeline.
Environments and approvals
The generated file deploys straight from the stage. To require an approval before production:
- Create an Environment in Azure DevOps (Pipelines, Environments) and add the approval.
- Adopt the file (
npx mdlake adopt azure-pipelines.yml). - Change the deploying job to a
deploymentjob that names the environment.
This is a change in the file you own after adopting it, so keep it small.
Problems
| Symptom | Fix |
|---|---|
| The run waits at “This pipeline needs permission to access a resource” | Open the run and permit the service connection or the variable group. |
npm run check fails with a lake error | The token is missing, expired or too narrow. See The lake in CI, and caches. |
| The variable is empty in the script | A secret variable must be mapped in env, as the generated file does. If you added a script, add the mapping. |
AuthorizationFailed on the Web App task | The connection’s identity needs Website Contributor on that Web App. |
| The service connection cannot be created | You need rights in the Entra tenant to create app registrations, or ask an admin to create the connection. |
| The cache never hits | The key contains lake.lock. It hits only when the file is unchanged and the previous run finished successfully. |
pr: none means no run on pull requests | By design. Add a pr: trigger after adopting the file, but remember that a pull request run must not deploy. |
What’s next
- GitHub Actions for the equivalent workflow.
- The lake in CI, and caches.
- Troubleshooting.
