1
Diagnóstico: por qué tarda 25 minutos
Análisis del estado actual
Problema raíz: Sin
--disk_cache ni --remote_cache, Bazel recalcula todo desde cero en cada build CI. Con 40 librerías y 3 apps, el grafo de dependencias acumula miles de acciones que se re-ejecutan innecesariamente.| Causa | Tiempo perdido | Impacto | Solución |
|---|---|---|---|
| Sin remote cache en CI | ~14 min | GCP Remote Cache | |
| Targets demasiado grandes (1 BUILD por app) | ~5 min | Granularidad fina | |
Sin --jobs=auto optimizado |
~3 min | .bazelrc CI config | |
| Webpack aún en el pipeline (pre-Bazel) | ~2 min | Migración a rules_js | |
| Sin análisis de targets impactados | ~1 min | bazel query + rdeps |
$ bazel analyze-profile — Diagnóstico antes de optimizar
BASH
# 1. Genera perfil de build para análisis
bazel build //... --profile=~/datamesh-profile.json \
--noenable_bzlmod
# 2. Analiza bottlenecks
bazel analyze-profile ~/datamesh-profile.json
# 3. Log detallado de acciones lentas (>2s)
bazel build //... --execution_log_json_file=exec_log.json
jq 'select(.actualOutputs != null) | {target: .targetLabel, dur: .elapsedNs}' \
exec_log.json | sort -k2 -n | tail -20
2
Remote Cache en GCP (Artifact Registry)
Mayor impacto individual: -14 min en CI
GCP Artifact Registry soporta el protocolo de Remote Cache de Bazel de forma nativa. DataMesh ya tiene cuenta GCP con Cloud Build, por lo que esta integración no requiere infraestructura adicional.
setup-gcp-cache.sh — Configuración inicial (una sola vez)
BASH
#!/bin/bash
# Ejecutar como admin GCP — solo la primera vez
PROJECT_ID="datamesh-prod"
REGION="europe-west1"
REPO_NAME="bazel-cache"
SA_NAME="bazel-ci-sa"
# 1. Crear repositorio en Artifact Registry
gcloud artifacts repositories create $REPO_NAME \
--repository-format=apt \
--location=$REGION \
--project=$PROJECT_ID \
--description="Bazel Remote Cache — DataMesh monorepo"
# 2. Service Account para CI (GitHub Actions)
gcloud iam service-accounts create $SA_NAME \
--display-name="Bazel CI Service Account" \
--project=$PROJECT_ID
# 3. Permisos mínimos
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/artifactregistry.writer"
# 4. Crear clave y exportar como Secret en GitHub
gcloud iam service-accounts keys create bazel-sa-key.json \
--iam-account="${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
echo "Añade bazel-sa-key.json como secret GCP_BAZEL_SA_KEY en GitHub"
3
Configuración .bazelrc para DataMesh
Archivo central de rendimiento
.bazelrc
BAZEL
# ================================================
# DataMesh Intelligence — .bazelrc
# Optimizado para monorepo TypeScript + Python/ML
# ================================================
# --- Flags base ---
build --enable_platform_specific_config
build --incompatible_enable_cc_toolchain_resolution
build --experimental_strict_conflict_checks
# --- Rendimiento local ---
build --jobs=auto
build --local_cpu_resources=HOST_CPUS*.80
build --local_ram_resources=HOST_RAM*.70
# --- Caché local en disco (siempre activo) ---
build --disk_cache=~/.cache/bazel-datamesh
build --repository_cache=~/.cache/bazel-repo-datamesh
# --- Remote Cache GCP (config:remote-cache) ---
build:remote-cache --remote_cache=https://storage.googleapis.com/datamesh-bazel-cache
build:remote-cache --google_default_credentials
build:remote-cache --remote_upload_local_results=true
build:remote-cache --remote_timeout=3600
build:remote-cache --remote_max_connections=200
# --- CI: activar remote cache automáticamente ---
build:ci --config=remote-cache
build:ci --build_metadata=ROLE=CI
build:ci --noshow_progress
build:ci --show_result=0
build:ci --jobs=32 # 4 runners × 8 CPUs
# --- Plataformas ---
build:linux --platforms=//platforms:linux_x86_64
build:macos --platforms=//platforms:macos_arm64
# --- Tests ---
test --test_output=errors
test --test_summary=terse
test --cache_test_results=yes
# --- Modos de compilación ---
build:opt --compilation_mode=opt
build:dbg --compilation_mode=dbg
# Importar config local del dev (git-ignored)
try-import %workspace%/user.bazelrc
4
Builds TypeScript — apps/dashboard
React 18 con rules_js + aspect_rules_ts
Clave: Un único
BUILD.bazel con glob para toda la app es el anti-patrón más común. Dividir por feature module permite caché granular: cambiar charts/ no invalida auth/.apps/dashboard/BUILD.bazel
STARLARK
load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
load("@aspect_rules_js//js:defs.bzl", "js_library", "js_binary")
load("@npm//:defs.bzl", "npm_link_all_packages")
load("@aspect_rules_jest//jest:defs.bzl", "jest_test")
npm_link_all_packages(name = "node_modules")
# --- Feature: Charts (analytics visualizations) ---
ts_project(
name = "charts_ts",
srcs = glob(["src/charts/**/*.tsx", "src/charts/**/*.ts"]),
declaration = True,
tsconfig = "//:tsconfig.json",
deps = [
":node_modules/react",
":node_modules/recharts",
"//libs/analytics-utils:utils", # lib compartida
],
visibility = ["//visibility:public"],
)
# --- Feature: Auth ---
ts_project(
name = "auth_ts",
srcs = glob(["src/auth/**/*.tsx", "src/auth/**/*.ts"]),
declaration = True,
tsconfig = "//:tsconfig.json",
deps = [
":node_modules/react",
":node_modules/@auth0/auth0-react",
"//libs/api-client:client",
],
visibility = ["//visibility:public"],
)
# --- App bundle principal ---
js_binary(
name = "dashboard",
entry_point = "src/main.tsx",
data = [
":charts_ts",
":auth_ts",
":node_modules/react-dom",
],
visibility = ["//visibility:public"],
)
# --- Tests ---
jest_test(
name = "dashboard_test",
config = "//:jest.config.js",
data = [
":charts_ts",
":auth_ts",
],
node_modules = "//:node_modules",
)
5
Builds Python/ML — libs/demand-forecast
PyTorch 2.1 con rules_python
libs/demand-forecast/BUILD.bazel
STARLARK
load("@rules_python//python:defs.bzl", "py_library", "py_test", "py_binary")
load("@pip//:requirements.bzl", "requirement")
py_library(
name = "demand_forecast",
srcs = glob(["src/**/*.py"]),
deps = [
requirement("torch"),
requirement("numpy"),
requirement("pandas"),
"//libs/data-pipeline:pipeline",
],
visibility = ["//visibility:public"],
)
py_test(
name = "demand_forecast_test",
srcs = glob(["tests/**/*.py"]),
deps = [
":demand_forecast",
requirement("pytest"),
requirement("pytest-mock"),
],
size = "medium",
timeout = "moderate",
tags = ["requires-gpu"], # se omite en runners sin GPU
)
py_binary(
name = "train_demand",
srcs = ["train.py"],
deps = [":demand_forecast"],
data = ["//data:retail_training_data"],
)
6
Regla Custom: ml_model_package
Empaquetado de modelos PyTorch para producción
Caso de uso: DataMesh exporta modelos .pt + metadatos + requirements al Artifact Registry para despliegue en Cloud Run. Esta regla custom unifica el proceso en un único target Bazel reproducible.
tools/bazel/rules/ml_model.bzl
STARLARK
"""Regla custom para empaquetar modelos ML de DataMesh.
Produce un .tar.gz con:
model.pt — pesos del modelo PyTorch
metadata.json — versión, métricas, fecha
requirements.txt — dependencias de inferencia
"""
def _ml_model_package_impl(ctx):
model_file = ctx.file.model
metadata = ctx.file.metadata
requirements = ctx.file.requirements
output = ctx.actions.declare_file(ctx.attr.name + ".tar.gz")
# Script de empaquetado generado dinámicamente
pack_script = ctx.actions.declare_file(ctx.attr.name + "_pack.sh")
ctx.actions.write(
output = pack_script,
content = """#!/bin/bash
set -euo pipefail
TMPDIR=$(mktemp -d)
cp {model} $TMPDIR/model.pt
cp {metadata} $TMPDIR/metadata.json
cp {requirements} $TMPDIR/requirements.txt
tar -czf {output} -C $TMPDIR .
rm -rf $TMPDIR
""".format(
model = model_file.path,
metadata = metadata.path,
requirements = requirements.path,
output = output.path,
),
is_executable = True,
)
ctx.actions.run(
inputs = [model_file, metadata, requirements],
outputs = [output],
executable = pack_script,
mnemonic = "MLModelPackage",
progress_message = "Packaging ML model %s" % ctx.label,
)
return [DefaultInfo(
files = depset([output]),
runfiles = ctx.runfiles(files = [output]),
)]
ml_model_package = rule(
implementation = _ml_model_package_impl,
attrs = {
"model": attr.label(
allow_single_file = [".pt", ".pth"],
mandatory = True,
doc = "Archivo de pesos PyTorch (.pt/.pth)",
),
"metadata": attr.label(
allow_single_file = [".json"],
mandatory = True,
doc = "metadata.json con versión y métricas",
),
"requirements": attr.label(
allow_single_file = [".txt"],
mandatory = True,
doc = "requirements de inferencia (sin extras dev)",
),
},
doc = "Empaqueta un modelo PyTorch para despliegue en Cloud Run.",
)
# ---- Macro de conveniencia ----
def ml_model(name, model, metadata, requirements, **kwargs):
"""Atajo que combina py_binary (train) + ml_model_package (export)."""
ml_model_package(
name = name + "_package",
model = model,
metadata = metadata,
requirements = requirements,
**kwargs,
)
models/demand-v3/BUILD.bazel — Uso de la regla custom
STARLARK
load("//tools/bazel/rules:ml_model.bzl", "ml_model_package")
ml_model_package(
name = "demand_v3_release",
model = "demand_v3.pt",
metadata = "metadata.json",
requirements = "requirements-inference.txt",
visibility = ["//visibility:public"],
)
# Uso: bazel build //models/demand-v3:demand_v3_release
# Output: bazel-bin/models/demand-v3/demand_v3_release.tar.gz
7
Queries de Dependencias para DataMesh
Análisis de impacto y auditoría del grafo
¿Qué afecta si cambio data-pipeline?
bazel query \
"rdeps(//..., //libs/data-pipeline:pipeline)" \
--output=label
# Muestra todos los targets que dependen
# de data-pipeline (para tests inteligentes)
Targets impactados por git diff
CHANGED=$(git diff --name-only origin/main \
| sed 's|^|//|; s|/[^/]*$||' \
| sort -u | tr '\n' '+')
bazel query \
"rdeps(//..., set($CHANGED))"
Grafo de dependencias visual
bazel query \
"deps(//apps/dashboard:dashboard)" \
--output=graph \
| dot -Tpng -o deps_dashboard.png
# Requiere: apt install graphviz
Tests de integración únicamente
bazel query \
"attr(tags, 'integration', //...)"
# Luego ejecutar solo esos:
# bazel test $(bazel query ...)
Tamaño del grafo de build
bazel query "deps(//...)" \
--output=package \
| wc -l
# Referencia DataMesh: ~340 paquetes
Dependencias circulares (detectar)
bazel query \
"somepath(//libs/api-client:client,
//libs/analytics-utils:utils)"
# Si devuelve path → hay ciclo potencial
8
GitHub Actions con Remote Cache
Pipeline CI optimizado para DataMesh
.github/workflows/bazel-ci.yml
YAML
name: DataMesh CI — Bazel Build & Test
on:
push:
branches: [main, "release/**"]
pull_request:
branches: [main]
jobs:
bazel-build-test:
runs-on: ubuntu-22.04
permissions:
contents: read
id-token: write # Para Workload Identity GCP
steps:
- uses: actions/checkout@v4
- name: Autenticar en GCP (Workload Identity)
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_BAZEL_SA_KEY }}
- name: Instalar Bazelisk
run: |
curl -Lo /usr/local/bin/bazel \
https://github.com/bazelbuild/bazelisk/releases/latest/download/bazelisk-linux-amd64
chmod +x /usr/local/bin/bazel
- name: Cache — Repositorios externos
uses: actions/cache@v4
with:
path: ~/.cache/bazel-repo-datamesh
key: bazel-repo-${{ hashFiles('WORKSPACE.bazel', 'requirements*.txt') }}
- name: Detectar targets impactados
id: targets
run: |
BASE=${{ github.event.pull_request.base.sha || 'HEAD~1' }}
CHANGED=$(git diff --name-only $BASE HEAD | \
grep -E '\.(py|ts|tsx|bzl)$' | \
sed 's|/[^/]*$||' | sort -u)
if [ -z "$CHANGED" ]; then
echo "targets=//..." >> $GITHUB_OUTPUT
else
QUERY="rdeps(//..., set($(echo $CHANGED | tr ' ' '\n' | sed 's|^|//|' | tr '\n' ' ')))"
TARGETS=$(bazel query "$QUERY" --output=label 2>/dev/null | tr '\n' ' ')
echo "targets=$TARGETS" >> $GITHUB_OUTPUT
fi
- name: Bazel Build
run: |
bazel build --config=ci ${{ steps.targets.outputs.targets }}
- name: Bazel Test
run: |
bazel test --config=ci \
--test_tag_filters=-requires-gpu \
${{ steps.targets.outputs.targets }}
- name: Resumen de cache
if: always()
run: |
bazel info --config=ci | grep remote
9
Migración Webpack → rules_js
Plan de transición sin romper CI
Estado actual Antes
- ✗webpack.config.js en apps/dashboard — fuera de Bazel
- ✗npm scripts: build, test, lint — sin caché
- ✗2 pipelines CI: uno Bazel (libs) + uno Node (apps)
- ✗Imposible hacer análisis de impacto cruzado
Estado objetivo Después
- ✓apps/dashboard con BUILD.bazel + rules_js/webpack
- ✓Un único pipeline Bazel con remote cache
- ✓bazel build //apps/dashboard:bundle reemplaza npm run build
- ✓Análisis rdeps cross-stack (TS + Python)
1
Wrap Webpack en genrule transitoria
Envuelve el build de Webpack existente en un
genrule para que sea un target Bazel. Sin romper nada, CI sigue verde.⏱ 2h
2
Instalar aspect_rules_js + toolchain Node
Añadir al WORKSPACE.bazel las reglas y
nodejs_register_toolchains(node_version = "20.9.0").⏱ 1h
3
Migrar libs compartidas (una a una)
Empezar por
//libs/api-client y //libs/analytics-utils. Verificar que los tests pasan con bazel test.⏱ 1-2 días por lib
4
Migrar apps/dashboard a BUILD.bazel nativo
Reemplazar la genrule transitoria con targets granulares
ts_project por feature module. Eliminar webpack.config.js.⏱ 3-4 días
5
Eliminar npm scripts del CI
Actualizar
.github/workflows para usar solo bazel build / bazel test. Eliminar pasos de npm install en CI.⏱ 1 día
10
Plan de Acción — 4 semanas
De 25 min a <5 min en CI
Semana 1 — Fundamentos
Remote Cache + .bazelrc optimizado
• Crear bucket GCP y Service Account para CI
• Aplicar nuevo .bazelrc con disk_cache + remote_cache
• Verificar hit rate en el primer CI (objetivo: >60%)
• Reducer de 25 min a ~10 min esperado
• Aplicar nuevo .bazelrc con disk_cache + remote_cache
• Verificar hit rate en el primer CI (objetivo: >60%)
• Reducer de 25 min a ~10 min esperado
Semana 2 — Granularidad
BUILD.bazel por feature en apps/dashboard
• Dividir BUILD.bazel monolítico en 6-8 targets granulares
• Implementar análisis rdeps en el workflow de CI
• Targets afectados: sólo los módulos con cambios
• Reducción adicional de ~4 min en builds incrementales
• Implementar análisis rdeps en el workflow de CI
• Targets afectados: sólo los módulos con cambios
• Reducción adicional de ~4 min en builds incrementales
Semana 3 — Regla ML
ml_model_package + pipeline de modelos
• Implementar regla custom ml_model_package en tools/bazel/rules/
• Migrar los 3 modelos (demand, churn, pricing) al nuevo target
• Integrar bazel build //models/...:*_package en CI de release
• Pipeline de modelos reproducible y versionado
• Migrar los 3 modelos (demand, churn, pricing) al nuevo target
• Integrar bazel build //models/...:*_package en CI de release
• Pipeline de modelos reproducible y versionado
Semana 4 — Migración Webpack
Eliminar Webpack del CI, pipeline unificado
• Wrap Webpack en genrule → migrar libs → migrar dashboard
• Un único pipeline Bazel en GitHub Actions
• Objetivo final: <6 min build limpio, <90s incremental
• Hit rate cache remoto objetivo: >85%
• Un único pipeline Bazel en GitHub Actions
• Objetivo final: <6 min build limpio, <90s incremental
• Hit rate cache remoto objetivo: >85%
Resultado esperado al finalizar: Build CI limpio de 25 min → 6 min. Build incremental de 18 min → 90 segundos. Los 28 ingenieros de DataMesh recuperan ~2h/día en tiempo de espera colectivo. ROI estimado: €420/mes en compute + productividad.