GitHub Actions vs GitLab CI para backend, web y móvil
Comparativa de GitHub Actions y GitLab CI para monorepos: configuración de runners, manejo de secretos, cachés y patrones prácticos de pipeline para backend, web y móvil.

Con qué lucha la gente en CI para múltiples apps
Cuando un repositorio compila un backend, una app web y apps móviles, el CI deja de ser "solo ejecutar tests". Se convierte en un controlador de tráfico para diferentes toolchains, distintos tiempos de build y reglas de lanzamiento distintas.
El problema más común es simple: un cambio pequeño dispara demasiado trabajo. Una edición de docs lanza la firma de iOS, o un ajuste en el backend fuerza una reconstrucción completa de la web, y de pronto cada merge se siente lento y arriesgado.
En setups multi-app, aparecen pronto unos cuantos problemas:
- Runner drift: las versiones de SDK difieren entre máquinas, por lo que los builds se comportan distinto entre CI y local.
- Expansión de secretos: claves API, certificados de firma y credenciales de tiendas se duplican entre jobs y entornos.
- Confusión de cachés: una clave de caché incorrecta crea builds obsoletos, pero no tener cache hace todo dolorosamente lento.
- Reglas de release mezcladas: los backends quieren despliegues frecuentes, mientras que los móviles están restringidos y requieren verificaciones extras.
- Legibilidad del pipeline: la configuración crece hasta formar un muro de jobs que nadie quiere tocar.
Por eso la elección entre GitHub Actions y GitLab CI importa más en monorepos que en proyectos de una sola app. Necesitas formas claras de dividir trabajo por ruta, compartir artefactos de forma segura y evitar que jobs paralelos se estorben.
Una comparación práctica se reduce a cuatro cosas: configuración y escalado de runners, almacenamiento y alcance de secretos, caché y artefactos, y qué tan fácil es expresar "solo compilar lo que cambió" sin convertir el pipeline en una sopa de reglas frágiles.
Esto va sobre fiabilidad y mantenibilidad del día a día, no sobre qué plataforma tiene más integraciones o una UI más bonita. Tampoco sustituye decisiones sobre tus herramientas de build (Gradle, Xcode, Docker, etc.). Te ayuda a elegir el CI que facilite mantener una estructura limpia.
Cómo están estructurados GitHub Actions y GitLab CI
La diferencia más grande es cómo cada plataforma organiza pipelines y la reutilización, y eso empieza a importar cuando backend, web y móvil comparten el mismo repo.
GitHub Actions almacena la automatización en archivos YAML bajo .github/workflows/. Los workflows se disparan en eventos como pushes, pull requests, cron o ejecuciones manuales. GitLab CI se centra en .gitlab-ci.yml en la raíz del repo, con archivos opcionales incluidos, y los pipelines suelen ejecutarse en pushes, merge requests, schedules y jobs manuales.
GitLab está construido alrededor de stages. Defines etapas (build, test, deploy) y asignas jobs a etapas que corren en orden. GitHub Actions está construido alrededor de workflows que contienen jobs. Los jobs corren en paralelo por defecto y añades dependencias cuando algo debe esperar.
Para ejecutar la misma lógica sobre muchos targets, las matrices de GitHub son una opción natural (iOS vs Android, múltiples versiones de Node). GitLab puede hacer un fan-out similar usando jobs paralelos y variables, pero a menudo terminas conectando más piezas manualmente.
La reutilización se ve distinta también. En GitHub, los equipos suelen apoyarse en reusable workflows y composite actions. En GitLab, la reutilización suele venir de include, plantillas compartidas y anchors/extends en YAML.
Las aprobaciones y los entornos protegidos también difieren. GitHub usa a menudo entornos protegidos con revisores requeridos y secrets por entorno para que los deploys a producción se pausen hasta aprobarse. GitLab combina ramas/etiquetas protegidas, entornos protegidos y jobs manuales para que solo roles específicos puedan hacer un despliegue.
Configuración de runners y ejecución de jobs
La configuración de runners es donde las dos plataformas empiezan a sentirse distintas en el uso diario. Ambas pueden ejecutar jobs en runners hospedados (no manejas la máquina) o en runners self-hosted (tú gestionas máquina, actualizaciones y seguridad). Los runners hospedados son más sencillos para empezar; los self-hosted son necesarios para velocidad, herramientas especiales o acceso a redes privadas.
Una división práctica en muchos equipos es tener runners Linux para backend y web, y macOS solo cuando hace falta compilar iOS. Android puede ejecutarse en Linux, pero es pesado, así que el tamaño del runner y el espacio en disco importan.
Hospedados vs self-hosted: qué gestionas
Los runners hospedados son buenos cuando quieres una configuración predecible sin mantenimiento. Los self-hosted tienen sentido cuando necesitas versiones específicas de Java/Xcode, caches más rápidos o acceso a la red interna.
Si eliges self-hosted, define roles de runner temprano. La mayoría de repos van bien con un conjunto pequeño: un runner Linux general para backend/web, un runner Linux más potente para Android, un runner macOS para empaquetado y firma de iOS, y un runner separado para despliegues con permisos más estrictos.
Elegir el runner correcto por job
Ambos sistemas te permiten dirigir jobs a runners (labels en GitHub, tags en GitLab). Mantén nombres ligados a cargas de trabajo, como linux-docker, android o macos-xcode15.
La falta de aislamiento es donde vienen muchos builds inestables. Archivos residuales, caches compartidos corruptos o herramientas instaladas "a mano" en una máquina self-hosted pueden generar fallos aleatorios. Espacios de trabajo limpios, versiones de herramientas fijadas y limpieza programada de runners suelen dar retorno rápidamente.
La capacidad y los permisos son otros puntos recurrentes, especialmente con la disponibilidad y el coste de macOS. Un buen default es: los runners de build pueden compilar, los de deploy pueden desplegar, y las credenciales de producción viven en el conjunto más pequeño posible de jobs.
Secretos y variables de entorno
Los secretos son donde los pipelines multi-app se vuelven riesgosos. Lo fundamental es similar (almacenar secretos en la plataforma, inyectarlos en tiempo de ejecución), pero el alcance se siente distinto.
GitHub Actions normalmente define secretos a nivel de repositorio y organización, con una capa adicional de Environment. Esa capa de Environment es útil cuando producción necesita una puerta manual y un conjunto de valores distinto de staging.
GitLab CI usa variables de CI/CD a nivel de proyecto, grupo o instancia. También soporta variables por entorno, además de protecciones como "protected" (solo disponibles en ramas/etiquetas protegidas) y "masked" (ocultas en logs). Esos controles ayudan cuando un monorepo sirve a varios equipos.
El modo de fallo principal es la exposición accidental: salida de depuración, un comando fallido que imprime variables, o un artefacto que incluye por error un archivo de configuración. Trata logs y artefactos como si fuesen compartidos por defecto.
En pipelines de backend + web + móvil, los secretos suelen incluir credenciales de cloud, URLs de bases de datos y claves de terceros, material de firma (certificados/perfiles de iOS, keystore y contraseñas de Android), tokens de registries (npm, Maven, CocoaPods) y tokens de automatización (proveedores de email/SMS, bots de chat).
Para múltiples entornos (dev, staging, prod), mantén nombres consistentes y cambia valores por alcance de entorno en vez de copiar jobs. Eso mantiene la rotación y el control de acceso manejables.
Algunas reglas previenen la mayoría de incidentes:
- Prefiere credenciales de corta vida (por ejemplo OIDC a proveedores cloud cuando esté disponible) sobre claves de larga vida.
- Aplica el principio de menor privilegio: identidades de deploy separadas para backend, web y móvil.
- Enmascara secretos y evita imprimir variables de entorno, incluso al fallar.
- Restringe secretos de producción a ramas/etiquetas protegidas y revisores obligatorios.
- Nunca almacenes secretos en artefactos de build, ni siquiera temporalmente.
Un ejemplo simple y de alto impacto: los jobs móviles deberían recibir secretos de firma solo en releases etiquetados, mientras que jobs de deploy del backend pueden usar un token limitado en merges a main. Ese cambio reduce mucho el radio de impacto si un job está mal configurado.
Caché y artefactos para builds más rápidos
La mayoría de pipelines lentos son lentos por una razón aburrida: descargan y recompilan lo mismo una y otra vez. El cache evita trabajo repetido. Los artefactos resuelven otro problema: conservar las salidas exactas de una ejecución específica.
Qué cachear depende de lo que compiles. Los backends se benefician de caches de dependencias y compilador (por ejemplo, el cache de módulos de Go). Las webs se benefician del cache del gestor de paquetes y herramientas de build. Los móviles suelen necesitar cache de Gradle y del Android SDK en Linux, y caches de CocoaPods o Swift Package Manager en macOS. Ten cuidado con cachear agresivamente salidas de iOS (como DerivedData) a menos que entiendas los trade-offs.
Ambas plataformas siguen el mismo patrón básico: restaurar cache al inicio del job y guardar el cache actualizado al final. La diferencia diaria está en el control. GitLab hace el comportamiento de cache y artefactos explícito en un solo archivo, incluido el tiempo de expiración. GitHub Actions suele apoyarse en acciones separadas para cache, lo que es flexible pero más fácil de configurar mal.
Las claves de cache importan más en monorepos. Buenas claves cambian cuando las entradas cambian y se mantienen estables de otra forma. Los lockfiles (go.sum, pnpm-lock.yaml, yarn.lock, y similares) deberían impulsar la clave. También ayuda incluir un hash de la carpeta específica de la app que estás construyendo en vez de todo el repo, y mantener caches separados por app para que un cambio no invalide todo.
Usa artefactos para los entregables que quieres conservar de esa ejecución exacta: bundles de release, APK/IPA, informes de tests, coverage y metadatos de build. Los caches son aceleradores de buena voluntad; los artefactos son registros.
Si los builds siguen lentos, busca caches sobredimensionados, claves que cambian cada ejecución (timestamps y SHAs completos son culpables comunes) y salidas cacheadas que no son reutilizables entre runners.
Encaje en monorepo: múltiples pipelines sin caos
Un monorepo se vuelve desordenado cuando cada push dispara tests de backend, builds de web y firmas móviles, aunque solo hayas cambiado un README. El patrón limpio es: detectar qué cambió y ejecutar solo los jobs que importan.
En GitHub Actions, eso suele significar workflows separados por app con filtros por ruta para que cada workflow corra solo cuando sus archivos cambien. En GitLab CI, suele implicar un único archivo de pipeline usando rules:changes (o child pipelines) para crear o saltar grupos de jobs según rutas.
Los paquetes compartidos son donde se rompe la confianza. Si packages/auth cambia, tanto backend como web podrían necesitar rebuild aunque sus carpetas no hayan cambiado. Trata las rutas compartidas como disparadores para múltiples pipelines y mantiene límites de dependencia claros.
Un mapa de disparadores simple que reduce sorpresas:
- Los jobs de backend corren con cambios en
backend/**opackages/**. - Los jobs web corren con cambios en
web/**opackages/**. - Los jobs móviles corren con cambios en
mobile/**opackages/**. - Los cambios solo en docs ejecutan una comprobación rápida (formato, spellcheck).
Paraleliza lo que sea seguro (unit tests, linting, build web). Serializa lo que debe controlarse (despliegues, releases en tiendas). Tanto needs en GitLab como las dependencias de jobs en GitHub te ayudan a ejecutar comprobaciones rápidas pronto y detener el resto si fallan.
Mantén la firma móvil aislada del CI diario. Coloca las claves de firma en un entorno dedicado con aprobación manual y ejecuta la firma solo en releases etiquetados o en una rama protegida. Los pull requests normales aún pueden construir apps sin firmar para validación sin exponer secretos sensibles.
Paso a paso: un pipeline limpio para backend, web y móvil
Un pipeline multi-app limpio empieza con un naming que deje la intención obvia. Escoge un patrón y síguelo para que la gente pueda ojear logs y saber qué corrió.
Un esquema que se mantiene legible:
- Pipelines:
pr-checks,main-build,release - Entornos:
dev,staging,prod - Artefactos:
backend-api,web-bundle,mobile-debug,mobile-release
A partir de ahí, mantén jobs pequeños y promueve solo lo que pasó verificaciones anteriores:
-
PR checks (cada pull request): ejecuta tests rápidos y lint solo para las apps que cambiaron. Para backend, construye un artefacto desplegable (una imagen de contenedor o un bundle de servidor) y guárdalo para que pasos posteriores no lo recompilen.
-
Web build (PR + main): compila la web en un bundle estático. En PRs, conserva la salida como artefacto (o despliega en un entorno de preview si tienes uno). En main, produce un bundle versionado apto para
devostaging. -
Mobile debug builds (solo PRs): construye un APK/IPA de debug. No firmes para release. El objetivo es feedback rápido y un archivo que testers puedan instalar.
-
Release builds (solo tags): cuando se empuja una etiqueta como
v1.4.0, ejecuta builds completos de backend y web más builds móviles firmados. Genera outputs listos para tienda y conserva notas de release junto a los artefactos. -
Aprobaciones manuales: coloca aprobaciones entre
stagingyprod, no antes de las pruebas básicas. Los desarrolladores pueden disparar builds, pero solo roles aprobados deberían desplegar a producción y acceder a secretos de producción.
Errores comunes que hacen perder tiempo
Los equipos suelen perder semanas por hábitos de workflow que crean builds frágiles en silencio.
Una trampa es depender demasiado de runners compartidos. Cuando muchos proyectos compiten por el mismo pool, obtienes timeouts aleatorios, jobs lentos y builds móviles que fallan solo en horas pico. Si backend, web y móvil importan, aísla jobs pesados en runners dedicados (o al menos colas separadas) y mantén límites de recursos explícitos.
Los secretos son otro sumidero de tiempo. Las claves de firma móvil y certificados son fáciles de manejar mal. Un error común es almacenarlos demasiado ampliamente (disponibles en cada rama y job) o filtrarlos en logs verbosos. Mantén material de firma limitado a ramas/etiquetas protegidas y evita cualquier paso que imprima valores secretos (incluso strings base64).
El caché puede salir mal cuando equipos cachean directorios enormes o confunden caches con artefactos. Cachea solo entradas estables. Conserva como artefactos las salidas que necesitas luego.
Finalmente, en monorepos, disparar todo el pipeline por cada cambio quema minutos y paciencia. Si alguien modifica un README y reconstruyes iOS, Android, backend y web, la gente deja de confiar en el CI.
Una lista rápida que ayuda:
- Usa reglas basadas en rutas para que solo corran las apps afectadas.
- Separa los jobs de test de los jobs de deploy.
- Mantén las claves de firma restringidas a workflows de release.
- Cachea entradas pequeñas y estables, no carpetas enteras de build.
- Planifica capacidad predecible de runners para builds móviles pesados.
Comprobaciones rápidas antes de elegir una plataforma
Antes de elegir, haz unas comprobaciones que reflejen cómo trabajas realmente. Te evitan escoger una herramienta que está bien para una app pero dolorosa cuando añades móviles, múltiples entornos y releases.
Enfócate en:
- Plan de runners: hospedados, self-hosted o mixto. Los builds móviles suelen empujar a un mix porque iOS necesita macOS.
- Plan de secretos: dónde viven, quién puede leerlos y cómo rotan. Producción debe ser más restrictiva que staging.
- Plan de caché: qué cacheas, dónde se almacena y cómo se forman las claves. Si la clave cambia en cada commit, pagarás sin ganar velocidad.
- Plan para monorepo: filtros por ruta y una forma limpia de compartir pasos comunes (lint, tests) sin copiar y pegar.
- Plan de release: tags, aprobaciones y separación de entornos. Sé explícito sobre quién puede promover a producción y qué pruebas necesita.
Pon a prueba esas respuestas con un escenario pequeño. En un monorepo con un backend en Go, una web en Vue y dos apps móviles: un cambio solo de docs debería hacer casi nada; un cambio en backend debería ejecutar tests del backend y construir un artefacto API; un cambio en la UI móvil debería compilar solo Android y iOS.
Si no puedes describir ese flujo en una página (disparadores, caches, secretos, aprobaciones), haz un piloto de una semana en ambas plataformas con el mismo repo. Elige la que se sienta aburrida y predecible.
Ejemplo: flujo realista de build y release en un monorepo
Imagina un repo con tres carpetas: backend/ (Go), web/ (Vue) y mobile/ (iOS y Android).
Día a día quieres feedback rápido. En releases quieres builds completos, firma y pasos de publicación.
Una división práctica:
- Branches de feature: ejecuta lint + unit tests para las partes cambiadas, construye backend y web, y opcionalmente un build Android de debug. Omite iOS salvo que realmente lo necesites.
- Tags de release: ejecuta todo, crea artefactos versionados, firma las apps móviles y sube imágenes/binarios a tu almacenamiento de release.
La elección de runners cambia cuando hay móvil. Go y Vue están contentos en Linux casi en cualquier lado. iOS requiere runners macOS, lo que puede condicionar la decisión más que cualquier otra cosa. Si tu equipo quiere control total de las máquinas, GitLab CI con runners self-hosted puede ser más fácil de operar como flota. Si prefieres menos trabajo ops y configurar rápido, los runners hospedados de GitHub son cómodos, pero los minutos y la disponibilidad de macOS se vuelven parte de tu planificación.
El cache es donde se ahorra tiempo real, pero el mejor cache varía por app. Para Go, cachea descargas de módulos y el cache de compilación. Para Vue, cachea la tienda del gestor de paquetes y recompila solo cuando cambien los lockfiles. Para móvil, cachea Gradle y el SDK de Android en Linux; cachea CocoaPods o Swift Package Manager en macOS, y espera caches más grandes y más invalidación.
Una regla de decisión que suele funcionar: si tu código ya está en una plataforma, empieza ahí. Cambia solo si los runners (especialmente macOS), permisos o cumplimiento te obligan.
Siguientes pasos: elegir, estandarizar y automatizar con seguridad
Elige la herramienta que encaje con dónde están tu código y tu gente. La diferencia suele aparecer en la fricción diaria: revisiones, permisos y qué tan rápido alguien puede diagnosticar un build roto.
Empieza simple: un pipeline por app (backend, web, móvil). Cuando esté estable, extrae pasos compartidos a plantillas reutilizables para reducir copy-paste sin difuminar la propiedad.
Escribe los alcances de secretos como escribirías quién tiene llaves de una oficina. Los secretos de producción no deberían ser legibles por cualquier rama. Pon un recordatorio de rotación (trimestral es mejor que nunca) y acuerda cómo funcionan las revocaciones de emergencia.
Si construyes con un generador no-code que produce código real, trata la generación/export como un paso de CI de primera clase. Por ejemplo, AppMaster (appmaster.io) genera backends Go, apps web Vue3 y móviles Kotlin/SwiftUI, así que tu pipeline puede regenerar código al cambiar, y luego compilar solo los targets afectados.
Cuando tengas un flujo en el que confía tu equipo, hazlo el predeterminado para repos nuevos y mantenlo aburrido: disparadores claros, runners predecibles, secretos restringidos y releases que solo se ejecutan cuando tú quieres.
FAQ
Por lo general, elige la plataforma donde ya vive tu código y tu equipo; cambia solo si la disponibilidad de runners (especialmente macOS), permisos o cumplimiento te obligan. El coste diario suele venir de la disponibilidad de ejecutores, el alcance de los secretos y lo fácil que sea expresar “solo compilar lo que cambió” sin reglas frágiles.
GitHub Actions suele sentirse más simple para comenzar rápido y para builds con matrices, con workflows repartidos en varios YAML. GitLab CI tiende a ser más centralizado y orientado a etapas, lo que puede facilitar razonar cuando el pipeline crece y quieres un único lugar para controlar caches, artefactos y el orden de los jobs.
Trata a macOS como un recurso escaso y úsalo solo cuando necesites empaquetado o firma de iOS. Una línea base común es: runners Linux para backend y web, un runner Linux más potente para Android, un runner macOS reservado para iOS, y un runner de despliegue separado con permisos más estrictos.
El "runner drift" ocurre cuando el mismo job se comporta distinto porque los SDKs y herramientas varían entre máquinas. Evítalo fijando versiones de herramientas, evitando instalaciones manuales en runners self-hosted, usando espacios de trabajo limpios y limpiando o reconstruyendo periódicamente las imágenes de los runners para no acumular diferencias invisibles.
Haz que los secretos estén disponibles solo para el conjunto mínimo de jobs que los necesitan y mantén los secretos de producción detrás de ramas/etiquetas protegidas con aprobaciones. Para móvil, lo más seguro es inyectar material de firma solo en releases etiquetados; los pull requests deben construir variantes de debug sin firmar.
Usa cachés para acelerar trabajo repetido y artefactos para preservar salidas exactas de una ejecución. Un cache es un intento de optimización y puede cambiar con el tiempo; un artefacto es un entregable almacenado como un bundle, un informe de tests o un APK/IPA de release que quieres conservar y trazar hasta una ejecución.
Basar las claves de cache en entradas estables como lockfiles y acotarlas a la parte del repo que estás construyendo evita que cambios no relacionados invaliden todo. Evita claves que cambien cada ejecución (fechas, hashes completos de commit) y mantén caches separados por app para que backend, web y móvil no compitan por el mismo cache.
Configura disparadores por ruta para que docs u otras carpetas no inicien jobs caros, y trata las carpetas compartidas como disparadores explícitos para las apps que dependen de ellas. Si una carpeta compartida cambia, está bien reconstruir múltiples objetivos, pero haz ese mapeo deliberado para que el pipeline sea predecible.
Mantén claves de firma y credenciales de tienda fuera de las ejecuciones cotidianas del CI, restringiéndolas detrás de tags, ramas protegidas y aprobaciones. Para pull requests, construye variantes de debug sin firma para recibir feedback rápido sin exponer credenciales de alto riesgo.
Sí, pero trata la generación como un paso de CI de primera clase con entradas y salidas claras para facilitar el cacheo y la reejecución predecible. Si usas una herramienta como AppMaster (appmaster.io) que genera código real, regenera en los cambios relevantes y luego construye solo los objetivos afectados (backend, web o móvil) según lo que haya cambiado tras la generación.


