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
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-commitOnPR— comprobaciones por pull requestOnMainMerge— integración al fusionar en mainOnTag— 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 diffinforma de la divergencia entre el modelo y los artefactos generados antes de publicar. Unrelease.ymleditado 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:
- Una imagen ejecutable multi-arch —
docker run …/ontoref/ontoref-daemon:VERSION 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 concrossy 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 diffmuestran 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.