Cuando tienes una plataforma de observabilidad con SDKs en Python, TypeScript, C# y Databricks, el release de una nueva versión implica coordinar cuatro publicaciones en cuatro feeds distintos. Si haces eso manualmente, eventualmente publicas tres de cuatro y el cuarto se te olvida.
Este post es el workflow de GitHub Actions que automatiza ese proceso: un solo trigger de release, cuatro publicaciones en paralelo, gates de calidad previos, idempotencia para re-runs seguros.
La estructura del repositorio
sdk-monorepo/
├── .github/
│ └── workflows/
│ ├── ci.yml # Test en cada PR
│ └── release.yml # Publicar al crear release
├── sdk-python/
│ ├── src/
│ ├── tests/
│ └── pyproject.toml
├── sdk-typescript/
│ ├── src/
│ ├── tests/
│ └── package.json
├── sdk-csharp/
│ ├── src/
│ ├── tests/
│ └── *.csproj
└── sdk-databricks/
├── src/
└── pyproject.toml
Un repositorio, cuatro SDKs, un workflow de release. El monorepo garantiza que cuando publicas, publicas todos.
El trigger: GitHub Release
El workflow se activa al crear un release en GitHub, no al hacer push. Esto da control explícito sobre cuándo se publica:
name: SDK Release
on:
release:
types: [published]
env:
PYTHON_FEED: https://pkgs.dev.azure.com/org/project/_packaging/feed/pypi/simple/
NPM_REGISTRY: https://pkgs.dev.azure.com/org/project/_packaging/feed/npm/registry/
NUGET_SOURCE: https://pkgs.dev.azure.com/org/project/_packaging/feed/nuget/v3/index.json
El release en GitHub tiene un tag (v1.4.0) que es la fuente de verdad de la versión. Los pyproject.toml y package.json deben estar sincronizados con ese tag antes de crear el release.
Job 1: Quality gates
Antes de publicar, verificar que todos los tests pasan en todos los lenguajes:
jobs:
quality-gates:
name: Quality gates — ${{ matrix.sdk }}
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
sdk: [python, typescript, csharp, databricks]
include:
- sdk: python
dir: sdk-python
test-cmd: pytest tests/ -v --tb=short
- sdk: typescript
dir: sdk-typescript
test-cmd: npm test
- sdk: csharp
dir: sdk-csharp
test-cmd: dotnet test --configuration Release
- sdk: databricks
dir: sdk-databricks
test-cmd: pytest tests/ -v
steps:
- uses: actions/checkout@v4
- name: Setup Python
if: matrix.sdk == 'python' || matrix.sdk == 'databricks'
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Setup Node
if: matrix.sdk == 'typescript'
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Setup .NET
if: matrix.sdk == 'csharp'
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.x'
- name: Install dependencies
run: |
cd ${{ matrix.dir }}
case "${{ matrix.sdk }}" in
python|databricks) pip install -e ".[dev]" ;;
typescript) npm ci ;;
csharp) dotnet restore ;;
esac
- name: Run tests
run: |
cd ${{ matrix.dir }}
${{ matrix.test-cmd }}
fail-fast: true detiene todo si cualquier SDK falla los tests. No tiene sentido publicar tres SDKs si uno no pasa quality gates.
Job 2: Publish en paralelo
Después de que todos los quality gates pasan, publicar los cuatro SDKs en paralelo:
publish:
name: Publish — ${{ matrix.sdk }}
needs: quality-gates
runs-on: ubuntu-latest
permissions:
id-token: write # Para WIF con Azure
contents: read
strategy:
fail-fast: false # Si uno falla, continuar con los otros
matrix:
sdk: [python, typescript, csharp, databricks]
steps:
- uses: actions/checkout@v4
- name: Azure Login (WIF)
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Get Azure Artifacts token
id: artifacts-token
run: |
TOKEN=$(az account get-access-token \
--resource https://pkgs.dev.azure.com \
--query accessToken -o tsv)
echo "::add-mask::$TOKEN"
echo "token=$TOKEN" >> $GITHUB_OUTPUT
fail-fast: false en el publish job permite que si Python falla (por ejemplo, por un error de red), TypeScript y C# sigan publicando. Los errores se reportan individualmente.
Publicación Python (con skip idempotente)
- name: Publish Python SDK
if: matrix.sdk == 'python'
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ steps.artifacts-token.outputs.token }}
run: |
cd sdk-python
pip install build twine
VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
echo "Publishing Python SDK version: $VERSION"
# Skip if already published (idempotencia)
if pip index versions mi-org-observability \
--index-url ${{ env.PYTHON_FEED }} 2>/dev/null | grep -q "$VERSION"; then
echo "Version $VERSION already published, skipping"
exit 0
fi
python -m build --wheel
twine upload --repository-url ${{ env.PYTHON_FEED }} dist/*.whl
Publicación TypeScript (con .npmrc dinámico)
El feed de Azure Artifacts para npm requiere autenticación en .npmrc. Generarlo dinámicamente en el step evita guardar el token en el repositorio:
- name: Publish TypeScript SDK
if: matrix.sdk == 'typescript'
run: |
cd sdk-typescript
# .npmrc con token dinámico
FEED_DOMAIN="pkgs.dev.azure.com"
TOKEN="${{ steps.artifacts-token.outputs.token }}"
B64_TOKEN=$(echo -n "PAT:$TOKEN" | base64)
cat > .npmrc << EOF
registry=${{ env.NPM_REGISTRY }}
always-auth=true
//${FEED_DOMAIN}/:_authToken=${TOKEN}
EOF
VERSION=$(node -e "console.log(require('./package.json').version)")
echo "Publishing TypeScript SDK version: $VERSION"
# Skip if already published
if npm view mi-org-observability@$VERSION --registry ${{ env.NPM_REGISTRY }} 2>/dev/null; then
echo "Version $VERSION already published, skipping"
exit 0
fi
npm ci
npm run build
npm publish --registry ${{ env.NPM_REGISTRY }}
Publicación C# con NuGet
- name: Publish C# SDK
if: matrix.sdk == 'csharp'
run: |
cd sdk-csharp
# Registrar feed con token
dotnet nuget add source ${{ env.NUGET_SOURCE }} \
--name AzureArtifacts \
--username PAT \
--password "${{ steps.artifacts-token.outputs.token }}" \
--store-password-in-clear-text
VERSION=$(grep -oP '(?<=<Version>)[^<]+' src/**/*.csproj | head -1)
echo "Publishing C# SDK version: $VERSION"
dotnet build --configuration Release
dotnet pack --configuration Release --no-build
# --skip-duplicate hace el publish idempotente
dotnet nuget push "**/*.nupkg" \
--source AzureArtifacts \
--skip-duplicate
Job 3: Release summary
Después de todas las publicaciones, un job final agrega un comentario al release con el resultado:
release-summary:
name: Release summary
needs: publish
runs-on: ubuntu-latest
if: always()
permissions:
contents: write
steps:
- name: Update release notes
uses: softprops/action-gh-release@v2
with:
body: |
## Published packages
| SDK | Feed | Status |
|-----|------|--------|
| Python | Azure Artifacts PyPI | ${{ needs.publish.result == 'success' && '✓' || '✗' }} |
| TypeScript | Azure Artifacts npm | ${{ needs.publish.result == 'success' && '✓' || '✗' }} |
| C# | Azure Artifacts NuGet | ${{ needs.publish.result == 'success' && '✓' || '✗' }} |
| Databricks | Wheel storage | ${{ needs.publish.result == 'success' && '✓' || '✗' }} |
Released: ${{ github.ref_name }}
append_body: true
El if: always() garantiza que el summary se ejecuta aunque algún publish falle. append_body: true agrega al body del release sin sobreescribir las notas manuales.
Lo que aprendí
Matrix strategy en publish: fail-fast: false. En quality gates quieres fallar rápido. En publish, quieres publicar lo que puedas y reportar errores por separado. Un error de red en npm no debe cancelar el publish de NuGet.
Los tokens de Azure Artifacts son efímeros por diseño. WIF genera tokens de corta vida. Generarlos en el step (no en env global) garantiza que están frescos cuando se usan.
Un release en GitHub es el mecanismo de coordinación correcto. Push a main puede triggear CI. El release tag es el trigger de publicación. Esa separación previene publicaciones accidentales desde branches.
El monorepo de SDKs es la única forma de garantizar consistencia. SDKs en repositorios separados significan workflows separados, versiones que se desincrónizan, y la posibilidad de hacer release de uno sin el otro.