Ingeniería de releases desde un solo modelo

Pre-commit, CI, justfiles, imágenes OCI multi-arch y un instalador curl|sh — generados desde un modelo tipado, no mantenidos a mano

Jesús Pérez 8 min de lectura
La ingeniería de releases es la superficie más copiada y menos gobernada de cualquier organización multi-repo: YAML de CI pegado entre servicios hasta que no hay dos pipelines que coincidan. ontoref modela toda la superficie de release como una única capa de flujo de trabajo NCL tipada y genera los artefactos reales desde ella — así el pipeline es consultable como cualquier otro hecho arquitectónico, y la deriva falla antes de publicarse. ADR-038 añadió distribución OCI multi-arch como una entrada de catálogo más un generador, no como un fork.
Ingeniería de releases desde un solo modelo

Ingeniería de releases desde un solo modelo

La ingeniería de releases es la superficie más copiada y menos gobernada de cualquier organización multi-repo. El YAML de CI se pega entre servicios y se parchea repo a repo hasta que no hay dos pipelines que coincidan. El procedimiento de construir-y-publicar vive en la cabeza de quien lo montó. Y nada ata cómo publicamos a las decisiones arquitectónicas que lo restringen. Cuando un registry se mueve, cambia una política de firma o se añade una arquitectura, cada repo se edita a mano y la deriva es invisible — hasta que un release se rompe.

ontoref adopta la postura contraria: toda la superficie de release es un modelo tipado, y los artefactos reales se generan desde él. Este post recorre qué significa eso en concreto, usando la decisión que lo llevó más lejos: distribuir ontoref como imagen OCI multi-arch y como instalador curl | sh (ADR-038).

Una capa de flujo de trabajo, cuatro triggers

La superficie de release vive en un único modelo NCL de flujo de trabajo (ontology/workflow.ncl) con cuatro capas con trigger:

  • OnCommit — hooks de pre-commit
  • OnPR — comprobaciones por pull request
  • OnMainMerge — integración al fusionar en main
  • OnTag — la capa de release

Dentro de esas capas, el trabajo es tipado. Los build kinds llevan 'Bundle y 'Container; los distribution kinds llevan 'ContainerRegistry, 'Artifact, 'Package. Nada de esto son strings en un fichero YAML — son constructores en un modelo tipado que un generador lee.

Los generadores convierten el modelo en los artefactos que realmente ejecutas:

generate-precommit      → .pre-commit-config.yaml
generate-woodpecker     → .woodpecker/*.yml
generate-justfile       → justfiles/*.just
generate-distribution   → .woodpecker/release.yml
render-installer        → install/install.sh
generate-release-justfile → justfiles/release.just

El mismo NCL declara la CI y el build y la distribución. Hay una única fuente de verdad, y cada fichero emitido es regenerable desde ella.

Por qué esto supera a la CI programable

La objeción evidente: la CI programable ya existe — Dagger, Earthly, task runners de monorepo. Convierten los pipelines en código. Pero el código no es un modelo consultable gobernado por decisiones arquitectónicas. Con esas herramientas el pipeline es un programa; con ontoref el pipeline es un hecho que puedes interrogar a través de la misma superficie describe / HTTP / MCP que la ontología y los ADRs.

En concreto, eso aporta dos cosas que tanto el YAML a mano como la CI programable carecen:

  • Detección de deriva. onre workflow diff informa de la divergencia entre el modelo y los artefactos generados antes de publicar. Un release.yml editado a mano se detecta, no se descubre en producción.
  • Anclaje a ADRs. La superficie de release está atada a las decisiones que la restringen. El catálogo de distribución no es config flotante; es evidencia de una propuesta de valor y está guardado por restricciones tipadas en un ADR.

ADR-038: añadir distribución OCI sin un fork

Aquí está la parte que prueba el patrón. Hasta hace poco ontoref solo podía instalarse desde un checkout de fuentes. Se querían dos rutas de instalación, que comparten un único origen de artefacto en el registry OCI:

  1. Una imagen ejecutable multi-arch — docker run …/ontoref/ontoref-daemon:VERSION
  2. curl … | sh — un instalador que detecta SO/arch, descarga el bundle correspondiente (binario + capa de datos + wrappers) e instala en un prefijo.

El mecanismo no fue un montón de scripts sueltos. La capa de flujo de trabajo ya tenía la costura exacta: build_kind ya llevaba 'Container, distribution_kind ya llevaba 'ContainerRegistry | 'Artifact | 'Package, y el catálogo distributions estaba vacío sin generador. La capa se había construido anticipando exactamente esto.

Así que el movimiento coherente fue poblar el catálogo y añadir el generador que faltaba — no inventar una ruta de empaquetado paralela que onre describe nunca podría ver. Un proveedor nuevo es una entrada de catálogo más un generador, no un fork. Esa es toda la forma del cambio, y onre workflow list muestra después la capa de release como cualquier otro artefacto de build.

Build-once: la imagen y el bundle son el mismo binario

Un modelo tipado te deja enunciar una propiedad dura y hacerla cumplir. La de ADR-038 es build-once: el binario musl se cross-compila una vez por arch (cross), y tanto el bundle de instalación como la imagen ejecutable consumen ese mismo binario. La imagen se ensambla con buildah desde el output de cross más la capa de datos — sin docker buildx, sin QEMU, sin docker-in-docker en el runner.

Esto es más barato y determinista: un índice multi-arch construido desde binarios por-arch ya compilados, no ejecutando un builder amd64 bajo emulación arm64. Y es una restricción, no una convención — el modelo prohíbe recompilar por arch para construir la imagen, así que un futuro contribuidor que tire del familiar docker buildx --platform queda frenado por el modelo en vez de publicar silenciosamente un binario divergente.

Inciso — el problema de caché que no tienes que resolver. Los equipos que compilan un workspace grande de Rust dentro de Docker invierten esfuerzo real en domar cargo-chef: cada cambio en código interno invalida la capa de caché de dependencias, y los arreglos (stubbing de los crates del workspace, imágenes base etiquetadas por hash del lockfile) son delicados. ontoref esquiva toda la categoría compilando el binario una vez con cross y ensamblando la imagen desde él. No hay compilación de workspace dentro de Docker que cachear, así que no hay caché que invalidar.

El axioma que la decisión tenía que proteger

Distribuir una imagen de daemon ejecutable arriesga leer ontoref como una dependencia de ejecución — y Protocolo, no Runtime es un axioma invariante. El modelo hace explícita la resolución en vez de confiar en ella. El instalador curl|sh instala un CLI + capa de datos que funcionan sin el daemon (ADR-029: el daemon es una caché, no una dependencia dura); la imagen ejecutable es un acelerador opcional para despliegues en contenedor. Una restricción tipada — daemon-image-optional-not-runtime — vigila que un cambio futuro no pueda hacer el daemon obligatorio sin avisar.

Este es el caso raro en que formalización y adopción se mueven juntas. El miedo siempre es que más formalización aumente la fricción de adopción. Aquí la formalización — un catálogo de distribución NCL más un generador — produjo el artefacto que más baja la fricción: una instalación de una línea con curl | sh en un host pelado, sin fuentes, sin cargo, sin nickel (nickel va incluido).

Qué obtienes por modelarlo

  • ontoref se instala en un host Linux pelado con curl … | sh — sin checkout, sin cargo, sin nickel.
  • El daemon corre como contenedor desde el mismo origen de registry, multi-arch, firmado con cosign y con un SBOM atestado.
  • La distribución es NCL consultable: onre workflow list / onre workflow diff muestran la capa de release y atrapan la deriva; la regeneración es determinista.
  • Una puerta de publicación (smoke-bundle) extrae el bundle recién ensamblado y ejecuta el binario antes de publicar nada — un artefacto roto rompe el pipeline en vez de llegar a los usuarios.

El punto no es que ontoref tenga una CI bonita. Es que cómo publicamos ya no es conocimiento tribal ni YAML copiado. Es un hecho tipado, anclado a las decisiones que lo restringen, regenerable bajo demanda, e incapaz de derivar sin que el modelo lo diga primero.

¿Te ha resultado útil? Valóralo
¿Tienes algo que aportar? Cuéntame qué opinas, qué sugieres, o si seguimos explorando este tema.
· lecturas

Usamos cookies para que este sitio funcione, entender el uso del servicio y apoyar acciones de marketing. Política de cookies para más información.