- Al migrar de vuelta un CI complejo a GitHub Actions, se combinaron merge queue, varios runners, builds de Rust, imágenes de Docker y pruebas de integración, y el costo de depuración terminó siendo mayor que el de la configuración
- Todos los cambios que entran a
main deben pasar las pruebas, y los errores menores se corrigen automáticamente, como formato, dependencias sin usar y lint
- Para forzar tanto la validación antes como después de la merge queue, fue necesario hacer que los nombres de los jobs en ambas etapas fueran idénticos; de lo contrario, los checks podían quedarse detenidos o podían fusionarse cambios con fallas
GITHUB_TOKEN, los permissions del workflow, tokens personalizados y las excepciones de forks y self-hosted runners se entrelazan y vuelven difícil de entender el modelo de seguridad, con mucho margen para errores
- Ejecutar contenedores de Docker, usar workflows en YAML y las pruebas locales limitadas ralentizaron el desarrollo, pero el nuevo script de CI redujo mucho el tiempo de merge
Volver a un CI complejo con GitHub Actions
- Durante las últimas 2 semanas se reescribieron los scripts de CI en GitHub Actions
- Esta es la tercera reorganización del CI
- Primero fue GitHub Actions
- Después se migró a Earthly
- Como Earthly dejó de mantenerse, se volvió a GitHub Actions
- El CI actual maneja al mismo tiempo merge queue, varios runners, builds de Rust, imágenes de Docker y pruebas de integración pesadas
- Se usan en conjunto runners self-hosted, blacksmith.sh y GitHub-hosted
- Fusionar un solo PR consume cerca de 1 hora de CI en varios runners paralelos
Condiciones que debe cumplir el CI
- Todos los cambios que entren a
main deben pasar todas las pruebas
- Errores menores como formato, dependencias sin usar o problemas de lint no deberían terminar en fallo, sino corregirse automáticamente
- Los artefactos probados en CI deben ser iguales a los artefactos reales de release
- Para una buena experiencia de desarrollo, el CI debe terminar rápido
- GitHub Actions soporta técnicamente estas condiciones, pero el proceso de configuración viene con trampas ocultas, comportamientos inconsistentes y depuración difícil
Merge queue y checks de estado
- La pieza clave para mantener limpio el branch
main es la merge queue de GitHub
- La merge queue hace rebase del PR sobre
main antes de ejecutar el CI
- La ejecución necesaria del CI se divide en dos etapas
- Antes de entrar a la cola se ejecuta el CI para corregir automáticamente problemas menores
- Dentro de la cola se vuelve a ejecutar el CI para validar el estado final de la fusión
- Para hacer obligatorias ambas ejecuciones en GitHub Actions, hubo que asignar exactamente el mismo nombre de job en las dos etapas
- GitHub trata ambas ejecuciones como el mismo check, así que solo permite fusionar si ambas tienen éxito
- Esta solución se encontró tras varias horas de depuración y gracias a una respuesta en Stack Overflow
- Otros enfoques pueden hacer que los checks de estado queden esperando antes de entrar a la cola y que el job ni siquiera arranque, o que cambios que deberían fallar dentro de la merge queue terminen fusionándose de todos modos
Un modelo de seguridad de GitHub Actions difícil de entender
- Después del reciente caso en que una GitHub Action popular fue comprometida, surgió la recomendación de “fijar dependencias por hash”, pero en los comentarios la reacción fue que casi nadie hace eso
- GitHub Actions incluye un token por defecto llamado
GITHUB_TOKEN
- Este token se inicializa con permisos por defecto
- Esos permisos por defecto pueden configurarse en Actions → General → Workflow Permissions dentro de la configuración del repositorio
- Si los permisos por defecto de
GITHUB_TOKEN son restrictivos, hay que elevarlos para las actions y comandos que lo necesiten; si son permisivos, se pueden quitar algunos permisos desde el archivo del workflow
- Un mejor valor por defecto sería empezar con ningún permiso y que el usuario agregue solo los necesarios
- Hay muchos tipos de permisos, y salvo que se sea experto en GitHub, es difícil entender qué protege cada uno
Excepciones de tokens y permisos
- Para crear automáticamente releases de GitHub con
softprops/action-gh-release, se usa un token personalizado CI_RELEASE
- name: Release on GitHub
if: env.version_exists == 'false'
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ env.CURRENT_VERSION }}
generate_release_notes: true
make_latest: true
token: ${{ secrets.CI_RELEASE }}
- Con el token por defecto, la release en sí se completa, pero no se disparan workflows después de la release
- Como no hay ninguna indicación visible, hubo que encontrar un issue dejado por alguien con el mismo problema para entender la causa
- También es posible elevar permisos dentro del YAML del workflow
- Resulta extraño que la estructura permita elevar permisos dentro del mismo código que se busca proteger
- Según la documentación de GitHub, con la clave
permissions se pueden agregar o quitar permisos de lectura para repositorios fork, pero normalmente no se pueden otorgar permisos de escritura
- La excepción es cuando un administrador activa la opción Send write tokens to workflows from pull requests en la configuración de GitHub Actions
- Con tantas excepciones y trampas, el modelo de seguridad de GitHub Actions es potente, pero también amplía la superficie de ataque y la posibilidad de errores
La incertidumbre de los self-hosted runners
- La documentación de GitHub no recomienda usar self-hosted runners en repositorios públicos
- La razón es que un fork de un repositorio público puede ejecutar código riesgoso en la máquina del self-hosted runner a través de un PR
- GitHub también ofrece una configuración de self-hosted runner que exige aprobar la ejecución de PRs de colaboradores externos
- La documentación no da una respuesta clara sobre si usar esa configuración junto con self-hosted runners es realmente seguro, y tampoco hay consenso en internet
- La complejidad es alta y sigue siendo difícil tener una certeza del 100%
El choque entre Docker y GitHub Actions
- GitHub Actions permite ejecutar jobs dentro de un contenedor
- La ventaja es poder preempaquetar dependencias en un dev container y evitar reinstalarlas cada vez
- En la práctica, se repiten los problemas de permisos de archivos
- El contenedor puede compilar archivos con un usuario, mientras que el runner de GitHub puede ejecutarse con otro uid/gid
- Como resultado, puede no haber acceso a archivos dentro del contenedor, al workspace de GitHub o a directorios temporales del host
- El directorio
$HOME también puede no coincidir
- El dev container puede instalar herramientas en
/home/ubuntu
- Dentro de GitHub Actions,
$HOME puede ser /github/home
- Las herramientas que dependen de archivos bajo
$HOME pueden no encontrar lo que necesitan
- También pueden romperse actions que interactúan con el sistema host
- La caché de GitHub está limitada a 10GB, así que se montó un disco NVMe para caché con la sticky disk action de blacksmith
- Dentro del contenedor no funcionaba, y se resolvió después de un cambio en blacksmith.sh
- El propio campo
container también tiene limitaciones
- No se puede sobrescribir el entrypoint
- Tampoco se puede ejecutar solo algunos steps dentro del contenedor y el resto fuera
Desarrollo y depuración de workflows en YAML
- La lógica de GitHub Actions se escribe en YAML, y cuanto más compleja se vuelve, más fácil es equivocarse
- La validación GitHub YAML linter de RustRover ayudó, pero hace falta una verificación estática mejor
- En local es difícil probar suficientemente el comportamiento real del CI
- act es una herramienta conocida, pero solo soporta una pequeña parte de lo que se quería hacer en el CI
- La mejor forma de depurar fue crear otro repositorio igual e iterar con
git commit -a -m "wip" && git push test-ci branch hasta que el CI se comportara como se esperaba
Separación de workflows y reutilización de artefactos
- Para no ejecutar siempre todo el pipeline completo de CI, se mantuvieron pequeños los workflows individuales
- Al final de cada workflow se suben artefactos, y los workflows posteriores los descargan para no tener que recompilar desde cero
- También es posible descargar artefactos de ejecuciones anteriores para probar de forma aislada un workflow
- Sin embargo, al descargarlos desde una ejecución previa hay que proporcionar un token a la action
download-artifact
- Ese token puede ser el token por defecto, pero sigue sin estar claro por qué hay que indicarlo explícitamente
- El archivo principal del workflow termina siendo una cadena que invoca otros archivos YAML
jobs:
invoke-build-rust:
name: Build Rust
uses: ./.github/workflows/build-rust.yml
invoke-build-java:
name: Build Java
uses: ./.github/workflows/build-java.yml
invoke-tests-unit:
name: Unit Tests
needs: [invoke-build-rust, invoke-build-java]
uses: ./.github/workflows/test-unit.yml
invoke-tests-adapter:
name: Adapter Tests
needs: [invoke-build-rust]
uses: ./.github/workflows/test-adapters.yml
secrets: inherit
invoke-build-docker:
name: Build Docker
needs: [invoke-build-rust, invoke-build-java]
uses: ./.github/workflows/build-docker.yml
invoke-tests-integration:
name: Integration Tests
needs: [invoke-build-docker]
uses: ./.github/workflows/test-integration.yml
invoke-tests-java:
name: Java Tests
needs: [invoke-build-java]
uses: ./.github/workflows/test-java.yml
- Algunos jobs necesitan
secrets: inherit
- Cuando un workflow llama a otro workflow, los secrets no se comparten por defecto
- Esa era la causa de un problema donde fallaba todo el pipeline de CI, aunque ejecutar steps individuales sí funcionaba
Merges más rápidos, depuración todavía costosa
- El nuevo script de CI redujo mucho el tiempo de merge y el resultado deja satisfecho al autor
- Aun así, llegar a ese punto tomó demasiado tiempo, y cuando aparece un problema debería ser mucho más fácil depurarlo
Aún no hay comentarios.