Azure Artifacts with uv
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)