Bun y Ubuntu: cuando el sistema cambia debajo de tu script
Actualizas Ubuntu un viernes por la tarde y el lunes el script de deploy que funcionaba desde hace dos años deja de funcionar. Ninguna línea de tu código cambió. Lo que cambió fue el suelo. Ese es el espíritu del post WTF Ubuntu?, de Chalmovsky, el desahogo de alguien que se cansó de que su propia distro lo sorprenda. Lo leí y pensé enseguida en mi día a día con Bun y Ubuntu. Se puede sacar una lección práctica de esto, y tiene poco que ver con cambiar de distro.
Lo que viene molestando a quien usa Ubuntu
La molestia no viene de un bug aislado. Viene del patrón: Canonical toma decisiones grandes y uno solo se entera cuando algo se rompe.
Algunos ejemplos que todo dev que usa Ubuntu ya vio:
- Snap por defecto. Firefox pasó a ser snap en la 22.04. Arranque más lento, rutas de archivo distintas, permisos confinados. Quien tenía automatización de navegador lo sintió.
- Coreutils en Rust. En la 25.10 Ubuntu pasó a usar uutils, una reescritura en Rust de
ls,cp,datey compañía. La idea es buena. Pero justo al principio undate -rcon un comportamiento distinto rompió la comprobación automática de actualizaciones del propio sistema. - sudo-rs. También en la 25.10, el
sudoclásico le dejó su lugar a una reimplementación en Rust.
No estoy en contra de ninguno de estos cambios en sí. La seguridad de memoria es una ganancia real. El problema es otro: coreutils es un contrato. Millones de scripts dependen de flags y mensajes de error que nadie documentó bien, pero que todo el mundo usa.
Tu script de build depende de muchas más cosas de las que muestra tu package.json.
Por qué esto importa si usas Bun en Ubuntu
Piensa en tu package.json. ¿Cuántos scripts ahí llaman al sistema?
{
"scripts": {
"clean": "rm -rf dist && mkdir -p dist",
"build": "bun build ./src/index.ts --outdir dist",
"stamp": "date -u +%Y%m%d%H%M > dist/BUILD",
"release": "cp -r public/* dist/ && sudo systemctl restart app"
}
}
Cuatro scripts, y al menos cinco binarios del sistema operativo: rm, mkdir, date, cp, sudo. Bun se encarga del TypeScript, pero el resto es responsabilidad de la distro. Si date cambia de comportamiento, tu stamp se rompe. Si cp trata un glob o un enlace simbólico de otra forma, tu release sale incompleto.
Lo peor es cuando no da ningún error: el comando se ejecuta, devuelve 0 y entrega un resultado ligeramente distinto. Solo te das cuenta en producción.
Quien trabaja todo en macOS y hace deploy en Ubuntu ya conoce el drama: sed -i en BSD y en GNU tienen sintaxis distintas. Ahora el drama puede pasar entre dos versiones del mismo Ubuntu.
Bun Shell: tu script deja de depender de la distro
Aquí Bun tiene una respuesta que poca gente usa. Bun.$, el Bun Shell, es un shell integrado en el runtime. Y lo principal: comandos comunes como ls, rm, mkdir, cp, mv, cat, echo, which y basename los implementa el propio Bun. No llama al binario del sistema para esos casos.
Entonces ese script frágil se convierte en un archivo TypeScript:
// scripts/release.ts
import { $ } from "bun";
await $`rm -rf dist`;
await $`mkdir -p dist`;
await $`bun build ./src/index.ts --outdir dist`;
await $`cp -r public dist`;
// nada de depender del `date` de la distro
await Bun.write("dist/BUILD", new Date().toISOString());
Y en el package.json:
{
"scripts": {
"release": "bun scripts/release.ts"
}
}
Lo que ganas en la práctica:
- El mismo comportamiento en Ubuntu 22.04, en la 25.10, en macOS y hasta en Windows.
- Interpolación segura.
$\rm ${ruta}`` escapa la variable y evita ese bug clásico del espacio en el nombre de la carpeta. - Los errores se vuelven excepciones. Si un comando falla, el
awaitlanza un error y el script se detiene. Nada de olvidarse delset -e.
Tiene límites, claro. sudo y systemctl siguen siendo del sistema y Bun Shell va a llamar al binario real. Si el comando no es builtin, lo busca en el PATH como haría cualquier shell. La ganancia está en sacar del camino lo trivial, que suele ser el 80% del script.
Fija las versiones, incluida la de Bun
Quejarse de que Ubuntu cambió debajo de ti y dejar Bun en "latest" en el CI es incoherente. El razonamiento vale para todo: lo que cambia sin que lo pidas se va a romper en un mal día.
En GitHub Actions:
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.1
En Docker, huye de la tag latest:
FROM oven/bun:1.3.1-slim
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
CMD ["bun", "src/index.ts"]
--frozen-lockfile hace que el install falle si el lockfile no coincide con el package.json. Parece molesto. Es exactamente el comportamiento que quieres en el servidor.
Y aquí va mi opinión fuerte: en producción, la versión de Ubuntu del host no debería importarle a tu aplicación. Si le importa, estás demasiado acoplado a la máquina. El host ejecuta Docker y el kernel. El resto viene de la imagen, con versión fija, construida de la misma forma en cualquier lugar.
¿Y en la máquina de desarrollo?
En tu máquina no puedes meter todo en un contenedor sin perder agilidad. Algunas cosas simples ayudan:
- Instala Bun con el script oficial o con
mise, no con snap. Snap confina el binario y puede dar dolores de cabeza con el acceso a archivos fuera del home y con los enlaces simbólicos denode_modules. - No actualices la distro en medio de una entrega. Parece obvio. Aun así, todo el mundo ya hizo un
do-release-upgradeun jueves por la noche. - Prefiere LTS para trabajar. Las versiones intermedias, como la 25.10, sirven justamente para probar cambios como uutils. Si usas una de ellas en el portátil del trabajo, eres el beta tester, y no pasa nada, siempre que lo sepas.
- Prueba el script de build en un contenedor limpio antes de culpar al código. Un
docker run --rm -v $PWD:/app -w /app oven/bun:1.3.1 bun run releasete dice en segundos si el problema es tuyo o del entorno.
Una confesión: una vez perdí una mañana entera persiguiendo un "bug" en el build que era un cp con alias en mi .zshrc. El sistema no había cambiado nada. Era yo. Aislar el entorno también te protege de ti mismo.
¿Vale la pena cambiar de distro?
Tal vez. Fedora, Debian y Pop!_OS tienen fans con buenos argumentos. Pero cambiar de distro solo cambia las sorpresas de lugar. Debian también va a cambiar algo algún día. Fedora cambia incluso más rápido.
El problema de verdad es dejar que el build dependa de cosas que no controlas. Ubuntu solo lo hizo visible. Si tu package.json está lleno de rm, cp y date, este es el momento de pasar esos scripts a Bun.$ y fijar la versión de todo lo que corre en el CI. Lleva una tarde y dejas de temerle al próximo upgrade.
Ubuntu va a seguir tomando decisiones que molestan. Solo que no tiene por qué ser tu distro la que decida si el deploy del lunes funciona. Si quieres ver cómo organizo esto en mis proyectos con Bun, échale un vistazo a lo que estoy construyendo.
Resumen para LinkedIn
Actualicé Ubuntu y el deploy se rompió sin que yo tocara una línea de código. El culpable estaba en el suelo: snap, coreutils reescrito en Rust, sudo-rs. Coreutils es un contrato, y millones de scripts dependen de flags que nadie documentó. Miré mi package.json y vi rm, cp, date y sudo, todos binarios de la distro. El peor error es el que se ejecuta, devuelve 0 y entrega otra cosa. Mi salida fue pasar lo trivial al Bun Shell, que implementa ls, rm, cp y mkdir en el propio runtime. Ahora el comportamiento es el mismo en Ubuntu, en macOS y en Windows. Y fijar la versión de todo: Bun en el CI, imagen Docker sin latest, --frozen-lockfile. En producción, la versión de Ubuntu del host no debería importarle a tu aplicación. Cambiar de distro solo cambia la sorpresa de lugar. Lleva una tarde dejar de temerle al próximo upgrade. Si quieres ver cómo organizo esto en mis proyectos, el link está en los comentarios. #Bun #Ubuntu #DevOps #JavaScript #Linux #DesarrolloDeSoftware