Table of Contents

Introduction

I love the new uv Python package/project manager and am beginning to adopt it at work. However, getting it working well with Azure Artifacts and Pipelines took a bit of fiddling so I wanted to write up my findings to supplement the official documentation.

2025-03-12 update: Since I wrote this article, the official documentation has been dramatically improved and covers most of the same content.

Setup

Before creating the virtual environment, ensure you have the keyring tool installed with the artifacts-keyring package. This will require a .NET 8+ runtime.

1uv tool install keyring --with artifacts-keyring

Now, in the pyproject.toml file, setup the following:

 1[tool.uv]
 2# tells uv to use subprocess mode for this keyring provider
 3keyring-provider = "subprocess"
 4
 5[[tool.uv.index]]
 6# name is arbitrary
 7name = "azure-devops"
 8# replace items in curly braces with your actual org/project/feed names
 9# the username must be provided in the URL and be "VssSessionToken" exactly
10url = "https://VssSessionToken@pkgs.dev.azure.com/{organization}/{project}/_packaging/{feed}/pypi/simple/"
11# if you want to publish a package as well, set the following URL too
12publish-url = "https://pkgs.dev.azure.com/{organization}/{project}/_packaging/{feed}/pypi/upload/"
13# setup Azure Artifacts as the default package index
14default = true

Now, uv sync or other commands which create the virtual environment should work.

To use this in an Azure Pipeline takes a little more configuration. You now need to provide uv with a valid access token since it can’t obtain one interactively. Additionally, the keyring provider needs to be disabled so that the provided username and password is used instead. In the Pipeline YAML file, this can be done like so:

1env:
2  # https://docs.astral.sh/uv/configuration/environment/#uv_index_name_password
3  # https://docs.astral.sh/uv/configuration/indexes/#providing-credentials
4  # This environment variable must match the name you gave the index,
5  # except capitalized and the non-alphanumeric characters replaced with underscores
6  UV_INDEX_AZURE_DEVOPS_USERNAME: x # username is arbitrary
7  UV_INDEX_AZURE_DEVOPS_PASSWORD: $(System.AccessToken)
8  UV_KEYRING_PROVIDER: disabled

Here is a full example of installing uv, persisting the cache, and installing packages. This example is cross-platform and should work on both Windows and Linux runners.

 1variables:
 2  # These will be set as global environment variables
 3  UV_INDEX_AZURE_DEVOPS_USERNAME: x
 4  UV_KEYRING_PROVIDER: disabled
 5  # This environment variable keeps the cache in a predictable location
 6  UV_CACHE_DIR: $(Agent.TempDirectory)/cache/uv
 7
 8steps:
 9  - script: pipx install uv
10    displayName: Install uv
11
12  - task: Cache@2
13    displayName: Cache uv data
14    inputs:
15      key: '"uv-cache" | "$(Agent.OS)" | .python-version | uv.lock'
16      restoreKeys: |
17        "uv-cache" | "$(Agent.OS)" | .python-version
18        "uv-cache" | "$(Agent.OS)"
19        "uv-cache"
20      path: $(UV_CACHE_DIR)
21
22  - script: uv sync
23    displayName: Install dependencies
24    env:
25      # This needs to be explicitly mapped as it contains a secret.
26      UV_INDEX_AZURE_DEVOPS_PASSWORD: $(System.AccessToken)

If you want to publish a package using uv, replace the uv sync step with these instead:

 1- script: uv build
 2  displayName: Build package
 3  env:
 4    # This is still required, as uv will fetch packages required by the build system
 5    UV_INDEX_AZURE_DEVOPS_PASSWORD: $(System.AccessToken)
 6
 7# Ensure the Pipeline has permission to publish packages
 8# https://learn.microsoft.com/en-us/azure/devops/artifacts/feeds/feed-permissions?view=azure-devops#pipelines-permissions
 9- script: uv publish --index azure-devops
10  displayName: Publish package
11  env:
12    # The environment variable used for publishing packages is different
13    UV_PUBLISH_TOKEN: $(System.AccessToken)

References