CI/CD e Ingeniería de Releases
Esta página recorre el camino completo de un cambio de código, desde el momento en que un desarrollador abre un pull request hasta llegar a producción. Hay tres sistemas principales involucrados:
- GitHub Actions valida el código en cada pull request.
- Concourse construye releases, prueba Helm charts y despliega en los entornos.
- Cepler controla qué entornos se actualizan y en qué orden.
La filosofía de diseño es simple: cada paso debe completarse exitosamente antes de que comience el siguiente, y hay puntos de control humanos en los momentos más importantes. Nada llega a producción por accidente.
Visión General de Alto Nivel
Ahora veamos cada paso en detalle.
Paso 1: Verificaciones de Pull Request (GitHub Actions)
Cuando un desarrollador abre un pull request contra main, GitHub Actions lanza un conjunto de verificaciones que se ejecutan en paralelo. Todas y cada una deben pasar antes de que el PR pueda ser fusionado; no hay excepciones.
Qué se verifica
| Workflow | Qué hace | Por qué importa |
|---|---|---|
| nextest | Ejecuta todas las pruebas unitarias y de integración de Rust mediante nix run .#nextest | Detecta errores lógicos y regresiones en el backend |
| bats | Levanta la pila completa de la aplicación y ejecuta pruebas BATS de extremo a extremo | Verifica que todo el sistema funcione en conjunto, no solo piezas individuales |
| cypress | Ejecuta pruebas de navegador Cypress contra el panel de administración y el portal del cliente | Asegura que la interfaz realmente funcione; también genera capturas de pantalla para manuales regulatorios |
| check-code-apps | Ejecuta lint, verificación de tipos y compilación de ambos frontends Next.js | Detecta errores de TypeScript, violaciones de lint y compilaciones rotas en el frontend |
| flake-check | Ejecuta nix flake check para validar el Nix flake | Asegura que el sistema de compilación esté saludable |
| codeql | Análisis estático CodeQL de GitHub para JS/TS y Rust | Encuentra posibles vulnerabilidades de seguridad mediante análisis estático |
| pnpm-audit | Audita las dependencias npm en busca de vulnerabilidades conocidas | Bloquea PRs que introduzcan dependencias con CVEs de alta severidad |
| data-pipeline | Aprovisiona un entorno desechable de BigQuery, ejecuta las pruebas del pipeline de datos y luego lo destruye | Valida que el pipeline de datos Dagster/dbt siga funcionando con los cambios de esquema |
| cocogitto | Verifica que los mensajes de commit sigan el formato de conventional commits | Necesario porque el número de versión y el changelog se generan automáticamente a partir de los mensajes de commit |
| spelling | Ejecuta la herramienta typos para detectar errores ortográficos comunes | Simple pero detecta errores tipográficos embarazosos en código y documentación |
| lana-bank-docs | Compila el sitio completo de documentación (documentación de API, documentación versionada, validación de capturas de pantalla) | Detecta compilaciones rotas de documentación, descripciones de API faltantes y configuraciones inválidas del sitio de documentación |
Cómo Nix caching lo hace rápido
Compilar el código Rust desde cero toma mucho tiempo. Para evitar hacerlo en cada PR, todos los workflows de GitHub Actions descargan binarios pre-compilados de un caché binario compartido de Cachix llamado galoymoney.
Este es el patrón que verás en cada archivo de workflow:
- uses: DeterminateSystems/nix-installer-action@v16
- uses: cachix/cachix-action@v15
with:
name: galoymoney
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
skipPush: true
La parte skipPush: true es clave: GitHub Actions solo lee del caché, nunca escribe en él. El caché se llena mediante un pipeline separado de Concourse (descrito en la sección Pipeline de Caché Nix más abajo). Esta separación existe porque Concourse tiene workers de caché potentes con almacenamiento persistente, mientras que los runners de GitHub Actions son efímeros y producirían cargas redundantes.
La mayoría de los workflows también recuperan 10-20 GB de espacio en disco al inicio eliminando software preinstalado (imágenes Docker, Android SDK, etc.) con el que vienen los runners de GitHub. Las compilaciones grandes de Rust necesitan ese margen.
Qué sucede cuando una verificación falla
Si alguna verificación falla, el PR queda bloqueado para fusión. El desarrollador corrige el problema, hace push nuevamente y las verificaciones se re-ejecutan. No hay forma de evadir una verificación fallida.
Paso 2: Construcción de un Release (Concourse, repositorio lana-bank)
Una vez que un PR se fusiona en main, el pipeline de release de Concourse toma el control. Este pipeline reside en el directorio ci/release/ del repositorio lana-bank y está escrito usando plantillas YTT.
El pipeline tiene una cadena de dependencias clara:
2a. Re-ejecución de pruebas en main
Podrías preguntarte: ya ejecutamos pruebas en GitHub Actions en el PR, entonces, ¿por qué ejecutarlas de nuevo? Porque el PR fue probado contra una versión potencialmente desactualizada de main. Entre el momento en que se abrió el PR y el momento en que se fusionó, otros PRs pueden haber sido incorporados. Ejecutar pruebas nuevamente sobre el commit fusionado real detecta problemas de integración que solo aparecen cuando múltiples cambios se combinan.
Tres jobs se ejecutan en paralelo:
- test-integration ejecuta
cargo nextest— la misma suite de pruebas Rust de las verificaciones del PR. - test-bats ejecuta las pruebas BATS de extremo a extremo (con hasta 2 intentos, ya que las pruebas E2E pueden ser inestables).
- flake-check valida el Nix flake.
Los tres deben pasar antes de que se construya algo.
2b. Construcción del Release Candidate (build-rc)
Una vez que pasan las pruebas, el pipeline construye un release candidate (RC). La idea detrás de los RCs es que puedes construir y probar múltiples candidatos antes de comprometerte con un release final. Esto es lo que sucede:
-
Determinar el número de versión. Un script de Nix llamado
next-versionusa cocogitto para escanear los mensajes de conventional commits desde el último release y determinar la siguiente versión semántica. Por ejemplo, si el último release fue0.41.0y ha habido un commitfeat:, la siguiente versión se convierte en0.42.0-rc.1. Si ya se construyó otro RC para esta versión, se incrementa arc.2,rc.3, y así sucesivamente. -
Inyectar la versión en las aplicaciones frontend. El script
prep-release-apps.shescribeNEXT_PUBLIC_APP_VERSION=0.42.0-rc.1en los archivos.envtanto del panel de administración como del portal del cliente, para que la interfaz pueda mostrar qué versión está ejecutándose. -
Compilar el binario Rust.
nix build --impure .#lana-cli-releaseproduce el binariolana-cli. La flag--impurees necesaria porque la compilación lee variables de entorno comoVERSIONyCOMMITHASHque fueron establecidas por el pipeline de CI. -
Construir cuatro imágenes Docker y subirlas a Google Artifact Registry (
gcr.io/galoyorg):lana-bank— el servidor principal (construido desdeDockerfile.rc, que copia el binario pre-compilado en una imagen base distroless)lana-bank-admin-panel— el frontend del panel de administraciónlana-bank-customer-portal— el frontend del portal del clientedagster-code-location-lana-dw— el código del pipeline de datos de Dagster
-
Etiquetar las imágenes. Cada imagen recibe tanto una etiqueta
edge(que significa "último RC") como una etiqueta específica de versión como0.42.0-rc.1.
2c. Apertura del PR de Promoción de RC (open-promote-rc-pr)
Después de que las imágenes RC son construidas, el pipeline abre automáticamente un pull request de vuelta en el repositorio lana-bank. Este PR hace varias cosas:
- Genera una entrada de CHANGELOG usando git-cliff, que lee los mensajes de conventional commits y los agrupa en categorías (características, correcciones de errores, etc.).
- Regenera la documentación de API y los esquemas de eventos, y crea una instantánea versionada del sitio de documentación.
- Sube todo a una rama llamada
bot-promote-rcy abre un PR borrador etiquetado comopromote-rc.
Este PR es la puerta humana en el pipeline. Un ingeniero revisa el changelog para asegurarse de que se vea correcto, verifica que el RC se vea bien en cualquier prueba ad-hoc, y luego fusiona el PR cuando está listo para cortar un release. Nada sucede automáticamente desde aquí — el release solo procede cuando un humano dice "adelante".
También hay una verificación de seguridad: la GitHub Action promote-rc-file-check verifica que este PR solo modifique archivos CHANGELOG.md y docs-site/**. Si el bot incluyó accidentalmente otros cambios, la verificación falla y bloquea la fusión.
2d. Corte del Release Final (release)
Cuando alguien fusiona el PR de promote-rc, el job release se activa. Hace tres cosas:
-
Construye las imágenes Docker finales. Estas son las mismas cuatro imágenes que el RC, pero ahora etiquetadas con el número de versión limpio (por ejemplo,
0.42.0) y tambiénlatest. Las imágenes de release usanDockerfile.releaseen lugar deDockerfile.rc— la diferencia es que el Dockerfile de release descarga el binario desde los artefactos del GitHub Release en lugar de copiarlo desde un paso de compilación. -
Crea un GitHub Release. Esto incluye el binario
lana-clicomo artefacto descargable y el changelog como notas del release. El release se etiqueta con el número de versión. -
Actualiza el contador de versión. El pipeline almacena la versión actual en una rama git dedicada llamada
version(solo un archivo de texto con el número de versión). Esta se incrementa para que el próximo RC comience desde la base correcta.
2e. Actualización del Helm Chart (bump-image-in-chart)
Inmediatamente después del release, el pipeline necesita informar al Helm chart sobre las nuevas imágenes. Lo hace abriendo un PR en el repositorio galoy-private-charts:
-
Obtiene el digest SHA256 de cada imagen Docker recién construida. Se usan digests en lugar de etiquetas porque son inmutables — una etiqueta como
latestpuede apuntar a una imagen diferente después, pero un digest siempre se refiere exactamente a los mismos bytes. Esto es importante para la seguridad en producción. -
Actualiza
values.yamlen el Helm chart con los nuevos digests y versión:lanaBank:
image:
digest: "sha256:0a858023..." # METADATA:: repository=https://github.com/GaloyMoney/lana-bank;commit_ref=e348f09;app=lana-bank;
adminPanel:
image:
digest: "sha256:acdb373d..."
customerPortal:
image:
digest: "sha256:5d98584b..."Observa el comentario
METADATAjunto a cada digest. Esta es una pista que vincula la imagen con el commit fuente exacto desde el que fue construida. Es invaluable al depurar problemas en producción — puedes mirar el digest de la imagen en ejecución, encontrar este comentario en el chart y rastrearlo hasta el código fuente. -
También copia módulos de Terraform (
tf/bq-setupytf/honeycomb) del repositorio fuente al chart, para que el chart siempre incluya la configuración de infraestructura correspondiente. -
Abre un PR en galoy-private-charts con un cuerpo que incluye un enlace al diff del código (por ejemplo, "comparar old_ref...new_ref en GitHub"). Esto facilita ver exactamente qué cambios de código están incluidos en esta actualización del chart.
-
Este PR se fusiona automáticamente mediante un workflow de GitHub (
bot-automerge-lana.yml) que vigila PRs con las etiquetasgaloybotylana-bank. No se necesita intervención humana aquí — el testflight (descrito a continuación) es lo que valida el chart.
Cómo funcionan los números de versión
Las versiones siguen Versionado Semántico y se derivan automáticamente de los mensajes de conventional commits usando cocogitto:
- Commits
feat:producen un incremento minor (por ejemplo, 0.41.0 -> 0.42.0) - Commits
fix:producen un incremento patch (por ejemplo, 0.42.0 -> 0.42.1) feat!:oBREAKING CHANGEproducen un incremento major (por ejemplo, 0.42.0 -> 1.0.0)
Por esto la GitHub Action cocogitto impone el formato de conventional commits en cada PR — si los mensajes de commit no siguen la convención, la versión no puede calcularse automáticamente.
La versión actual se almacena en una rama git llamada version como un archivo de texto plano. Es gestionada por el semver resource de Concourse.
Paso 3: Pruebas del Helm Chart (galoy-private-charts)
El repositorio galoy-private-charts contiene el Helm chart que agrupa lana-bank y todos los servicios que necesita para ejecutarse. Piensa en el Helm chart como una "receta de despliegue" — describe no solo el servidor lana-bank, sino también la base de datos, el proveedor de identidad, el API gateway, el pipeline de datos y todo lo demás.