serhii.net

In the middle of the desert you can say anything you want

04 Aug 2025

Using a private gitlab repository with uv

Scenario:

  • your uv source package (that lives in a gitlab source project) depends on a target package from a gitlab target project’s package registry.
  • you want uv add etc. to work transparently
  • you want gitlab CI/CD pipelines to work transparently

TL;DR

  • Have a gitlab token with at least read_api scope and Developer+ role
  • Find the address of the registry through Gitlab UI
    • https://__token__:glpat-secret-token@gitlab.de/api/v4/projects/1111/packages/pypi/simple
    • https://gitlab.de/api/v4/projects/1111/packages/pypi/simple
    • FROM A GROUP1: https://gitlab.example.com/api/v4/groups/<group_id>/-/packages/pypi/simple
    • ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi
  • pyproject: add it registry, see below
  • to authenticate UV, you can use:
    • token inside URI, or
    • UV_INDEX_PRIVATE_REGISTRY_USERNAME/PASSWORD env variables, replacing PRIVATE_REGISTRY with the name you gave to it in pyproject.toml
    • ~/.netrc file: .netrc - everything curl

Gitlab

  • In Gitlab, use/create an access token with read_xxx permissions in the project — read_api, read_repository, read_registry are enough — and Developer role.
  • To get the package registry address,
    • Use this URI for the group registry (it’s the best for multiple projects in the same group): https://gitlab.example.com/api/v4/groups/<group_id>/-/packages/pypi/simple
      • You can get the group ID from the group page, hamburger menu in upper-right, “copy group id”. Will be an int.
    • open the registry, click on a package, click “install” and you’ll see it all: Pasted image 20250804151836.png
      • different packages in different projects will have different registries!
    • The project repository URI is generally ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi/simple

UV

Add the new package registry as index

tool.uv.index
name = "my-registry"
url = "https://__token__:glpat-secret-token@gitlab.de/api/v4/projects/1111/packages/pypi/simple"
# authenticate = "always" # see below
# ignore-error-codes = [401]

The URI either contains token inside URI or doesn’t. The examples below are /projects/xxx, ofc a group registry works as well. - https://__token__:glpat-secret-token@gitlab.de/api/v4/projects/1111/packages/pypi/simple - token inside the URI - https://gitlab.de/api/v4/projects/1111/packages/pypi/simple — auth happening through env. variables or ~/.netrc

Uv auth

Through the server URI

url = "https://__token__:glpat-secret-token@gitlab.de/api/v4/projects/1111/packages/pypi/simple"

Through env variables:
export UV_INDEX_PRIVATE_REGISTRY_USERNAME=__token__ 
export UV_INDEX_PRIVATE_REGISTRY_PASSWORD=glpat-secret-token

PRIVATE_REGISTRY needs to be replaced with the name of the registry . So e.g. for the pyproject above it’s UV_INDEX_**MY_REGISTRY**_USERNAME.

Through a .netrc file

From Authentication | uv / HTTP Authentication and PyPI packages in the package registry | GitLab Docs: Create a ~/.netrc:

machine gitlab.example.com
login __token__
password <personal_token>

It will use these details, when you uv add ... -v you’d see a line like

DEBUG Checking netrc for credentials for https://gitlab.de/api/v4/projects/1111/packages/pypi/simple/packagename/
DEBUG Found credentials in netrc file for https://gitlab.de/api/v4/projects/1111/packages/pypi/simple/packagename/

NB git will also use these credentials — so if the token’s scope doesn’t allow e.g. pushing, you won’t be able to git push. Use a wider scope or a personal access token (or env. variables)

Usage

  • When you uv add yourpackage, uv looks for packages in all registries
    • The usual pypi one is on by default
  • If gitlab doesn’t find one in the gitlab registry or the user is unauthenticated, gitlab by default transparently mirrors pypi
  • If auth fails for any of the indexes, uv will fail loudly — you can ignore-error-codes = [401] to make uv keep looking inside the other registries

Gitlab CI/CD pipelines

CI/CD pipelines have to have access to the package as well, when they run.

GitLab CI/CD job token | GitLab Docs:

You can use a job token to authenticate with GitLab to access another group or project’s resources (the target project). By default, the job token’s group or project must be added to the target project’s allowlist.

In the target project (the one that needs to be resolved, the one with the private registry), in Settings->CI/CD -> Job token permissions add the source project (the one that will access the packages during CI/CD).

You can just add the group parent of all projects as well, then you don’t have to add any individual ones.

Then $CI_JOB_TOKEN can be used to access the target projects. For example, through a ~/.netrc file (note the username!)

machine gitlab.example.com
username gitlab-ci-token
password $CI_JOB_TOKEN

Bonus round: gitlab-ci-local

I love firecow/gitlab-ci-local.

When running gitlab-ci-local things, the CI_JOB_TOKEN variable is empty. You can create a .gitlab-ci-local-variables.yaml (don’t forget to gitignore it!) with this variable, it’ll get used automatically and your local CI/CD pipelines will run as well:

CI_JOB_TOKEN=glpat-secret-token
Nel mezzo del deserto posso dire tutto quello che voglio.
comments powered by Disqus