Tools: Tar en macOS destroza archivos en Linux: lo validé en mi pipeline real de Railway y documenté los 3 casos que nadie menciona

Tools: Tar en macOS destroza archivos en Linux: lo validé en mi pipeline real de Railway y documenté los 3 casos que nadie menciona

Tar en macOS destroza archivos en Linux: lo validé en mi pipeline real de Railway y documenté los 3 casos que nadie menciona

Tar macOS Linux error de extracción en producción: el contexto que importa

Los 3 casos reales donde tar macOS rompe un pipeline de Railway

Caso 1: Los archivos ._* de metadata Apple

Caso 2: Permisos que cambian silenciosamente

Caso 3: Paths con espacios en nombres de archivos

Los errores que cometí antes de entender el patrón

Mi configuración actual en Railway

Esto conecta con algo más grande que tar

FAQ: tar macOS Linux error de extracción en producción

Conclusión: el fix es fácil, pero la trampa no es técnica Hay un hilo en Hacker News que resurfaceó esta semana con 107 puntos sobre un artículo de 2024: tar en macOS crea archivos que Linux no puede extraer limpiamente. La comunidad reaccionó como siempre: "usá GNU tar", "instalá gtar con homebrew", "esto es conocido desde hace años". Y sí, todo eso es correcto. Pero hay algo que nadie está diciendo: los 3 escenarios específicos donde esto efectivamente rompe producción no son iguales entre sí, y cada uno tiene un fix distinto. Aprendí esto de la peor manera posible — con un deploy fallido a las 11pm que me llevó dos horas diagnosticar. Mi tesis es que la respuesta "usá GNU tar" es necesaria pero insuficiente si no sabés exactamente por qué tu caso particular explota. Desde que migré de Vercel a Railway en 2024 (un fin de semana que me enseñó más sobre infraestructura real que meses de tutoriales), mi pipeline de deployment depende de artefactos .tar.gz que genero en macOS y extraigo en contenedores Linux. Durante meses funcionó bien. Hasta que no funcionó. El problema de fondo es que BSD tar (el que viene en macOS) y GNU tar (el que corre en Ubuntu, Alpine, Debian) no son el mismo programa. Comparten nombre y sintaxis básica, pero difieren en cómo manejan metadatos extendidos. macOS agrega metadata del sistema de archivos HFS+/APFS que GNU tar no espera encontrar, y cuando la encuentra, puede ignorarla silenciosamente, fallar con warnings que no interrumpen el proceso, o — el peor caso — extraer archivos corruptos sin avisarte. Verificá qué versión de tar tenés en macOS: No son el mismo programa. Nunca lo fueron. Este fue el primer bug que encontré. Cuando macOS crea un .tar.gz desde una carpeta que tocaste con el Finder (o que tuvo atributos extendidos en algún momento), incluye archivos ._nombre_archivo con metadata HFS. Son invisibles en el Finder, pero están en el tar. En mi caso específico, tenía un script de Railway que tomaba el primer archivo .js del directorio para calcular un hash de verificación. El script encontraba ._chunk-abc123.js antes que chunk-abc123.js y el hash fallaba. El deploy completaba, pero la verificación post-deploy lanzaba una alerta. Tardé 90 minutos en conectar esos puntos. La variable de entorno COPYFILE_DISABLE=1 es la más limpia porque actúa en el momento de creación. Sin embargo, si ya tenés archivos .tar.gz viejos en storage, necesitás la opción de filtrado en extracción. Este me costó más porque no había error. El deploy completaba verde, la app levantaba, pero ciertos endpoints devolvían 403. El contenedor no podía leer archivos que, en mi máquina local, tenían permisos 644. El problema: BSD tar en macOS puede serializar permisos de manera diferente para archivos con ACLs (Access Control Lists) de APFS. Cuando GNU tar los extrae, interpreta esos permisos de una forma que puede resultar en bits distintos a los originales. No pasa siempre. Pasa cuando el archivo tuvo algún atributo extendido en algún momento de su historia en el filesystem de macOS. Es el tipo de bug que aparece en producción pero no en staging porque staging tiene otro historial de archivos. La segunda opción es superior porque resuelve el problema en el origen, no en el destino. Si el problema aparece en el destino, ya dependés de que todos los Dockerfiles tengan el fix — y eventualmente alguien va a crear uno nuevo sin él. Este es el más silencioso y el que el artículo de HN original no menciona con suficiente detalle. Si empaquetás desde macOS y algún archivo en el path tiene un espacio (cosa que en macOS el Finder hace completamente normal), el comportamiento de extracción en Linux depende de la versión exacta de GNU tar y cómo procesás la lista de archivos. El tar en sí extrae correctamente con tar -xzf. El problema aparece cuando cualquier script downstream procesa la lista de archivos asumiendo que no hay espacios. En mi caso era un script de invalidación de CDN que leía los paths del tar para saber qué cachés limpiar. La renominación en origen es más robusta porque eliminás la fuente del problema. La iteración con read es un parche que funciona pero que el próximo developer va a romper cuando copie el loop sin entender por qué estaba escrito así. Error 1: Confiar en que "el tar funcionó antes, va a funcionar siempre". Los archivos ._* aparecieron después de que empecé a abrir esa carpeta de assets con el Finder para preview. Antes del Finder, no había metadata. Después del Finder, sí. El pipeline era el mismo; el filesystem no. Error 2: Leer solo los exit codes. GNU tar extrae archivos ._* con exit code 0. No hay error. Tu deploy está "verde" y en producción tenés basura metida en el directorio de build. Necesitás validación post-extracción, no solo exit codes. Error 3: Instalar gtar pero seguir usando tar en los scripts. Después de brew install gnu-tar, en macOS el binario se llama gtar, no tar. Si seguís escribiendo tar en el script de build, seguís usando BSD tar. Lo hice durante una semana. Este override de PATH es lo que terminé usando para mantener los scripts existentes sin modificarlos. Después de validar los tres casos, mi pipeline de build en macOS quedó así: Y en el Dockerfile de Railway, el paso de extracción tiene su propia verificación: Esta doble verificación — en el momento de crear y en el momento de extraer — es lo que me da confianza real. No confío en que el proceso siempre sea perfecto; confío en que si falla, lo voy a saber antes del deploy y no después. Hace unas semanas escribí sobre mis specs en YAML para agentes y sobre la migración de pgbackrest a Barman. En ambos casos el patrón fue el mismo: una herramienta estándar que "funciona" en la mayoría de los casos, hasta que encuentra el edge case específico de producción. Tar es otro ejemplo de esto. El riesgo real no es que tar sea difícil. Es que tar es tan familiar que nadie lo considera un punto de falla. Cuando el deploy se rompe a las 11pm, nadie piensa "probablemente sea tar". Y eso es exactamente por qué estos bugs duelen más de lo que deberían. ¿Por qué tar en macOS genera archivos ._* y cuándo aparecen?

Los archivos ._nombre son Resource Forks de HFS+/APFS — un mecanismo heredado para almacenar metadata de archivos. Aparecen cuando un archivo tuvo atributos extendidos en algún momento: permisos especiales, metadatos de Finder, etiquetas de color, o simplemente cuando el Finder abrió la carpeta para mostrar previews. No aparecen en todos los archivos; aparecen en los que tocó el sistema de archivos de macOS de cierta manera. Es no-determinístico desde la perspectiva del desarrollador. ¿COPYFILE_DISABLE=1 es suficiente o necesito GNU tar igual?COPYFILE_DISABLE=1 evita que BSD tar incluya metadata extendida en el momento de creación. Es suficiente para el Caso 1 (archivos ._*). Para el Caso 2 (permisos con ACLs) y el Caso 3 (paths con espacios en scripts downstream), necesitás GNU tar y normalización de permisos. En la práctica, uso ambas cosas juntas porque el costo es cero y la combinación cubre más casos. ¿GNU tar en macOS via Homebrew tiene algún tradeoff?El único tradeoff real es que se instala como gtar, no como tar, para no romper el sistema. Si sobreescribís el PATH para que tar apunte a GNU tar, tenés que ser consciente de que algunas herramientas del sistema macOS asumen BSD tar con comportamientos específicos. En la práctica, en 18 meses usando el override de PATH no tuve ningún problema, pero es algo que hay que saber. ¿Esto afecta a GitHub Actions o solo a builds locales?Afecta principalmente a builds locales en macOS y a cualquier runner de CI que corra en macOS. Los runners de GitHub Actions en Ubuntu ya usan GNU tar, así que el problema no aparece ahí. El riesgo real es cuando comprimís en macOS local y subís el artefacto para que un sistema Linux lo extraiga — que es exactamente el workflow de deploy manual o semi-manual. ¿Hay alguna forma de detectar si un tar.gz existente tiene metadata Apple sin extraerlo?Sí, con una línea: Podés incluir esto como validación en el CI antes de publicar el artefacto. ¿Por qué Docker build no protege contra esto?

Docker build copia los archivos al contexto de build, pero si el .tar.gz ya tiene metadata Apple dentro, esa metadata viaja dentro del tar — Docker no inspecciona el contenido del tar al copiarlo. El problema ocurre cuando tu Dockerfile hace RUN tar -xzf y extrae el tar corrupto dentro del contenedor. Docker ve un comando que termina con exit code 0 y asume que todo está bien. GNU tar + COPYFILE_DISABLE=1 + verificación post-extracción resuelve los tres casos. La parte técnica está documentada arriba y podés copiarla en cinco minutos. La trampa real es de actitud: tar es tan viejo y tan familiar que nadie lo agrega a la lista de cosas que pueden fallar. Yo tampoco lo tenía en la lista. Hasta que tuve un deploy roto a las 11pm con logs completamente verdes y media hora mirando código que no tenía ningún error. Si trabajás con Kimi K2, Claude o cualquier LLM para generación de código, ninguno te va a advertir sobre este problema a menos que lo conozcas y lo preguntes explícitamente. Si tu stack toca Railway o cualquier infra containerizada, el problema puede aparecer sin ningún indicador visible. Mi recomendación concreta: auditá los tars que actualmente tenés en producción o en storage con tar -tzf archivo.tar.gz | grep "^\._". Si devuelve resultados, tenés trabajo pendiente. Si no devuelve nada, bien — pero igual agregá la verificación al pipeline para que el próximo build local en macOS no rompa esa garantía silenciosamente. Este es el tipo de problema que aparece en los logs de Railway como síntoma de otra cosa. Y eso es exactamente lo que lo hace caro. Fuente original: Hacker News Este artículo fue publicado originalmente en juanchi.dev Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

# En macOS tar --version # Output típico: # bsdtar 3.5.3 - libarchive 3.5.3 zlib/1.2.11 liblzma/5.0.5 bz2lib/1.0.8 # En tu contenedor Linux (Alpine, Ubuntu, etc.) tar --version # Output típico: # tar (GNU tar) 1.34 # Copyright (C) 2021 Free Software Foundation, Inc. # En macOS tar --version # Output típico: # bsdtar 3.5.3 - libarchive 3.5.3 zlib/1.2.11 liblzma/5.0.5 bz2lib/1.0.8 # En tu contenedor Linux (Alpine, Ubuntu, etc.) tar --version # Output típico: # tar (GNU tar) 1.34 # Copyright (C) 2021 Free Software Foundation, Inc. # En macOS tar --version # Output típico: # bsdtar 3.5.3 - libarchive 3.5.3 zlib/1.2.11 liblzma/5.0.5 bz2lib/1.0.8 # En tu contenedor Linux (Alpine, Ubuntu, etc.) tar --version # Output típico: # tar (GNU tar) 1.34 # Copyright (C) 2021 Free Software Foundation, Inc. # Comprimí desde macOS sin precaución: tar -czf artefacto.tar.gz ./dist/ # En el contenedor Linux, extraí y listé: tar -tzf artefacto.tar.gz | grep "^\._" # Output: # ._index.html # ._main.css # ._chunk-abc123.js # ... (uno por cada archivo del build) # Comprimí desde macOS sin precaución: tar -czf artefacto.tar.gz ./dist/ # En el contenedor Linux, extraí y listé: tar -tzf artefacto.tar.gz | grep "^\._" # Output: # ._index.html # ._main.css # ._chunk-abc123.js # ... (uno por cada archivo del build) # Comprimí desde macOS sin precaución: tar -czf artefacto.tar.gz ./dist/ # En el contenedor Linux, extraí y listé: tar -tzf artefacto.tar.gz | grep "^\._" # Output: # ._index.html # ._main.css # ._chunk-abc123.js # ... (uno por cada archivo del build) # Opción A: Eliminar metadata antes de comprimir (en macOS) COPYFILE_DISABLE=1 tar -czf artefacto.tar.gz ./dist/ # Opción B: Filtrar en extracción (en Linux) tar -tzf artefacto.tar.gz | grep -v "^\._" | tar -xzf artefacto.tar.gz -T - # Opción C: La que uso yo en el Dockerfile de Railway # Instalar GNU tar en macOS via Homebrew y usarlo explícitamente -weight: 500;">brew -weight: 500;">install gnu-tar # Luego en el script de build: gtar -czf artefacto.tar.gz --exclude="._*" --exclude=".DS_Store" ./dist/ # Opción A: Eliminar metadata antes de comprimir (en macOS) COPYFILE_DISABLE=1 tar -czf artefacto.tar.gz ./dist/ # Opción B: Filtrar en extracción (en Linux) tar -tzf artefacto.tar.gz | grep -v "^\._" | tar -xzf artefacto.tar.gz -T - # Opción C: La que uso yo en el Dockerfile de Railway # Instalar GNU tar en macOS via Homebrew y usarlo explícitamente -weight: 500;">brew -weight: 500;">install gnu-tar # Luego en el script de build: gtar -czf artefacto.tar.gz --exclude="._*" --exclude=".DS_Store" ./dist/ # Opción A: Eliminar metadata antes de comprimir (en macOS) COPYFILE_DISABLE=1 tar -czf artefacto.tar.gz ./dist/ # Opción B: Filtrar en extracción (en Linux) tar -tzf artefacto.tar.gz | grep -v "^\._" | tar -xzf artefacto.tar.gz -T - # Opción C: La que uso yo en el Dockerfile de Railway # Instalar GNU tar en macOS via Homebrew y usarlo explícitamente -weight: 500;">brew -weight: 500;">install gnu-tar # Luego en el script de build: gtar -czf artefacto.tar.gz --exclude="._*" --exclude=".DS_Store" ./dist/ # En macOS, creé un archivo y verifiqué permisos: ls -la config/settings.json # -rw-r--r-- 1 juan staff 2048 jun 15 22:31 config/settings.json # Empaquetá con BSD tar: tar -czf config.tar.gz config/ # En el contenedor Linux, extraí y verifiqué: tar -xzf config.tar.gz ls -la config/settings.json # -rw------- 1 1000 1000 2048 jun 15 22:31 config/settings.json # ↑ Los permisos cambiaron de 644 a 600 — el grupo y otros perdieron lectura # En macOS, creé un archivo y verifiqué permisos: ls -la config/settings.json # -rw-r--r-- 1 juan staff 2048 jun 15 22:31 config/settings.json # Empaquetá con BSD tar: tar -czf config.tar.gz config/ # En el contenedor Linux, extraí y verifiqué: tar -xzf config.tar.gz ls -la config/settings.json # -rw------- 1 1000 1000 2048 jun 15 22:31 config/settings.json # ↑ Los permisos cambiaron de 644 a 600 — el grupo y otros perdieron lectura # En macOS, creé un archivo y verifiqué permisos: ls -la config/settings.json # -rw-r--r-- 1 juan staff 2048 jun 15 22:31 config/settings.json # Empaquetá con BSD tar: tar -czf config.tar.gz config/ # En el contenedor Linux, extraí y verifiqué: tar -xzf config.tar.gz ls -la config/settings.json # -rw------- 1 1000 1000 2048 jun 15 22:31 config/settings.json # ↑ Los permisos cambiaron de 644 a 600 — el grupo y otros perdieron lectura # En el Dockerfile, forzar permisos explícitos después de extraer: RUN tar -xzf artefacto.tar.gz && \ find ./config -name "*.json" -exec chmod 644 {} \; && \ find ./scripts -name "*.sh" -exec chmod 755 {} \; # O mejor: en el script de build en macOS, normalizar permisos antes de empaquetar: find ./dist -type f -exec chmod 644 {} \; find ./dist -type d -exec chmod 755 {} \; COPYFILE_DISABLE=1 gtar -czf artefacto.tar.gz ./dist/ # En el Dockerfile, forzar permisos explícitos después de extraer: RUN tar -xzf artefacto.tar.gz && \ find ./config -name "*.json" -exec chmod 644 {} \; && \ find ./scripts -name "*.sh" -exec chmod 755 {} \; # O mejor: en el script de build en macOS, normalizar permisos antes de empaquetar: find ./dist -type f -exec chmod 644 {} \; find ./dist -type d -exec chmod 755 {} \; COPYFILE_DISABLE=1 gtar -czf artefacto.tar.gz ./dist/ # En el Dockerfile, forzar permisos explícitos después de extraer: RUN tar -xzf artefacto.tar.gz && \ find ./config -name "*.json" -exec chmod 644 {} \; && \ find ./scripts -name "*.sh" -exec chmod 755 {} \; # O mejor: en el script de build en macOS, normalizar permisos antes de empaquetar: find ./dist -type f -exec chmod 644 {} \; find ./dist -type d -exec chmod 755 {} \; COPYFILE_DISABLE=1 gtar -czf artefacto.tar.gz ./dist/ # En macOS tenía un directorio de assets: ls "dist/static/Open Graph/" # og-image.png # og-video.mp4 # Al empaquetar con BSD tar, el path quedó como: tar -tzf artefacto.tar.gz | grep "Open" # dist/static/Open Graph/og-image.png # dist/static/Open Graph/og-video.mp4 # En Linux, al extraer con un script que procesaba la lista: for archivo in $(tar -tzf artefacto.tar.gz); do # ⚠️ Esto rompe: "Open" y "Graph/og-image.png" son dos tokens separados echo "Procesando: $archivo" done # En macOS tenía un directorio de assets: ls "dist/static/Open Graph/" # og-image.png # og-video.mp4 # Al empaquetar con BSD tar, el path quedó como: tar -tzf artefacto.tar.gz | grep "Open" # dist/static/Open Graph/og-image.png # dist/static/Open Graph/og-video.mp4 # En Linux, al extraer con un script que procesaba la lista: for archivo in $(tar -tzf artefacto.tar.gz); do # ⚠️ Esto rompe: "Open" y "Graph/og-image.png" son dos tokens separados echo "Procesando: $archivo" done # En macOS tenía un directorio de assets: ls "dist/static/Open Graph/" # og-image.png # og-video.mp4 # Al empaquetar con BSD tar, el path quedó como: tar -tzf artefacto.tar.gz | grep "Open" # dist/static/Open Graph/og-image.png # dist/static/Open Graph/og-video.mp4 # En Linux, al extraer con un script que procesaba la lista: for archivo in $(tar -tzf artefacto.tar.gz); do # ⚠️ Esto rompe: "Open" y "Graph/og-image.png" son dos tokens separados echo "Procesando: $archivo" done # Mal: iterar con for sobre $(tar -t...) for archivo in $(tar -tzf artefacto.tar.gz); do invalidar_cache "$archivo" # rompe con espacios done # Bien: usar --null y read para manejar espacios correctamente tar -tzf artefacto.tar.gz | while IFS= read -r archivo; do invalidar_cache "$archivo" # funciona con espacios done # Mejor: evitar el problema desde macOS renombrando antes de empaquetar find ./dist -name "* *" -exec bash -c 'mv "$0" "${0// /_}"' {} \; COPYFILE_DISABLE=1 gtar -czf artefacto.tar.gz ./dist/ # Mal: iterar con for sobre $(tar -t...) for archivo in $(tar -tzf artefacto.tar.gz); do invalidar_cache "$archivo" # rompe con espacios done # Bien: usar --null y read para manejar espacios correctamente tar -tzf artefacto.tar.gz | while IFS= read -r archivo; do invalidar_cache "$archivo" # funciona con espacios done # Mejor: evitar el problema desde macOS renombrando antes de empaquetar find ./dist -name "* *" -exec bash -c 'mv "$0" "${0// /_}"' {} \; COPYFILE_DISABLE=1 gtar -czf artefacto.tar.gz ./dist/ # Mal: iterar con for sobre $(tar -t...) for archivo in $(tar -tzf artefacto.tar.gz); do invalidar_cache "$archivo" # rompe con espacios done # Bien: usar --null y read para manejar espacios correctamente tar -tzf artefacto.tar.gz | while IFS= read -r archivo; do invalidar_cache "$archivo" # funciona con espacios done # Mejor: evitar el problema desde macOS renombrando antes de empaquetar find ./dist -name "* *" -exec bash -c 'mv "$0" "${0// /_}"' {} \; COPYFILE_DISABLE=1 gtar -czf artefacto.tar.gz ./dist/ # Verificar qué tar está ejecutando tu script: which tar # /usr/bin/tar → BSD tar (macOS default) which gtar # /opt/homebrew/bin/gtar → GNU tar (Homebrew) # Si querés que tar sea GNU tar sin cambiar los scripts: echo 'export PATH="/opt/homebrew/opt/gnu-tar/libexec/gnubin:$PATH"' >> ~/.zshrc source ~/.zshrc tar --version # Ahora debería mostrar GNU tar # Verificar qué tar está ejecutando tu script: which tar # /usr/bin/tar → BSD tar (macOS default) which gtar # /opt/homebrew/bin/gtar → GNU tar (Homebrew) # Si querés que tar sea GNU tar sin cambiar los scripts: echo 'export PATH="/opt/homebrew/opt/gnu-tar/libexec/gnubin:$PATH"' >> ~/.zshrc source ~/.zshrc tar --version # Ahora debería mostrar GNU tar # Verificar qué tar está ejecutando tu script: which tar # /usr/bin/tar → BSD tar (macOS default) which gtar # /opt/homebrew/bin/gtar → GNU tar (Homebrew) # Si querés que tar sea GNU tar sin cambiar los scripts: echo 'export PATH="/opt/homebrew/opt/gnu-tar/libexec/gnubin:$PATH"' >> ~/.zshrc source ~/.zshrc tar --version # Ahora debería mostrar GNU tar #!/bin/bash # scripts/build-artefacto.sh # Genera el tar.gz para deployment en Railway set -euo pipefail BUILD_DIR="./dist" OUTPUT="artefacto-$(date +%Y%m%d-%H%M%S).tar.gz" # 1. Normalizar permisos antes de empaquetar echo "→ Normalizando permisos..." find "$BUILD_DIR" -type f -exec chmod 644 {} \; find "$BUILD_DIR" -type d -exec chmod 755 {} \; find "$BUILD_DIR" -name "*.sh" -exec chmod 755 {} \; # 2. Eliminar archivos de metadata macOS echo "→ Limpiando metadata Apple..." find "$BUILD_DIR" -name "._*" -delete find "$BUILD_DIR" -name ".DS_Store" -delete # 3. Crear el tar con GNU tar y sin metadata extendida echo "→ Empaquetando con GNU tar..." COPYFILE_DISABLE=1 gtar \ --exclude="._*" \ --exclude=".DS_Store" \ --exclude=".AppleDouble" \ --exclude=".LSOverride" \ -czf "$OUTPUT" \ "$BUILD_DIR" # 4. Verificar que no haya archivos de metadata en el tar resultante METADATA_COUNT=$(tar -tzf "$OUTPUT" | grep -c "^\._" || true) if [ "$METADATA_COUNT" -gt 0 ]; then echo "❌ ERROR: El tar contiene $METADATA_COUNT archivos de metadata Apple" exit 1 fi echo "✅ Artefacto creado: $OUTPUT" echo " Archivos: $(tar -tzf "$OUTPUT" | wc -l | tr -d ' ')" #!/bin/bash # scripts/build-artefacto.sh # Genera el tar.gz para deployment en Railway set -euo pipefail BUILD_DIR="./dist" OUTPUT="artefacto-$(date +%Y%m%d-%H%M%S).tar.gz" # 1. Normalizar permisos antes de empaquetar echo "→ Normalizando permisos..." find "$BUILD_DIR" -type f -exec chmod 644 {} \; find "$BUILD_DIR" -type d -exec chmod 755 {} \; find "$BUILD_DIR" -name "*.sh" -exec chmod 755 {} \; # 2. Eliminar archivos de metadata macOS echo "→ Limpiando metadata Apple..." find "$BUILD_DIR" -name "._*" -delete find "$BUILD_DIR" -name ".DS_Store" -delete # 3. Crear el tar con GNU tar y sin metadata extendida echo "→ Empaquetando con GNU tar..." COPYFILE_DISABLE=1 gtar \ --exclude="._*" \ --exclude=".DS_Store" \ --exclude=".AppleDouble" \ --exclude=".LSOverride" \ -czf "$OUTPUT" \ "$BUILD_DIR" # 4. Verificar que no haya archivos de metadata en el tar resultante METADATA_COUNT=$(tar -tzf "$OUTPUT" | grep -c "^\._" || true) if [ "$METADATA_COUNT" -gt 0 ]; then echo "❌ ERROR: El tar contiene $METADATA_COUNT archivos de metadata Apple" exit 1 fi echo "✅ Artefacto creado: $OUTPUT" echo " Archivos: $(tar -tzf "$OUTPUT" | wc -l | tr -d ' ')" #!/bin/bash # scripts/build-artefacto.sh # Genera el tar.gz para deployment en Railway set -euo pipefail BUILD_DIR="./dist" OUTPUT="artefacto-$(date +%Y%m%d-%H%M%S).tar.gz" # 1. Normalizar permisos antes de empaquetar echo "→ Normalizando permisos..." find "$BUILD_DIR" -type f -exec chmod 644 {} \; find "$BUILD_DIR" -type d -exec chmod 755 {} \; find "$BUILD_DIR" -name "*.sh" -exec chmod 755 {} \; # 2. Eliminar archivos de metadata macOS echo "→ Limpiando metadata Apple..." find "$BUILD_DIR" -name "._*" -delete find "$BUILD_DIR" -name ".DS_Store" -delete # 3. Crear el tar con GNU tar y sin metadata extendida echo "→ Empaquetando con GNU tar..." COPYFILE_DISABLE=1 gtar \ --exclude="._*" \ --exclude=".DS_Store" \ --exclude=".AppleDouble" \ --exclude=".LSOverride" \ -czf "$OUTPUT" \ "$BUILD_DIR" # 4. Verificar que no haya archivos de metadata en el tar resultante METADATA_COUNT=$(tar -tzf "$OUTPUT" | grep -c "^\._" || true) if [ "$METADATA_COUNT" -gt 0 ]; then echo "❌ ERROR: El tar contiene $METADATA_COUNT archivos de metadata Apple" exit 1 fi echo "✅ Artefacto creado: $OUTPUT" echo " Archivos: $(tar -tzf "$OUTPUT" | wc -l | tr -d ' ')" # Dockerfile — fragmento relevante FROM node:20-alpine AS runner WORKDIR /app # Copiar el artefacto COPY artefacto.tar.gz . # Extraer con verificación explícita RUN tar -xzf artefacto.tar.gz && \ rm artefacto.tar.gz && \ # Verificar que no haya archivos ._* que se colaron METADATA=$(find . -name "._*" | wc -l) && \ if [ "$METADATA" -gt 0 ]; then \ echo "ERROR: Metadata Apple detectada en extracción" && exit 1; \ fi && \ echo "Extracción limpia: $(find . -type f | wc -l) archivos" # Dockerfile — fragmento relevante FROM node:20-alpine AS runner WORKDIR /app # Copiar el artefacto COPY artefacto.tar.gz . # Extraer con verificación explícita RUN tar -xzf artefacto.tar.gz && \ rm artefacto.tar.gz && \ # Verificar que no haya archivos ._* que se colaron METADATA=$(find . -name "._*" | wc -l) && \ if [ "$METADATA" -gt 0 ]; then \ echo "ERROR: Metadata Apple detectada en extracción" && exit 1; \ fi && \ echo "Extracción limpia: $(find . -type f | wc -l) archivos" # Dockerfile — fragmento relevante FROM node:20-alpine AS runner WORKDIR /app # Copiar el artefacto COPY artefacto.tar.gz . # Extraer con verificación explícita RUN tar -xzf artefacto.tar.gz && \ rm artefacto.tar.gz && \ # Verificar que no haya archivos ._* que se colaron METADATA=$(find . -name "._*" | wc -l) && \ if [ "$METADATA" -gt 0 ]; then \ echo "ERROR: Metadata Apple detectada en extracción" && exit 1; \ fi && \ echo "Extracción limpia: $(find . -type f | wc -l) archivos" # Listar archivos ._* sin extraer tar -tzf artefacto.tar.gz | grep "^\._" # Si no devuelve nada, el tar está limpio de metadata Apple # Listar archivos ._* sin extraer tar -tzf artefacto.tar.gz | grep "^\._" # Si no devuelve nada, el tar está limpio de metadata Apple # Listar archivos ._* sin extraer tar -tzf artefacto.tar.gz | grep "^\._" # Si no devuelve nada, el tar está limpio de metadata Apple