Deployment von Bitbucket nach WordPress

Avatar of Scott Fennell
Scott Fennell am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Von allen Projekten, an denen ich in den letzten Jahren gearbeitet habe, sticht eines als mein Favorit hervor: Ich habe ein WordPress-Plugin namens Great Eagle (Tolkien-Referenz) geschrieben, das es meinem Team ermöglicht, Themes und Plugins von unseren privaten Bitbucket-Repos über die normale wp-admin Updates-Oberfläche zu installieren und zu aktualisieren.

Dieses Plugin hat unseren Entwicklungsbetrieb in Bezug auf Best Practices der Entwicklung auf ein neues Level gehoben, auf Arten, die wir nie erwartet oder beabsichtigt hatten. Es zwingt uns, korrekte Versionsnummern zu verwenden, da wir ohne sie nicht mehr deployen können. Es zwingt uns, unsere Arbeit in Bitbucket zu speichern, da wir ohne sie nicht mehr deployen können. Es zwingt uns, die Kommandozeile auf dem Weg zum Deployment unserer Arbeit zu nutzen (womit ich einfach nur meine, git push origin master, meine), was uns dann dazu brachte, phpUnit zu verwenden. Jetzt können wir nicht mehr deployen, es sei denn, unsere Tests sind erfolgreich. Wir haben das Nirvana der testgetriebenen Entwicklung erreicht, alles nur, weil wir mit dem irrelevanten Schritt des Deployments von Git begonnen haben.

Wenn das alles standardmäßig und offensichtlich klingt, großartig. Ich würde mich freuen, von Ihnen zu lernen. Wenn das wie exotisches Rigmarol klingt, wissen Sie was? Dieser Artikel ist für Sie.

Haftungsausschluss: Meine Arbeit an diesem Plugin ist stark beeinflusst von, und in einigen Fällen plagiiert von, dem ausgezeichneten GitHub Updater Plugin von Andy Fragen. Der Grund, warum ich mein eigenes geschrieben habe, ist, dass wir Hunderte von Themes und Plugins in Bitbucket haben und ich einige Skalierungsprobleme hatte, als ich GHU testete, die seither behoben wurden. Wahrscheinlich habe ich zu früh aufgegeben, da dieses Plugin seit Jahren aktiv und fachmännisch entwickelt wird. Mehr als alles andere wollten wir einfach eine Version, die wir komplett selbst warten. Ich werde einige Gists aus meinem Plugin vorstellen, aber letztendlich **empfehle ich den Benutzern, sich an GHU zu halten**, da es wahrscheinlich besser für die meisten Leute geeignet ist und ich auch nicht die Dynamik dieses großartigen Projekts schmälern möchte.

Voraussetzungen

Meine Beispiele zeigen eine Multisite-Installation, aber das ist nicht besonders wichtig. Dies funktioniert auch auf einer Single-Site-Installation einwandfrei. Ich verwende derzeit die WordPress-Version 4.8-alpha-39626, aber auch das ist nicht besonders wichtig.

Von größter Bedeutung ist meine Annahme, dass alle Themes und Plugins an Ihrem Arbeitsplatz jeweils in ihrem eigenen Bitbucket-Repo gespeichert sind. Das ist eine ziemliche Annahme! Kein Witz: Als wir damit begannen, stellten wir ein Unternehmen ein, das manuell ein Repo für jedes unserer Themes und Plugins erstellte. Zuvor haben wir SVN (schlecht!) verwendet.

Wie funktioniert es?

Es gibt drei (ungefähre) Schritte

1) Erstellen Sie eine Benutzeroberfläche für den Benutzer, um eine API-Anfrage an Bitbucket zu stellen und alle unsere Repository-Daten in die WordPress-Datenbank zu spiegeln. Nicht alle Daten über jedes Repo, wirklich nur der Slug-Name, den wir als Schlüssel für tiefere Abfragen verwenden.

Ein Formular, mit dem der Benutzer unser Bitbucket-Konto in die Datenbank spiegeln kann.

Eine Alternative wäre, dies automatisch zu erstellen, sobald es leer ist, aber vorerst bin ich glücklich, die vollständige Kontrolle darüber zu haben, wann eine so große Anzahl von API-Anfragen ausgeführt wird.

2) Sobald wir ein paar Informationen über alle unsere Repos gespiegelt haben, können wir ein jQuery-Autocomplete anbieten, um einige Repos für die Datenanalyse auszuwählen, wobei wir mehrere weitere API-Aufrufe für jedes einzelne durchführen, um Zugriff auf tiefere Informationen wie Versionsnummer und Download-URL zu erhalten.

Nachdem wir nun einen lokalen Spiegel unserer Bitbucket-Repos haben, können wir ein Autocomplete zur Auswahl einiger davon für die Installation oder Aktualisierung befüllen.

Warum nicht sofort alle diese Details für alle Repos erfassen? Weil wir Hunderte von Repos haben und es mehrere Aufrufe pro Repo braucht, um alle relevanten Informationen wie die Versionsnummer zu erfassen. Das würde wahrscheinlich 15-30 Minuten und über 1.000 API-Reisen dauern.

3) Sobald wir detaillierte Informationen über die Handvoll Repos haben, die wir im Moment nutzen möchten, können wir zwei wichtige Dinge darüber feststellen. Erstens: Ist es in WordPress installiert? Wenn nicht, wird es in einer Benutzeroberfläche angezeigt, damit wir es installieren können. Zweitens: Wenn es installiert ist, ist es auf der neuesten Version? Wenn nicht, wird es in der normalen wp-admin Updates-Oberfläche angezeigt.

Einige der Plugins in unserem Bitbucket-Konto sind nicht in unserem WordPress-Netzwerk installiert.
Wir haben die Benutzeroberfläche von Great Eagle verwendet, um eines davon zu installieren.
Unser Plugin ist in einem privaten Bitbucket-Repo gehostet, aber hier ist es in unserer normalen Update-Warteschlange.

Falls ein Repo nicht lesbar ist (vielleicht fehlen ihm die richtigen Docblocks oder Namenskonventionen), wird es aus all diesen Schritten ausgeschlossen. Dies ist uns nur bei einer kleinen Handvoll schlecht benannter Plugins passiert, aber es kann ärgerlich sein, da die Änderung der Plugin-Ordner- und Dateinamen das Plugin deaktivieren kann.

Hoppla. Wie funktioniert das genau?

Gute Frage. Ich werde erklären, was die kniffligen Teile waren und einige Codeausschnitte aus meinem Plugin teilen.

Erstellung der Liste der Repos

Die maximale Anzahl von Repos pro API-Aufruf beträgt 100. So funktioniert die Bitbucket-API. Wir haben weitaus mehr als das in unserem Konto, daher müssen wir Bitbucket in einer Schleife aufrufen.

<?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;

}

Bestimmung der „Haupt“-Plugin-Datei

WordPress ist sehr unvoreingenommen, wenn es um die Benennung von Plugins geht. In den meisten Fällen enthält ein Plugin-Ordner tatsächlich genau ein Plugin, und dieses Plugin hat eine „Haupt“-Datei, die einen Docblock enthält, um den Plugin-Namen, die Beschreibung, den Autor und vor allem die Versionsnummer zu vermitteln. Da diese Datei beliebigen Namen tragen kann, ist die Bestimmung, welche Datei die Haupt-Plugin-Datei ist, eine offene Frage. Der Ansatz, den ich gewählt habe, ist die Annahme, dass das Plugin einigen Namenskonventionen entspricht, die wir in unserer Arbeit zu verwenden versuchen.

<?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;

}

Bestimmung der Versionsnummer

Gegeben der Haupt-Plugin- oder Theme-Datei können wir in den Docblock dieser Datei eintauchen, um die Versionsnummer zu ermitteln. Hier ist, wie ich es mache.

<?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;

  }

Während ich dabei bin, möchte ich die hilfreiche version_compare()-Funktion von PHP loben, die die meisten gängigen Versionssyntaxen parsen kann.

/**
 * 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;

}

Parsen der readme.txt

Wir verwenden die readme.txt in unseren Plugins tatsächlich für nichts und daher parst mein Great Eagle Plugin sie auch nicht großartig. Wenn Sie jedoch Readme-Informationen integrieren möchten, empfehle ich diese Bibliothek von Ryan McCue zum Parsen.

Die Sache mit privaten Repos

Unsere Repos sind alle privat – so machen wir im Moment eben Geschäfte. Um sie abzufragen, müssen wir Anmeldeinformationen einbinden. In diesem Beispiel mache ich das über Basic Auth.

<?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;

}

Ich mache das über Filterung, anstatt Argumente an wp_remote_get() zu übergeben, weil ich WordPress mit diesen Anmeldeinformationen vorbereitet haben muss, wenn es seine Aufrufe während seiner normalen Theme- und Plugin-Update-Aufrufe macht, die jetzt zufällig an Bitbucket gehen.

Es wäre besser, OAuth statt Basic Auth zu verwenden, aber nach ziemlich viel Recherche bin ich zu dem Schluss gekommen, dass es keinen Weg gibt, das zu tun. Der Stolperstein liegt darin, dass der Rohinhalt der Datei zu diesem Zeitpunkt nicht Teil der Bitbucket-API ist, er wird einfach auf ihrer Website gehostet wie jede andere statische Ressource, wie zum Beispiel dieses öffentliche Test-Theme (es ist zu Demozwecken öffentlich, aber auch hier, wenn es privat wäre, könnten Sie es über Basic Auth aufrufen). Ich habe dieses bescheidene Feature-Request als Beweis meiner Bemühungen. Als Sicherheitsmaßnahme empfehle ich die Verwendung der neuen Application Passwords Funktion von Bitbucket, um ein Konto speziell und nur für skriptbasierte Aufrufe wie diesen zu erstellen, bei dem dieses App-Passwort nur Lesezugriff hat. Um es klarzustellen, mit Basic Auth gibt es ein Universum (vielleicht dieses), in dem ein Packet-Sniffing-Gegner unsere Plugin-Dateien lesen kann. Das ist für mich im Moment in Ordnung.

Hinzufügen unserer Repos zur Update-Warteschlange

Wenn es einen Schlüssel gibt, um in diesem ganzen Prozess Fuß zu fassen, dann ist es die Funktion wp_update_plugins(). Das ist eine riesige Funktion, die der Kern verwendet, um alle installierten Plugins zu durchlaufen, festzustellen, welche eine verfügbare Aktualisierung haben, und das Ergebnis in einem Transient zu speichern. Der Schlüssel ist, dass der Transient dann für Filterungen freigegeben wird, was genau das ist, was mein Plugin tut.

<?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;

}

Es hat mich ewig gekostet, das zu durchdringen, und es hat Monate gedauert, dieses Plugin zu schreiben. Sie sollten wahrscheinlich einfach GHU verwenden. Es ist ziemlich ähnlich. Dennoch, wenn Sie einige Dinge anpassen möchten und keine Plugins von Drittanbietern ausführen möchten, hilft Ihnen vielleicht der obige Code, Ihr eigenes zu schreiben.

Was ist also der Sinn der Sache?

Der Punkt ist nicht so sehr, *wie* man seinen eigenen Git-Deployer-Plugin baut oder welches vorhandene man verwenden sollte. Das können Sie selbst herausfinden. Das wirklich Interessante ist, zu sehen, was mit uns passiert ist, als wir angefangen haben, von Git zu deployen. Einige der Nebeneffekte waren tiefgreifend überraschend und positiv.

Auf Wiedersehen, FTP

FTP stinkt aus vielen Gründen.

  • FTP-Zugriff ist ein Angriffsvektor.
  • Keine einfache Möglichkeit, Änderungen zu verfolgen oder rückgängig zu machen.
  • Keine einfache Möglichkeit, mehreren Personen gleichzeitig an demselben Projekt arbeiten zu lassen.
  • Menschliches Versagen. Es ist ziemlich einfach, falsch zu ziehen und fallen zu lassen, was zu einem WSOD oder schlimmer führt.
  • Das habe ich nie erwartet, aber es ist offensichtlich, wenn man ein Plugin auf vielen Installationen aktualisiert, dass diese Git-Methode viel schneller ist als FTP.

Mit einem Git-Deployment-System wie dem, das ich in diesem Artikel befürworte und erkläre, können Sie sogar den gesamten FTP-Zugriff auf Ihre Produktionsumgebung deaktivieren. Ernsthaft: Sie brauchen ihn nicht.

Hallo, richtige Versionierung

Ich empfehle die Verwendung eines Git-Deploy-Tools, das Docblocks verwendet, um die Versionsnummer zu ermitteln, und die Versionsnummer verwendet, um festzustellen, ob das Theme oder Plugin aktualisiert werden muss. Dies zwingt Ihr Team, ordnungsgemäße Versionsnummern zu verwenden, was ein schöner erster Schritt auf dem Weg vom schnellen Erstellen von Themes zur reifen Verwaltung einer langlebigen Codebasis ist.

Ich bin so begeistert von Unit-Tests

Wenn Sie keine Unit-Tests durchführen, wissen Sie wahrscheinlich, dass Sie es tun sollten. Mit Git-Deployment kann dies sowohl automatisch als auch zwingend erfolgen.

Wir verwenden die Kommandozeile, um unsere Arbeit von unserem lokalen MAMP nach Bitbucket zu verschieben, wie z. B. git push origin master. Jedes unserer Plugins verfügt über eine Grunt-Aufgabe, um unsere phpUnit-Tests vor einem Git-Commit auszuführen, und wenn die Tests fehlschlagen, schlägt auch der Commit fehl.

Wir binden Grunt mit GitHooks an unseren Commit und führen unsere Unit-Tests über Exec aus. Wenn die Tests fehlschlagen, schlägt auch das Deployment fehl.

Es gibt keine Möglichkeit, die Tests zu umgehen, da es keine Möglichkeit gibt, Git zum Deployen zu umgehen!

Rollbacks

Bei dieser Methode gibt es keine Rückgängigmachungen im eigentlichen Sinne. Vielmehr rollt man nur vorwärts. Was auch immer Sie reparieren oder wiederherstellen möchten, bringen Sie es in den Master, erhöhen Sie die Versionsnummer, pushen Sie und deployen Sie.

Personal

Diese Art der Reifung kann weitreichende geschäftliche Auswirkungen haben. Stellen Sie sich vor: Sie haben Nicht-Entwickler-Supportmitarbeiter an vorderster Front, die versuchen, ein Problem für einen Kunden zu debuggen. Früher hätten sie diese Anfrage in eine Entwickler-Ticket-Warteschlange stellen müssen, während der Kunde Stunden oder Tage auf eine Lösung wartet. Nicht mehr. Jetzt kann Ihr First-Line-Support-Mitarbeiter zum Netzwerkadministrator navigieren und sehen, dass auf dieser Umgebung das betreffende Plugin veraltet ist. Sie können das Plugin sofort über die normale wp-admin-Oberfläche aktualisieren. Das Ticket wird vom First-Line-Support gelöst, ohne dass das Entwicklerteam involviert ist. Vielleicht kosten diese First-Line-Mitarbeiter weniger als Entwickler, oder vielleicht verfügen sie über ein tiefes Fachwissen im Account-Management. In jedem Fall müssen Sie kein Entwicklerticket mehr eröffnen, um Updates für Ihre internen Plugins bereitzustellen. Entscheidend.

Aufstieg der Maschinen

Vor diesem Prozess waren wir ein ganz gewöhnliches Entwicklerteam, das Themes und Plugins für Kunden erstellte, per Cowboy-FTP deployte, unsere Arbeit nicht versionierte. Warum? Weil wir faul waren. Warum? Weil wir menschlich waren. Wir sind nicht mehr faul, weil wir nicht mehr menschlich sind, zumindest beim Deployen. Wir sind ein Kommandozeilen-Skript und eine Reihe von API-Anfragen, und egal wie faul wir sind, wir müssen ordnungsgemäße Deployment-Praktiken befolgen, weil wir die FTP-Anmeldeinformationen für unsere Entwickler abgeschafft haben! Darüber hinaus ist es eine schnellere Art zu deployen, frei von jeglichen Klick-und-Ziehen-Fehlern.

Können Sie das über Nacht übernehmen? Okay, nein. Es ist ein langer und teurer Prozess, und er ist *vielleicht* nichts für Sie, aber ehrlich gesagt wahrscheinlich doch. Ich denke, es gibt etwa 1.000 Entwicklungsagenturen da draußen, die dies sorgfältig in Betracht ziehen sollten.