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:
| Rol | Puede |
|---|---|
| Reader | Instalar paquetes |
| Contributor | Publicar paquetes |
| Collaborator | Publicar + gestionar versions |
| Owner | Todo |
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.