Skip to content
mdlake

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

Bash
npx mdlake hosting init --ci azure-devops --name acme-site

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

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

PartSetting
TriggerPushes to main
Pull requestspr: none: no pipeline run for pull requests
Agentubuntu-latest
NodeThe NodeTool@0 task with 22.x

The check steps are the same on every model, in this order:

  1. NodeTool@0 for Node 22. On a split .NET site, UseDotNet@2 with 10.0.x.
  2. git --version, so the log shows that git is present.
  3. npm ci.
  4. Three Cache@2 tasks for .mdlake/lake, node_modules/.astro/assets and node_modules/.cache/mdlake-assets. The keys are mdlake0 | "$(Agent.OS)" | lake.lock, mdlake1 | ... and mdlake2 | ..., so a new lake commit starts fresh. See The lake in CI, and caches.
  5. npx playwright install --with-deps chromium.
  6. npm run check, with MDLAKE_LAKE_TOKEN, SITE_URL and PUBLIC_TURNSTILE_SITE_KEY in its env.

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:

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

StageDepends onWhat it does
ChecknoneThe check steps, then PublishPipelineArtifact@1 publishes dist as the pipeline artifact site.
ApiCheckBuilds the backend and deploys it with AzureWebApp@1 (appType: webAppLinux, appName: $(AZURE_API_APP_NAME)).
SiteApiDownloadPipelineArtifact@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

  1. In your Azure DevOps project, open Pipelines.
  2. Choose New pipeline, select where the code is (Azure Repos Git or GitHub), and select the repository.
  3. Choose Existing Azure Pipelines YAML file and pick /azure-pipelines.yml on main.
  4. Do not run it yet. First add the variables.

Variables

Open the pipeline, choose Edit, then Variables, and add:

NameSecretStaticServerSplitMeaning
MDLAKE_LAKE_TOKENyesyesyesyesRead-only token for the lake. See The lake token.
SITE_URLnoyesyesyesThe public address.
PUBLIC_TURNSTILE_SITE_KEYnowhen usedwhen usedwhen usedThe public Turnstile site key.
AZURE_STATIC_WEB_APPS_API_TOKENyesyesyesThe Static Web App deployment token. See Get the deployment token.
AZURE_SERVICE_CONNECTIONnoyesyesThe name of the service connection.
AZURE_WEBAPP_NAMEnoyesThe Web App name.
AZURE_API_APP_NAMEnoyesThe 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.

  1. Open Project settings, then Service connections, then New service connection.
  2. Choose Azure Resource Manager and the option Workload identity federation (automatic app registration is the quickest).
  3. Scope it to the resource group of the site (rg-acme-site in the examples).
  4. Give it a name, for example acme-site-azure, and enter that name in the variable AZURE_SERVICE_CONNECTION.
  5. 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

  1. Open the pipeline and choose Run pipeline, branch main.
  2. Approve the resource authorizations when asked.
  3. Watch the Check stage. The first run builds every image and fetches the lake, so it is the slowest.
  4. 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:

  1. Create an Environment in Azure DevOps (Pipelines, Environments) and add the approval.
  2. Adopt the file (npx mdlake adopt azure-pipelines.yml).
  3. Change the deploying job to a deployment job that names the environment.

This is a change in the file you own after adopting it, so keep it small.

Problems

SymptomFix
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 errorThe token is missing, expired or too narrow. See The lake in CI, and caches.
The variable is empty in the scriptA 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 taskThe connection’s identity needs Website Contributor on that Web App.
The service connection cannot be createdYou need rights in the Entra tenant to create app registrations, or ask an admin to create the connection.
The cache never hitsThe 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 requestsBy design. Add a pr: trigger after adopting the file, but remember that a pull request run must not deploy.

What’s next