Skip to content
| Marketplace
Sign in
Azure DevOps>Azure Pipelines>Locally
Locally

Locally

Locally Build

|
1 install
| (0) | Free
Run Locally in Azure Pipelines: install the CLI and plugins, start `locally ci`, save and load snapshots, and tear it down at the end of the job.
Get it free
ooooo                                      oooo  oooo              
`888'                                      `888  `888              
 888          .ooooo.   .ooooo.   .oooo.    888   888  oooo    ooo 
 888         d88' `88b d88' `\"Y8 `P  )88b   888   888   `88.  .8'
 888         888   888 888        .oP\"888   888   888    `88..8'
 888       o 888   888 888   .o8 d8(  888   888   888     `888'
o888ooooood8 `Y8bod8P' `Y8bod8P' `Y888\"\"8o o888o o888o     .8'
                                                       .o..P'
                                                       `Y8P'

azure-devops-extension

This repository contains the Locally extension for Azure DevOps, which adds several Azure Pipelines tasks for Locally Build:

  • SetupLocally@1 - installs and configures Locally.
  • InstallLocallyPlugins@1 - allows for installing Locally Plugins.
  • TeardownLocally@1 - provides a means of stopping Locally once it's been launched in CI mode.
  • SaveLocallySnapshot@1 - takes a Locally Snapshot of everything in Locally, and optionally uploads it as a pipeline artifact and/or shares it with your Locally Team.
  • LoadLocallySnapshot@1 - replaces everything in Locally with a snapshot.

These are the Azure Pipelines equivalent of the locallybuild/setup-locally GitHub Actions.

Azure DevOps Services (dev.azure.com) is supported, on Microsoft-hosted and self-hosted Linux, macOS and Windows agents (x64 and arm64). Azure DevOps Server can't issue OIDC tokens, so it can only be used with a CI Key.

Installing the extension

Install the Locally extension from the Visual Studio Marketplace into your Azure DevOps Organisation. Once it's installed, every pipeline in that Organisation can use the tasks above.

OIDC Configuration for a Team/User (obtaining teamUUID or userUUID)

Locally launches via OIDC, which is configured either against your User or against a Team (Locally Account -> Teams -> [Name] -> Team CI (OIDC).

In Azure Pipelines the OIDC token is issued for a service connection, so there are two halves to this: a service connection in Azure DevOps, and a trust for it in Locally.

1. Create (or pick) a service connection in Azure DevOps

Locally needs an Azure Resource Manager service connection that uses Workload Identity Federation. Create one just for Locally, whose identity has no role assignments in Azure (see the warning below), rather than reusing one you deploy with:

  1. In your Azure DevOps project, open Project settings -> Service connections.
  2. Click New service connection and select Azure Resource Manager.
  3. Choose Workload identity federation as the credential, and complete the form.
  4. Give the pipeline(s) that will run Locally permission to use the service connection.

Locally never uses this service connection to talk to Azure: Azure DevOps only uses it to issue the token.

[!WARNING] The token Azure DevOps issues for a service connection is the same one Microsoft Entra accepts to sign in as that service connection's identity. Azure DevOps can't issue one for a different audience, so until it expires, anything that holds the token (the locally CLI, and the Locally service it signs in to) could exchange it for access to whatever that identity can reach in Azure.

So use a dedicated service connection whose app registration or managed identity has no role assignments: the token is then only good for proving to Locally which service connection the job used. If the form asks you to grant a role on a subscription or resource group, remove that role assignment afterwards. Don't reuse a service connection that can deploy to your Azure subscriptions.

Once it's created, open the service connection and copy its Issuer and Subject identifier. You'll need both in the next step.

2. Add a trust for it in Locally

If you've not already configured OIDC within Locally, on either the Team CI (OIDC) or User CI (OIDC) page linked above, you can:

  1. Click Add Trust in the top-right of the page.
  2. Select Azure DevOps.
  3. Under Issuer, paste the service connection's Issuer (for example https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000/v2.0).
  4. Under Subject identifier, paste the service connection's Subject identifier (for example /eid1/c/pub/t/.../sc/.../...).

At the end of this flow, you'll see the Team/User UUID to use with the tasks within this repository.

The trust matches the subject exactly, and each service connection has its own, so a trust allows exactly one service connection. To allow another, add another trust.

For reference, these are the claims in the token that Azure DevOps issues:

Claim New service connections (Entra issuer) Older service connections
Issuer (iss) https://login.microsoftonline.com/<tenant ID>/v2.0 https://vstoken.dev.azure.com/<organisation ID>
Subject (sub) /eid1/c/pub/t/<...>/sc/<organisation ID>/<service connection ID> sc://<organisation>/<project>/<service connection name>
Audience (aud) fb60f99c-7a34-4190-8149-302f77469936 api://AzureADTokenExchange

If the trust doesn't match, locally ci fails and prints the issuer and subject it was given, so you can compare them with the trust.

Example: Using SetupLocally@1

To use Locally against your Team, you can use the following Azure Pipelines configuration:

steps:
  - task: SetupLocally@1
    inputs:
      serviceConnection: my-service-connection # see above
      teamUUID: 'abc123'
      # plugins: recommended  # default; or `none`, `all`, `custom`

To use Locally in CI against your User, you can use the following Azure Pipelines configuration:

steps:
  - task: SetupLocally@1
    inputs:
      serviceConnection: my-service-connection # see above
      userUUID: 'bcd234' # see above
      # plugins: recommended  # default; or `none`, `all`, `custom`

By default, both of these will install the Recommended set of Plugins, but you can specify a custom list to install for example:

steps:
  - task: SetupLocally@1
    inputs:
      serviceConnection: my-service-connection
      teamUUID: $(LOCALLY_TEAM_UUID)
      plugins: custom
      pluginsList: |
        Microsoft.ServiceBus
        Microsoft.Storage

pluginsList accepts one plugin per line (as above) or a comma-separated list (Microsoft.ServiceBus,Microsoft.Storage). The newline form is easier to read and gives cleaner diffs as the list grows.

The full list of arguments:

Name Required Default Description
authentication no serviceConnection How locally ci signs in: serviceConnection (OIDC) or ciKey
serviceConnection when authentication=serviceConnection '' An Azure Resource Manager service connection that uses Workload Identity Federation
ciKey when authentication=ciKey '' A CI Key, passed in from a secret variable
teamUUID one of¹ '' Locally team UUID (from account.locally.build)
userUUID one of¹ '' Locally user UUID (from account.locally.build)
version no latest CLI version (latest or e.g. v2026.11)
plugins no recommended One of none, recommended, all, custom
pluginsList when plugins=custom '' Comma- or newline-separated plugin list
start no true Generate TLS, install plugins, start locally ci. Set to false to only install the CLI
configureDNS no true Route *.locally to Locally's DNS once started, and check it resolves. Needs passwordless sudo on Linux and macOS agents, which Microsoft-hosted agents have
acceptSqlServerLicence no false Accept the Microsoft SQL Server licence by setting LOCALLY_ACCEPT_SQL_SERVER_LICENCE - must be explicitly set to true
containerRuntime no auto Container runtime for locally ci (auto, docker or podman), set as LOCALLY_CONTAINER_RUNTIME. auto uses docker on Microsoft-hosted Linux agents and Locally's own detection elsewhere. Needs Locally 2026.10.01 or later
expectedChecksum no '' SHA-256 to verify CLI tarball against

¹ With a service connection, exactly one of teamUUID or userUUID must be set, depending on whether the OIDC trust is configured at team or user scope. Setting both fails; setting neither fails. With a CI Key neither is needed, since the key says which team or user it belongs to.

See tasks/SetupLocallyV1/task.json for the full input reference.

Example: Using SetupLocally@1 with a CI Key

Where a service connection isn't an option (Azure DevOps Server, for example), you can create a CI Key in your Locally Account, store it as a secret variable in the pipeline, and pass it in:

steps:
  - task: SetupLocally@1
    inputs:
      authentication: ciKey
      ciKey: $(LOCALLY_CI_KEY)

A CI Key is a long-lived secret that you have to store and rotate, so we'd recommend using OIDC where possible.

Using Locally in later steps

Once SetupLocally@1 has run, locally is on the PATH and later steps in the job find the running locally ci without any further configuration. Use locally run to run your tools against it, which hands them Locally's CA, credentials and endpoints:

steps:
  - task: SetupLocally@1
    inputs:
      serviceConnection: my-service-connection
      teamUUID: $(LOCALLY_TEAM_UUID)

  - script: locally run terraform apply -auto-approve
    displayName: Terraform Apply against Locally

  - task: TeardownLocally@1
    condition: always()

Anything run outside of locally run (a bare curl, say) needs to trust Locally's CA, which is at $(LOCALLY_TLS_CA_FILE_PATH).

SetupLocally@1 sets the following variables for later steps:

Variable Description
LOCALLY_CLI_PATH The absolute path to the Locally CLI
LOCALLY_WORKING_DIRECTORY Locally's working directory, which contains ci.log
LOCALLY_PLUGIN_DIRECTORY Where plugins are installed
LOCALLY_TLS_CA_FILE_PATH The CA certificate that Locally's endpoints are signed by
LOCALLY_TLS_CERT_FILE_PATH / LOCALLY_TLS_KEY_FILE_PATH The certificate and key Locally serves
LOCALLY_TEAM_UUID / LOCALLY_USER_UUID Whichever of teamUUID or userUUID was set
LOCALLY_ACCEPT_SQL_SERVER_LICENCE true, only when acceptSqlServerLicence was set
LOCALLY_CONTAINER_RUNTIME The container runtime, when one was chosen

The OIDC token and the CI Key are never set as variables: they're only passed to the locally processes that sign in.

Caching plugins

The GitHub Action caches the plugin directory itself. An Azure Pipelines task can't do that, so to avoid downloading plugins on every run, add the Cache@2 task before SetupLocally@1:

steps:
  - task: Cache@2
    inputs:
      key: 'locally-plugins | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | v2026.11 | recommended'
      path: $(Agent.TempDirectory)/locally-plugins

  - task: SetupLocally@1
    inputs:
      serviceConnection: my-service-connection
      teamUUID: $(LOCALLY_TEAM_UUID)
      version: v2026.11

Include the Locally version and the plugin set in the key, so that changing either starts a fresh cache.

Example: Using InstallLocallyPlugins@1

Use this task to install Locally plugins later in a job, for example after running SetupLocally@1 with plugins: none. It requires SetupLocally@1 to have run earlier in the job, and reuses its sign-in.

steps:
  - task: InstallLocallyPlugins@1
    inputs:
      plugins: custom
      pluginsList: |
        Microsoft.ServiceBus
        Microsoft.Storage

As above, pluginsList takes one plugin per line or a comma-separated list.

The full list of arguments:

Name Required Default Description
plugins no none One of none, recommended, all, custom
pluginsList when plugins=custom '' Comma- or newline-separated plugin list
acceptSqlServerLicence no false Accept the Microsoft SQL Server licence by setting LOCALLY_ACCEPT_SQL_SERVER_LICENCE - must be explicitly set to true (otherwise the variable set by SetupLocally@1 is inherited)
authentication no inherit inherit reuses the sign-in from SetupLocally@1. Set serviceConnection or ciKey only when SetupLocally@1 ran with start: false, and so never signed in
serviceConnection when authentication=serviceConnection '' An Azure Resource Manager service connection that uses Workload Identity Federation
ciKey when authentication=ciKey '' A CI Key, passed in from a secret variable
teamUUID no¹ '' Locally team UUID (falls back to the LOCALLY_TEAM_UUID variable set by SetupLocally@1)
userUUID no¹ '' Locally user UUID (falls back to the LOCALLY_USER_UUID variable set by SetupLocally@1)

¹ With authentication: serviceConnection, exactly one of teamUUID or userUUID must be resolvable (via input or inherited variable) for any value of plugins other than none. Setting both fails.

See tasks/InstallLocallyPluginsV1/task.json for the full input reference.

Example: Using TeardownLocally@1

Use this task as the final step of any job that called SetupLocally@1. It stops locally ci, removes the *.locally DNS configuration added by configureDNS, and, on job failure, uploads ci.log as a pipeline artifact for debugging.

On self-hosted agents this matters: without it, the agent is left routing *.locally to Locally's DNS port after Locally has stopped.

steps:
  - task: TeardownLocally@1
    condition: always()
    # inputs:
    #   uploadLogsOnFailure: true  # default

The full list of arguments:

Name Required Default Description
uploadLogsOnFailure no true Whether to upload ci.log as a pipeline artifact when the job has failed

The artifact is named locally-ci-log-<job name>-<job attempt>, since artifact names have to be unique within a run.

See tasks/TeardownLocallyV1/task.json for the full input reference.

Example: Using SaveLocallySnapshot@1

Use this task to take a snapshot of all of the resources and data in Locally, to load again later or to hand to someone else. It requires SetupLocally@1 to have run earlier in the job, and reuses its sign-in. Snapshots need the Standard plan or above, and sharing them with a team needs the Team plan or above.

[!WARNING] Snapshots contain the credentials of your resources (Key Vault values, keys, passwords). Anyone who can download a pipeline artifact can read an unencrypted snapshot, so set passphrase (from a secret variable) before uploading or sharing one.

steps:
  - task: SaveLocallySnapshot@1
    inputs:
      name: deployed
      upload: artifact
      passphrase: $(LOCALLY_SNAPSHOT_PASSPHRASE)

upload takes a comma-separated list or one entry per line, so upload: artifact, team does both.

The full list of arguments:

Name Required Default Description
name yes The name to save the snapshot under: letters, digits, ., _ and -, starting with a letter or digit (at most 100 characters)
description no '' What this snapshot holds and why it was taken (one line, at most 500 characters)
passphrase no '' Encrypt the snapshot with this passphrase, passed in from a secret variable
upload no none Where to upload the snapshot once it's saved: any of none (keep it on the agent only), artifact (a pipeline artifact) and team (share it with your Locally Team)
artifactName no locally-snapshot-<name> The artifact's name, when upload includes artifact
team no '' The Locally Team's name or ID, when upload includes team (only needed when you're in more than one team)
timeout no '' Give up if running operations haven't finished within this time (a Go duration, e.g. 5m). Defaults to waiting until they finish

Artifact names have to be unique within a run, so when more than one job uploads a snapshot, give each a different name (or artifactName).

Give the step a name to read its output variables from later steps, for example $(save.snapshotPath):

Name Description
snapshotName The name the snapshot was saved under
snapshotPath The path of the snapshot file on the agent
artifactName The name of the artifact, when one was uploaded

See tasks/SaveLocallySnapshotV1/task.json for the full input reference.

Example: Using LoadLocallySnapshot@1

Use this task to replace everything in Locally with a snapshot - to start a job from an environment that's already been deployed, say, or to put Locally back as it was between two sets of tests. It requires SetupLocally@1 to have run earlier in the job, and reuses its sign-in.

Everything in Locally is replaced, and only if the whole snapshot can be loaded: otherwise it's left as it was. Plugins that the snapshot needs and that aren't installed are installed first.

To put Locally back as it was earlier in the job:

steps:
  - task: SaveLocallySnapshot@1
    inputs:
      name: before

  - script: locally run terraform apply -auto-approve

  - task: LoadLocallySnapshot@1
    inputs:
      snapshot: before

To load a snapshot that another pipeline (or an earlier run) uploaded as an artifact, download it with DownloadPipelineArtifact@2 and give snapshot the folder it was downloaded to. The task loads the one .locallysnap file in that folder:

steps:
  - task: DownloadPipelineArtifact@2
    inputs:
      buildType: specific
      project: $(System.TeamProject)
      definition: <pipeline ID>
      buildVersionToDownload: latestFromBranch
      branchName: refs/heads/main
      artifactName: locally-snapshot-deployed
      targetPath: $(Pipeline.Workspace)/locally-snapshot

  - task: LoadLocallySnapshot@1
    inputs:
      snapshot: $(Pipeline.Workspace)/locally-snapshot
      passphrase: $(LOCALLY_SNAPSHOT_PASSPHRASE)

To load one shared with your Locally Team, which needs the Team plan or above:

steps:
  - task: LoadLocallySnapshot@1
    inputs:
      snapshot: shared-baseline
      source: team
      passphrase: $(LOCALLY_SNAPSHOT_PASSPHRASE)

A team's snapshot is downloaded under a name of its own, loaded, then removed from the agent, so it can't replace a snapshot that's already there.

The full list of arguments:

Name Required Default Description
snapshot yes With source: local, a snapshot's name, the path of a .locallysnap file, or the path of a folder holding exactly one. With source: team, the name of a snapshot shared with your Locally Team
source no local Where the snapshot comes from: local (on the agent) or team
team no '' The Locally Team's name or ID, with source: team (only needed when you're in more than one team)
passphrase when the snapshot is encrypted '' The snapshot's passphrase, passed in from a secret variable
timeout no '' Give up if running operations haven't finished within this time (a Go duration, e.g. 5m). Defaults to waiting until they finish

See tasks/LoadLocallySnapshotV1/task.json for the full input reference.

Development

npm install
npm test               # unit tests
npm run build          # type-check, and bundle each task into tasks/<task>/index.js
npm run package        # out/LocallyBuild.locally-<version>.vsix

Each task is a small entry point (src/setup.ts, src/install-plugins.ts, src/teardown.ts, src/snapshot-save.ts, src/snapshot-load.ts) around a flow (src/*-flow.ts) that's written against an interface rather than the agent, so that the whole flow can be unit tested.

Trying the tasks without publishing the extension

Azure Pipelines only runs tasks that an Organisation has installed, normally from a Marketplace extension. To try a change without publishing, npm run upload installs the tasks straight into one Organisation: it builds them, then uploads each task's folder (its task.json, the bundled index.js and the azure-pipelines-task-lib it needs at runtime) with tfx build tasks upload. Every pipeline in that Organisation can then use them.

It needs a personal access token (User settings -> Personal access tokens) with the Agent Pools (Read & manage) scope:

npm ci
LOCALLY_ADO_ORG_URL=https://dev.azure.com/<organisation> LOCALLY_ADO_PAT=<token> npm run upload

Run it again after changing a task, having bumped Patch in that task's task.json: an Organisation won't take a second upload of the same version, so a task whose version is already there is skipped.

This repository's azure-pipelines.yml tries all five tasks with one job per platform: by default linux_amd64, windows_amd64 and darwin_amd64 (Azure Pipelines' Microsoft-hosted macOS agents are Intel), which you can change when queueing a run. By default it signs in with a CI Key, which needs one secret variable, LOCALLY_CI_KEY. To try OIDC instead, choose serviceConnection when queueing a run and name a Workload Identity Federation service connection; that needs a trust for the service connection in your Locally Account, and the variable LOCALLY_TEAM_UUID. The snapshot tasks need the Locally Account to be on the Standard plan or above.

To remove the tasks again, run npx tfx build tasks delete --task-id <id> --service-url https://dev.azure.com/<organisation> --token <token> for each. The IDs are in each task.json.

Publishing the extension

The publish workflow packages the extension and publishes it to the Visual Studio Marketplace, as the version in vss-extension.json. To release:

  1. Bump version in vss-extension.json, and Patch (or Minor) in the task.json of each task that's changed, and update CHANGELOG.md.
  2. Once that's on main, push a tag for the version, for example v1.0.1 for 1.0.1. The workflow fails if the tag and the version don't match.

There's no token to store or rotate: it uses the Marketplace's Trusted Publishing, which exchanges GitHub's OIDC token for a Marketplace credential that lasts 15 minutes. That needs a policy on the LocallyBuild publisher's Trusted Publishing page, in the Marketplace's publisher management, with locallybuild as the repository owner, azure-devops-extension as the repository and publish.yml as the workflow.

License

This extension is licensed under Apache-2.0. The Locally CLI it installs is governed by separate terms, see NOTICE.

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
  • Your Privacy Choices
  • Consumer Health Privacy
© 2026 Microsoft