Me encontré por primera vez con el concepto de integración continua (CI) cuando se lanzó el proyecto Mozilla. Incluía un servidor de compilación rudimentario como parte del proceso, y esto fue revolucionario en ese momento. Mantenía un proyecto de C++ que tardó 2 horas en compilarse y vincularse.
Rara vez pasamos por una compilación limpia que creaba problemas de composición, ya que se comprometía un código incorrecto en el proyecto.
Mucho ha cambiado desde aquellos viejos tiempos. Los productos de CI están por todas partes y, como desarrolladores de Java, disfrutamos de una riqueza de capacidades como nunca antes. Pero me estoy adelantando… Comencemos con lo básico.
La integración continua es una práctica de desarrollo de software en la que los cambios de código se construyen y prueban automáticamente de manera frecuente y consistente.
El objetivo de CI es detectar y resolver los problemas de integración lo antes posible, reduciendo el riesgo de que se produzcan errores y otros problemas en la producción.
CI a menudo va de la mano con la entrega continua (CD), cuyo objetivo es automatizar todo el proceso de entrega de software, desde la integración del código hasta la implementación en producción.
El objetivo de CD es reducir el tiempo y el esfuerzo necesarios para implementar nuevas versiones y revisiones, lo que permite a los equipos ofrecer valor a los clientes con mayor rapidez y frecuencia.
Con CD, cada cambio de código que pasa las pruebas de CI se considera listo para su implementación, lo que permite a los equipos implementar nuevas versiones en cualquier momento con confianza. No hablaré sobre la entrega continua en esta publicación, pero regresaré a ella porque hay mucho que discutir.
Soy un gran admirador del concepto, pero hay algunas cosas que debemos monitorear.
Hay muchas herramientas poderosas de integración continua. Aquí hay algunas herramientas de uso común:
Jenkins : Jenkins es una de las herramientas de CI más populares y ofrece una amplia gama de complementos e integraciones para admitir varios lenguajes de programación y herramientas de compilación. Es de código abierto y ofrece una interfaz fácil de usar para configurar y administrar canalizaciones de compilación.
Está escrito en Java y, a menudo, era mi "herramienta de acceso". Sin embargo, es un dolor de administrar y configurar. Hay algunas soluciones de "Jenkins como servicio" que también limpian su experiencia de usuario, que es algo deficiente.
Tenga en cuenta que no mencioné las acciones de GitHub, a las que llegaremos en breve. Hay varios factores a considerar cuando se comparan herramientas de CI:
En general, Jenkins es conocido por su versatilidad y su extensa biblioteca de complementos, lo que lo convierte en una opción popular para equipos con procesos de compilación complejos. Travis CI y CircleCI son conocidos por su facilidad de uso e integración con herramientas SCM populares, lo que los convierte en una buena opción para equipos pequeños y medianos.
GitLab CI/CD es una opción popular para los equipos que utilizan GitLab para la gestión de su código fuente, ya que ofrece capacidades integradas de CI/CD. Bitbucket Pipelines es una buena opción para los equipos que utilizan Bitbucket para la gestión del código fuente, ya que se integra a la perfección con la plataforma.
El hospedaje de agentes es un factor importante a considerar al elegir una solución de CI. Hay dos opciones principales para el alojamiento de agentes: basado en la nube y local.
Al elegir una solución de CI, es importante tener en cuenta las necesidades y los requisitos específicos de su equipo.
Por ejemplo, si tiene una canalización de compilación grande y compleja, una solución local como Jenkins puede ser una mejor opción, ya que le brinda más control sobre la infraestructura subyacente.
Por otro lado, si tiene un equipo pequeño con necesidades simples, una solución basada en la nube como Travis CI puede ser una mejor opción, ya que es fácil de configurar y administrar.
El estado determina si los agentes conservan sus datos y configuraciones entre compilaciones.
Hay un animado debate entre los defensores de CI con respecto al mejor enfoque. Los agentes sin estado proporcionan un entorno limpio y fácil de reproducir. Los elijo para la mayoría de los casos y creo que son el mejor enfoque.
Los agentes apátridas también pueden ser más caros, ya que son más lentos de configurar. Dado que pagamos por los recursos de la nube, ese costo puede sumarse. Pero la razón principal por la que algunos desarrolladores prefieren los agentes con estado es la capacidad de investigar.
Con un agente sin estado, cuando falla un proceso de CI, por lo general no le queda otro medio de investigación que los registros.
Con un agente con estado, podemos iniciar sesión en la máquina e intentar ejecutar el proceso manualmente en la máquina dada. Podríamos reproducir un problema que falló y obtener información gracias a eso.
Una empresa con la que trabajé eligió Azure en lugar de GitHub Actions porque Azure permitía agentes con estado. Esto era importante para ellos al depurar un proceso de CI fallido.
No estoy de acuerdo con eso, pero es una opinión personal. Siento que pasé más tiempo resolviendo problemas de limpieza de agentes incorrectos que lo que me beneficié investigando un error. Pero esa es una experiencia personal, y algunos amigos míos inteligentes no están de acuerdo.
Las compilaciones repetibles se refieren a la capacidad de producir exactamente los mismos artefactos de software cada vez que se realiza una compilación, independientemente del entorno o el momento en que se realiza la compilación.
Desde una perspectiva de DevOps, tener compilaciones repetibles es esencial para garantizar que las implementaciones de software sean consistentes y confiables.
Las fallas intermitentes son la ruina de DevOps en todas partes y es doloroso rastrearlas.
Desafortunadamente, no hay una solución fácil. Por mucho que nos gustaría, algo de descamación encuentra su camino en proyectos con una complejidad razonable. Es nuestro trabajo minimizar esto tanto como sea posible. Hay dos bloqueadores para las compilaciones repetibles:
Al definir dependencias, debemos centrarnos en versiones específicas. Hay muchos esquemas de control de versiones, pero durante la última década, el control de versiones semántico estándar de tres números se apoderó de la industria.
Este esquema es inmensamente importante para CI, ya que su uso puede afectar significativamente la repetibilidad de una compilación, por ejemplo, con maven podemos hacer:
<dependency> <groupId>group</groupId> <artifactId>artifact</artifactId> <version>2.3.1</version> </dependency>
Esto es muy específico y excelente para la repetibilidad. Sin embargo, esto podría quedar obsoleto rápidamente. Podemos reemplazar el número de versión con LATEST
o RELEASE
, que obtendrá automáticamente la versión actual. Esto es malo ya que las compilaciones ya no serán repetibles.
Sin embargo, el enfoque de tres números codificados también es problemático. Suele ocurrir que una versión de parche representa una corrección de seguridad para un error. En ese caso, nos gustaría actualizar hasta la última actualización menor, pero no versiones más nuevas.
Por ejemplo, para ese caso anterior, me gustaría usar la versión 2.3.2
implícitamente y no 2.4.1
. Esto compensa cierta repetibilidad por errores y actualizaciones de seguridad menores.
Pero una mejor manera sería usar el complemento Maven Versions e invocar periódicamente el comando mvn versions:use-latest-releases
. Esto actualiza las versiones a la última para mantener nuestro proyecto actualizado.
Esta es la parte sencilla de las compilaciones repetibles. La dificultad está en las pruebas escamosas. Este es un dolor tan común que algunos proyectos definen una "cantidad razonable" de pruebas fallidas, y algunos proyectos vuelven a ejecutar la compilación varias veces antes de reconocer la falla.
Una de las principales causas de la descamación de la prueba es la fuga de estado. Las pruebas pueden fallar debido a efectos secundarios sutiles que quedaron de una prueba anterior. Lo ideal es que una prueba se limpie después de sí misma para que cada prueba se ejecute de forma aislada.
En un mundo perfecto, ejecutaríamos cada prueba en un entorno fresco completamente aislado, pero esto no es práctico. Significaría que las pruebas tardarían demasiado en ejecutarse y tendríamos que esperar mucho tiempo para el proceso de CI.
Podemos escribir pruebas con varios niveles de aislamiento; a veces necesitamos un aislamiento completo y es posible que necesitemos hacer girar un contenedor para una prueba. Pero la mayoría de las veces, no lo hacemos y la diferencia de velocidad es significativa.
La limpieza después de las pruebas es muy desafiante. A veces, las fugas de estado de herramientas externas, como la base de datos, pueden causar una falla de prueba irregular. Para garantizar la repetibilidad de la falla, es una práctica común clasificar los casos de prueba de manera consistente; esto garantiza que las ejecuciones futuras de la compilación se ejecuten en el mismo orden.
Este es un tema muy debatido. Algunos ingenieros creen que esto fomenta las pruebas con errores y oculta problemas que solo podemos descubrir con un orden aleatorio de pruebas. Desde mi experiencia, esto sí encontró errores en las pruebas, pero no en el código.
Mi objetivo no es crear pruebas perfectas, por lo que prefiero ejecutar las pruebas en un orden coherente, como el orden alfabético.
Es importante mantener estadísticas de las pruebas fallidas y nunca simplemente presionar reintentar. Al rastrear las pruebas problemáticas y el orden de ejecución de una falla, a menudo podemos encontrar la fuente del problema.
La mayoría de las veces, la causa raíz de la falla ocurre debido a la limpieza defectuosa en una prueba anterior, por lo que el orden es importante y su consistencia también es importante.
Estamos aquí para desarrollar un producto de software, no una herramienta de CI. La herramienta CI está aquí para mejorar el proceso. Desafortunadamente, muchas veces la experiencia con la herramienta de CI es tan frustrante que terminamos dedicando más tiempo a la logística que a la escritura del código.
A menudo, pasé días tratando de pasar una verificación de CI para poder fusionar mis cambios. Cada vez que me acerco, otro desarrollador fusionaría su cambio primero y rompería mi compilación.
Esto contribuye a una experiencia de desarrollador menos que estelar, especialmente cuando un equipo escala y pasamos más tiempo en la cola de CI que fusionando nuestros cambios. Hay muchas cosas que podemos hacer para aliviar estos problemas:
En última instancia, esto se conecta directamente con la productividad de los desarrolladores. Pero no tenemos perfiladores para este tipo de optimizaciones. Tenemos que medir cada vez; esto puede ser laborioso.
GitHub Actions es una plataforma de integración continua/entrega continua (CI/CD) integrada en GitHub. No tiene estado, aunque permite el autohospedaje de los agentes hasta cierto punto. Me estoy centrando en él, ya que es gratuito para proyectos de código abierto y tiene una cuota gratuita decente para proyectos de código cerrado.
Este producto es un competidor relativamente nuevo en el campo, no es tan flexible como la mayoría de las otras herramientas de CI mencionadas anteriormente. Sin embargo, es muy conveniente para los desarrolladores gracias a su profunda integración con GitHub y agentes sin estado.
Para probar GitHub Actions, necesitamos un nuevo proyecto que, en este caso, generé usando JHipster con la configuración que se ve aquí:
Creé un proyecto separado que demuestra el uso de GitHub Actions aquí. Tenga en cuenta que puede seguir esto con cualquier proyecto; aunque incluimos instrucciones maven en este caso, el concepto es muy simple.
Una vez que se crea el proyecto, podemos abrir la página del proyecto en GitHub y pasar a la pestaña de acciones.
Veremos algo como esto:
En la esquina inferior derecha, podemos ver el tipo de proyecto Java con Maven. Una vez que elegimos este tipo, pasamos a la creación de un archivo maven.yml
como se muestra aquí:
Desafortunadamente, el maven.yml predeterminado sugerido por GitHub incluye un problema. Este es el código que vemos en esta imagen:
name: Java CI with Maven on: push: branches: [ "master" ] pull_request: branches: [ "master" ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up JDK 11 uses: actions/setup-java@v3 with: java-version: '11' distribution: 'temurin' cache: maven - name: Build with Maven run: mvn -B package --file pom.xml # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - name: Update dependency graph uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6
Las últimas tres líneas actualizan el gráfico de dependencia. Pero esta característica falla, o al menos falló para mí. Eliminarlos resolvió el problema. El resto del código es una configuración YAML estándar.
Las líneas pull_request
y push
cerca de la parte superior del código declaran que las compilaciones se ejecutarán tanto en una solicitud de extracción como en una inserción en el maestro. Esto significa que podemos ejecutar nuestras pruebas en una solicitud de extracción antes de confirmar. Si la prueba falla, no nos comprometemos.
Podemos prohibir la confirmación con pruebas fallidas en la configuración del proyecto. Una vez que confirmemos el archivo YAML, podemos crear una solicitud de extracción y el sistema ejecutará el proceso de compilación por nosotros. Esto incluye ejecutar las pruebas ya que el objetivo del "paquete" en maven ejecuta las pruebas de forma predeterminada.
El código que invoca las pruebas está en la línea que comienza con "ejecutar" cerca del final. Esta es efectivamente una línea de comando estándar de Unix. A veces, tiene sentido crear un script de shell y simplemente ejecutarlo desde el proceso de CI.
A veces es más fácil escribir un buen script de shell que lidiar con todos los archivos YAML y los ajustes de configuración de varias pilas de CI.
También es más portátil si elegimos cambiar la herramienta de CI en el futuro. Aquí, no lo necesitamos, ya que maven es suficiente para nuestras necesidades actuales.
Podemos ver la solicitud de extracción exitosa aquí:
Para probar esto, podemos agregar un error al código cambiando el punto final “/api”
a “/myapi”
. Esto produce la falla que se muestra a continuación. También desencadena un correo electrónico de error enviado al autor de la confirmación.
Cuando ocurre tal falla, podemos hacer clic en el enlace "Detalles" en el lado derecho. Esto nos lleva directamente al mensaje de error que ves aquí:
Desafortunadamente, este suele ser un mensaje inútil que no brinda ayuda en la resolución del problema. Sin embargo, si se desplaza hacia arriba, se mostrará la falla real que, por lo general, se resalta convenientemente para nosotros, como se ve aquí:
Tenga en cuenta que a menudo hay múltiples fallas, por lo que sería prudente desplazarse más hacia arriba. En este error, podemos ver que la falla fue una aserción en la línea 394
de AccountResourceIT que puede ver aquí, tenga en cuenta que los números de línea no coinciden. En este caso, la línea 394
es la última línea del método:
@Test @Transactional void testActivateAccount() throws Exception { final String activationKey = "some activation key"; User user = new User(); user.setLogin("activate-account"); user.setEmail("[email protected]"); user.setPassword(RandomStringUtils.randomAlphanumeric(60)); user.setActivated(false); user.setActivationKey(activationKey); userRepository.saveAndFlush(user); restAccountMockMvc.perform(get("/api/activate?key={activationKey}", activationKey)).andExpect(status().isOk()); user = userRepository.findOneByLogin(user.getLogin()).orElse(null); assertThat(user.isActivated()).isTrue(); }
Esto significa que la llamada de aserción falló. isActivated()
devolvió false
y falló la prueba. Esto debería ayudar a un desarrollador a reducir el problema y comprender la causa raíz.
Como mencionamos antes, CI se trata de la productividad del desarrollador. Podemos ir mucho más allá que simplemente compilar y probar. Podemos hacer cumplir los estándares de codificación, filtrar el código, detectar vulnerabilidades de seguridad y mucho más.
En este ejemplo, integremos Sonar Cloud, que es una poderosa herramienta de análisis de código (linter). Encuentra errores potenciales en su proyecto y lo ayuda a mejorar la calidad del código.
SonarCloud es una versión basada en la nube de SonarQube que permite a los desarrolladores inspeccionar y analizar continuamente su código para encontrar y solucionar problemas relacionados con la calidad, la seguridad y el mantenimiento del código. Admite varios lenguajes de programación como Java, C#, JavaScript, Python y más.
SonarCloud se integra con herramientas de desarrollo populares como GitHub, GitLab, Bitbucket, Azure DevOps y más. Los desarrolladores pueden usar SonarCloud para obtener comentarios en tiempo real sobre la calidad de su código y mejorar la calidad general del código.
Por otro lado, SonarQube es una plataforma de código abierto que proporciona herramientas de análisis de código estático para desarrolladores de software. Proporciona un tablero que muestra un resumen de la calidad del código y ayuda a los desarrolladores a identificar y solucionar problemas relacionados con la calidad, la seguridad y la capacidad de mantenimiento del código.
Tanto SonarCloud como SonarQube brindan funcionalidades similares, pero SonarCloud es un servicio basado en la nube y requiere una suscripción, mientras que SonarQube es una plataforma de código abierto que se puede instalar en las instalaciones o en un servidor en la nube.
En aras de la simplicidad, usaremos SonarCloud, pero SonarQube debería funcionar bien. Para empezar, vamos a sonarcloud.io y nos registramos. Idealmente con nuestra cuenta de GitHub. Luego se nos presenta una opción para agregar un repositorio para el monitoreo de Sonar Cloud como se muestra aquí:
Cuando seleccionamos la opción Analizar nueva página, necesitamos autorizar el acceso a nuestro repositorio de GitHub. El siguiente paso es seleccionar los proyectos que deseamos agregar a Sonar Cloud como se muestra aquí:
Una vez que seleccionamos y procedemos al proceso de configuración, debemos elegir el método de análisis. Como usamos GitHub Actions, debemos elegir esa opción en la siguiente etapa, como se ve aquí:
Una vez configurado esto, ingresamos a la etapa final dentro del asistente de Sonar Cloud como se ve en la siguiente imagen. Recibimos un token que podemos copiar (la entrada 2 está borrosa en la imagen) y lo usaremos en breve.
Tenga en cuenta que también hay instrucciones predeterminadas para usar con maven que aparecen una vez que hace clic en el botón con la etiqueta "Maven".
Volviendo al proyecto en GitHub, podemos pasar a la pestaña de configuración del proyecto (que no debe confundirse con la configuración de la cuenta en el menú superior). Aquí, seleccionamos "Secretos y variables" como se muestra aquí:
En este apartado podemos añadir un nuevo repositorio secreto, en concreto la clave y el valor SONAR_TOKEN que copiamos de SonarCloud como podéis ver aquí:
GitHub Repository Secrets es una función que permite a los desarrolladores almacenar de forma segura información confidencial asociada con un repositorio de GitHub, como claves API, tokens y contraseñas, que se requieren para autenticar y autorizar el acceso a varios servicios o plataformas de terceros utilizados por el repositorio. .
El concepto detrás de GitHub Repository Secrets es brindar una manera segura y conveniente de administrar y compartir información confidencial, sin tener que exponer la información públicamente en código o archivos de configuración.
Mediante el uso de secretos, los desarrolladores pueden mantener la información confidencial separada del código base y protegerla para que no quede expuesta o comprometida en caso de una brecha de seguridad o acceso no autorizado.
Los secretos del repositorio de GitHub se almacenan de forma segura y solo pueden acceder a ellos los usuarios autorizados a los que se les ha otorgado acceso al repositorio. Los secretos se pueden usar en flujos de trabajo, acciones y otros scripts asociados con el repositorio.
Se pueden pasar como variables de entorno al código para que pueda acceder y utilizar los secretos de forma segura y fiable.
En general, GitHub Repository Secrets brinda una forma simple y efectiva para que los desarrolladores administren y protejan la información confidencial asociada con un repositorio, lo que ayuda a garantizar la seguridad y la integridad del proyecto y los datos que procesa.
Ahora necesitamos integrar esto en el proyecto. Primero, necesitamos agregar estas dos líneas al archivo pom.xml. Tenga en cuenta que debe actualizar el nombre de la organización para que coincida con el suyo. Estos deben ir en la sección en el XML:
<sonar.organization>shai-almog</sonar.organization> <sonar.host.url>https://sonarcloud.io</sonar.host.url>
Tenga en cuenta que el proyecto JHipster que creamos ya tiene soporte para SonarQube, que debe eliminarse del archivo pom antes de que este código funcione.
Después de esto, podemos reemplazar la parte "Crear con Maven" del archivo maven.yml
con la siguiente versión:
- name: Build with Maven env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=shai-almog_HelloJHipster package
Una vez que hagamos eso, SonarCloud proporcionará informes para cada solicitud de extracción fusionada en el sistema como se muestra aquí:
Podemos ver un informe que incluye una lista de errores, vulnerabilidades, olores y problemas de seguridad. Hacer clic en cada uno de esos problemas nos lleva a algo como esto:
Tenga en cuenta que tenemos pestañas que explican exactamente por qué el problema es un problema, cómo solucionarlo y más. Esta es una herramienta notablemente poderosa que sirve como uno de los revisores de código más valiosos del equipo.
Dos elementos interesantes adicionales que vimos antes son los informes de cobertura y duplicación. SonarCloud espera que las pruebas tengan una cobertura de código del 80% (activar el 80% del código en una solicitud de extracción), esto es alto y se puede configurar en la configuración.
También señala el código duplicado que podría indicar una violación del principio Don't Repeat Yourself (DRY).
CI es un tema enorme con muchas oportunidades para mejorar el flujo de su proyecto. Podemos automatizar la detección de errores. Optimice la generación de artefactos, la entrega automatizada y mucho más. Pero en mi humilde opinión, el principio central detrás de CI es la experiencia del desarrollador.
Está aquí para hacernos la vida más fácil.
Cuando se hace mal, el proceso de IC puede convertir esta increíble herramienta en una pesadilla. Pasar las pruebas se convierte en un ejercicio inútil. Volvemos a intentarlo una y otra vez hasta que finalmente podamos fusionarnos. Esperamos durante horas para fusionarnos debido a las colas lentas y llenas de gente.
Esta herramienta que se suponía que ayudaría se convierte en nuestra némesis. Este no debería ser el caso. CI debería hacernos la vida más fácil, no al revés.