{"id":31304,"date":"2019-10-31T21:40:34","date_gmt":"2019-10-31T18:40:34","guid":{"rendered":"https:\/\/prohoster.info\/blog\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki\/"},"modified":"2019-10-31T21:40:34","modified_gmt":"2019-10-31T18:40:34","slug":"evolyutsiya-ci-v-komande-mobilnoj-razrabotki","status":"publish","type":"post","link":"https:\/\/prohoster.info\/es\/blog\/administrirovanie\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki","title":{"rendered":"La evoluci\u00f3n de CI en el equipo de desarrollo m\u00f3vil","gt_translate_keys":[{"key":"rendered","format":"text"}]},"content":{"rendered":"<p>Hoy en d\u00eda, la mayor\u00eda de los productos de software se desarrollan en equipos. Las condiciones para el \u00e9xito del desarrollo en equipo se pueden representar en un esquema simple.<\/p>\n<p><img decoding=\"async\" alt=\"La evoluci\u00f3n de CI en el equipo de desarrollo m\u00f3vil\" src=\"\/wp-content\/uploads\/2019\/04\/b283cfd7772d8f479be0754ebbe51b19.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nUna vez que escribas tu c\u00f3digo, debes asegurarte de que:<\/p>\n<ol>\n<li>Funciona.<\/li>\n<li>No rompe nada, incluyendo el c\u00f3digo que han escrito tus compa\u00f1eros.<\/li>\n<\/ol>\n<p>\nSi ambas condiciones se cumplen, est\u00e1s en el camino hacia el \u00e9xito. Para verificar f\u00e1cilmente estas condiciones y no desviarte de un camino ventajoso, se ide\u00f3 la Integraci\u00f3n Continua.<\/p>\n<p>CI es un proceso de trabajo en el que integras tu c\u00f3digo en el c\u00f3digo general del producto con la mayor frecuencia posible. Y no solo integras, sino que adem\u00e1s revisas constantemente que todo funcione. Dado que necesitas hacer muchas verificaciones y con frecuencia, vale la pena pensar en la automatizaci\u00f3n. Se puede verificar todo manualmente, pero no es recomendable, y aqu\u00ed est\u00e1 el porqu\u00e9.<br \/>\n<noindex><a rel=\"nofollow\" name=\"habracut\"><\/a><\/noindex><\/p>\n<ul>\n<li><strong>Las personas son costosas<\/strong>. La hora de trabajo de un programador cuesta m\u00e1s que la hora de trabajo de cualquier servidor.<\/li>\n<li><strong>Las personas cometen errores<\/strong>. Por lo tanto, pueden surgir situaciones en las que se ejecuten pruebas en la rama incorrecta o se compila el commit equivocado para los testers.<\/li>\n<li><strong>Las personas son perezosas<\/strong>. De vez en cuando, cuando termino alguna tarea, me surge el pensamiento: '\u00bfPara qu\u00e9 comprobar esto? Solo escrib\u00ed dos l\u00edneas; seguro que todo funciona!' Creo que a algunos de ustedes tambi\u00e9n les vienen a la mente esos pensamientos a veces. Pero siempre hay que verificar.<\/li>\n<\/ul>\n<p>\nC\u00f3mo se implement\u00f3 y desarroll\u00f3 la Integraci\u00f3n Continua en el equipo de desarrollo m\u00f3vil de Avito, c\u00f3mo pasaron de 0 a 450 compilaciones al d\u00eda, y qu\u00e9 las m\u00e1quinas de compilaci\u00f3n re\u00fanen 200 horas al d\u00eda, lo cuenta Nikolai Nesterov (<noindex><a rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/users\/nnesterov\/\" class=\"user_link\">nnesterov<\/a><\/noindex>) \u2014 participante de todos los cambios evolutivos en CI\/CD de la aplicaci\u00f3n Android.<\/p>\n<p>La narraci\u00f3n est\u00e1 basada en el ejemplo del equipo de Android, pero la mayor\u00eda de los enfoques son aplicables tambi\u00e9n en iOS.<\/p>\n<p><center><div class=\"youtube-placeholder\" data-id=\"lz8MNATTUCU\" onclick=\"loadVideo(this)\">\r\n        <img decoding=\"async\" src=\"https:\/\/img.youtube.com\/vi\/lz8MNATTUCU\/hqdefault.jpg\" alt=\"Reproducir video\" loading=\"lazy\" width=\"480\" height=\"360\" style=\"width:100%;height:auto;\">\r\n        <div class=\"play-button\"><\/div>\r\n    <\/div><\/center><br \/>\nHace mucho tiempo, en el equipo de Android de Avito, solo trabajaba una persona. Por definici\u00f3n, no necesitaba nada de Integraci\u00f3n Continua: no hab\u00eda con qui\u00e9n integrarse.<\/p>\n<p>Pero la aplicaci\u00f3n crec\u00eda, surg\u00edan cada vez m\u00e1s tareas nuevas y, por lo tanto, el equipo tambi\u00e9n crec\u00eda. En alg\u00fan momento lleg\u00f3 el momento de formalizar m\u00e1s el proceso de integraci\u00f3n del c\u00f3digo. Se decidi\u00f3 utilizar Git flow.<\/p>\n<p><img decoding=\"async\" alt=\"La evoluci\u00f3n de CI en el equipo de desarrollo m\u00f3vil\" src=\"\/wp-content\/uploads\/2019\/04\/f433effc5ab5a59de3d6bd20a7a87057.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nLa conceptualizaci\u00f3n de Git flow es conocida: en el proyecto hay una rama com\u00fan llamada develop, y para cada nueva funcionalidad, los desarrolladores crean una rama separada, hacen commits en ella, la empujan y, cuando desean fusionar su c\u00f3digo en la rama develop, abren una pull request. Para compartir conocimientos y discutir enfoques, hemos implementado revisiones de c\u00f3digo, es decir, los colegas deben revisar y confirmar el c\u00f3digo de los dem\u00e1s.<\/p>\n<h2>Revisiones<\/h2>\n<p>\nVer el c\u00f3digo no es suficiente, por lo tanto se implementan revisiones autom\u00e1ticas.<\/p>\n<ul>\n<li>Primero comprobamos <strong>la construcci\u00f3n de ARC<\/strong>.<\/li>\n<li>Muchos <strong>tests de Junit<\/strong>.<\/li>\n<li><strong>Calculamos la cobertura de c\u00f3digo<\/strong>, dado que estamos ejecutando pruebas.<\/li>\n<\/ul>\n<p>\nPara entender c\u00f3mo se deben ejecutar estas pruebas, observemos el proceso de desarrollo en Avito.<\/p>\n<p>Esquem\u00e1ticamente, se puede representar as\u00ed:<\/p>\n<ul>\n<li>El desarrollador escribe c\u00f3digo en su port\u00e1til. Se pueden ejecutar pruebas de integraci\u00f3n aqu\u00ed mismo \u2014 ya sea mediante un hook de commit o simplemente ejecutando pruebas en segundo plano.<\/li>\n<li>Despu\u00e9s de que el desarrollador haya empujado el c\u00f3digo, abre una pull request. Para que su c\u00f3digo entre en la rama develop, debe pasar por una revisi\u00f3n de c\u00f3digo y obtener el n\u00famero necesario de aprobaciones. Se pueden incluir pruebas y compilaciones aqu\u00ed: mientras no todas las compilaciones sean exitosas, la pull request no puede fusionarse.<\/li>\n<li>Una vez que la pull request se fusiona y el c\u00f3digo entra en develop, se puede elegir un momento conveniente: por ejemplo, de noche, cuando todos los servidores est\u00e1n libres, y ejecutar tantas pruebas como se pueda.<\/li>\n<\/ul>\n<p>\nA nadie le gust\u00f3 ejecutar pruebas en su propio port\u00e1til. Cuando el desarrollador termina una funcionalidad, quiere empujarla r\u00e1pidamente y abrir la pull request. Si en ese momento se est\u00e1n ejecutando algunas pruebas largas, no solo es inc\u00f3modo, sino que tambi\u00e9n ralentiza el desarrollo: mientras el port\u00e1til est\u00e1 comprobando algo, no se puede trabajar normalmente.<\/p>\n<p>Nos gust\u00f3 mucho ejecutar pruebas por la noche, porque hay mucho tiempo y servidores, se puede disfrutar. Pero, desafortunadamente, cuando el c\u00f3digo de la funcionalidad entra en develop, el desarrollador ya tiene mucha menos motivaci\u00f3n para corregir los errores que encontr\u00f3 CI. De vez en cuando me encontraba pensando, al ver el informe matutino con todos los errores encontrados, que los arreglar\u00eda en otro momento, porque actualmente tengo una nueva tarea interesante en Jira que realmente quiero comenzar.<\/p>\n<p>Si las pruebas bloquean la pull request, entonces hay suficiente motivaci\u00f3n, porque mientras las compilaciones no se pongan en verde, el c\u00f3digo no entrar\u00e1 en develop, y por lo tanto, la tarea no se completar\u00e1.<\/p>\n<p>Al final, elegimos esta estrategia: por la noche realizamos la m\u00e1xima cantidad posible de pruebas, y las m\u00e1s cr\u00edticas y, lo m\u00e1s importante, r\u00e1pidas, las ejecutamos en la solicitud de extracci\u00f3n. Pero no nos detenemos ah\u00ed; paralelamente optimizamos la velocidad de las pruebas para poder pasarlas de modo nocturno a pruebas en la solicitud de extracci\u00f3n.<\/p>\n<p>En ese momento, todas nuestras compilaciones pasaban bastante r\u00e1pido, as\u00ed que simplemente activamos como bloqueador en la solicitud de extracci\u00f3n la compilaci\u00f3n ARK, pruebas Junit y el c\u00e1lculo de la cobertura de c\u00f3digo. Lo activamos, pensamos... y nos deshicimos de la cobertura de c\u00f3digo, porque consideramos que no la necesit\u00e1bamos.<\/p>\n<p><strong><em>La configuraci\u00f3n b\u00e1sica de CI nos tom\u00f3 dos d\u00edas (las estimaciones de tiempo aqu\u00ed y en adelante son aproximadas, necesarias para dar una idea de la escala). <\/em><\/strong><\/p>\n<p>Despu\u00e9s de esto comenzamos a pensar m\u00e1s: \u00bfestamos verificando correctamente? \u00bfEstamos ejecutando las compilaciones en la solicitud de extracci\u00f3n de manera adecuada?<\/p>\n<p>Ejecutamos la compilaci\u00f3n en el \u00faltimo commit de la rama desde la cual se abri\u00f3 la solicitud de extracci\u00f3n. Pero las verificaciones de ese commit solo pueden mostrar que el c\u00f3digo que escribi\u00f3 el desarrollador funciona. No demuestran que no rompi\u00f3 nada. En realidad, debemos verificar el estado de la rama develop despu\u00e9s de que se haya integrado la caracter\u00edstica.<\/p>\n<p><img decoding=\"async\" alt=\"La evoluci\u00f3n de CI en el equipo de desarrollo m\u00f3vil\" src=\"\/wp-content\/uploads\/2019\/04\/c1a179b68b02c04031177f9bf51a81ed.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nPara esto, escribimos un sencillo script bash. <strong>premerge.sh:<\/strong><\/p>\n<pre><code class=\"bash\">#!\/usr\/bin\/env bash\n\nset -e\n\ngit fetch origin develop\n\ngit merge origin\/develop<\/code><\/pre>\n<p>\nAqu\u00ed simplemente se traen todos los cambios m\u00e1s recientes de develop y se fusionan en la rama actual. Agregamos el script premerge.sh como primer paso de todas las compilaciones y comenzamos a verificar exactamente lo que queremos, es decir, <strong>integraci\u00f3n.<\/strong>.<\/p>\n<p><strong><em>Para la localizaci\u00f3n de problemas, b\u00fasqueda de soluciones y redacci\u00f3n de este script, nos llev\u00f3 tres d\u00edas.<\/em><\/strong><\/p>\n<p>La aplicaci\u00f3n fue evolucionando, aparec\u00edan m\u00e1s tareas, el equipo crec\u00eda y premerge.sh a veces comenz\u00f3 a defraudarnos. Cambios conflictivos ingresaban en develop, rompiendo la compilaci\u00f3n.<\/p>\n<p>Un ejemplo de c\u00f3mo sucede esto:<\/p>\n<p><img decoding=\"async\" alt=\"La evoluci\u00f3n de CI en el equipo de desarrollo m\u00f3vil\" src=\"\/wp-content\/uploads\/2019\/04\/6762d9ce4e431549455c49f2317a8f20.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nDos desarrolladores comienzan a trabajar en las caracter\u00edsticas A y B al mismo tiempo. El desarrollador de la caracter\u00edstica A descubre una funci\u00f3n no utilizada en el proyecto, <code>answer()<\/code> y, como un buen explorador, la elimina. Mientras tanto, el desarrollador de la caracter\u00edstica B en su rama agrega una nueva llamada a esta funci\u00f3n.<\/p>\n<p>Los desarrolladores terminan su trabajo y al mismo tiempo abren la solicitud de extracci\u00f3n. Se inician las compilaciones, premerge.sh verifica ambas solicitudes de extracci\u00f3n con respecto al estado fresco de develop: todas las verificaciones est\u00e1n verdes. Despu\u00e9s de esto, se fusiona la solicitud de extracci\u00f3n de la caracter\u00edstica A, se fusiona la solicitud de extracci\u00f3n de la caracter\u00edstica B... \u00a1Boom! Develop se rompe, porque en el c\u00f3digo de develop hay una llamada a una funci\u00f3n inexistente.<\/p>\n<p><img decoding=\"async\" alt=\"La evoluci\u00f3n de CI en el equipo de desarrollo m\u00f3vil\" src=\"\/wp-content\/uploads\/2019\/04\/0261dcc9f7f2e5e018ab136081b4679b.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nCuando develop no se compila, esto es <strong>cat\u00e1strofe local<\/strong>. Todo el equipo no puede reunir nada para las pruebas.<\/p>\n<p>Resulta que, con mayor frecuencia, me ocup\u00e9 de tareas de infraestructura: anal\u00edtica, red, bases de datos. Es decir, fui yo quien escribi\u00f3 las funciones y clases que utilizan otros desarrolladores. Por eso, a menudo me encontraba en situaciones similares. Incluso hubo un tiempo en el que ten\u00eda esta imagen colgada.<\/p>\n<p><img decoding=\"async\" alt=\"La evoluci\u00f3n de CI en el equipo de desarrollo m\u00f3vil\" src=\"\/wp-content\/uploads\/2019\/04\/0eb1aadd05b33ce995027ab52d5c1e53.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nDado que eso no nos satisfac\u00eda, comenzamos a explorar opciones para prevenirlo.<\/p>\n<h2>C\u00f3mo no romper develop<\/h2>\n<p>\nPrimera opci\u00f3n: <strong>reconstruir todos los pull requests al actualizar develop. <\/strong>Si en nuestro ejemplo el pull request con la caracter\u00edstica A llega primero a develop, el pull request de la caracter\u00edstica B se reconstruir\u00e1 y, por lo tanto, no pasar\u00e1 las pruebas debido a un error de compilaci\u00f3n.<\/p>\n<p>Para entender cu\u00e1nto tiempo llevar\u00e1 esto, consideremos el ejemplo de dos PR. Abrimos dos PR: dos compilaciones, dos ejecuciones de pruebas. Despu\u00e9s de que el primer PR se mezcle en develop, el segundo necesita ser reconstruido. En total, para dos PR se necesitan tres ejecuciones de pruebas: 2 + 1 = 3.<\/p>\n<p>En principio, est\u00e1 bien. Pero revisamos las estad\u00edsticas, y una situaci\u00f3n t\u00edpica en nuestro equipo era tener 10 PR abiertos, y entonces el n\u00famero de pruebas ser\u00eda la suma de la progresi\u00f3n: 10 + 9 +\u2026 + 1 = 55. Es decir, para aceptar 10 PR, hay que reconstruir 55 veces. Y eso en una situaci\u00f3n ideal, cuando todas las pruebas pasan a la primera, cuando nadie abre un pull request adicional mientras se procesan esos diez.<\/p>\n<p>Imag\u00ednate como desarrollador, que necesitas apresurarte a presionar el bot\u00f3n \"merge\" primero, porque si lo hace el vecino, entonces tendr\u00e1s que esperar a que todas las compilaciones se realicen de nuevo... No, eso no funcionar\u00e1, ralentizar\u00e1 gravemente el desarrollo.<\/p>\n<p>Segunda posible manera: <strong>compilar pull requests despu\u00e9s de code review. <\/strong>Es decir, abres un pull request, obtienes la cantidad necesaria de aprobaciones de tus colegas, corriges lo que sea necesario, y despu\u00e9s ejecutas las compilaciones. Si son exitosas, el pull request se mezcla con develop. En este caso, no hay reinicios adicionales, pero la retroalimentaci\u00f3n se ralentiza considerablemente. Yo, como desarrollador, al abrir un pull request, quiero ver inmediatamente si se compila. Por ejemplo, si alguna prueba falla, necesito arreglarla r\u00e1pidamente. En el caso de una compilaci\u00f3n retrasada, la retroalimentaci\u00f3n se ralentiza, lo que significa que todo el desarrollo tambi\u00e9n se ralentiza. Esto tampoco nos satisfizo.<\/p>\n<p>Al final, solo qued\u00f3 la tercera opci\u00f3n \u2014 <strong>hacerlo a mano<\/strong>. Todo nuestro c\u00f3digo y todos nuestros archivos fuente se almacenan en el repositorio del servidor Bitbucket. Por lo tanto, tuvimos que desarrollar un complemento para Bitbucket.<\/p>\n<p><img decoding=\"async\" alt=\"La evoluci\u00f3n de CI en el equipo de desarrollo m\u00f3vil\" src=\"\/wp-content\/uploads\/2019\/04\/5e4b1f770ea1a3449b616c3101640294.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nEste complemento redefine el mecanismo de fusi\u00f3n de solicitudes de extracci\u00f3n. Comienza de manera est\u00e1ndar: se abre una PR, se ejecutan todas las compilaciones y se realiza la revisi\u00f3n de c\u00f3digo. Pero una vez que se ha pasado la revisi\u00f3n de c\u00f3digo y el desarrollador decide pulsar \u00abfusionar\u00bb, el complemento verifica el estado de desarrollo respecto al que se ejecutaron las pruebas. Si despu\u00e9s de las compilaciones el desarrollo se actualiz\u00f3, el complemento no permitir\u00e1 que se fusione esa solicitud de extracci\u00f3n en la rama principal. Simplemente reiniciar\u00e1 las compilaciones en relaci\u00f3n con el desarrollo m\u00e1s reciente.<\/p>\n<p><img decoding=\"async\" alt=\"La evoluci\u00f3n de CI en el equipo de desarrollo m\u00f3vil\" src=\"\/wp-content\/uploads\/2019\/04\/0edee880e1f2d286a0c64033103ac136.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nEn nuestro ejemplo con cambios en conflicto, tales compilaciones no pasar\u00e1n debido a un error de compilaci\u00f3n. Por lo tanto, el desarrollador de la funci\u00f3n B tendr\u00e1 que corregir el c\u00f3digo y reiniciar las verificaciones, momento en el cual el complemento aplicar\u00e1 autom\u00e1ticamente el pull request.<\/p>\n<p>Antes de implementar este complemento, ten\u00edamos un promedio de 2.7 lanzamientos de verificaci\u00f3n por cada pull request. Con el complemento, se convirti\u00f3 en 3.6 lanzamientos. Esto fue satisfactorio para nosotros.<\/p>\n<p>Cabe destacar que este complemento tiene una desventaja: reinicia la compilaci\u00f3n solo una vez. Es decir, todav\u00eda queda una peque\u00f1a ventana a trav\u00e9s de la cual podr\u00edan entrar cambios en conflicto en develop. Pero la probabilidad de que esto ocurra es baja, y decidimos aceptar este compromiso entre el n\u00famero de lanzamientos y el riesgo de fallos. En dos a\u00f1os, solo sucedi\u00f3 una vez, por lo que probablemente no fue en vano.<\/p>\n<p><strong><em>Nos tom\u00f3 dos semanas desarrollar la primera versi\u00f3n del complemento para Bitbucket. <\/em><\/strong><\/p>\n<h3>Nuevas verificaciones<\/h3>\n<p>\nMientras tanto, nuestro equipo continu\u00f3 creciendo. Se a\u00f1adieron nuevas verificaciones.<\/p>\n<p>Pensamos: \u00bfpor qu\u00e9 arreglar errores si se pueden prevenir? Y por eso implementamos <strong>an\u00e1lisis est\u00e1tico de c\u00f3digo<\/strong>. Comenzamos con lint, que forma parte del Android SDK. Pero en ese momento no sab\u00eda trabajar con c\u00f3digo Kotlin, y ya ten\u00edamos el 75% de la aplicaci\u00f3n escrita en Kotlin. As\u00ed que junto con lint, se a\u00f1adieron las verificaciones integradas de <strong>Android Studio.<\/strong><\/p>\n<p>Para ello, tuvimos que hacer muchas acrobacias: tomar Android Studio, empaquetarla en Docker y ejecutarla en CI con un monitor virtual, para que pensara que estaba corriendo en una laptop real. Pero funcion\u00f3.<\/p>\n<p>Tambi\u00e9n en ese momento comenzamos a escribir muchas <strong>pruebas de instrumentaci\u00f3n<\/strong> e implementamos <strong>pruebas de capturas de pantalla<\/strong>. Esto es cuando se genera una captura de pantalla de referencia para una peque\u00f1a vista espec\u00edfica, y la prueba consiste en tomar una captura de pantalla de esa vista y compararla p\u00edxel por p\u00edxel con la referencia. Si hay alguna discrepancia, significa que algo fall\u00f3 en la maquetaci\u00f3n o hay alg\u00fan problema con los estilos.<\/p>\n<p>Pero las pruebas de instrumentaci\u00f3n y las pruebas de captura de pantalla deben ejecutarse en dispositivos: en emuladores o en dispositivos reales. Teniendo en cuenta que hay muchas pruebas y se ejecutan con frecuencia, se necesita una granja completa. Crear nuestra propia granja consume demasiados recursos, as\u00ed que encontramos una opci\u00f3n lista: Firebase Test Lab.<\/p>\n<h3>Firebase Test Lab<\/h3>\n<p>\nSe eligi\u00f3 porque Firebase es un producto de Google, es decir, deber\u00eda ser confiable y es poco probable que desaparezca. Los precios son accesibles: 5$ por hora de uso de un dispositivo real, 1$ por hora de uso de un emulador.<\/p>\n<p><strong><em>La implementaci\u00f3n de Firebase Test Lab en nuestro CI tom\u00f3 aproximadamente tres semanas.<\/em><\/strong><\/p>\n<p>Sin embargo, el equipo continu\u00f3 creciendo y, desafortunadamente, Firebase comenz\u00f3 a fallarnos. En ese momento no ten\u00eda ning\u00fan SLA. A veces, Firebase hac\u00eda esperar hasta que estuvieran disponibles suficientes dispositivos para las pruebas, en lugar de comenzar a ejecutarlas de inmediato, como dese\u00e1bamos. La espera en la cola pod\u00eda llevar hasta media hora, lo cual es mucho tiempo. Las pruebas de instrumentaci\u00f3n se ejecutaban en cada PR, y los retrasos ralentizaban mucho el desarrollo, y luego lleg\u00f3 la factura mensual con una suma considerable. En resumen, se decidi\u00f3 abandonar Firebase y construir una soluci\u00f3n interna, ya que el equipo hab\u00eda crecido lo suficiente.<\/p>\n<h3>Docker + Python + bash<\/h3>\n<p>\nTomamos Docker, metimos emuladores en \u00e9l, y escribimos un programa simple en Python que, en el momento adecuado, lanza la cantidad necesaria de emuladores en la versi\u00f3n requerida y, cuando es necesario, los detiene. Y, por supuesto, un par de scripts en bash, \u00bfc\u00f3mo podr\u00edamos estar sin ellos?<\/p>\n<p><strong><em>La creaci\u00f3n de nuestro propio entorno de prueba tom\u00f3 cinco semanas.<\/em><\/strong><\/p>\n<p>Como resultado, cada pull request ten\u00eda una extensa lista de verificaciones bloqueantes de fusi\u00f3n:<\/p>\n<ul>\n<li>Construcci\u00f3n de ARC;<\/li>\n<li>Pruebas Junit;<\/li>\n<li>Lint;<\/li>\n<li>Verificaciones de Android Studio;<\/li>\n<li>Pruebas de instrumentaci\u00f3n;<\/li>\n<li>Pruebas de captura de pantalla.<\/li>\n<\/ul>\n<p>\nEsto preven\u00eda muchos posibles fallos. T\u00e9cnicamente, todo funcionaba, pero los desarrolladores se quejaban de que esperar los resultados tomaba demasiado tiempo.<\/p>\n<p>\u00bfDemasiado tiempo? Examinamos los datos de Bitbucket y TeamCity en un sistema de an\u00e1lisis y entendimos que <strong>el tiempo promedio de espera es de 45 minutos<\/strong>. Es decir, un desarrollador que abre un pull request espera en promedio 45 minutos los resultados de los builds. En mi opini\u00f3n, eso es demasiado y no se puede trabajar as\u00ed.<\/p>\n<p>Por supuesto, decidimos acelerar todas nuestras compilaciones.<\/p>\n<h2>Acelerando<\/h2>\n<p>\nAl ver que a menudo las compilaciones estaban en cola, primero <strong>compramos m\u00e1s hardware<\/strong> \u2014 el desarrollo extensivo es lo m\u00e1s sencillo. Las compilaciones dejaron de estar en cola, pero el tiempo de espera solo se redujo un poco, porque algunas verificaciones en s\u00ed mismas tardaban mucho tiempo.<\/p>\n<h3>Eliminamos las verificaciones demasiado largas<\/h3>\n<p>\nNuestra integraci\u00f3n continua podr\u00eda detectar este tipo de errores y problemas.<\/p>\n<ul>\n<li><strong>No compila<\/strong>. La integraci\u00f3n continua puede atrapar un error de compilaci\u00f3n cuando, debido a cambios conflictivos, algo no se compila. Como ya mencion\u00e9, en ese momento nadie puede compilar nada, el desarrollo se detiene y todos se ponen nerviosos.<\/li>\n<li><strong>Un error en el comportamiento<\/strong>. Por ejemplo, cuando la aplicaci\u00f3n se compila, pero al hacer clic en un bot\u00f3n se cierra, o el bot\u00f3n ni siquiera responde. Esto es malo porque dicho error puede llegar al usuario.<\/li>\n<li><strong>Un error en el dise\u00f1o<\/strong>. Por ejemplo, el bot\u00f3n responde, pero se ha movido 10 p\u00edxeles hacia la izquierda.<\/li>\n<li><strong>Aumento de la deuda t\u00e9cnica<\/strong>.<\/li>\n<\/ul>\n<p>\nAl observar esta lista, nos dimos cuenta de que solo los dos primeros puntos son cr\u00edticos. Queremos detectar tales problemas en primer lugar. Los errores de dise\u00f1o se identifican en la etapa de revisi\u00f3n de dise\u00f1o y se pueden corregir f\u00e1cilmente. El trabajo con la deuda t\u00e9cnica requiere un proceso y planificaci\u00f3n separados, por lo que decidimos no verificarlo en el pull request.<\/p>\n<p>Bas\u00e1ndonos en esta clasificaci\u00f3n, revisamos toda la lista de verificaciones. <strong>Eliminamos Lint<\/strong> y trasladamos su ejecuci\u00f3n a la noche: solo para que entregue un informe de cu\u00e1ntos problemas hay en el proyecto. Acordamos trabajar por separado con la deuda t\u00e9cnica, y <strong>nos deshicimos por completo de las verificaciones de Android Studio<\/strong>. Android Studio en Docker para ejecutar inspecciones suena interesante, pero causa muchos problemas en el soporte. Cualquier actualizaci\u00f3n de las versiones de Android Studio es una lucha contra errores inexplicables. Tambi\u00e9n era complicado mantener las pruebas de captura de pantalla, porque la biblioteca no funcionaba muy estable, hab\u00eda disparos falsos. <strong>Eliminamos las pruebas de captura de pantalla de la lista de verificaciones<\/strong>.<\/p>\n<p>En \u00faltima instancia, nos quedaron:<\/p>\n<ul>\n<li>Construcci\u00f3n de ARC;<\/li>\n<li>Pruebas Junit;<\/li>\n<li>Pruebas de Instrumentaci\u00f3n.<\/li>\n<\/ul>\n<h3>Cach\u00e9 remoto de Gradle<\/h3>\n<p>\nSin verificaciones pesadas, todo ha mejorado. Pero no hay l\u00edmite para la perfecci\u00f3n.<\/p>\n<p>Nuestra aplicaci\u00f3n ya estaba dividida en aproximadamente 150 m\u00f3dulos gradle. Normalmente, en tal caso, el cach\u00e9 remoto de Gradle funciona bien, y decidimos probarlo.<\/p>\n<p>El cach\u00e9 remoto de Gradle es un servicio que puede almacenar en cach\u00e9 los artefactos de compilaci\u00f3n para tareas individuales en m\u00f3dulos separados. Gradle, en lugar de compilar el c\u00f3digo realmente, se conecta por HTTP al cach\u00e9 remoto y pregunta si alguien ya ejecut\u00f3 esa tarea. Si es as\u00ed, simplemente descarga el resultado.<\/p>\n<p><strong><em>Iniciar el cach\u00e9 remoto de Gradle es f\u00e1cil, porque Gradle proporciona una imagen de Docker. Logramos hacerlo en tres horas.<\/em><\/strong><\/p>\n<p>Solo necesit\u00e1bamos iniciar Docker y escribir una l\u00ednea en el proyecto. Pero aunque se puede iniciar r\u00e1pidamente, para que todo funcione bien, se necesitar\u00e1 bastante tiempo.<\/p>\n<p>A continuaci\u00f3n se muestra el gr\u00e1fico de fallos de cach\u00e9.<\/p>\n<p><img decoding=\"async\" alt=\"La evoluci\u00f3n de CI en el equipo de desarrollo m\u00f3vil\" src=\"\/wp-content\/uploads\/2019\/04\/458ef23b5506b04f3bd385be0c18607a.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nAl principio, el porcentaje de fallos de cach\u00e9 era de aproximadamente 65. Despu\u00e9s de tres semanas, logramos reducir este valor al 20%. Result\u00f3 que las tareas que compila la aplicaci\u00f3n de Android tienen dependencias transitivas extra\u00f1as, lo que caus\u00f3 que Gradle fallara en el cach\u00e9.<\/p>\n<p>Al activar el cach\u00e9, aceleramos significativamente la compilaci\u00f3n. Pero adem\u00e1s de la compilaci\u00f3n, tambi\u00e9n se ejecutan pruebas de instrumentaci\u00f3n, y estas tardan mucho. Es posible que no sea necesario ejecutar todas las pruebas en cada solicitud de extracci\u00f3n. Para averiguarlo, utilizamos el an\u00e1lisis de impacto.<\/p>\n<h3>An\u00e1lisis de impacto<\/h3>\n<p>\nEn la solicitud de extracci\u00f3n, recopilamos el diff de git y encontramos los m\u00f3dulos de Gradle modificados.<\/p>\n<p><img decoding=\"async\" alt=\"La evoluci\u00f3n de CI en el equipo de desarrollo m\u00f3vil\" src=\"\/wp-content\/uploads\/2019\/04\/471ca37206a0da4d5741c8ee1dbb7f03.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nTiene sentido ejecutar solo las pruebas de instrumentaci\u00f3n que verifican los m\u00f3dulos modificados y todos los m\u00f3dulos de los que dependen. No tiene sentido ejecutar pruebas para m\u00f3dulos vecinos: all\u00ed no se modific\u00f3 el c\u00f3digo, y nada puede romperse.<\/p>\n<p>Con las pruebas de instrumentaci\u00f3n, no es tan simple, porque deben estar en el m\u00f3dulo superior Application. Aplicamos una heur\u00edstica con an\u00e1lisis de bytecode para entender a qu\u00e9 m\u00f3dulo pertenece cada prueba.<\/p>\n<p><strong><em>La modernizaci\u00f3n del trabajo de las pruebas de instrumentaci\u00f3n para que solo verifiquen los m\u00f3dulos implicados tom\u00f3 alrededor de ocho semanas.<\/em><\/strong><\/p>\n<p>Las medidas para acelerar las verificaciones funcionaron con \u00e9xito. Pasamos de 45 minutos a unos 15. Esperar un cuarto de hora para la compilaci\u00f3n ya es aceptable.<\/p>\n<p>Pero ahora los desarrolladores han comenzado a quejarse de que no pueden entender qu\u00e9 compilaciones se est\u00e1n ejecutando, d\u00f3nde ver los logs, por qu\u00e9 la compilaci\u00f3n fall\u00f3, qu\u00e9 prueba fall\u00f3, etc.<\/p>\n<p><img decoding=\"async\" alt=\"La evoluci\u00f3n de CI en el equipo de desarrollo m\u00f3vil\" src=\"\/wp-content\/uploads\/2019\/04\/aa76151174cd4e5a45ff281818e312f5.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nLos problemas con la retroalimentaci\u00f3n ralentizan el desarrollo, por lo que hemos tratado de proporcionar la informaci\u00f3n m\u00e1s clara y detallada posible sobre cada PR y construcci\u00f3n. Comenzamos con comentarios en Bitbucket para el PR indicando qu\u00e9 construcci\u00f3n fall\u00f3 y por qu\u00e9, y enviamos mensajes directos en Slack. Al final, creamos una p\u00e1gina de panel de control de PR con una lista de todas las construcciones que se est\u00e1n ejecutando actualmente y su estado: en cola, en ejecuci\u00f3n, fallido o completado. Se puede hacer clic en la construcci\u00f3n y acceder a su registro.<\/p>\n<p><img decoding=\"async\" alt=\"La evoluci\u00f3n de CI en el equipo de desarrollo m\u00f3vil\" src=\"\/wp-content\/uploads\/2019\/04\/2d16b7a6b6d23e23903d291ef1526999.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\n<strong><em>Se dedicaron seis semanas a la retroalimentaci\u00f3n detallada.<\/em><\/strong><\/p>\n<h2>Planes<\/h2>\n<p>\nPasemos a la historia m\u00e1s reciente. Al resolver la cuesti\u00f3n de la retroalimentaci\u00f3n, alcanzamos un nuevo nivel: decidimos construir nuestra propia granja de emuladores. Cuando hay muchas pruebas y emuladores, se vuelve complicado gestionarlos. Como resultado, todos nuestros emuladores se trasladaron a un cl\u00faster de k8s con gesti\u00f3n de recursos flexible.<\/p>\n<p>Adem\u00e1s, hay otros planes.<\/p>\n<ul>\n<li><strong>Restaurar Lint<\/strong> (y otro an\u00e1lisis est\u00e1tico). Ya estamos trabajando en esta direcci\u00f3n.<\/li>\n<li>Ejecutar todas las pruebas de extremo a extremo <strong>en PR como bloqueador<\/strong> en todas las versiones del SDK.<\/li>\n<\/ul>\n<p>\nAs\u00ed que hemos seguido la evoluci\u00f3n de la Integraci\u00f3n Continua en Avito. Ahora quiero dar algunos consejos desde la perspectiva de un veterano.<\/p>\n<h1>Consejos<\/h1>\n<p>\nSi pudiera dar solo un consejo, ser\u00eda este:<\/p>\n<blockquote><p>\u00a1Por favor, tengan cuidado con los scripts de shell!<\/p><\/blockquote>\n<p>\nBash es una herramienta muy flexible y poderosa, es muy conveniente y r\u00e1pido escribir scripts. Pero se puede caer en una trampa, y nosotros, lamentablemente, ca\u00edmos en ella.<\/p>\n<p>Todo comenz\u00f3 con scripts simples que se ejecutaban en nuestras m\u00e1quinas de construcci\u00f3n:<\/p>\n<pre><code class=\"bash\">#!\/usr\/bin\/env bash\n.\/gradlew assembleDebug<\/code><\/pre>\n<p>\nPero, como es sabido, todo evoluciona y se complica con el tiempo: vamos a ejecutar un script desde otro, vamos a pasarle algunos par\u00e1metros; al final, tuve que escribir una funci\u00f3n que determina en qu\u00e9 nivel de anidamiento de bash estamos, para insertar las comillas adecuadas y que todo funcione.<\/p>\n<p><img decoding=\"async\" alt=\"La evoluci\u00f3n de CI en el equipo de desarrollo m\u00f3vil\" src=\"\/wp-content\/uploads\/2019\/04\/244a574f7b5d3b4fc77cfc4ccda2db69.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nPueden imaginarse el esfuerzo necesario para desarrollar tales scripts. Les aconsejo que no caigan en esta trampa.<\/p>\n<p>\u00bfCon qu\u00e9 se puede sustituir?<\/p>\n<ul>\n<li>Cualquier lenguaje de scripting. Es m\u00e1s conveniente programar en <strong>Python o Kotlin Script<\/strong> porque eso es programaci\u00f3n, no script.<\/li>\n<li>O describir toda la l\u00f3gica de las construcciones en forma de <strong>tareas gradle personalizadas<\/strong> para su proyecto.<\/li>\n<\/ul>\n<p>\nDecidimos optar por la segunda opci\u00f3n y ahora estamos eliminando gradualmente todos los scripts de bash y escribiendo muchas tareas gradle personalizadas.<\/p>\n<p><strong>Consejo n\u00ba 2: mantener la infraestructura en c\u00f3digo.<\/strong><\/p>\n<p>Es conveniente cuando la configuraci\u00f3n de Continuous Integration no se almacena en la interfaz UI de Jenkins o TeamCity, etc., sino en forma de archivos de texto directamente en el repositorio del proyecto. Esto permite la versionabilidad. No ser\u00e1 dif\u00edcil revertir o compilar el c\u00f3digo en otra rama.<\/p>\n<p>Los scripts se pueden almacenar en el proyecto. \u00bfY qu\u00e9 hacer con el entorno?<\/p>\n<p><strong>Consejo n\u00ba 3: Docker puede ayudar con el entorno.<\/strong><\/p>\n<p>A los desarrolladores de Android sin duda les ayudar\u00e1, pero a iOS no, lamentablemente.<\/p>\n<p>Este es un ejemplo de un archivo docker simple que contiene jdk y android-sdk:<\/p>\n<pre><code class=\"plaintext\">FROM openjdk:8\n\nENV SDK_URL=\"https:\/\/dl.google.com\/android\/repository\/sdk-tools-linux-3859397.zip\" \n    ANDROID_HOME=\"\/usr\/local\/android-sdk\" \n    ANDROID_VERSION=26 \n    ANDROID_BUILD_TOOLS_VERSION=26.0.2\n\n# Descargar Android SDK\nRUN mkdir \"$ANDROID_HOME\" .android \n    &amp;&amp; cd \"$ANDROID_HOME\" \n    &amp;&amp; curl -o sdk.zip $SDK_URL \n    &amp;&amp; unzip sdk.zip \n    &amp;&amp; rm sdk.zip \n    &amp;&amp; yes | $ANDROID_HOME\/tools\/bin\/sdkmanager --licenses\n\n# Instalar Android Build Tool y bibliotecas\nRUN $ANDROID_HOME\/tools\/bin\/sdkmanager --update\nRUN $ANDROID_HOME\/tools\/bin\/sdkmanager \"build-tools;${ANDROID_BUILD_TOOLS_VERSION}\" \n    \"platforms;android-${ANDROID_VERSION}\" \n    \"platform-tools\"\n\nRUN mkdir \/application\nWORKDIR \/application\n<\/code><\/pre>\n<p>\nEscrib\u00ed este archivo docker (te dir\u00e9 en secreto que no es necesario escribirlo, puedes obtener uno ya hecho de GitHub) y al construir la imagen, obtienes una m\u00e1quina virtual en la que puedes compilar la aplicaci\u00f3n y ejecutar pruebas de Junit.<\/p>\n<p>Dos argumentos principales de por qu\u00e9 esto tiene sentido: escalabilidad y repetibilidad. Con Docker, puedes levantar r\u00e1pidamente una docena de agentes de build que tendr\u00e1n exactamente el mismo entorno que el anterior. Esto facilita mucho la vida a los ingenieros de CI. Incluir android-sdk en Docker es bastante simple, con los emuladores es un poco m\u00e1s complicado: tendr\u00e1s que esforzarte un poco (bueno, o nuevamente descargar uno ya hecho de GitHub).<\/p>\n<p><strong>Consejo n\u00ba 4: no olvides que las pruebas se realizan no por el hecho de probar, sino para las personas.<\/strong><\/p>\n<p>Para los desarrolladores, es muy importante recibir retroalimentaci\u00f3n r\u00e1pida y, sobre todo, clara: qu\u00e9 se rompi\u00f3, qu\u00e9 prueba fall\u00f3, d\u00f3nde ver el build log.<\/p>\n<p><strong>Consejo n\u00ba 5: s\u00e9 pragm\u00e1tico en el desarrollo de Continuous Integration.<\/strong><\/p>\n<p>Ten claro qu\u00e9 tipos de errores deseas prevenir, cu\u00e1nto est\u00e1s dispuesto a gastar en recursos, tiempo y tiempo de m\u00e1quina. Las pruebas demasiado largas, por ejemplo, se pueden mover a la noche. Y puedes renunciar a aquellas que detectan errores no muy importantes.<\/p>\n<p><strong>Consejo n\u00ba 6: utiliza herramientas listas.<\/strong><\/p>\n<p>Ahora hay muchas empresas que ofrecen CI en la nube.<\/p>\n<p><img decoding=\"async\" alt=\"La evoluci\u00f3n de CI en el equipo de desarrollo m\u00f3vil\" src=\"\/wp-content\/uploads\/2019\/04\/e57aabd49ec01d14e83b64331fd84ec5.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nPara equipos peque\u00f1os, esta es una buena salida. No se necesita mantener nada, solo pagas un poco de dinero, re\u00fanes tu aplicaci\u00f3n y hasta ejecutas pruebas de instrumentaci\u00f3n.<\/p>\n<p><strong>Consejo n\u00ba 7: en un gran equipo, las soluciones internas son m\u00e1s rentables.<\/strong><\/p>\n<p>Pero tarde o temprano, con el crecimiento del equipo, las soluciones internas se volver\u00e1n m\u00e1s rentables. Hay un punto a considerar con estas soluciones. En econom\u00eda hay una ley de rendimientos decrecientes: en cualquier proyecto, cada mejora adicional se vuelve m\u00e1s dif\u00edcil, requiere cada vez m\u00e1s inversiones.<\/p>\n<p>La econom\u00eda describe toda nuestra vida, incluida la Integraci\u00f3n Continua. He construido un gr\u00e1fico de esfuerzo en cada etapa del desarrollo de nuestra Integraci\u00f3n Continua.<\/p>\n<p><img decoding=\"async\" alt=\"La evoluci\u00f3n de CI en el equipo de desarrollo m\u00f3vil\" src=\"\/wp-content\/uploads\/2019\/04\/97bf8bff64f3ac9ae587fae195251da2.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nEs evidente que cada mejora se vuelve cada vez m\u00e1s dif\u00edcil. Mirando este gr\u00e1fico, se puede entender que el desarrollo de la Integraci\u00f3n Continua debe alinearse con el crecimiento del tama\u00f1o del equipo. Para un equipo de dos personas, gastar 50 d\u00edas en desarrollar una granja interna de emuladores no es una buena idea. Pero, al mismo tiempo, para un gran equipo no involucrarse en la Integraci\u00f3n Continua tampoco es una buena idea, porque se gastar\u00e1 a\u00fan m\u00e1s tiempo en problemas de integraci\u00f3n, reparaci\u00f3n de comunicaciones, etc.<\/p>\n<p>Comenzamos con la idea de que la automatizaci\u00f3n es necesaria, porque las personas son caras, cometen errores y son perezosas. Pero tambi\u00e9n son las personas las que automatizan. Por lo tanto, esos mismos problemas est\u00e1n relacionados con la automatizaci\u00f3n.<\/p>\n<ul>\n<li>Automatizar es caro. Recuerda el gr\u00e1fico del esfuerzo.<\/li>\n<li>En la automatizaci\u00f3n, las personas cometen errores.<\/li>\n<li>A veces, da mucha pereza automatizar, porque ya funciona todo. \u00bfPor qu\u00e9 mejorar algo, por qu\u00e9 toda esta Integraci\u00f3n Continua?<\/li>\n<\/ul>\n<p>\nPero tengo estad\u00edsticas: en el 20% de las compilaciones se detectan errores. Y esto no ocurre porque nuestros desarrolladores escriban mal el c\u00f3digo. Sucede porque los desarrolladores est\u00e1n seguros de que, si cometen un error, no llegar\u00e1 a develop; las pruebas automatizadas lo atrapar\u00e1n. Por lo tanto, los desarrolladores pueden dedicar m\u00e1s tiempo a escribir c\u00f3digo y a hacer cosas interesantes, en lugar de ejecutar y verificar algo localmente.<\/p>\n<p><strong>Practica la Integraci\u00f3n Continua. Pero con moderaci\u00f3n.<\/strong><\/p>\n<blockquote><p>Por cierto, Nikolai Nesterov no solo hace excelentes presentaciones, sino que tambi\u00e9n forma parte del comit\u00e9 del programa <noindex><a rel=\"nofollow\" href=\"https:\/\/appsconf.ru\/moscow\/2019\">AppsConf<\/a><\/noindex> y ayuda a otros a preparar intervenciones sustanciosas para ustedes. La plenitud y utilidad del programa de la pr\u00f3xima conferencia se puede evaluar por los temas en <noindex><a rel=\"nofollow\" href=\"https:\/\/appsconf.ru\/moscow\/2019\/schedule\">el horario<\/a><\/noindex>. Y para m\u00e1s detalles, vengan el 22-23 de abril al Espacio de Informaci\u00f3n.<\/p><\/blockquote>\n<p>Fuente: <a content=\"nofollow\" rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/oleg-bunin\/blog\/447608\/\">habr.com<\/a><\/p>","protected":false,"gt_translate_keys":[{"key":"rendered","format":"html"}]},"excerpt":{"rendered":"<p>\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u044b\u0445 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432 \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u0445. \u0423\u0441\u043b\u043e\u0432\u0438\u044f \u0443\u0441\u043f\u0435\u0445\u0430 \u043a\u043e\u043c\u0430\u043d\u0434\u043d\u043e\u0439 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0432 \u0432\u0438\u0434\u0435 \u043f\u0440\u043e\u0441\u0442\u043e\u0439 \u0441\u0445\u0435\u043c\u044b. \u041d\u0430\u043f\u0438\u0441\u0430\u0432 \u043a\u043e\u0434, \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0443\u0431\u0435\u0434\u0438\u0442\u044c\u0441\u044f, \u0447\u0442\u043e \u043e\u043d: \u0420\u0430\u0431\u043e\u0442\u0430\u0435\u0442. \u041d\u0438\u0447\u0435\u0433\u043e \u043d\u0435 \u043b\u043e\u043c\u0430\u0435\u0442, \u0432 \u0442\u043e\u043c \u0447\u0438\u0441\u043b\u0435 \u043a\u043e\u0434, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0430\u043f\u0438\u0441\u0430\u043b\u0438 \u0432\u0430\u0448\u0438 \u043a\u043e\u043b\u043b\u0435\u0433\u0438. \u0415\u0441\u043b\u0438 \u043e\u0431\u0430 \u0443\u0441\u043b\u043e\u0432\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044e\u0442\u0441\u044f, \u0442\u043e \u0432\u044b \u043d\u0430 \u043f\u0443\u0442\u0438 \u043a \u0443\u0441\u043f\u0435\u0445\u0443. \u0427\u0442\u043e\u0431\u044b \u043b\u0435\u0433\u043a\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u044d\u0442\u0438 \u0443\u0441\u043b\u043e\u0432\u0438\u044f \u0438 \u043d\u0435 \u0441\u0432\u043e\u0440\u0430\u0447\u0438\u0432\u0430\u0442\u044c \u0441 [&hellip;]<\/p>\n","protected":false,"gt_translate_keys":[{"key":"rendered","format":"html"}]},"author":1,"featured_media":23270,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[688],"tags":[],"class_list":["post-31304","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-administrirovanie"],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO 5.0.2 - aioseo.com -->\n\t<meta name=\"description\" content=\"\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u044b\u0445 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432 \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u0445.\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<meta name=\"author\" content=\"Yuri Gagarin\"\/>\n\t<link rel=\"canonical\" href=\"https:\/\/prohoster.info\/es\/blog\/administrirovanie\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki\" \/>\n\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.2\" \/>\n\t\t<meta property=\"og:locale\" content=\"es_ES\" \/>\n\t\t<meta property=\"og:site_name\" content=\"ProHoster | \u041a\u0443\u043f\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u044b\u0439 \u0445\u043e\u0441\u0442\u0438\u043d\u0433 \u0434\u043b\u044f \u0441\u0430\u0439\u0442\u043e\u0432 \u0441 \u0437\u0430\u0449\u0438\u0442\u043e\u0439 \u043e\u0442 DDoS, VPS VDS \u0441\u0435\u0440\u0432\u0435\u0440\u044b\" \/>\n\t\t<meta property=\"og:type\" content=\"article\" \/>\n\t\t<meta property=\"og:title\" content=\"\ud83e\udd47\u042d\u0432\u043e\u043b\u044e\u0446\u0438\u044f CI \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0435 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0439 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 | ProHoster\" \/>\n\t\t<meta property=\"og:description\" content=\"\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u044b\u0445 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432 \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u0445.\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/prohoster.info\/es\/blog\/administrirovanie\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki\" \/>\n\t\t<meta property=\"og:image\" content=\"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg\" \/>\n\t\t<meta property=\"og:image:secure_url\" content=\"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg\" \/>\n\t\t<meta property=\"og:image:width\" content=\"350\" \/>\n\t\t<meta property=\"og:image:height\" content=\"350\" \/>\n\t\t<meta property=\"article:published_time\" content=\"2019-10-31T18:40:34+00:00\" \/>\n\t\t<meta property=\"article:modified_time\" content=\"2019-10-31T18:40:34+00:00\" \/>\n\t\t<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/prohoster\" \/>\n\t\t<meta property=\"article:author\" content=\"https:\/\/www.facebook.com\/prohoster\" \/>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"\ud83e\udd47La evoluci\u00f3n de CI en el equipo de desarrollo m\u00f3vil | ProHoster","description":"Hoy en d\u00eda, la mayor\u00eda de los productos de software se desarrollan en equipos.","canonical_url":"https:\/\/prohoster.info\/es\/blog\/administrirovanie\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":null,"og:locale":"es_ES","og:site_name":"ProHoster | \u041a\u0443\u043f\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u044b\u0439 \u0445\u043e\u0441\u0442\u0438\u043d\u0433 \u0434\u043b\u044f \u0441\u0430\u0439\u0442\u043e\u0432 \u0441 \u0437\u0430\u0449\u0438\u0442\u043e\u0439 \u043e\u0442 DDoS, VPS VDS \u0441\u0435\u0440\u0432\u0435\u0440\u044b","og:type":"article","og:title":"\ud83e\udd47\u042d\u0432\u043e\u043b\u044e\u0446\u0438\u044f CI \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0435 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0439 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 | ProHoster","og:description":"\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u044b\u0445 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432 \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u0445.","og:url":"https:\/\/prohoster.info\/es\/blog\/administrirovanie\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki","og:image":"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg","og:image:secure_url":"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg","og:image:width":350,"og:image:height":350,"article:published_time":"2019-10-31T18:40:34+00:00","article:modified_time":"2019-10-31T18:40:34+00:00","article:publisher":"https:\/\/www.facebook.com\/prohoster","article:author":"https:\/\/www.facebook.com\/prohoster"},"aioseo_meta_data":{"post_id":"31304","title":null,"description":null,"keywords":null,"keyphrases":null,"primary_term":null,"canonical_url":null,"og_title":null,"og_description":null,"og_object_type":"default","og_image_type":"default","og_image_url":null,"og_image_width":null,"og_image_height":null,"og_image_custom_url":null,"og_image_custom_fields":null,"og_video":null,"og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"default","twitter_image_type":"default","twitter_image_url":null,"twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_title":null,"twitter_description":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"","isEnabled":true},"graphs":[]},"schema_type":null,"schema_type_options":null,"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":null,"robots_max_videopreview":null,"robots_max_imagepreview":"large","priority":null,"frequency":null,"local_seo":null,"seo_analyzer_scan_date":"2026-01-21 05:30:21","breadcrumb_settings":null,"limit_modified_date":false,"reviewed_by":null,"ai":null,"created":"2021-03-01 03:19:49","updated":"2026-01-21 05:30:21","focus_keyword":null,"additional_keywords":null,"truseo_locale":null},"gt_translate_keys":[{"key":"link","format":"url"}],"_links":{"self":[{"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/posts\/31304","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/comments?post=31304"}],"version-history":[{"count":0,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/posts\/31304\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/media\/23270"}],"wp:attachment":[{"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/media?parent=31304"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/categories?post=31304"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/tags?post=31304"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}