azure-artifactsnpmnugetpypipythondevopspackagesazure

Cómo centralicé los packages privados de Python, npm y NuGet del equipo en Azure

Cuando el equipo tiene librerías internas en Python, TypeScript y C#, necesitas un registro privado. Las opciones son: GitHub Packages, GitLab Package Registry, JFrog Artifactory, o Azure Artifacts. Si el stack ya es Azure, Azure Artifacts es la opción natural — está integrado con el mismo tenant de Azure AD, los mismos service principals, la misma facturación.

Este post es la configuración práctica: cómo crear los feeds, cómo autenticar desde developer local y desde CI, y el patrón upstream que evita bloquear la instalación de paquetes públicos.

Upstream sources: por qué importan

El error más común al configurar un feed privado: configurar el registry del proyecto para apuntar SOLO al feed privado. El resultado: pip install requests falla porque requests no está en tu feed privado.

La solución es upstream sources. Azure Artifacts puede actuar como proxy transparente de registros públicos:

pip install mi-org-kpi-core  →  busca en feed privado → encontrado ✓
pip install requests          →  busca en feed privado → no encontrado
                               →  upstream: pypi.org   → encontrado ✓
                               →  cachea en tu feed    → próxima vez más rápido

Con upstream configurado, el desarrollador configura UN solo registry y obtiene tanto paquetes privados como públicos.

Crear el feed con upstream

Desde Azure DevOps → Artifacts → New Feed:

Name: mi-org-internal
Visibility: Only people in my organization
Upstream sources: ✓ Include packages from common public sources

Los upstream sources incluyen por defecto: PyPI (Python), npmjs.com (JavaScript), nuget.org (C#), Maven Central (Java). El feed actúa como caché para paquetes públicos.

Alternativamente, desde az CLI (requiere Azure DevOps extension):

az devops configure --defaults organization=https://dev.azure.com/mi-org project=mi-proyecto

# Crear feed
az artifacts universal publish \
  --organization https://dev.azure.com/mi-org \
  --project mi-proyecto \
  --feed mi-org-internal \
  --name placeholder \
  --version 0.0.1 \
  --description "Feed initialization"

Autenticación local — Python

Para instalar desde el feed privado en máquina local, hay dos enfoques:

1. pip.conf con token personal:

# ~/.config/pip/pip.conf (Linux/Mac)
# %APPDATA%\pip\pip.ini (Windows)

[global]
index-url = https://pkgs.dev.azure.com/mi-org/mi-proyecto/_packaging/mi-org-internal/pypi/simple/
extra-index-url = https://pypi.org/simple/

Para autenticar, el username es cualquier string y el password es un PAT (Personal Access Token) de Azure DevOps con scope Packaging (Read & Write):

# Con keyring (recomendado — no guarda el token en texto plano)
pip install keyring artifacts-keyring
pip install mi-org-kpi-core  # Pedirá PAT la primera vez, lo cachea

2. pip.conf con credenciales en URL (no recomendado para repos compartidos):

[global]
index-url = https://PAT_TOKEN@pkgs.dev.azure.com/mi-org/mi-proyecto/_packaging/mi-org-internal/pypi/simple/

Funciona pero el PAT queda en texto plano. Solo para máquinas personales, nunca en Dockerfiles o archivos comiteados.

Autenticación local — npm

.npmrc en el directorio del proyecto (o en ~/.npmrc para configuración global):

registry=https://pkgs.dev.azure.com/mi-org/mi-proyecto/_packaging/mi-org-internal/npm/registry/
always-auth=true
//pkgs.dev.azure.com/mi-org/:_authToken=${AZURE_ARTIFACTS_TOKEN}

El token en variable de entorno evita commitearlo. Para obtener el token:

# PAT de Azure DevOps con scope Packaging Read & Write
export AZURE_ARTIFACTS_TOKEN=<tu-PAT-aqui>
npm install @mi-org/observability

Con vsts-npm-auth (herramienta oficial de Microsoft, solo Windows):

npm install -g vsts-npm-auth
vsts-npm-auth -config .npmrc  # Genera token automáticamente desde Azure AD

Autenticación local — NuGet

<!-- nuget.config en la raíz del proyecto -->
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <add key="AzureArtifacts"
         value="https://pkgs.dev.azure.com/mi-org/mi-proyecto/_packaging/mi-org-internal/nuget/v3/index.json" />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
  <packageSourceCredentials>
    <AzureArtifacts>
      <add key="Username" value="PAT" />
      <add key="ClearTextPassword" value="%AZURE_ARTIFACTS_TOKEN%" />
    </AzureArtifacts>
  </packageSourceCredentials>
</configuration>

Las credenciales usan %VAR% (Windows) o $VAR (Linux) — el token viene del entorno, no del archivo.

Autenticación CI — tokens efímeros

En CI (GitHub Actions, Azure Pipelines), no uses PATs de larga duración. Usa el token de acceso de Azure generado al momento:

- name: Get Azure Artifacts token
  id: token
  run: |
    TOKEN=$(az account get-access-token \
      --resource https://pkgs.dev.azure.com \
      --query accessToken -o tsv)
    echo "::add-mask::$TOKEN"
    echo "value=$TOKEN" >> $GITHUB_OUTPUT

# Para Python en CI
- name: Configure pip
  run: |
    pip config set global.index-url \
      "https://pkgs.dev.azure.com/mi-org/mi-proyecto/_packaging/mi-org-internal/pypi/simple/"
    pip config set global.extra-index-url "https://pypi.org/simple/"

- name: Install with token
  env:
    PIP_USERNAME: PAT
    PIP_PASSWORD: ${{ steps.token.outputs.value }}
  run: pip install -r requirements.txt

El token generado por az account get-access-token tiene TTL de ~1 hora — más que suficiente para un job de CI, y nunca necesita rotación manual.

Publicar desde CI

Para publicar nuevas versiones desde GitHub Actions:

# Python
- name: Publish Python package
  env:
    TWINE_USERNAME: PAT
    TWINE_PASSWORD: ${{ steps.token.outputs.value }}
  run: |
    twine upload \
      --repository-url https://pkgs.dev.azure.com/mi-org/mi-proyecto/_packaging/mi-org-internal/pypi/upload/ \
      dist/*.whl

# npm
- name: Publish npm package
  run: |
    TOKEN="${{ steps.token.outputs.value }}"
    B64=$(echo -n ":$TOKEN" | base64)
    npm config set //pkgs.dev.azure.com/mi-org/:_auth=$B64
    npm publish --registry https://pkgs.dev.azure.com/mi-org/mi-proyecto/_packaging/mi-org-internal/npm/registry/

# NuGet (--skip-duplicate hace el push idempotente)
- name: Publish NuGet package
  run: |
    dotnet nuget add source \
      "https://pkgs.dev.azure.com/mi-org/mi-proyecto/_packaging/mi-org-internal/nuget/v3/index.json" \
      --name AzureArtifacts \
      --username PAT \
      --password "${{ steps.token.outputs.value }}" \
      --store-password-in-clear-text

    dotnet nuget push "**/*.nupkg" \
      --source AzureArtifacts \
      --skip-duplicate

Permisos por feed

Azure Artifacts tiene cuatro niveles de acceso:

RolPuede
ReaderInstalar paquetes
ContributorPublicar paquetes
CollaboratorPublicar + gestionar versions
OwnerTodo

En producción: el service principal de CI tiene rol Contributor. Los developers tienen rol Reader para feeds de producción (no pueden publicar accidentalmente). Solo la pipeline de release publica.

Lo que aprendí

Upstream sources no son opcionales. Sin upstream, cada paquete público que el equipo necesita tiene que estar en el feed privado. Con upstream, el feed privado actúa como caché transparente.

Tokens efímeros > PATs de larga duración en CI. az account get-access-token genera un token que dura una hora. No tiene que rotarse. No puede filtrarse a largo plazo. El equivalente de un PAT permanente es un riesgo innecesario.

Un solo feed para todos los lenguajes simplifica la configuración. Azure Artifacts maneja PyPI, npm y NuGet en el mismo feed. El developer configura un solo origen de autenticación (Azure AD) para todos los lenguajes.

El nuget.config comiteado es la configuración correcta para C#. A diferencia de Python o npm, NuGet usa nuget.config en el proyecto — es parte del repositorio, con las credenciales viniendo del entorno.

Volver al blog