29 oct 2025·7 min de lectura

Compilaciones reproducibles para bases de código heredadas: detén la deriva

Aprende cómo lograr compilaciones reproducibles en bases de código heredadas: fija la versión de Node, aplica lockfiles y alinea dev, CI y producción.

Compilaciones reproducibles para bases de código heredadas: detén la deriva

Por qué los proyectos heredados siguen rompiéndose entre máquinas

Las bases de código heredadas suelen fallar de la forma más molesta: de manera inconsistente. Un desarrollador ejecuta la app bien, otro obtiene un error críptico. CI pasa por la mañana y falla por la tarde. Un cambio pequeño que parece inocuo se despliega y en producción se comporta diferente.

Esa deriva de "funciona en mi máquina" significa que el código no es lo único que decide si tu compilación tiene éxito. Diferencias ocultas entre laptops, runners de CI y servidores de producción cambian qué se instala, cómo se ejecuta y qué se despliega realmente.

Lo peor es la aleatoriedad. Dejas de confiar en los tests porque son inestables. Pierdes tiempo persiguiendo bugs que no puedes reproducir. Y empiezas a aplicar parches arriesgados (como fijar una dependencia a mano) solo para salir del paso, lo que puede crear nuevas sorpresas después.

La mayoría de las causas son simples y solucionables:

  • Diferentes versiones de Node.js (incluso diferencias menores pueden romper módulos nativos o herramientas)
  • Lockfiles ausentes o ignorados, así las instalaciones obtienen árboles de dependencias ligeramente distintos
  • Herramientas globales (npm, Yarn, pnpm, TypeScript, ESLint) que difieren por máquina
  • Scripts postinstall que se comportan diferente según el SO o el shell
  • Cachés en CI que ocultan problemas que una instalación limpia revelaría

El objetivo es sencillo: las mismas entradas deben producir la misma salida en todas partes. Misma versión de Node, mismo gestor de paquetes, mismo grafo de dependencias, mismos pasos de compilación, mismos artefactos.

Una vez que eliminas la deriva, las fallas dejan de parecer aleatorias. Cuando algo se rompe, se rompe para todos, en el mismo sitio, con el mismo error. Ahí es cuando un proyecto heredado vuelve a ser mantenible.

Qué significa una compilación reproducible en proyectos Node

Una compilación reproducible significa que puedes tomar la misma base de código, ejecutar los mismos comandos y obtener el mismo resultado cada vez. En proyectos Node, ese "resultado" no es solo "funciona en mi laptop". Debe comportarse igual para cualquier miembro del equipo, en CI y en producción.

Si una máquina usa Node 18 y otra Node 20, o una instalación obtiene paquetes más nuevos que otra, realmente no tienes el mismo proyecto.

En un repo Node sano, esto debe ser consistente:

  • Instalación: un clone limpio se instala sin arreglos manuales
  • Salida de build: las mismas fuentes producen artefactos funcionalmente idénticos
  • Tests: la misma ejecución de tests pasa o falla por las mismas razones
  • Scripts: npm run build (o similar) se comporta igual en todas partes
  • Errores: cuando algo falla, falla igual, no de forma aleatoria

Algunas cosas no serán idénticas por diseño. Timestamps en bundles, rutas específicas de la máquina, variables de entorno y llamadas a servicios externos pueden añadir ruido. La solución no es fingir que no existen. Es hacer deterministas las dependencias, versiones de herramientas y pasos de build, y mantener la configuración de runtime separada.

Suele ser evidente que falta reproducibilidad cuando una instalación limpia falla, o cuando CI falla pero las laptops pasan. Otra señal clara es cuando borrar node_modules cambia el comportamiento, o cuando dos compañeros obtienen versiones diferentes de la misma dependencia tras ejecutar install.

Si no puedes clonar el repo en una máquina nueva y llegar a una compilación que pase con un conjunto pequeño y documentado de comandos, la deriva ya ha empezado.

Elige una única fuente de verdad para versiones y scripts

Los proyectos heredados se rompen porque las reglas viven en la cabeza de la gente. Un dev usa Node 18, CI usa Node 20, producción sigue en 16 y nadie se da cuenta hasta que una dependencia cambia comportamiento. Elige un lugar en el repo donde la verdad esté escrita y sea aplicada.

Empieza decidiendo dónde declarar versiones. Ponlas en archivos que viajen con el código, no en una línea del README que se quede obsoleta. Opciones comunes son un archivo de versión de Node gestionado por el repo, una configuración del gestor de paquetes y (si usas contenedores) una etiqueta de imagen base que no flote.

Luego, acuerden los puntos de entrada de build. Todos deben ejecutar los mismos comandos para instalar, compilar y testear. Si hay múltiples formas de compilar (scripts personalizados, flags ad-hoc, carpetas distintas), la deriva volverá.

Una regla útil: si CI no puede ejecutarlo desde un checkout limpio usando los scripts del proyecto, no forma parte del build.

Antes de cambiar nada, captura una línea base desde un estado limpio y escríbela: el comando usado, la versión de Node, el gestor de paquetes y si los tests pasan. Eso te dará una referencia cuando algo falle después de endurecer las reglas.

Fija versiones de Node y del gestor de paquetes

La mayoría de los bugs de "funciona en mi máquina" empiezan antes de que corra tu código. Si una persona está en Node 18, CI en Node 20 y producción en Node 16, estás probando tres apps distintas.

Fija Node en un lugar que los desarrolladores de verdad sigan. Un simple .nvmrc o .node-version en el repo hace visible la versión esperada en cuanto alguien abre el proyecto. Respáldalo en package.json para que las herramientas puedan avisar (o fallar) cuando la versión sea incorrecta.

Luego fija la versión del gestor de paquetes. Confiar en el npm/yarn/pnpm instalado globalmente invita a cambios silenciosos en la resolución de dependencias. Bloquéalo en el proyecto para que todos instalen de la misma manera en todos los entornos.

{
  "engines": {
    "node": ">=20 <21"
  },
  "packageManager": "[email protected]"
}

Añade una verificación rápida de versión que se ejecute antes de installs o tests. Debe fallar con un mensaje claro, no con un error misterioso 10 minutos después. Manténla estricta:

  • Comprobar que node -v coincide con tu versión mayor fijada
  • Comprobar que la versión del gestor de paquetes coincide con packageManager
  • Fallar CI inmediatamente si cualquiera no coincide

Forzar lockfiles e instalaciones deterministas

Los lockfiles son la diferencia entre "todos instalamos dependencias" y "todos instalamos las mismas dependencias." Esa igualdad es lo que evita rupturas aleatorias cuando una dependencia transitiva lanza un parche.

Primero, elige un gestor de paquetes y apégate a él. Herramientas mixtas crean deriva silenciosa: una persona usa npm, otra usa Yarn, CI usa pnpm y terminas con árboles de dependencias distintos incluso si package.json no cambió.

Limpia el repo para que haya solo un lockfile que coincida con tu herramienta elegida (package-lock.json, yarn.lock o pnpm-lock.yaml). Si ves más de uno, trátalo como un bug, no como una preferencia.

Usa comandos de instalación que rechacen sorpresas

Las instalaciones deterministas fallan rápido cuando el lockfile y package.json no coinciden. Eso es lo que quieres.

# npm
npm ci

# Yarn (Berry)
yarn install --immutable

# pnpm
pnpm install --frozen-lockfile

Si la instalación falla, arregla el lockfile correctamente en vez de relajar las reglas. La idea es parar los cambios ocultos.

Haz que el lockfile no sea opcional

Trata los cambios en el lockfile como cambios de código: revísalos y bloquea merges que los olviden.

  • CI falla si package.json cambió pero el lockfile no
  • CI falla si el repo contiene múltiples lockfiles
  • Los revisores rechazan "corrí install y actualizó un montón de cosas" sin una razón clara
  • Las actualizaciones de dependencias se agrupan y explican, no se mezclan con PRs de features

Haz que CI se comporte como una máquina local limpia

Get it fixed this week
Most projects are completed in 48-72 hours after a free code audit.

Mucha deriva sobrevive porque las laptops llevan estado oculto. CI debe ser lo contrario: una máquina nueva, cada vez, usando los mismos pasos de instalación y build que el equipo usa localmente.

Trata cada ejecución de CI como un checkout nuevo. No confíes en node_modules sobrantes, archivos generados o herramientas globales. Si el build solo pasa cuando algo ya existe, no es un build real.

Mantén un script como fuente de verdad. Si los desarrolladores ejecutan npm run build, CI debe ejecutar ese mismo script exacto, no una cadena de comandos personalizada.

Un enfoque práctico para CI:

  • Hacer checkout en un workspace limpio en cada ejecución
  • Instalar solo desde el lockfile (sin instalaciones "best effort")
  • Ejecutar los mismos scripts que local: lint, test, build
  • Fallar cuando algo importante esté fuera (conflictos de peer deps, vars de entorno faltantes, errores de tipos)
  • Guardar artefactos solo después de que el build tenga éxito

El caching puede ayudar, pero también puede ocultar problemas. Cachea solo lo que sea seguro reutilizar e invalídalo cuando cambien las dependencias.

  • Cachea el caché de descargas del gestor de paquetes (no node_modules)
  • Keyea el caché con el hash del lockfile
  • Expira el caché cuando cambie Node.js o la versión del gestor de paquetes

Alinea producción con lo que CI realmente compiló

Muchos bugs de "funciona en mi máquina" aparecen tras el deploy porque producción no ejecuta lo mismo que CI probó. Trata a CI como el lugar donde se decide la realidad y haz que producción lo iguale.

Primero, elige dónde se hacen los builds y mantente en esa decisión:

  • Compilar en CI y desplegar el artefacto terminado (o la imagen de contenedor), o
  • Compilar en producción, pero entonces producción debe usar exactamente la misma versión de Node, gestor de paquetes y comandos de instalación que CI

Mezclar ambos es como conseguir cambios sorpresa en dependencias.

Si usas Docker, fija la etiqueta de la imagen base en vez de una etiqueta flotante. Un pequeño cambio en la imagen base puede cambiar Node, OpenSSL o librerías del sistema y crear un "mismo código, comportamiento distinto" al desplegar. Actualiza la imagen base a propósito y deja que CI la pruebe.

Mantén las variables de entorno separadas de la salida del build. Los secretos y valores específicos de entorno deben inyectarse en runtime, no incorporarse en un bundle compilado o cometerse en archivos de config. Esto es tanto un asunto de seguridad como de reproducibilidad.

Finalmente, verifica que el runtime desplegado realmente coincide con lo que fijaste. Si CI usa Node 20, producción no debería ejecutar silenciosamente Node 18.

Paso a paso: elimina la deriva sin romper al equipo

Make the prototype production-ready
We turn broken AI-generated prototypes into production-ready software with expert verification.

La deriva suele empezar pequeña: alguien actualiza Node, otro borra el lockfile, CI usa otro comando de instalación y producción obtiene un árbol de dependencias ligeramente distinto. Arréglalo por fases para no bloquear el trabajo diario.

Empieza con una línea base y luego endurece reglas gradualmente:

  • Captura la realidad actual: versión de Node, gestor de paquetes, comando de instalación y si hay un lockfile y se usa realmente
  • Elige y fija versiones esperadas: añade un archivo de versión de Node y bloquea la versión del gestor de paquetes para que todos usen las mismas herramientas
  • Haz las instalaciones deterministas en todas partes: actualiza CI para usar instalaciones limpias y construir desde un workspace limpio en cada ejecución
  • Prueba que funciona desde cero: haz una prueba de "clone fresco" en una máquina nueva o carpeta limpia y luego build usando ajustes parecidos a producción
  • Aplica reglas después de validar: añade comprobaciones fail-fast (Node mismatch, cambios faltantes en lockfile) y protege el lockfile de ediciones casuales

Un patrón común en prototipos heredados generados por IA es que fijar Node y forzar instalaciones congeladas transforma una falla aleatoria en una clara y consistente (dependencia faltante, requirement de engine incompatible o un script que solo funcionaba en una laptop). Una vez consistente, es reparable.

Errores comunes que recrean el "funciona en mi máquina"

La mayoría de los equipos empiezan con buenas intenciones y luego pequeños atajos vuelven a traer la deriva. El objetivo es simple: el mismo código debe compilar igual en una laptop, en CI y en producción.

Una trampa común es usar una instalación "best effort" en CI. npm install puede actualizar el lockfile, jalar árboles de dependencias ligeramente distintos o comportarse diferente entre versiones de npm. Así obtienes un build verde localmente y rojo en CI al día siguiente.

Otro error es tratar node_modules como parte del proyecto. Cometerlo, cachearlo en exceso o asumir que ya está presente oculta problemas reales de dependencias. Entonces una máquina nueva (o un runner limpio de CI) se convierte en el primer sitio donde aparecen los problemas.

También: elige un solo gestor de paquetes. Cuando un repo mezcla artefactos de npm, Yarn y pnpm, la gente "arregla" un problema cambiando comandos. Eso suele funcionar una vez y luego cambia silenciosamente el grafo de dependencias.

Los causantes de deriva que aparecen una y otra vez:

  • CI usa una instalación no determinista (por ejemplo, actualizando el lockfile durante el build)
  • El repo depende de node_modules existentes en vez de una instalación limpia
  • Varios gestores de paquetes en el mismo repo con múltiples lockfiles
  • Builds que dependen de CLIs globales (instalados en la máquina de alguien, faltantes en CI)
  • Imágenes Docker o runtimes sin fijar (por ejemplo, usando latest)

Lista rápida antes de confiar en el build

Antes de pasar otro día "arreglando CI", demuestra que el proyecto puede compilar desde cero en una máquina limpia. Si falla ahí, fallará en producción tarde o temprano.

Las 5 comprobaciones que atrapan la mayor parte de la deriva

Empieza con una prueba de clone fresco. En una laptop nueva o una carpeta temporal limpia (sin node_modules viejo), ejecuta install, build y tests exactamente como está escrito en el repo. Si necesitas pasos extra que no están documentados, no tienes aún un build fiable.

Confirma versiones. La versión de Node.js y la del gestor de paquetes deben coincidir con lo que el repo espera, no con lo que esté instalado globalmente.

Revisa el lockfile. Debe haber exactamente un lockfile, debe estar comprometido y solo debe cambiar cuando actualices dependencias intencionalmente.

Asegura que CI instala de forma determinista. CI debe usar el comando de instalación limpio para tu herramienta (por ejemplo npm ci en lugar de npm install) para que no pueda reescribir el lockfile en silencio ni jalar paquetes transitivos más nuevos.

Verifica que producción coincida con lo que CI compiló. La versión de Node en runtime en producción debe coincidir con la versión fijada y tu deploy debe enviar la misma salida de build que creó CI (no volver a compilar desde cero con un entorno distinto).

Ejemplo: estabilizar un prototipo heredado generado por IA

Get a plan you can follow
Get a clear list of what’s breaking reproducibility and the exact order to fix it.

Un fundador hereda una app Node generada por una herramienta de IA. Funciona en la laptop del desarrollador original, pero CI falla con errores vagos como "Cannot find module", "Unsupported engine" o tests que pasan localmente y fallan en CI.

Tras una comprobación rápida, el patrón es familiar:

  • Dev local está en Node 20, CI en Node 18 y producción aún en Node 16
  • No hay lockfile (o está ignorado), así cada instalación obtiene versiones de dependencias ligeramente distintas
  • CI restaura dependencias cacheadas, por lo que nunca se comporta como una máquina limpia

La solución no es sofisticada. Es hacer el build determinista y luego forzar a todos los entornos a seguirlo.

Fija Node.js (y el gestor de paquetes), añade o restaura el lockfile, cambia las instalaciones de CI a modo estricto y haz al menos una instalación fría local (borra node_modules, instala desde cero) para demostrar que tu laptop no está ocultando problemas.

El resultado que buscas es aburrido: el mismo commit produce el mismo grafo de dependencias y el mismo resultado de build en todas partes.

Siguientes pasos si tu build heredado sigue inestable

Si sigues viendo bugs de "funciona en mi máquina" después de lo básico, deja de cambiar cinco cosas a la vez. Estandariza en un orden estricto: versiones primero, lockfiles segundo y luego reglas de CI. Cada paso debe eliminar variables.

Escribe lo que vas a tratar como verdad para este repo: la versión de Node.js, el gestor de paquetes y su versión, y el comando de instalación único que todos deben usar. Mantén el conjunto de reglas pequeño y visible.

Si la base de código está desordenada (especialmente prototipos generados por IA), las compilaciones reproducibles son la primera tarea de reparación, no un extra. Hasta que las compilaciones sean predecibles, cualquier otra corrección es más difícil de verificar.

Si necesitas ayuda rápida, FixMyMess (fixmymess.ai) se enfoca en tomar apps generadas por IA que están rotas y llevarlas a producción. Una auditoría de código gratuita puede señalar de dónde viene la deriva (versiones, lockfiles, scripts ocultos) y luego el equipo puede arreglar el build y los problemas subyacentes en un solo paso.

Preguntas Frecuentes

Why does the app work on one laptop but fail on another?

Start by confirming everyone is running the same Node major version and the same package manager version. Then delete node_modules, reinstall from the lockfile using a strict install command, and rerun the failing script.

If it still differs, compare the exact error output and the environment variables used in each place (local, CI, production) to find what’s changing.

What does “reproducible build” actually mean for a Node repo?

In Node projects, reproducible builds mean a clean clone of the repo can install, build, and test with the same commands and get the same outcome across machines. The key is that dependencies and tooling resolve the same way every time.

You’re not trying to make timestamps or machine paths identical; you’re trying to remove version and install drift so failures stop being random.

How do we pin the Node.js version so people actually follow it?

Pin Node in the repo so it’s visible and enforceable, like using .nvmrc or .node-version, and also declare it in package.json under engines. Then make CI fail early if the Node version doesn’t match.

The fastest win is consistency: one pinned major version used by developers, CI, and production.

How do we stop different npm/yarn/pnpm versions from changing installs?

Set the package manager version in package.json using the packageManager field and make CI use that exact tool. This prevents “same code, different dependency tree” issues that happen when different npm/yarn/pnpm versions resolve dependencies differently.

If someone upgrades their global tooling, the project still installs the same way because the repo defines the expected version.

What’s the simplest way to enforce lockfiles and deterministic installs?

Use the strict install command for your package manager and treat lockfile mismatches as errors, not warnings. Strict installs force the install to match what was previously resolved, instead of silently pulling newer transitive dependencies.

If strict install fails, update the lockfile on purpose and commit it, rather than loosening rules to “get it green.”

How should we cache dependencies in CI without hiding problems?

Avoid caching node_modules and cache only the package manager’s download cache, keyed on the lockfile. That keeps builds fast without preserving broken state.

If you do cache aggressively and CI “mysteriously” passes, you can end up shipping a build that only works because of leftovers. A clean install in CI is the reality check.

Should we build in CI or build in production?

Pick one: either build in CI and deploy the built artifact/container, or build in production but then production must match CI’s Node version, package manager, and install mode. Mixing approaches is a common source of surprise changes.

Also make sure production isn’t using an unpinned runtime or base image that can change underneath you.

How do we handle environment variables and secrets without breaking reproducibility?

Keep secrets and environment-specific values out of build outputs and out of the repo. Inject them at runtime via environment variables or your deployment platform’s config.

This improves both security and predictability because the same build artifact can be used across environments without baking in machine-specific settings.

Our CI is flaky—what changes usually fix it fastest?

Make CI run the exact same scripts developers run, from a clean checkout, using strict installs. Then remove alternate build paths so there’s one official way to install, test, and build.

If the repo relies on global CLIs, move them into devDependencies and call them via project scripts so every environment uses the same versions.

Can FixMyMess help stabilize an inherited AI-generated Node app quickly?

When inherited or AI-generated projects keep drifting, the fastest path is often a focused audit that pins versions, restores a single lockfile, and makes CI install and build from scratch the same way every time. Once failures are consistent, the underlying code issues are much easier to fix.

If you want it handled end-to-end, FixMyMess can audit the repo for drift sources and typically stabilize builds quickly so the app becomes maintainable again.