Implementación de Bitbucket a WordPress | Programar Plus

De todos los proyectos en los que he trabajado en los últimos años, hay uno que se destaca como mi favorito: escribí un complemento de WordPress llamado Great Eagle (referencia de Tolkien) que permite a mi equipo instalar y actualizar temas y complementos desde nuestro Bitbucket privado. repos, a través de la interfaz de usuario de actualizaciones de wp-admin normal.

Este complemento ha arrasado con nuestra tienda de desarrollo en lo que respecta a las mejores prácticas de desarrollo, de formas que nunca esperábamos o pretendíamos. Nos obliga a usar números de versión adecuados porque ahora no podemos implementar sin ellos. Nos obliga a almacenar nuestro trabajo en Bitbucket porque ahora no podemos implementar sin él. Nos obliga a usar la línea de comando en ruta para implementar nuestro trabajo (con lo que simplemente quiero decir, git push origin master), que luego nos llevó a usar phpUnit. Ahora no podemos implementar a menos que pasen nuestras pruebas. Llegamos al nirvana del desarrollo impulsado por pruebas, todo porque comenzamos con el paso no relacionado de implementar desde git.

Si todo esto suena estándar y obvio, genial. Me encantaría tener la oportunidad de aprender de ti. Si esto suena a galimatías exótico, ¿adivinen qué? Este articulo es para tí.

Descargo de responsabilidad: mi trabajo en este complemento está muy influenciado y, en algunos casos, plagiado por el excelente complemento GitHub Updater, de Andy Fragen. La razón por la que escribí la mía es porque tenemos cientos de temas y complementos en Bitbucket, y estaba teniendo algunos problemas de escala cuando estaba audicionando GHU, que desde entonces se han abordado. Probablemente me deshice demasiado pronto, ya que ese complemento ha estado bajo un desarrollo activo y experto durante años. Más que nada, solo queríamos una versión que estuviera totalmente bajo nuestro propio mantenimiento. Presentaré algunas ideas de mi complemento, pero en última instancia Recomiendo que los usuarios difieran a GHU porque es probable que se adapte mejor a la mayoría de las personas, y tampoco quiero aprovechar el impulso de ese increíble proyecto.

Prerrequisitos

Mis ejemplos demuestran una instalación en varios sitios, pero eso no es particularmente importante. Esto también funciona bien en una instalación de un solo sitio. Estoy en la versión 4.8-alpha-39626 de WordPress en este momento, pero eso tampoco es muy importante.

Lo más importante es mi suposición de que todos los temas y complementos en su lugar de trabajo están almacenados en su propio repositorio de Bitbucket. ¡Es una gran suposición! No es broma: cuando nos embarcamos en esto, contratamos a una empresa para que creara manualmente un repositorio para cada uno de nuestros temas y complementos. Estábamos usando SVN (¡mal!) Antes de esta migración.

¿Como funciona?

Hay tres (ish) pasos:

1) Cree una interfaz de usuario para que el usuario realice una solicitud de API a Bitbucket y refleje todos los datos de nuestro repositorio en la base de datos de WordPress. No todos los datos sobre cada repositorio, en realidad solo el nombre del slug, que usaremos como clave para consultas más profundas.

Un formulario para que el usuario refleje nuestra cuenta de Bitbucket en la base de datos.

Una alternativa sería compilar esto automáticamente siempre que esté vacío, pero por ahora, estoy feliz de tener un control completo sobre cuándo se ejecuta una serie tan grande de solicitudes de API.

2) Una vez que tenemos un poco de información reflejada para todos nuestros repositorios, podemos ofrecer un autocompletado de jQuery para elegir algunos repositorios para el desglose de datos, donde hacemos varias llamadas API más para cada uno de ellos, dándonos acceso a información más profunda como número de versión y URL de descarga.

Ahora que tenemos un espejo local de nuestros repositorios de Bitbucket, podemos completar un autocompletado para seleccionar algunos de ellos para instalarlos o actualizarlos.

¿Por qué no recopilar todos esos detalles para todos los repositorios de inmediato? Porque tenemos cientos de repositorios y se necesitan varias llamadas por repositorio para obtener toda la información pertinente, como, por ejemplo, el número de versión. Probablemente tomaría entre 15 y 30 minutos y más de 1,000 viajes de API.

3) Una vez que tengamos información detallada sobre el puñado de repositorios que queremos usar en este momento, podemos determinar dos cosas importantes sobre ellos. Primero, ¿está instalado en WordPress? De lo contrario, aparecerá en una interfaz de usuario para que lo instalemos. En segundo lugar, si está instalado, ¿tiene la última versión? De lo contrario, aparecerá en la IU normal de actualizaciones de wp-admin.

Algunos de los complementos de nuestra cuenta de Bitbucket no están instalados en nuestra red de WordPress.
Usamos la interfaz de usuario de Great Eagle para instalar uno de ellos.
Nuestro complemento está alojado en un repositorio privado de Bitbucket, pero aquí está en nuestra cola de actualización normal.

En la remota posibilidad de que un repositorio no sea legible (tal vez carece de docblocks o convenciones de nomenclatura adecuados), se omite de todos estos pasos. Esto solo nos ha sucedido con un puñado de complementos mal nombrados, pero puede ser molesto ya que cambiar la carpeta del complemento y los nombres de los archivos puede desactivar el complemento.

Eh. ¿Como funciona exactamente?

Buena pregunta. Explicaré cuáles fueron las partes complicadas y compartiré algo de código de mi complemento.

Construyendo la lista de repositorios

El número máximo de repositorios por llamada a la API es 100. Así es como funciona la API de Bitbucket. Tenemos mucho más que eso en nuestra cuenta, por lo que tenemos que llamar a Bitbucket en un bucle:

<?php

/**
 * Store a "shallow" list of repos.
 */
public function set_repo_list() {

  ...
      
  // Let's get 100 per page, which is the maximum.
  $max_pagelen = 100;
  
  ....
  
  // Get the first page of repos.
  $page = 1;
  $call = new LXB_GE_Call( 'api', "repositories/$team", $max_pagelen, $page );

  $get = $call -> get();
  $out = $get['values'];

  // Now we know how many there are in total.
  $total = $get['size'];

  // How many pages does that make for?
  $num_pages = ceil( $total / $max_pagelen );

  // Query each subsequent page.  We already got the first one.
  while( $page < $num_pages ) {

    $page++;

    $next_call = new LXB_GE_Call( 'api', "repositories/$team", $max_pagelen, $page );
    $next_get   = $next_call -> get();
    $next_repos = $next_get['values'];

    $out = array_merge( $out, $next_repos );

  }

  // Sort the list by most recently updated.
  $out = $this -> sort( $out, 'updated_on' );

  $this -> repo_list = $out;

}

Determinar el archivo de complemento “principal”

WordPress es muy despreocupado cuando se trata de nombrar complementos. En la mayoría de los casos, una carpeta de complementos contiene, de hecho, exactamente un complemento, y ese complemento tendrá una especie de archivo “principal”, que contiene un docblock para transmitir el nombre del complemento, la descripción, el autor y, lo que es más importante, la versión. número. Debido a que ese archivo puede tener cualquier nombre, determinar qué archivo es el archivo de complemento principal es una cuestión abierta. El enfoque que he adoptado es asumir que el complemento se ajustará a algunas convenciones de nomenclatura que intentamos utilizar en nuestro trabajo.

<?php

function set_main_file_name() {

    // Grab the slug name for this Bitbucket repo.
  $slug = $this -> slug;
  
  // Grab the list of file names in this repo.
  $file_list = $this -> file_list;

  // There's a good chance that there is a file with the same name as the repo.
  if( in_array( "$slug.php", $file_list ) ) {

    $main_file_name = "$slug.php";

  // If not, there's a good chance there's a plugin.php file.
  } elseif( in_array( 'plugin.php', $file_list ) ) {

    $main_file_name="plugin.php";

  // If not, it's probably a theme.
  } elseif( in_array( 'style.css', $file_list ) && in_array( 'functions.php', $file_list ) ) {

    $main_file_name="style.css";

  // Else, oh well, couldn't find it.
  } else {

    $error          = sprintf( esc_html__( 'Could not identify a main file for repo %s.', 'bucketpress' ), $slug );
    $main_file_name = new BP_Error( __CLASS__, __FUNCTION__, __LINE__, func_get_args(), $error );

  }

  $this -> main_file_name = $main_file_name;

}

Determinar el número de versión

Dado el complemento principal o el archivo de tema, podemos profundizar en el docblock en ese archivo para determinar el número de versión. Así es como lo hago:

<?php

  /**
   * Get the value for a docblock line.
   * 
   * @param  string $key The key for a docblock line.
   * @return string The value for a docblock line.
   */
  function get_value_from_docblock( $key ) {

    // Grab the contents of the main file.
    $main_file_body = $this -> main_file_body;

    // Break the file into lines.
    $lines = $this -> formatting -> get_lines_from_string( $main_file_body );

    // Let's save ourselves some looping and assume the docblock is < 30 lines.
    $max_lines = 30;
    $i         = 0;

    foreach( $lines as $line ) {
        
      $i++;

      // If the line does not have the key, skip it.
      if( ! stristr( $line, $key . ':' ) ) { continue; }

      // We found the key!
      break;

      // Whoops, we made it to the end without finding the key.
      if( $i == $max_lines ) { return FALSE; }

    }

    // Break the line into the key/value pair.
    $key_value_pair = explode( ':', $line );

    // Remove the key from the line.
    array_shift( $key_value_pair );

    // Convert the value back into a string.
    $out = implode( ':', $line_arr );

    $out = trim( $out );

    return $out;

  }

Ya que estoy en eso, permítame aplaudir la ayuda de php version_compare() función, que puede analizar las sintaxis de versiones más comunes:

/**
 * Determine if this asset needs to be updated.
 * 
 * @return boolean Returns TRUE of the local version number
 * is lower than the remote version number, else FALSE.
 */
function needs_update() {

  $old_version = $this -> old_version;

  $new_version = $this -> new_version;

  $compare = version_compare( $old_version, $new_version );

  if( $compare == -1 ) { return TRUE; }

  return FALSE;

}

Analizando el archivo readme.txt

En realidad, no usamos el readme.txt para nada en nuestros complementos y, por lo tanto, mi complemento Great Eagle tampoco lo analiza mucho. Sin embargo, si desea incorporar información Léame, recomendaría esta biblioteca de Ryan McCue para analizarla.

El trato con repositorios privados

Todos nuestros repositorios son privados, así es como hacemos negocios en este momento. Para consultarlos, tenemos que filtrar algunos créditos. En este ejemplo, lo hago a través de la autenticación básica:

<?php

/**
 * Authenticate all of our calls to Bitbucket, so that we can access private repos.
 * 
 * @param  array  $args The current args for http requests.
 * @param  string $url  The url to which the current http request is going.
 * @return array        $args, filtered to include BB basic auth.
 */
public function authenticate_http( $args, $url ) {

  // Find out the url to Bitbucket.
  $call   = new LXB_GE_Call( 'web', FALSE );
  $bb_url = $call -> get_url();

  // If we're not calling a Bitbucket download, don't bother.
  if( ! stristr( $url, $bb_url ) ) { return $args; }
  if( ! stristr( $url, '.zip' ) ) { return $args; }

  // Okay, time to append basic auth to the args.
  $creds = $this -> creds;
  $args['headers']['Authorization'] = "Basic $creds";

  return $args;

}

Estoy haciendo esto a través de filtración, en lugar de pasar argumentos a wp_remote_get(), porque necesito que WordPress esté preparado con estas credenciales cuando realiza sus llamadas durante sus llamadas normales de actualización de temas y complementos, que ahora van a Bitbucket.

Sería mejor hacer Oauth en lugar de la autenticación básica, pero después de investigar un poco, llegué a la conclusión de que no hay forma de hacerlo. El obstáculo se debe a que el contenido del archivo sin procesar en realidad no es parte de la API de Bitbucket en este punto, simplemente está alojado en su sitio web como cualquier otro activo estático, como este tema de prueba público, por ejemplo (es público para propósitos de demostración, pero nuevamente, si era privado, se podía acceder a él a través de la autenticación básica). Tengo esta humilde solicitud de función para mostrar por mis esfuerzos. Como medida de seguridad, recomiendo usar la nueva función de contraseñas de aplicaciones de Bitbucket para crear una cuenta específicamente y solo para llamadas con script como esta, donde la contraseña de la aplicación solo tiene acceso de lectura. Entonces, para ser claros, con la autenticación básica hay un universo (tal vez este) en el que un enemigo que detecta paquetes está leyendo nuestros archivos de complementos. Estoy de acuerdo con eso, al menos por el momento.

Añadiendo nuestros repositorios a la cola de actualización

Si hay una clave para afianzarse en todo este proceso, se encuentra en la función wp_update_plugins (). Esa es una función enorme que usa el núcleo para recorrer todos los complementos instalados, determinar cuáles tienen una actualización disponible y guardar el resultado en un transitorio. La clave es que el transitorio se expone para el filtrado, que es exactamente lo que hace mi complemento:

<?php

add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'set_plugin_transient' ) );

/**
 * Inject our updates into core's list of updates.
 * 
 * @param  array $transient The existing list of assets that need an update.
 * @return The list of assets that need an update, filtered.
 */
public function set_plugin_transient( $transient ) {

  if( ! is_array( $this -> assets_to_update ) ) { return $transient; }

  foreach( $this -> assets_to_update as $asset ) {

    if( empty( $asset -> transient_key ) ) { continue; }

    if( ! $asset -> transient_content ) { continue; }

    $transient -> response[ $asset -> transient_key ] = $asset -> transient_content;

  }

  return $transient;

}

Me tomó una eternidad entrar en esto, y me tomó meses y meses escribir este complemento. Probablemente debería usar GHU en su lugar. Es bastante similar. Dicho esto, si desea modificar algunas cosas y no le gusta ejecutar complementos de terceros, tal vez el código anterior lo ayude a escribir el suyo.

Entonces, ¿cuál es el punto, exactamente?

El punto no es tanto cómo construir tu propio complemento de implementación de git, o cuál existente deberías usar. Puede resolver esas cosas usted mismo. Lo realmente interesante es ver lo que nos sucedió cuando comenzamos a implementar desde git. Algunos de los efectos secundarios fueron profundamente sorprendentes y positivos.

Hasta luego, FTP

FTP apesta por muchas razones.

  • El acceso FTP es un vector de ataque.
  • No hay una forma fácil de rastrear o revertir los cambios.
  • No es una forma sencilla de permitir que varias personas trabajen en el mismo proyecto al mismo tiempo.
  • Error humano. Es bastante fácil arrastrar y soltar mal, lo que lleva a un WSOD o algo peor.
  • Nunca esperé esto, pero es evidente cuando se actualiza un complemento en muchas instalaciones, que este método git es mucho más rápido que FTP.

Con un sistema de implementación de git como el que estoy defendiendo y explicando en este artículo, puede ir tan lejos como para deshabilitar todo el acceso FTP a su entorno de producción. En serio: no lo necesitarás.

Hola control de versiones adecuado

Recomiendo usar una herramienta de implementación de git que use docblocks para determinar el número de versión y use el número de versión para determinar si el tema o complemento necesita una actualización. Esto obliga a su equipo a utilizar los números de versión adecuados, lo que es un buen primer paso en el camino desde la creación de temas hasta la gestión madura de una base de código de larga duración.

Estoy tan entusiasmado con las pruebas unitarias ahora

Si no está realizando pruebas unitarias, probablemente sepa que debería hacerlo. Con la implementación de git, puede ser tanto automático como obligatorio.

Usamos la línea de comando para mover nuestro trabajo de nuestro MAMP local a Bitbucket, como en, git push origin master. Cada uno de nuestros complementos lleva una tarea de Grunt para ejecutar nuestras pruebas phpUnit después de la confirmación previa de git, y si las pruebas fallan, también falla la confirmación.

Vinculamos Grunt a nuestro compromiso usando GitHooks y ejecutamos nuestras pruebas unitarias a través de Exec. Si las pruebas fallan, también falla la implementación.

¡No hay forma de eludir las pruebas porque no hay forma de eludir a git para la implementación!

Retrocesos

No hay retrocesos per se con este método. Por el contrario, solo avanzas. Lo que sea que desee arreglar o restaurar, consígalo en maestro, aumente el número de versión, envíe e implemente.

Dotación de personal

Este tipo de maduración puede tener ramificaciones en toda la empresa. Imagínese esto: tiene personas de soporte que no son desarrolladores en la primera línea, tratando de depurar un problema para un cliente. En el pasado, habrían tenido que colocar esta solicitud en una cola de tickets de desarrollo, mientras el cliente espera horas o días para una resolución. Ya no. Ahora, su agente de soporte de primera línea puede navegar hasta el administrador de la red y ver que en este entorno el complemento en cuestión está desactualizado. Son libres de actualizar el complemento de inmediato a través de la interfaz normal de wp-admin. El ticket se resuelve mediante el soporte de primera línea sin la participación del equipo de desarrollo. Quizás esas personas de primera línea cuesten menos que los desarrolladores, o quizás tengan un amplio conjunto de habilidades en la administración de cuentas. De cualquier manera, ya no tendrá que abrir un ticket de desarrollo para implementar actualizaciones en sus complementos internos. Esencial.

Rebelión de las máquinas

Antes de este proceso, éramos en gran medida una tienda de desarrollo ordinaria produciendo temas y complementos para los clientes, haciendo FTP de vaquero, no versionando nuestro trabajo. ¿Por qué? Porque éramos vagos. ¿Por qué? Porque éramos humanos. Ya no somos perezosos porque ya no somos humanos, al menos cuando estamos desplegando. Somos una secuencia de comandos de línea de comandos y una serie de solicitudes de API, y no importa lo perezosos que seamos, tenemos que seguir las prácticas de implementación adecuadas porque ¡destruimos los créditos de FTP para nuestros desarrolladores! Además de todo eso, es una forma más rápida de implementar, sin fallas en el encendido de hacer clic y arrastrar.

¿Puedes subirte a bordo con esto durante la noche? De acuerdo, no. Es un proceso largo y costoso, y puede que no sea para usted, pero honestamente, probablemente lo sea. Creo que hay alrededor de 1,000 tiendas de desarrollo que deberían considerar esto cuidadosamente.