30 agosto 2021

Cómo usar Travis CI con proyectos Python

 

Cuando tu proyecto empiece a crecer, testearlo, compilarlo y desplegarlo se volverá complejo.

Lo normal es empezar haciéndolo todo manualmente pero pasado un tiempo empezar a automatizar todo lo posible mediante scripts. Usar scripts propios para compilar y empaquetar puede ser suficiente para muchos proyectos personales pero muchas veces los test funcionales tardan más de lo que estamos dispuestos a esperar delante del ordenados. En esos casos, delegar en otros la ejecución de los test funcionales es la solución y es ahí donde Travis-CI aparece.

Travis-CI ofrece un entorno de automatización para ejecutar tus test funcionales, así como cualquier script que deseemos ejecutar en función del éxito o fracaso de los tests. Puede que ya conozcas  Jenkins, en realidad Travis-CI es similar pero más simple y fácil. En un entorno empresarial lo normal es usar Jenkins para la integración continua pero para proyectos personales Travis-CI puede ser más que suficiente. Actualmente, Travis-CI es gratuito para proyectos open-source.

Cuando uno se conecta a Travis-CI se le pide conectar su cuenta de GitHub, así que esta resulta imprescindible. A partir de ese momento se usará la cuenta de Github para identificarse al acceder a Travis.

Una vez dentro de la cuenta de Travis, se puede pulsar el icono "+" para enlazar cualquiera de nuestros repositorios de Travis. Despuñes de enlazar un repositorio con Travis, hay que incluir en su raiz un fichero llamado .travis.yml (ojo al punto del principio), su contenido sirve para decirle a Travis qué hacer cuando detecte una actualización del repositorio.

Para tener algo que no sirva como ejemplo, vamos a analizar la configuración de travis usada para vdist. Hay otros ejemplos en la documentación de Travis pero creo que el proceso de empaqutado de Travis cubre suficientes características de Travis para dar una buena idea de lo que se puede conseguir. Vamos a centrarnos en examinar una foto fija de la configuración del fichero de .travis.yml de vdist. Por eso, lo mejor es mantener abierta una ventana con el contenido de .travis.yml mientras se lea este artículo. Voy a explicar el flujo de trabajo a alto nivel y luego iremos paso por paso viendo cada uno con más detalle.

El flujo de trabajo que describe ese fichero .travis.yml es el siguiente:


Flujo de trabajo de Travis con vdist (hacer click para verlo más grande)

Cuando actualizo la rama de staging de mi repositorio de Github, este visa a Travis mediante un webhook y luego descarga la última versión del código fuente y busca un fichero .travis.yml en su raiz. Con ese fichero Travis puede averiguar qué flujo de trabajo seguir. 

En concreto con vdist, Travis busca mis tests funcionales y los ejecuta usando un intérprete de python 3.6, así como uno marcado como nightly en los repositorios de Python (las versiones beta del intérprete). De esa manera puedo comprobar que mi código se ejecuta bien en mi versión objetivo de Python (3.6) y además puedo anticipar cualquier problema que pueda tener en la futura versión estale de Python. Si cualquier test falla al ejecutarse con Python 3.6 entonces el proceso de trabajo finaliza y se me manda un correo con un aviso. Sin embargo, si lo que falla son los tests que se ejecuten con la versión nightly se me manda un aviso por correo pero el flujo de trabajo continua, dado que sólo doy soporte a versiones estables de Python. De esa manera puedo saber si tengo que solucionar cualquier futuro problema con la próxima versión de python pero no interrumpe el proceso de empaquetado con la versión actual.

Si los tests resultan exitosos, la rama de staging se funde con la rama master en Github. En ese momento, Github activa dos webhooks a los siguientes servicios:

  • Docker Hub: Para reconstruir algunas imágenes optimizadas de docker que vdist usa al empaquetar una aplicación.
  • Read the Docs: Para reconstruir la documentación del sitio de la aplicación usando los documentos en markdown de la carpeta docs/ del repositorio de vdist.
Esos webhooks son una de las características más interesantes de Github ya que permiten integrar muchos servicios de otros proveedores en los flujos de trabajo de Github. 

Mientras Github mezcla ramas y activa webhooks, Travis comienza el proceso de empaquetado y de despliegue de los paquetes generados a algunos repositorios públicos. Se generan paquetes de tres tipos: paquetes wheel, paquetes deb  y paquetes rpm. Los paquetes wheel se despliegan a Pypi, mientras que los deb y rpm de despliegan en mi repositorio de Bintray and y en el de release del repositorio de vdist en Github.

Ese es el proceso a grandes rasgos. Vamos a ver como se implementa todo esto en Travis usando el fihero .travis.yml.


Modos de Travis


Cuando Travis se activa por una actualización en el repositorio, da comienzo lo que se denomina build.

Un build genera uno o más jobs. Cada job clona el repositorio en un entorno virtual y ejecuta una serie de fases. 

Cada job copia tu repositorio en un entorno virtual y lleva a cabo una serie de fases. Un job finaliza cuando ejecuta todas sus fases. Un build acaba cuando termina todos sus jobs.

Por defecto, Travis ejecuta un coclo de vida cuyas fases principales para sus jobs son:

  1. before_install
  2. install
  3. before-script
  4. script
  5. after_sucess or after_failure
  6. before_deploy
  7. deploy
  8. after_deploy
  9. after_script
En realidad, hay más fases opcionales y nisiruiera hay que implementarlas todas.  De hecho, sólo la fase de script es realmente obligatoria. La fase de script es donde normalmente se ejecutan los tests funcionales. Si estos son exitosos, se ejecutan las fases desde after_sucess a after_script, pero si no lo son sólo se ejecuta after_failure.  La fase install es dondo se suelen instalar las dependencias necesarias para ejecutar tus tests. La fase deploy es donde se suben los paquetes generados a tus diferentes repositorios, por eso lo normal es usar la fase de before_deploy para ejecutar los comandos necesarios para generar esos paquetes.

 

 

¿Por qué decimos que un build puede tener uno o más jobs? porque se puede configurar lo que denomina una build matrix. Se genera una build matrix al definir que queremos probar el codigo con múltiples intérpretes o múltiples variables de entorno. Por ejemplo, se puede definir que queremos probar el código contra python 2.7, 3.6 y una versión de desarrollo de 3.7, en cuyo caso se genera una build matrix de 3 jobs.
 
El problema con este modo es que la build matrix genera trabajos completos, por lo que cada uno ejecuta los tests y la fase deploy. Lo que pasa es que a veces queremos ejecutar varios tipos de tests pero sólo queremos ejecutar una única fase de despliegue. Por ejemplo, supopngamos que estamos construyendo un proyecto de python cuyo código está preparado para ejecutarse tanto en python 2.7 como en 3.6, en ese caso nos gustaría probar el código tanto  con 2.7 como con 3.6, pero en caso de éxito sólo queremos generar un paquete para subirlo a Pypi. Extrañamente, esa clase de flujo de trabajo no ha estado soportada por Travis hasta hace poco tiempo. Hasta hace poco, al intentar el modo por defecto uno se podía encontrar que al probar el proyecto tanto para python 2.7 como 3.6 se generaba y se desplegaba el paquete resultante 2 veces.
 
 

 
Afortunadamente, Travis ha evolucionado a lo que llaman el modo stage, el cual funciona realmente y soluciona el problema descrito en el párrafo anterior.  
 
Este modo introduce el concepto de etapa (stage). Una etapa es formalmente un grupo de jobs que se ejecutan en paralelo como parte de un proceso de construcción secuencial compuesto por varias etapas. Mientras que el modo antiguo ejecutaba los jobs en paralelo desde el comienzo hasta el final, la etapa organiza el trabajo mediante etapas secuenciales y dentro de esas etapas es donde pueden ejecutarse los jobs en paralelo.
 
 

 

En nustro ejemplo, se puede crear una etapa para ejecutar en paralelo dos jobs para probar la aplicación tanto con python 2.7 como 3.6 y después, en caso de éxito, se puede lanzar otra etapa para crear y desplegar un único paquete.
 
Como esto era exactamente lo que necesitaba para vdist, este modo precisamente (el stage mode) será el que explique en este artículo.


Configuración inicial

 
Echa un vistazo a nuestro .travis.yaml de ejemplo. De las líneas 1 a 11 tenemos:


En esas líneas definimos la configuración general de Travis.

Primero se fija el lenguaje nativo usando la etiqueta "language" de tal manera que Travis pueda facilitar un entorno virtual con las dependencias necesarias instaladas.

Travis facilita dos tipos de entornos virtuales: el entorno virtual por defecto es un contenedor de docker con linux, el cual es muy ligero y rápido de arrancarse; el segundo entorno virtual es una máquina linux virtualizada (mediante virtualización pesada, tipo Hiper-V o VMware), lo cual supones tiempos de arranque más largo pero a veces permiten hacer cosas que no son posibles con contenedores. Por ejemplo, vdist usa docker al ejecutarse (por eso incluyo en valos docker con la etiqueta "services" del fichero .travis.yaml) lo que me obliga a usar un entorno de virtualización pesado. De otra manera, si intentas ejecutar un entono de docker dentro de un contenedor de docker te vas a dar cuenta de que no se puede. Por eso, para lanzar un entorno virtual pesado hay que incluir la etiqueta "sudo: enabled".

Por defecto, Travis usa una versión bastante antigua de linux (Ubuntu Trusty). Cuando tengan nuevas versiones del sistema operativo, habrá que cambiar la etiqueta "dist: trusty" por aquella versión más moderna que hayan incluido.

A veces, puede ser que al usar un entorno virtual tan antiguo no se disponga de las dependencias que necesitamos. Para ayudar con eso, el equipo de Travis intenta mantener una imagen de Trusty especialmente actualizada. Para utilizar esa versión especialmente actualizada habría que usar la etiqueta "group: travis_latest".

 

Matrix de pruebas

Desde las líneas 14 a 33 tenemos:

 


Bajo la etiqueta "python: " se define con qué versiones de python queremos ejecutar nuestros tests.

Podríamos necesitar ejecutar los tests no sólo depediendo de las versiones del intérprete de python sino también dependiendo de diversas variables de entorno. La manera de hacerlo es fijando esas variables de entorno debajo de la etiqueta "matrix:".

Puedes fijar algunas condiciones a ser probadas y que te avisen si fallan sin que eso suponga el final de todo el job. Para ello se usa la etiqueta "allow_failures". En este caso dejo que mi build continue si el fallo se produce cuando ejecuto las pruebas con la versión de desarrollo de python (versión nightly), de esa manera me avisan de que mi aplicación va a fallar en la versión de python que se está preparando para ser lanzada, pero dejo que mi aplicación sea empaquetada si al menos funciona con la versión de python estable.

Se pueden definit las variables de entorno a utilizar en los scripts de construcción usando la etiqueta "global:".

Si cualquiera de esas variables tiene valores peligrosos para ser mostrados en un repositorio público, se pueden cifrar usando las herramientas de Travis. Para ello hay que asegurar que las herramientas de Travis estén instaladas. Dado que las herramientas de Travis están desarrolladas con Ruby, lo primero es instalar su intérprete:

dante@Camelot:~/$ sudo apt-get install python-software-properties dante@Camelot:~/$ sudo apt-add-repository ppa:brightbox/ruby-ng dante@Camelot:~/$ sudo apt-get update dante@Camelot:~/$ sudo apt-get install ruby2.1 ruby-switch dante@Camelot:~/$ sudo ruby-switch --set ruby2.1

 

A partir de ahí se podrá usar el gestor de paquetes de Ruby para instalar la herramienta de Travis:

dante@Camelot:~/$ sudo gem install travis



Con la herramienta de Travis instalada ya podemos usarla para cifrar cualquier valor que queramos:

dante@Camelot:~/$ sudo travis encrypt MY_PASSWORD=my_super_secret_password

 

Como resultado, la herramienta sacará una etiqueta "secure:" seguida por una cadena aparentemente aleatoria. Esa etiqueta, con la cadena aleatoria, se puede incluir en el fichero .travis.yaml. Lo que hemos hecho aquí es usar la clave pública de Travis para cifrar nuestra cadena. Cuando Travis lea nuestro fichero .travis.yaml usará su fichero privado para descifrar cada etiqueta "secure:" que encuentre. 


Filtrado de ramas


Este fragmento viene de las líneas 36 a 41 de nuestro fichero .travis.yaml de ejemplo. Por defecto, Travis se activa con cada push que dé en cualquier rama de nuestro repositorio pero lo normal es restringir la activación a ramas específicas. En mi caso, los builds se activan cuando se producen pushes sobre la rama de "staging" (desarrollo). 

 

Notificaciones

 

Como se puede ver en las líneas 44 a 51, se puede configurar a qué dirección de correo se le notificará tanto el éxito como el fracaso de los tests. 


Testing

De las líneas 54 a 67 configuramos las pruebas, el verdadero meollo de nuestro trabajo:




Como se puede ver, en realidad abarca 3 fases: "before_install", "install" and "script".

Puedes usar esas fases de la manera que te resulte más cómoda. Yo he usado "before_install" para instalar todos los paquetes de sistema necesarios para mis tests, mientras que en "install" he instalado todas las dependencias de python.

En la fase de "script" es donde se lanzan las pruebas. En mi caso he usado pytest para ejecutar mis pruebas. Hay que ser consciente de que Travis espera durante 10 minutos a recibir alguna salida de tus tests por pantalla. Si no se recibe alguna salida en ese plazo, Travis interpreta que el test se ha quedado colgado y lo cancela. Este comportamiento puede ser un problema si resulta ser normal que un test en concreto dure más de 10 minutos sin dar señales de vida. En ese caso, hay que lanzar el comando de test usando el comando "travis_wait N", donde N es el número de minutos que queremos que dure la espera. En mi caso, los tests de vdist son realmente largos, así que le pido a Travis que espere 30 minutos antes de dar por perdido el test.

 

Etapas

En nuestro ejemplo, las etapas (stages) se definen entre las líneas 71 a 130.

En realidad, hasta este punto la configuración no ha sido diferente de los que habría sido si hubiéramos usado el modo por defecto de Travis. Donde empiezan realmente las diferencias es cuando encontramos la etiqueta "jobs:" dado que esta marca dónde comienza la definición de las etapas. A partir de ahí, cada etiqueta de "stage:" marca el comienzo de la definición de una fase.

Tal y como dijimos, las etapas se ejecutan secuencialmente siguiendo el mismo orden en el que se encuentren en el fichero .travis.yaml. Si una de la etapas falla el job acaba en ese mismo punto.

Es lógico preguntarse por qué no se definen las pruebas como si fueran una etapa. En realidad puede hacerse y de hecho así se podría alterar el orden de las pruebas si no quisiéramos que se realizasen al principio. En todo caso, si las pruebas no se definen explícitamente como una etapa entonces se ejecutarán al principio, como si fueran una fase 0.

Analicemos las etapas de vdist.


Etapa para fundir la rama de staging en la master

Desde la línea 77 a la 81:
 


Si los tests han sido exitosos podemos estar seguros de que el código está lo suficientemente limpio como para incluir dicho código en la rama master. En el caso del ejemplo, fundir (merge) con la rama master activa los webhooks de RedTheDocs y Docker Hob.

Para mantener el fichero .travis.yaml limpio, he sacado todo el código necesario para la fusión a un script externo. Ese script ejecuta automáticamente los mismos comandos de consola que usaríamos para realizar la fusión manualmente.


Etapa para construir y desplegar un paquete a un servicio de alojamiento de paquetes

Hasta ahora hemos comprobado que nuestro code está OK, ahora es el momento de empaquetarlo en un paquete y subirlo allá de donde nuestros vayan a descargarlo.

Hay muchas manera de construir y empaqueta nuestro código (tantas como lenguajes de programación) y muchos servicios para alojar los paquetes. Para los servicios de alojamiento más habituales Travis ya cuenta con jobs de despliegue predefinidos que automatizan gran parte del trabajo. Podemos ver un ejemplo en las líneas 83 a 94:

 



Ahí se puede ver un trabajo de despliegue predefinido para paquetes de python consistente en dos pasos: empaquetado en un paquete wheel y subida al servicio de Pypi.

Los parámetros a incluir con cada job de despliegue predefinido varían de uno a otro. Lo mejor es consultar las instrucciones específicas de Travis para el job predefinido concreto que nos interese.

Pero habrá veces que el servicio de alojamiento que usemos no esté incluido en la lista de jobs predefinidos de Travis. En ese caso las cosas se vuelven más manuales, ya que siempre se puede optar por unsar una etiqueta script para ejecutar comando de shell, tal y como se puede ver en las líneas 95 a 102:



En esas líneas se puede ver cómo llamo a algunos scripts definidos en carpetas de mi codigo fuente. Esos scripts utilizan herramientas propias para empaquetar mi código en paquetes rpm y deb. Aunque en las siguientes líneas he usado los jobs predefinidos de despliegue para subir los paquetes generados a los servicios de alojamiento, podría haberlo hecho mediante scripts manuales. Todos los servicios de alojamiento de paquetes facilitan una manera de subir paquetes usando comandos de consola, de manera que siempre contamos con esa posibilidad si Travis no nos facilita un job predefinido para hacerlo.


Depuración


A menudo, nuestra configuración de Travis no nos funcionará a la primera así que tendremos que depurarla.

Para depurar un build de Travis lo primero es leer el log de salida. Si el log es tan grande como para que Travis no lo muestre por completo en el explorador entonces podremos descargar el log en crudo y examinarlo en nuestro editor de texto favorito. Hay que buscar en el log cualquier error inesperado.

Otra opción es ejecutar el script en una máuina virtual con la mismas versiones que use Travis. Si el error mostrado en el log se repite en la máquina virtual entonces tendremos todo lo necesario para averiguar qué es lo que falla.

La cosa se complica cuando el eror no ocurre en nuestra máquina virtual local. En ese caso el error se encuentra en cualquier peculiaridad del entorno de Travis que no pueda se replicado en una máuina virtual propia, por lo que no qeda otra que entrar en el entorno mismo de Travis para averiguar que es lo que está pasando.

Travis activa por defecto la depuración para los repositorios privados, en caso de que el tuyo lo sea encontrarás el botón de depuraciñin justo debajo del de "restart job":



Si se pulsa ese botón el build dará comienzo, pero pasados los primeros pasos se parará y abrirá un puerto de ssh para que nos conectemos. Aquí hay un mensaje de salida de ejemplo de lo que se verá tras pulsar ese botón:


Debug build initiated by BanzaiMan
Setting up debug tools.
Preparing debug sessions.
Use the following SSH command to access the interactive debugging environment:
ssh DwBhYvwgoBQ2dr7iQ5ZH34wGt@ny2.tmate.io
This build is running in quiet mode. No session output will be displayed.
This debug build will stay alive for 30 minutes.



En este ejemplo conectaríamos por SSH a través de DwBhYvwgoBQ2dr7iQ5ZH34wGt@ny2.tmate.io, lo que nos daría acceso a la consola de la máquina virtual utilizada por Travis para el build. Una vez dentro de la máquina virtual puedes ejecutar cada paso del proceso de construcción haciendo uso de los siguientes comandos:

travis_run_before_install
travis_run_install
travis_run_before_script
travis_run_script
travis_run_after_success
travis_run_after_failure
travis_run_after_script


Esos comandos activarán los respectivos pasos del proceso de construcción. Cuando finalmente aparezca el error, se puede examinar el entorno para averiguar quñe es lo que está fallando.

El problema es que el botón de debug no está disponible para repositorios publicos, aunque afortunadamente se puede seguir haciendo uso de esa característica aunque para ello serán necesarios algunos pasos extra. Para activar esa funcionalidad de depuración hay que solicitarlo al soporte de Travis a través del correo support@travis.ci.com. Pueden tardar unas cuantas horas en responder y activarlo.

Una vez que recibamos confirmación del soporte de Travis acerca de que la depuración está habilitada, seguiremos sin ver el botón de debug pero podremos hacer uso de ella mediante una llamada a la API de Travis. Se puede hacer esa llamada con el siguiente comando de consola:

dante@Camelot:~/project-directory$ curl -s -X POST \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -H "Travis-API-Version: 3" \ -H "Authorization: token ********************" \ -d "{\"quiet\": true}" \ https://api.travis-ci.org/job/{id}/debug


Para conseguir to token necesitarás ejecutar este comando antes:

dante@Camelot:~/project-directory$ travis login We need your GitHub login to identify you. This information will not be sent to Travis CI, only to api.github.com. The password will not be displayed. Try running with --github-token or --auto if you don't want to enter your password anyway. Username: dante-signal31 Password for dante-signal31: ***************************************** Successfully logged in as dante-signal31! dante@Camelot:~/project-directory$ travis token Your access token is **********************



Para conseguir un nñumero válido de {id} hay que entrar en el log del job que se quiera repetir y expandir la sección "Build system information". Ahí se puede encontrar el número que necesitamos en la línea "Job id:".

Justo después de lanzar la llamada a la api podremos hacer ssh a la máquina virtual para comenzar la depuración.

Para acabar la sesión de depuración podemos o cerrar todas las ventanas de ssh o cancelar el build desde el interfaz web.

Aunque hay que ser consciente de un peligro potencial. ¿Por qué esta funcionalidad no está disponible para repositorios públicos? porque el servidor de ssh no tiene autentificación por lo que cualquiera que sepa a donde conectar podrá obtener acceso a la consola de la máuina virtual. ¿Cómo podría averiguar un atacante a quñe URL conectar? mirando los logs del build, si tu repositorio es público los logs aparecen en abierto para cualquiera que quiera verlos en el portal de Travis. Si un atacante estuviese vigilando el log del build y en ese mismo momento iniciasemos una sesión de depuración, elatacante vería la misma cadena de conexión que nosotros y podrá conectarse a la misma máquina. Dentro de la máuina virtual de Travis, el atacante puede revisar las variables de entornos y sacar cualquier contraseña o token que tengamos en ellas. Lo bueno es que cualquier conexión de ssh nueva se conectará a la misma sesión de consola que nosotros, por lo que veríamos sus comandos aparecer en la sesión, dándonos la oportunidad de cancelar la sesión de depuración desde el interfaz web antes de que el atacante pueda alcanzar nada crítico.