Cliente API Badoo Jira: magia en Jira en PHP

Si ingresa “Jira Badoo” en la cadena de búsqueda en Habré, los resultados ocuparán más de una página: lo mencionamos en casi todas partes, porque juega un papel importante en nuestros procesos. Y cada uno de nosotros quiere un poco diferente de ella.



El desarrollador, que recibió la tarea para la revisión, espera que se indique una rama en la tarea, hay enlaces a diff y el registro de cambios. El desarrollador que escribió el código espera ver comentarios en Jira después de la revisión. El probador, que recibe la tarea después de ellos, quiere ver los resultados de la prueba y poder ejecutar los ensamblajes necesarios sin tener que ir a otras interfaces. Los gerentes de producto generalmente desean crear diez tareas de desarrollo al mismo tiempo presionando un solo botón.

Y todo esto está disponible hoy y sucede automáticamente. Implementamos la mayor parte de la magia en PHP usando la API Jira en constante evolución y usando su webhook . Y hoy queremos compartir con la comunidad nuestra versión del cliente para esta API.

Al principio, solo queríamos hablar sobre las ideas y el enfoque que usamos, y luego decidimos que definitivamente no había suficiente código para un artículo de ese tipo para mayor claridad. Así que había una versión de código abierto de Badoo Jira PHP Client . Muchas gracias a ShaggyRatte por ayudar con su descripción. Y bienvenido a kat!

Más detalles y contexto.
Si necesita más detalles sobre lo que hacemos con Jira, puede encontrarlos en nuestros otros artículos:

habr.com/en/company/badoo/blog/169417
habr.com/en/company/badoo/blog/433540
habr.com/en/company/badoo/blog/424655

Que puede hacer el?


De hecho, Badoo Jira PHP Client es un conjunto de clases de contenedor listas para responder a las API de Jira, la mayoría de las cuales pueden comportarse como ActiveRecord: saben cómo obtener datos sobre sí mismos y cómo actualizarlos en el servidor, admiten la inicialización diferida y el almacenamiento en caché de datos a nivel código Todas las entidades de Jira con las que tiene que trabajar constantemente están envueltas: Problema, Estado, Prioridad, Registro de cambios, Usuario, Versión, Componente , etc. (casi todo lo que ve en la interfaz).

Además, Badoo Jira PHP Client proporciona una jerarquía de clase única para todos los campos personalizados de Jira y puede generar definiciones de clase para cada campo personalizado que necesite.

$Issue = new \Badoo\Jira\Issue('SMPL-1'); $Issue->addComment('Sample comment text'); $Issue->attachFile('kitten.jpeg', 'pretty!', 'image/jpeg'); if ($Issue->getStatus()->getName() === 'Open') { $Issue->step('In Progress'); } 

 $DeveloperField = new \Example\CustomFields\Developer($Issue); $DeveloperField->setValue('username')->save(); 

 $User = \Badoo\Jira\User::get('username'); $User->watchIssue('SMPL-1'); $User->assign('SMPL-2'); 

Gracias a esto, la interacción con la API de PHP se vuelve más simple y más conveniente, y la documentación de su Jira se mueve directamente al código, lo que le permite utilizar la autocompletación en el IDE para muchas acciones estándar.

Pero lo primero es lo primero.

Características de la API que encontramos


Cuando comenzamos a usar activamente la API de Jira, solo estaba disponible a través del protocolo SOAP. Su versión REST apareció más tarde, y estábamos entre sus primeros usuarios. En ese momento, había muy pocos clientes REST disponibles públicamente escritos en PHP. Fue aún más difícil encontrar algo que pudiera integrarse fácilmente en nuestra base de código, pasando gradualmente de SOAP a REST. Así que no teníamos otra opción: decidimos continuar desarrollando nuestro propio cliente.

Así que vivimos arrastrando y soltando todo tipo de hacks y muletas del cliente SOAP y adquiriendo otros nuevos debido a las peculiaridades de REST. Como resultado, hemos desarrollado algunas clases muy audaces con un montón de código duplicado, y existe la necesidad de limpiar este desastre.

Los campos personalizados siempre han sido el lugar más doloroso para nosotros: tenemos más de 300 de ellos (al momento de escribir este artículo - 338), y este número está creciendo lentamente.

Extraños mensajes de error


A lo largo de la larga historia de interacción con la API, hemos visto muchas cosas diferentes. La mayoría de los mensajes de error son adecuados, pero hay aquellos para los que tienes que esforzarte mucho.

Por ejemplo, si Jira reconoce de repente un robot en su usuario, comenzará a mostrarle un captcha. En este caso, al acceder a la API, ignora descaradamente el encabezado Accept-Encoding: application / json y le proporciona HTML. Naturalmente, el cliente que está esperando a JSON puede no estar listo para este "hola".

Y aquí hay un ejemplo de trabajo con un campo personalizado:



Cuando escribe código y "prueba en este momento" lo está probando, es muy fácil entender que customfield_12664 es Desarrolladores . Y si tal error se produce en algún lugar de la producción (por ejemplo, porque alguien cambió la configuración y cambió la lista de valores aceptables para el campo Seleccionar), a menudo la única forma de identificar el campo es ingresar a Jira y encontrar el nombre desde allí. Además, en su interfaz, los ID no se muestran, solo los nombres.

Resulta que cuando desea averiguar el nombre del campo que condujo al error y corregir su configuración, puede hacerlo a través de una solicitud a la API, o utilizando otros métodos no obvios: hurgando en el código fuente de la página, abriendo la configuración de un campo arbitrario y corrigiendo la URL en la barra de direcciones, etc. Este proceso no puede llamarse conveniente, y cada vez que lleva demasiado tiempo realizar una tarea tan simple.

Pero los nombres de campo oscuros no se limitan a los problemas. Así es como se ve la interacción con la API si comete un error en la estructura de datos para actualizar el campo:




Muchos formatos de datos diferentes


Y no olvide que actualizar campos de diferentes tipos requiere diferentes estructuras de datos.

 $Jira->issue()->edit( 'SMPL-1', [ 'customfield_10200' => ['name' => 'denkoren'], 'customfield_10300' => ['value' => 'PHP'], 'customfield_10400' => [['value' => 'Android'], ['value' => 'iOS']], 'security' => ['id' => 100500], 'description' => 'Just text', ], ); 

Las respuestas API para ellos son, por supuesto, también diferentes.

Es posible tener esto en cuenta solo si trabaja constantemente con la API de Jira y no se distrae durante mucho tiempo resolviendo otros problemas. De lo contrario, estas características se quedan sin memoria en un par de semanas. Además, debe recordar el tipo de campo que necesita para "alimentarlo" con la estructura correcta. Cuando tiene cientos de campos personalizados, a menudo tiene que buscar en el código dónde todavía se usaba o subir al panel de administración de Jira.

Antes de escribir a nuestro cliente, Stack Overflow y Atlassian Community eran mis mejores amigos con respecto a la actualización de campos personalizados. Ahora, esta información se busca en Google fácil y rápidamente, pero cambiamos a la API REST cuando aún era bastante nueva y estaba desarrollada activamente: tardó diez minutos en encontrar una solicitud de cURL adecuada en Google, y luego tuvo que analizar este grupo de corchetes con los ojos y convertir en la estructura correcta para PHP, que a menudo no funcionaba en el primer intento.

En general, la interacción con los campos personalizados es el proceso cuya reorganización se requirió en primer lugar.

¿En qué consiste el cliente?


Clases para trabajar con campos personalizados.


En primer lugar, queríamos deshacernos de recordar las estructuras de datos para interactuar con la API y obtener nombres de campo legibles cuando ocurrían errores.

Como resultado, creamos una jerarquía de clase única para todos los campos personalizados. Resultó tres capas:

  1. Un padre abstracto común para todos: \ Badoo \ Jira \ CustomFields \ CustomField .
  2. Por una clase abstracta para cada tipo de campo: SelectField, UserField, TextField , etc.
  3. Por clase para cada campo específico: por ejemplo, Desarrollador o Revisor .

Estas clases se pueden escribir de forma independiente o se pueden crear automáticamente utilizando un generador de scripts especial (volveremos a ello).


Debido a esta estructura, para enseñarle al código a actualizar el valor de su campo personalizado de tipo Seleccionar lista (opción múltiple) , es suficiente crear una clase PHP heredada de SelectField . De hecho, cada campo Jira personalizado se convierte en un ActiveRecord regular en código PHP.

 namespace \Example\CustomFields; class Developer extends \Badoo\Jira\CustomFields\SingleUserField { const ID = 'customfield_10200'; const NAME = 'Developer'; } // ,  ,  ! 


En la misma clase, almacenamos información sobre el campo: de forma predeterminada, esta es una ID, el nombre del campo y una lista de valores disponibles, si es limitada (por ejemplo, para Casilla de verificación y Seleccionar ).

Ejemplos de campos en la interfaz de Jira y su clase correspondiente.


 class IssueFor extends \Badoo\Jira\CustomFields\SingleSelectField { const ID = 'customfield_10662'; const NAME = 'Issue for'; /* Available field values. */ const VALUE_BI = 'BI'; const VALUE_C_C = 'C\C++'; const VALUE_HTML_CSS = 'HTML\CSS'; const VALUE_JS = 'JS'; const VALUE_OTHER = 'Other'; const VALUE_PHP = 'PHP'; const VALUE_TRANSLATION = 'Translation'; const VALUES = [ self::VALUE_BI, self::VALUE_C_C, self::VALUE_HTML_CSS, self::VALUE_JS, self::VALUE_OTHER, self::VALUE_PHP, self::VALUE_TRANSLATION, ]; public function getItemsList() : array { return static::VALUES; } } 


Resulta que ese tipo de documentación es para su Jira, ubicado directamente en el código PHP. Cuando está tan cerca, es muy conveniente y acelera significativamente el desarrollo, al tiempo que reduce la cantidad de errores.

Además, los mensajes de error se vuelven más claros: en lugar de no decir nada, 'customfield_12664' falla, por ejemplo, algo como esto:

 Uncaught Badoo\Jira\Exception\CustomField: User 'asdkljfh' not found in Jira. Can't change 'Developer' field value. 

Clases para trabajar con objetos del sistema.


Jira tiene muchos datos con una estructura compleja: por ejemplo, los campos del sistema de estado y seguridad , enlaces entre tareas, usuarios, versiones, archivos adjuntos (archivos).

También los envolvimos en clases:

 //   $Status = $Issue->getStatus(); $Status->getName(); $Status->getId(); 

 // changelog  $History = \Badoo\Jira\Issue\History::forIssue('SMPL-1'); $seconds_in_status = $History->getTimeInStatus('Open'); 

 //  Jira $User = new \Badoo\Jira\User('sampleuser'); $User->assign('SMPL-1'); 

Tales envoltorios le dan a su IDE la capacidad de decir qué datos están disponibles y le permiten formalizar estrictamente las interfaces de función en su código. Utilizamos activamente declaraciones de tipo, casi siempre nos permite ver un error incluso mientras escribimos código gracias al resaltado del IDE. Y si aún se perdió el error, saldrá exactamente en el lugar donde apareció por primera vez, y no donde finalmente dejó caer su código.

Todavía hay métodos estáticos que le permiten obtener rápidamente un objeto por algún criterio:

 $users = \Badoo\Jira\User::search('<pattern>'); //    login, email  display name $Version = \Badoo\Jira\Version::byName('<project>', '<version name>'); //        $components = \Badoo\Jira\Component::forProject('<project>'); //      

Estos métodos obedecen las reglas generales para que sean fáciles de encontrar:
  • :: search () , si necesita buscar objetos por varios campos: \ Badoo \ Jira \ Issue :: search () busca tareas utilizando JQL, donde puede especificar muchos criterios de búsqueda, y \ Badoo \ Jira \ User :: search () busca al usuario al mismo tiempo por 'nombre' (inicio de sesión), 'correo electrónico' y 'displayName' (el nombre que se representa en la web);
  • :: by * () , si necesita obtener el objeto no por ID, sino por algún otro criterio: \ Badoo \ Jira \ User :: byEmail () está buscando un usuario por su dirección de correo electrónico;
  • :: for * () busca todos los objetos asociados con algo: \ Badoo \ Jira \ Version :: forProject
    da todas las versiones de un proyecto específico;
  • :: fromStdClass () creará un objeto a partir de datos en bruto que tenga una estructura adecuada, pero no recibida de la API, pero, por ejemplo, del webhook : en el cuerpo de la solicitud POST, Jira envía a JSON con información diferente sobre el evento, incluido el cuerpo de la tarea incluyendo todos los campos Según estos datos, puede crear el objeto \ Badoo \ Jira \ Issue y usarlo como de costumbre.

Clase \ Badoo \ Jira \ Issue


Me parece que la siguiente captura de pantalla de PhpStorm es bastante elocuente en sí misma:



En esencia, el objeto \ Badoo \ Jira \ Issue une todo lo descrito anteriormente en un solo sistema. Almacena toda la información sobre la tarea, tiene métodos para acceder rápidamente a los datos más utilizados, transferir tareas entre estados, etc.

Para crear un objeto en el caso más simple, es suficiente conocer solo la clave de la tarea.

Crea un objeto con solo una tecla de tarea en tu bolsillo
 $Issue = new \Badoo\Jira\Issue('SMPL-1'); 


También puede usar cualquier conjunto de datos fragmentado. Por ejemplo, la información de enlace entre las tareas que llegan de la API contiene solo unos pocos campos: id, resumen, estado, prioridad y tipo de problema. \ Badoo \ Jira \ Issue le permite recopilar un objeto de estos datos para que pueda devolverse inmediatamente y, por lo demás, acceder a la API.

Crear un objeto, almacenando en caché valores para algunos campos
  $IssueFromLink = \Badoo\Jira\Issue::fromStdClass( $LinkInfo, [ 'id', 'key', 'summary', 'status', 'priority', 'issuetype', ] ); 


Esto se logra mediante la inicialización diferida y el almacenamiento en caché de datos en el código. Este enfoque es especialmente conveniente porque solo puede intercambiar objetos \ Badoo \ Jira \ Issue en su código, independientemente del conjunto de campos con los que se crearon.

Obtenga los datos de tareas que faltan
 $IssueFromLink->getSummary(); //    API,    $IssueFromLink->getDescription(); //  API    description 


Cómo vamos a la API
En la API de Jira, es posible obtener no todos los campos para una tarea, sino solo los campos que se necesitan actualmente: por ejemplo, solo clave y resumen. Sin embargo, intencionalmente no vamos a Jira por un solo campo en el captador. En el ejemplo anterior, getDescription () actualizará la información sobre todos los campos a la vez. Como \ Badoo \ Jira \ Issue no tiene la menor idea de qué más necesita a continuación, es más rentable obtener todo de la API de inmediato, ya que fuimos allí de todos modos. Sí, la consulta "obtener solo una descripción" y la consulta "obtener todos los campos de forma predeterminada" durante un par de cientos de tickets toma diferentes tiempos, pero para uno esta diferencia no es tan notable.

 //Time for single field: 0.40271635055542 (second) //Time for all default fields: 0.84159119129181 (second) 

Según las cifras, está claro que cuando se reciben solo tres campos (uno en la solicitud), es más rentable obtener todo de una vez, en lugar de ir a la API para cada uno. El resultado de esta medición, de hecho, depende de la configuración de Jira y del servidor en el que se ejecuta. De tarea en tarea y de medición en medición, los números cambian y el tiempo para todos los campos predeterminados resulta ser establemente menor que tres veces para un solo campo y, a menudo, incluso menos de dos.

Sin embargo, cuando se trabaja con una gran cantidad de tareas, la diferencia se puede medir en segundos. Por lo tanto, cuando sabe que solo necesita clave y descripción para 500 tickets, la capacidad de obtenerlos con una consulta efectiva permanece en los métodos \ Badoo \ Jira \ Issue :: search () y \ Badoo \ Jira \ Issue :: byKeys () .


\ Badoo \ Jira \ Issue - generalmente sobre tareas en algunos Jira abstractos. Pero su (como la nuestra) Jira no es abstracta: tiene un conjunto muy específico de campos personalizados y su propio flujo de trabajo. Usas algunos de los campos y transiciones a menudo, así que no es muy conveniente ir tras ellos en todos los sentidos. Por lo tanto, \ Badoo \ Jira \ Issue se puede ampliar fácilmente con sus propios métodos específicos para una configuración específica de Jira.

Un ejemplo de extensión de clase mediante un método para obtener rápidamente un campo personalizado
 namespace \Deploy; class Issue extends \Badoo\Jira\Issue { // … /** * Get 'Developer' custom field as object */ function getDeveloperField() : \Deploy\CustomFields\Developer { return $this->getCustomField(\Deploy\CustomFields\Developer::class); } // ... } 



Emitir solicitud de creación


Crear una tarea en Jira es un procedimiento bastante complicado. Cuando hace esto a través de la interfaz web, se le muestra una pantalla especial (Crear pantalla) con un conjunto de campos. Algunos de ellos los puede completar simplemente porque lo desee, y otros están marcados como obligatorios. Al mismo tiempo, Crear pantalla puede ser único para cada proyecto e incluso para diferentes tipos de tareas en un proyecto. Por lo tanto, hay todo tipo de restricciones en los valores de campo y en la posibilidad misma de establecer el valor de campo en el proceso de creación de la tarea.

Lo más desagradable para los desarrolladores en esta situación es que estas restricciones se aplican a la API. Este último tiene una solicitud especial ( create-meta ha estado disponible en la API REST desde la versión 5.0), con la que puede obtener una lista de configuraciones de campo disponibles al crear una tarea. Sin embargo, un desarrollador que necesita "hacer algo simple en este momento" probablemente no se molestará con esto.

Como resultado, sucedió así: dado que la solicitud para crear una tarea puede ser bastante grande, a menudo le agregamos datos gradualmente y recibimos un error cuando intentamos enviar todo a Jira. Después de eso, tuve que buscar en el código todos los lugares donde algo cambió en la solicitud, y durante un largo y tedioso intento de comprender qué salió mal exactamente.

Por lo tanto, hicimos \ Badoo \ Jira \ Issue \ CreateRequest . Le permite ver el error antes, justo en el lugar donde está tratando de hacer algo mal: dele al campo algún tipo de valor curvo o cambie el campo que no está disponible. Por ejemplo, si intenta especificar un componente que no existe en el proyecto, la excepción se bloqueará en el lugar donde lo hizo y no donde finalmente envió la solicitud a la API.

El flujo de trabajo con CreateRequest se parece a esto
 $Request = new \Badoo\Jira\Issue\CreateRequest('DD', 'Task', $Client); $Request ->setSummary('summary') ->setDescription('description') ->setFieldValue('For QA', 'custom field with some comments for QA who will check the issue'); $Request->send(); 


Trabaja con API directamente


El conjunto de clases descrito anteriormente cubre la mayoría de las necesidades. Sin embargo, somos conscientes de que la mayoría está lejos de todo. Por lo tanto, también tenemos un pequeño cliente para trabajar directamente con la API: \ Badoo \ Jira \ REST \ Client .

Caso de uso del cliente
 $Jira = \Badoo\Jira\REST\Client::instance(); $Jira->setJiraUrl('https://jira.example.com/'); $Jira->setAuth('user', 'password') $IssueInfo = $Jira->issue()->get('SMPL-1'); 


Generador de clases para campos personalizados.


Para facilitar el trabajo con campos personalizados, cada campo debe tener su propia clase en el código. Los creamos nosotros mismos manualmente según sea necesario, pero antes de publicar el cliente decidimos que este enfoque podría no ser muy conveniente para los nuevos usuarios. Por lo tanto, creamos un generador especial que puede ir a la API de Jira para obtener una lista de campos personalizados y crear clases de plantillas para los tipos de campo conocidos.

Creemos que para la mayoría de las tareas es suficiente usar el script bin / generate CLI de nuestro repositorio. Puede pedirle que cuente sobre sí mismo a través de la opción --help / -h :

 ./bin/generate --help 

En el caso más simple, para la generación es suficiente especificar la URL de su Jira, el usuario, su contraseña, el espacio de nombres para las clases y el directorio donde colocar el código:

 ./bin/generate -u user -p password --jira-url https://jira.mlan --target-dir custom-fields --namespace CustomFields 

También implementamos la capacidad de agregar nuestras propias plantillas y generar clases para campos individuales. Esto se puede encontrar en la documentación .

Conclusión


Nos gusta lo que tenemos. Con este concepto, nuestras propias clases para campos personalizados, envoltorios para estados, versiones, usuarios, etc., hemos estado viviendo durante más de un año y nos sentimos muy bien. Antes de publicar el código, incluso ampliamos la funcionalidad y agregamos cosas maravillosas que no llegaron a sus manos durante mucho tiempo para usar el cliente, fue aún más conveniente: por ejemplo, agregamos la capacidad de actualizar varios campos en Issue en una solicitud y escribimos un generador de clases para campos personalizados.

En nuestra opinión, resultó ser algo bueno, que definitivamente debe entenderse si se adapta a sus tareas y requisitos. Debajo de la nuestra, simplemente en forma.

Enlace de nuevo: github.com/badoo/jira-client .

Gracias por leer hasta el final. Esperamos que este código ahora se beneficie y ahorre tiempo no solo para nosotros.

Source: https://habr.com/ru/post/475840/


All Articles