Pintando de Ops mi clon de Wordle

Diego Herrera
11 min readFeb 13, 2022

--

Días atrás os contaba cómo había enfocado la creación de un clon de Wordle para consola, y en el mismo artículo comenté también mi intención de publicar los detalles más Ops de este mini proyecto. Pues bien, lo prometido es deuda: estás en el lugar oportuno.

Wordle + DevOps = Win!
Wordle + DevOps = Win!

Antes de continuar, te dejo el enlace al repositorio, por si quieres trastear en paralelo a la lectura: https://github.com/vermicida/palabros

¡Marchando una de DevOps!

DevOps dejó de ser moda para ser un must, y prueba de ello es que todos los profesionales del sector apostamos ciegamente en sus bondades. Si has pasado los últimos 10 años en una gruta, es probable que no sepas qué es DevOps, así que te haré una breve introducción.

DevOps surgió como un movimiento natural para acercar posturas entre equipos de desarrollo y equipos de operaciones, que históricamente han perseguido objetivos distintos:

  • Los primeros buscan agilidad ➔ Tienen que programar y entregar nuevas funcionalidades al cliente lo más rápido posible
  • Los segundos buscan estabilidad ➔ tienen que asegurar, a través de la correcta administración del software desarrollado por los primeros, los acuerdos de servicio negociados con el cliente

Esto rara vez funciona, y suele generar un vórtice de malestar que va drenando la energía del equipo; esto se traduce de forma directa en un aumento de los tiempos de entrega y en un descenso pronunciado de la calidad de esas entregas. Es decir, se entrega tarde y mal.

DevOps propone una serie de principios para ayudar a desbloquear situaciones complejas y que todo fluya de forma más orgánica. A punto gordo, estos principios pueden resumirse en:

  • Monta equipos pequeños y multidisciplinares que puedan trabajar de forma independiente, evitando así bloqueos y dependencias con terceros. El equipo completo trabaja para un mismo fin, haciendo que sea un ente sistémico en el que todos los miembros comparten visión y objetivos.
  • Construye de forma iterativa cuestionando cada paso para detectar fallos y generar aprendizaje. Este nuevo conocimiento, local al equipo, debe documentarse, escalarse y hacerse global, de manera que otros equipos puedan beneficiarse de ello.
  • Automatiza todo lo posible los procesos que se presten a ello, de manera que el equipo pueda centrarse en tareas más creativas e importantes. Esto facilita la detección de errores en fases tempranas, permite liberar los procesos de manualidades complejas, y ayuda a minimizar los tiempos de entrega al cliente.

Cuanto más profundizas en la teoría tras DevOps, más piensas: si todo esto no son más que perogrulladas. Y lo son, pero la sensatez no ha llegado aún a todos los rincones del sector, y seguimos haciendo lo complicado frente a lo sencillo. Como dijo Roy Batty, el personaje del difunto Rutger Hauer, en Blade Runner: yo he visto cosas que vosotros no creeríais.

Implementar DevOps no es sencillo, y menos a nivel organizacional. Lo ideal es empezar con proyectos «greenfield» -que parten de cero-, involucrando a compañeros que se desvivan por la causa y consiguiendo poco a poco pequeños logros que permitan argumentar la tracción del modelo, y despertar así interés en otros equipos.

De forma individualizada también podemos hacer DevOps. Cualquier acción que habilite el cumplimento de los principios, es bienvenida. De hecho, en los siguientes párrafos te contaré qué acciones he elegido yo para palabros.

Los primeros pasos

Ahora que ya tenemos un poco de contexto sobre cómo DevOps puede ayudarnos en el ciclo de vida del desarrollo de software, somos capaces de identificar acciones a acometer en palabros para mejorar el proceso de entrega de valor; vamos, publicar software de la forma más elegante posible.

Siguiendo la simpleza mencionada en el artículo previo, evalué acciones posibles y me quedé con aquellas que, aún sencillas, cubrían el umbral de calidad mínimo que exijo para mis desarrollos personales. Son estas:

  • Documentar ➔ No hay nada más frustrante que intentar hacer funcionar software para el que no existe documentación alguna. No subestimes el poder de un README con un puñado de líneas comentando las bases para usar nuestro software; ayudará enormemente a tus usuarios. Yo, además, documenté el código y escribí dos artículos -estás leyendo uno de ellos-; no hacen falta Quijotes, pierde el miedo a documentar.
  • Testar el código ➔ Este tópico da para ríos de verborrea y debates airados; yo soy de empezar por lo simple e iterar conforme la necesidad aparece. ¿En qué se traduce esto? En tests unitarios de toda la vida. No creo en TDD/BDD para proyectos ágiles, ni tampoco en blindar el código con miles de tests complejos que no aportan nada. Si tu código es bueno, fácilmente «testable», y los tests unitarios cubren todos los casos de uso posibles, la calidad de tu software está asegurada.
  • Analizar el código ➔ Más concretamente, hacer análisis estático de código. Su propósito es verificar que el código no está expuesto a vulnerabilidades conocidas y, también, asegurar que se cumplen las reglas de codificación establecidas para el lenguaje de programación elegido. Si eres de los que, como yo, ve belleza en la disposición del código, adorarás algunas de las herramientas que te voy a comentar.

Teniendo claras las acciones, toca buscar las palancas que ayudarán a darles comienzo y consecución.

Tests unitarios

Empecemos por los tests unitarios, que es el arma que más a mano tenemos para combatir el mal.

Ejemplo aleatorio de test unitario

Como decía más arriba, un código claro y conciso lleva a tests unitarios simples y fáciles de mantener; el caso anterior es un ejemplo:

  • El método a testar, _normalize_word, acepta un texto cualquiera como entrada y retorna una copia del mismo eliminando tildes y diéresis
  • El test unitario, por tanto, debe validar que la salida del método es la que realmente esperamos en base a una entrada dada; para ello, nosotros mismos le damos al test la respuesta correcta con la que comparar. También es interesante asegurarnos de validar la otra posibilidad: que el método no devuelve aquello que no esperamos.

Más fácil, imposible.

En el ecosistema Python existen un par de herramientas que son estándares de facto para la ejecución de tests y evaluación de resultados, y que son respectivamente pytest y Coverage.py. Puedes controlar, por ejemplo, cuánto de tu código está cubierto con tests unitarios: es decir, cuántos casos de uso tienes correctamente validados. Esto da visibilidad de qué partes de tu código merecen especial atención, ya que al no estar testado el caso de uso concreto al que dan forma, existe una posible exposición a errores no controlados.

Análisis estático de código

No menos importante que los tests unitarios, es hacer análisis estático de nuestro código. En mi caso, y para Python, suelo trabajar con las siguientes herramientas:

  • pyupgrade busca en tu código el uso de sintaxis propia de versiones previas de Python y que está en vías de ser deprecada -si no lo está ya-. Si sueles programar sobre Python 3.6 y marcas como referencia Python 3.10, por ejemplo, pyupgrade intentará actualizar la sintaxis a la versión más reciente. Es una buena forma, también, de estar al día de las novedades del lenguaje en cuanto a sintaxis se refiere.
  • isort se encarga de ordenar las importaciones en tus módulos: primero los nativos del lenguaje, luego los de librerías de terceros y, por último, los de tu propio proyecto. En caso de importar más de un miembro de un mismo módulo, el orden será alfabético, priorizando mayúsculas sobre minúsculas. Lo hace por ti, así que despreocúpate de ordenar tus importaciones y céntrate en otras cosas más importantes.
  • black formatea nuestro código para cumplir con las normas de estilo del lenguaje -que no son pocas-. Separación entre clases y métodos, tamaño de línea, espacios de más donde no corresponde, normalización de comillas, etc.; no se le escapa nada. A mí me transmite una tremenda paz visual.
  • flake8 tiene como responsabilidad validar que todas normas de estilo y calidad del lenguaje se cumplen adecuadamente. Las herramientas previas tienen cometidos más atómicos, mientras que flake8 es más ambicioso en su análisis. Yo tengo por costumbre usarlo como último paso del análisis estático de mi código, ya que pyupgrade, isort y black habrán limado algunas aristas antes, y flake8 viene a la caza de los casos más rebuscados.

Si estás pensando que menudo coñazo tener que ejecutar tantas herramientas a mano, tengo buenas noticias para ti. ¿Sabes qué son los Git Hooks? Pues, básicamente, son eventos del ciclo de vida de algunas acciones de Git a los que podemos engancharnos para intervenirlos y personalizarlos. Por ejemplo, podemos apoyarnos en pre-commit para ejecutar el análisis estático de nuestro código de forma automática antes de hacer efectivo un commit contra el repositorio.

pre-commit haciendo su magia
pre-commit haciendo su magia

Si te fijas bien en la imagen anterior, verás el siguiente flujo:

  • Hago commit con una serie de cambios en palabros
  • pre-commit entra en acción y dispara un buen puñado de validaciones
  • isort detecta importaciones desordenadas en el código; pone orden, pero marca igualmente la validación como fallida
  • flake8 caza un par de gazapos: una importación que no se está usando y un método mal documentado; marca también la validación como fallida

Una o varias validaciones fallidas evitan que el commit se haga efectivo y, por tanto, que se integre código no deseado en la rama. Toca revisar todo aquello que no ha llegado al umbral de calidad marcado y probar de nuevo a hacer el commit.

Con esto conseguimos someter nuestro código a una revisión de calidad antes de llevarlo al repositorio, asegurando así la detección de problemas en fases tempranas. ¿Por qué delegar a procesos futuros esta tarea si el código puede salir de nuestro ordenador con la calidad esperada? Merece mucho la pena dedicar unos minutos a configurar estas validaciones; evitarán problemas en los siguientes pasos del ciclo de vida del desarrollo.

Así automatizaba, así, así

Hasta ahora hemos puesto foco en asegurar la calidad del código que sale de nuestras manos, pero también hay que verificar que se lleva bien con el código que desarrollan otros compañeros. Esto es, que el código, el nuestro y el de otros compañeros, se integra correctamente en una zona común y, a ser posible, de forma automática.

Esta necesidad podemos resolverla de varias formas, pero la que yo encuentro más sencilla y rápida de implementar es sumando las fuerzas de un «branching model» y unas «pipelines» de integración continua robustos.

Para el modelo de ramas no hace falta respetar al milímetro los más conocidos del mundillo: elige uno que cubra la mayor parte de tus necesidades y adapta los flecos que queden. Para palabros elegí un git-flow simplificado, pero opciones hay para todos los gustos: GitHub flow, GitLab Flow, Trunk Based Development, etc.

En cuanto al flujo de integración, consideré estas premisas:

  • Quiero testar el código antes de promocionarlo
  • Quiero publicar versiones dev de la aplicación para pruebas
  • Quiero publicar versiones estables asociadas a etiquetas

En unos garabatos rápidos de libreta llegué al siguiente diagrama:

Flujo de integración de palabros
Flujo de integración de palabros

El diagrama es bastante claro, pero aún así podemos hacer algo de zoom:

  • Las nuevas funcionalidades se desarrollan en ramas feature, que se integran en develop a través de pull requests. Cuando se crea una nueva pull request se ejecuta automáticamente la batería de tests definida contra el código que pretendemos integrar; es lo más parecido a validar localmente nuestro código.
  • Al dar por buena una pull request contra develop, se genera y publica, de forma desatendida, una versión dev de la aplicación en TestPyPI, que es repositorio estándar para ello. Aquí puede verse el histórico de versiones dev de palabros.
  • Cuando develop se considera maduro para liberar una versión pública, se hace una pull request contra master. De nuevo, volvemos a ejecutar la batería de tests de la aplicación, solo que esta vez el código objetivo es el de todos los compañeros que están trabajando en la aplicación.
  • Llegado el código a master, lo etiquetamos respetando las normas de formato de Semantic Versioning. Esta acción sirve de pistoletazo para la generación y publicación de una versión estable -release- de la aplicación en PyPI, que es el repositorio por defecto para estos menesteres. Aquí puede verse el histórico de versiones estables de palabros.

So far so good! Pero, ¿cómo se automatiza la ejecución de los tests en las pull requests y la publicación de versiones dev y estables cuando toca? Sabía que harías esa pregunta; sigue leyendo.

GitHub Actions

Existe un gran abanico de opciones en cuanto a software de integración continua se refiere, como pueden ser Jenkins, CircleCI o Spinnaker, pero, teniendo el código de palabros alojado en GitHub, ¿por qué recurrir a un tercero teniendo GitHub Actions tan a mano? No hay duda de que, para este caso, es la mejor opción posible.

Con GitHub Actions defines, de forma declarativa en formato YAML, cuándo, cómo y qué debe ejecutarse como flujo de integración continua de tu código, y la magia simplemente sucede. Existe un Marketplace cargado de Actions publicadas por otros desarrolladores, y entre ellas, con total seguridad, las hay cuyo cometido es justo lo que necesitas; te animo a echar un ojo antes de programar tu propia Action.

Vamos a ver un par de ejemplos de flujos de palabros.

El ejemplo anterior muestra el flujo de publicación de versiones estables en el repositorio PyPI:

  • Se ejecuta automáticamente al detectar una acción push contra una etiqueta con formato Semantic Versioning
  • Es un flujo de un solo job con múltiples pasos, y se ejecuta sobre un sistema operativo Ubuntu
  • El primer paso hace el «checkout» del repositorio, y el segundo genera una versión estable a partir del código y la publica en PyPI usando las credenciales almacenadas como secreto en GitHub

Si te has fijado bien, el segundo paso hace referencia a una Action local, no del Marketplace. Busqué antes de aventurarme, pero no encontré una opción concisa y liviana para generar y publicar versiones de aplicaciones Python haciendo uso de Poetry, así que la acabé desarrollando. No vamos a entrar en detalle en este artículo porque tiene algo de miga, pero te invito a echarle un ojo.

El flujo de publicación de versiones dev es exactamente igual, solo que usa las credenciales correspondientes del repositorio TestPyPI.

Vamos con otro ejemplo:

Este flujo corresponde a la ejecución de tests y análisis estático de código:

  • Se ejecuta automáticamente al detectar una pull request contra la rama develop o contra la rama master
  • Es un flujo de un solo job con múltiples pasos, y también se ejecuta sobre un sistema operativo Ubuntu
  • El primer paso hace el «checkout» del repositorio, y el segundo ejecuta aquellos tests marcados a true, y que son el análisis estático de código, la ejecución de tests unitarios y, por último, el verificación de cobertura de estos

Como en el flujo anterior, se hace uso de una Action propia. Las opciones que encontré en el Marketplace eran muy genéricas y permitían una gran configuración, pero impactaban enormemente en el tiempo de ejecución al necesitar instalar paquetería de sistema sobre la marcha. Preferí hacer una Action ad-hoc nada genérica para ganar tiempo. La tienes por aquí, por si la curiosidad te puede.

Una vez configurado todo, y ya trabajando sobre el repositorio en velocidad de crucero, verás cómo el panel de Actions va tomando forma:

Panel de GitHub Actions de palabros
Panel de GitHub Actions de palabros

That’s all folks!

Si has llegado hasta este punto en tu lectura, eres merecedor de todos mis respetos. Espero que te hayan gustado estos dos artículos o, al menos, que te hayan servido para perderle el miedo a eso de enfrentarse a un proyecto de software personal, y hacerlo de forma medianamente civilizada. Todo lo que aprendas por tu cuenta puedes llevarlo a un ámbito mayor y transmitir a otros las buenas prácticas descubiertas en el camino.

Un placer tenerte por aquí. ¡Otro día, más y mejor!

--

--

Diego Herrera

Hago software de vez en cuando. Mientras tanto, juego a videojuegos, toco la guitarra y veo cine asiático.