Search for:

Trello Android Git Branching-Strategie

Vor vielen Jahren verwendete Trello Android eine ziemlich einfache Git-Branching-Strategie – zumindest dachten wir das damals.

Wir wären voll entwickelt hauptsächlich Verwendung von Auszahlungsanträgen. hauptsächlich Es ist so konzipiert, dass es immer freigegeben werden kann, obwohl wir gelegentlich eine erstellen würden Freigabe / xyz Verzweigungen, wenn wir bedenken, dass einige Funktionen zusätzliche Zeit zum Testen benötigen.

Nachdem Atlassian uns übernommen hatte, stießen wir auf einige Probleme mit unserem Prozess.

Erstens ist Atlassian ein börsennotiertes Unternehmen und muss sich daher daran halten Sarbanes-Oxley-Act (SOX). Für uns bedeutete dies, dass wir die Release-Zweige mit SOX kompatibel machen mussten, wobei zusätzliche Regeln und Einschränkungen eingehalten werden mussten. Das Erstellen eines neuen Zweigs, der mit SOX kompatibel ist, verursachte eine Menge zusätzlicher Kosten, und das Löschen eines Zweigs war noch ekelhafter. Also, Die Filialen der Edition waren nun ein riesiges PITA zu erstellen und zu löschen.

Auch, Unsere alte Strategie neigte dazu, beim Beheben von Fehlern alptraumhafte Situationen zu schaffen. Angenommen, wir bereiten uns vor Ausgabe / 1.3.0aber es stellte sich heraus Version / 1.2.0 hatte einen schweren Fehler. Wir würden das beheben Notkorrektur / 1.2.1oh, aber wie man sicherstellt, dass die Fehlerbehebung erfolgreich war hauptsächlich ich Ausgabe / 1.3.0, scheiße, lass uns alles aufheben. Hoffen wir, dass der Hotfix keine zusätzlichen Hotfixes erfordert oder Dinge erledigt werden Ja wirklich hässlich.

(Ich habe versucht, ein Diagramm zu erstellen, das das Obige demonstriert, aber es war so ein Witz, dass ich Ihnen das alles erspart habe.)

Mit anderen Worten, die „einfache“ Lösung wurde angesichts leider alltäglicher Umstände ziemlich komplex und es fehlte eine klar definierte Strategie. Dieses Problem wurde durch das wachsende Team verschärft; Der Kommunikationsaufwand darüber, wo Code verbunden werden soll, wächst exponentiell mit der Anzahl der Entwickler.

Vor diesem Hintergrund haben wir uns auf die Suche nach einer besseren Verzweigungsstrategie gemacht.

Ich glaube nicht an eine Lösung für alle, und das gilt insbesondere für Code-Management-Strategien. Ihr Kontext, Ihre Ziele und Einschränkungen führen Sie zum richtigen Business-Tool.

Beispielsweise, GitHub-Flow Es ist großartig! Der einzige gemeinsame Zweig ist hauptsächlich, kombinieren Sie Funktionen über eine Auszahlungsanforderung und bam – setzen Sie sofort. Es ist fantastisch einfach! Aber GitHub Flow setzt auf kontinuierliche Implementierung, und Android-Apps können das nicht.

Daher haben wir für unseren ersten Schritt eine Liste mit Faktoren erstellt, die es zu berücksichtigen gilt.

Hier sind die Probleme, die wir zu lösen versuchten:

  • Das Erstellen eines SOX-kompatiblen Zweigs ist teuer.
  • Wir brauchen eine klar definierte Strategie für alle Situationen.
  • Unsere Strategie sollte auf mehr als ein paar Entwickler zugeschnitten sein.

Anschließend haben wir die kulturellen Vorlieben des Teams aufgelistet:

  • Wir überprüfen den Code vor dem Zusammenführen.
  • Wir vermeiden lang andauernde Feature Branches (statt Feature Flags zu verwenden).
  • Wir wollen das Release vor Auslieferung in unserer eigenen Filiale testen/reparieren können.

Zum Schluss noch ein paar Fakten zu unserer Umwelt:

  • Wir veröffentlichen regelmäßig drei Versionen: intern, Beta und Produktion.
  • Wir geben jeweils nur eine Version der Software für die Produktion frei; wir sollten niemals alte vererbte Editionen pflegen.
  • Android unterstützt keine kontinuierliche Bereitstellung.

Letztendlich haben wir darauf unsere Verzweigungsstrategie aufgebaut Dreifluss. Die Grundidee des Dreiklangs besteht darin, nur drei langlebige, stabile Zweige zu haben – einen für die Entwicklung, Kandidaten für die Veröffentlichung und Veröffentlichungen.

(Bildnachweis: Rod Hilton)

Unsere drei Filialen sind hauptsächlich, Kandidatich Lass los. hauptsächlich steht für kontinuierliche Weiterentwicklung. Wenn Sie eine neue Beta starten möchten, verbinden Sie sich hauptsächlich hinein Kandidat. Wenn diese Konstruktion stabil genug ist, verbinden Sie sich Kandidat hinein Lass los.

Wenn ein Fehler gefunden wird in Kandidatdann repariere es Kandidat und schließen Sie es wieder an hauptsächlich. Auch wenn ein schwerwiegender Fehler in gefunden wird Lass los und Sie müssen eine dringende Korrektur vornehmen, beheben Sie es Lass losund verbinden Sie dann dieses Update mit Kandidat und dann hauptsächlich.

(Beachten Sie, dass es niemals Verbindungen direkt von gibt hauptsächlich zu Lass los oder umgekehrt; Vorteil vergeht immer Kandidat bedeutet Einfachheit und Konsistenz, dass IMO die zusätzliche Arbeit wert ist.)

Wann immer Sie Code in einen der drei Zweige (hauptsächlich, Kandidat, Lass los) erstellen Sie einen eigenen Merkmalszweig (z. dlew / Funktion) und öffnen Sie eine Auszahlungsanforderung im Zielzweig.

Wir verwenden Feature-Labels, um zu vermeiden, dass Code bereitgestellt wird, bevor er an Benutzer ausgegeben wird. Das halbfertige Feature kann noch kombiniert werden Kandidat ich Lass losnur im deaktivierten Zustand.

Unsere Probleme wurden auf drei Arten behoben:

  • Stabile Branches bedeuten, dass Sie nie wieder einen SOX-kompatiblen Branch erstellen oder löschen müssen.
  • Jeder stabile Zweig kann auf unseren internen, Beta- oder Produktionsversionen veröffentlicht werden.
  • Wir haben ein Handbuch für die Entwicklung von Funktionen, das Lösen von Beta-Problemen und das Einrichten von Hotfixes.
  • Es ist eine einfache Strategie, die es einfach macht zu wissen, wo der Code zu verbinden ist (hauptsächlich für Entwickler, Kandidat für beta, Lass los für dringende Reparaturen).

Es ist auch mit unseren bestehenden Einstellungen kompatibel:

  • Wir können immer noch Auszahlungsanträge verwenden, um den Code zu überprüfen.
  • Es enthält explizit Feature-Flags und vermeidet lang andauernde Feature-Zweige.
  • Es gibt uns Zeit, einen Kandidaten für die Veröffentlichung zu backen, bevor wir ihn in Produktion nehmen.

Als Bonus fügt Three-Stream auch keinen zusätzlichen Speicherplatz hinzu, den wir nicht benötigen. Die Abschaffung der Unterstützung für Dinge wie kontinuierliche Implementierung und Legacy-Releases reduziert die Komplexität.

Der springende Punkt der Triade ist, dass Sie drei stabile Zweige haben, aber die Art und Weise, wie Sie diese drei Zweige verwalten, kann von Team zu Team variieren. Es ist daher erwähnenswert, dass wir uns getrennt haben originaler Artikel In zwei Wegen:

  • Kurzlebige Zweige – Wir sind große Fans von Code-Reviews, daher ist das Öffnen von Pull-Requests in Verzweigungen ein wichtiger Teil unseres Ablaufs. Remote-Zweige sind eine Voraussetzung für PR, aber der ursprüngliche Artikel diskutiert jede Art von Gräueltaten des Remote-Zweigs. Sie schlagen vor, einfach alle Verpflichtungen auf den Master zu verlagern – aber wie überprüft man dann den Code?

    Jetzt haben wir Nein Verwenden Sie langlebige Funktionszweige – wir stimmen zu, dass dies Albträume sind, die darauf warten, passiert zu werden, und dass Funktionsflags überlegen sind.

  • Zusammenführen zwischen Zweigen – Der Artikel verwendet Schubkraft für Lass los Ast. Während dies Ihre Git-Bäume schöner aussehen lässt, schreiben wir den Git-Verlauf nicht gerne um (und die Einhaltung von SOX hindert uns sowieso daran, den Verlauf umzuschreiben).

    Es gibt eine gewisse Eleganz, immer zwischen den drei Zweigen zu verschmelzen; Es ist immer derselbe Vorgang, von einem zum anderen zu wechseln, und macht die Codehistorie vollständig nachvollziehbar. Darüber hinaus bedeutet die Verwendung von Merge, dass Sie nur Drag-Requests verwenden können, um zwischen Zweigen zusammenzuführen – ohne sich mehrere Git-Befehle merken zu müssen.

Wir haben andere beliebte Git-Branching-Strategien aus verschiedenen Gründen ausgeschlossen:

  • Hüllenbasierte Entwicklung (auch bekannt als Stabile Hauptleitung oder Kaktus) – Diese Strategie war im Grunde das, was wir vorher gemacht haben.
  • GitFlow – Es ist viel komplexer als andere Branching-Strategien und erfordert die Erstellung vieler Branch-Editionen.
  • OneFlow – Einfacher als GitFlow, erfordert aber immer noch das Erstellen vieler Zweige der Version.
  • GitHub-Flow – Für den Betrieb ist eine kontinuierliche Abgabe erforderlich.

Ich schlage nicht vor, dass heute jeder rausgeht und das Stativ benutzt. Aufgrund unserer Einschränkungen ist es großartig für uns, aber es funktioniert möglicherweise nicht für alle. Insbesondere gibt es mindestens zwei Fälle, in denen Sie das unbedingt tun sollten Nein benutze es:

  • Wenn Sie eine kontinuierliche Bereitstellung durchführen können, verwenden Sie GitHub Flow. Es ist einfacher!
  • Wenn Sie mehrere Releases gleichzeitig unterstützen müssen (Legacy-Versionen + aktuelle Version), funktioniert Tri-Flow einfach nicht.

Mit Ausnahme dieser Fälle würde ich in Betracht ziehen, Tri-Flow eine Chance zu geben. Wir verwenden es seit über zwei Jahren ohne Probleme.

Wie Trello Android von Gson zu Mosha wurde

Trello Android wurde kürzlich eingestellt Gson zu Moshi für die JSON-Verarbeitung. Es war ein bisschen schwierig, also wollte ich den Prozess dokumentieren.

(Für den Kontext analysiert Trello Android hauptsächlich JSON. JSON wird selten serialisiert, daher liegt der Schwerpunkt hier hauptsächlich auf der Deserialisierung.)

Es gab drei Hauptgründe für den Wechsel von Gson nach Moshi: Sicherheit, Geschwindigkeitich schlechte Lebensentscheidungen.

Sicherheit – Gson versteht die Nullsicherheit von Kotlin nicht und platziert gerne Nullwerte in Eigenschaften ungleich Null. Auch die Standardwerte funktionieren nur gelegentlich (je nach Designeinstellung).

Geschwindigkeit – Genügend Benchmarks (1, 2, 3) haben gezeigt, dass Moshi normalerweise schneller ist als Gson. Nachdem wir uns umgedreht hatten, richteten wir uns ein einige Benchmarks um zu sehen, wie das Parsing in der realen Welt in unserer App verglichen wird, und wir haben eine 2x-3,5x-Beschleunigung gesehen:

Schlechte Lebensentscheidungen – Anstatt Gson zu verwenden, um JSON in einfache Modelle zu zerlegen, schrieben wir aufwändige, verwirrende, zerbrechliche benutzerdefinierte Deserialisierer, die zu viel Logik enthielten. Refactoring gab uns die Möglichkeit, diesen Architekturfehler zu korrigieren.


Warum wir uns für Moshi im Vergleich zu Wettbewerbern entschieden haben (z. Kotlin-Serialisierung), vertrauen wir im Allgemeinen Square-Bibliotheken, wir haben Moshi in der Vergangenheit für Projekte (sowohl bei der Arbeit als auch zu Hause) verwendet und fanden, dass es gut funktioniert. Wir haben keine eingehende Untersuchung von Alternativen durchgeführt.

Der erste Schritt bestand darin, sicherzustellen, dass wir Funktionsflags verwenden können, um zwischen der Verwendung unserer alten Gson-Implementierung und der neuen Moshi-Implementierung umzuschalten. Ich schrieb ein JsonInterop eine Klasse, die basierend auf dem Flag entweder alle JSON-Antworten mit Gson oder Moshi analysiert.

(Ich habe mich entschieden, die Verwendung von Tools wie z moshi-gson-interop weil ich testen wollte, ob das Moshi-Parsing vollständig funktioniert. Wenn Sie lieber gleichzeitig eine Mischung aus Gson und Moshi haben möchten, wäre diese Bibliothek nützlich.)

Gson bietet Ihnen die Möglichkeit, den verwendeten Standardschlüsselnamen zu überschreiben @SerializedName. Mit Moshi können Sie dasselbe tun @Json. Es ist alles gut und gut, aber es schien wirklich einfach, hier einen Fehler zu machen, wo die Assets in Gson Vs unter verschiedenen Namen analysiert werden. Moshi.

Also habe ich einige Unit-Tests geschrieben, die bestätigen würden, dass unsere generierten Moshi-Adapter das gleiche Ergebnis haben wie Gsons Parsing. Ich habe besonders getestet …

  • … dass Moshi für jede Klasse, die wir deserialisieren wollten, einen Adapter (nicht unbedingt den richtigen!) generieren kann. (Wenn er das nicht könnte, würde Moshi eine Ausnahme auslösen.)
  • … womit jedes Feld gekennzeichnet ist @SerializedName ist ebenfalls mit gekennzeichnet @Json (mit dem gleichen Schlüssel).

Zwischen diesen beiden Überprüfungen war es leicht zu finden, wenn ich den Fehler gemacht habe, unsere Klassen in späteren Schritten zu aktualisieren.

(Ich kann die Quelle hier nicht angeben, aber wir haben im Grunde verwendet Guavas ClassPath um alle unsere Klassen zu sammeln und sie dann auf Probleme zu scannen.)

Mit Gson können Sie generische JSON-Bäume analysieren JsonElement (und Freunde). Wir fanden dies in einigen Kontexten nützlich, wie z. B. beim Parsen von Socket-Updates (wo wir bis zu einer anfänglichen Verarbeitung nicht genau wissen würden, wie das Antwortmodell zu parsen ist).

Offensichtlich wird Moshi mit der Verwendung von Gsons Uhren nicht zufrieden sein, also sind wir dazu übergegangen, sie zu verwenden Map<String, Any?> (und manchmal List<Map<String, Any?>>) für generische Datenbäume. Sowohl Gson als auch Moshi können dies aufschlüsseln:

fun <T> fromJson(map: Map<String, Any?>?, clz: Class<T>): T? {
  return if (USE_MOSHI) {
    moshi.adapter(clz).fromJsonValue(map)
  }
  else {
    gson.fromJson(gson.toJsonTree(map), clz)
  }
}

Darüber hinaus ist Gson freundlich, die Straße zu analysieren Leser, aber Moshi nicht. Ich habe es mit gefunden BufferedSource war eine gute Alternative, da es für alten Gson-Code in Reader konvertiert werden kann.

Die einfachsten Adapter für Moshi sind die, die Sie gerade getroffen haben @JsonClass auf sie und nennen Sie es eines Tages. Leider hatten wir, wie ich bereits erwähnte, eine Menge unglücklicher angepasster Deserialisierungslogik in unserem Gson-Parser.

Es ist ziemlich einfach Schreiben Sie einen benutzerdefinierten Moshi-Adaptersondern weil es so war so viel benutzerdefinierte Logik in unseren Deserialisierern, würde das Schreiben eines einzigen Adapters das nicht brechen. Am Ende mussten wir Interspace-Modelle erstellen, um das rohe JSON zu parsen, und uns dann an die Modelle anpassen, an die wir gewöhnt waren.

Stellen Sie sich, um ein konkretes Beispiel zu geben, vor, wir hätten a data class Foo(val count: Int)aber das eigentliche JSON, das wir bekommen, hat die Form von:

{
  "data": { 
    "count": 5
  }
}

Bei Gson konnten wir nur manuell auf den Baum schauen und die Nummer abrufen data Thema, aber wir haben festgestellt, dass darin der Wahnsinn liegt. Wir würden lieber einfach mit einfachen POJOs parsen, aber wir wollen trotzdem irgendwann Foo rausschmeißen (damit wir nicht unsere gesamte Codebasis ändern müssen).

Um dieses Problem zu lösen, würden wir neue Modelle erstellen und diese in einem benutzerdefinierten Adapter wie diesem verwenden:

@JsonClass(generateAdapter = true) data class JsonFoo(val data: JsonData)

@JsonClass(generateAdapter = true) data class JsonData(val count: Int)

object FooAdapter {
  @FromJson
  fun fromJson(json: JsonFoo): Foo {
    return Foo(count = json.data.count)
  }
}

Voila! Jetzt kann der Parser immer noch Foo auswerfen, aber wir verwenden einfache POJOs, um unsere Daten zu modellieren. Es ist einfacher zu interpretieren und zu testen.

Erinnern Sie sich, wie ich sagte, dass Gson gerne Nullwerte in Nicht-Null-Modelle analysieren würde? Es stellte sich heraus, dass wir uns (leider) an allen möglichen Stellen auf dieses Verhalten verlassen haben. Insbesondere Trello-Sockets geben oft Teilmodelle zurück – während wir also normalerweise erwarten würden, dass eine Karte mit einem Namen zurückkommt, wird dies in einigen Fällen nicht der Fall sein.

Dies bedeutete, dass wir unsere Stürze in Fällen überwachen mussten, in denen Moshi (aufgrund des Nullwerts) explodieren würde, wenn Gson wie eine Hülle glücklich wäre. Hier glänzen die Funktionsflags wirklich, da Sie den fehlerhaften Parser nicht ahnungslosen Produktionsbenutzern aufdrängen wollen!

Nachdem ich ein Dutzend dieser Fehler behoben habe, habe ich das Gefühl, dass ich viel Wertschätzung für Nicht-JSON-Technologien mit gut definierten Schemas wie Protokollpuffer. Es gibt viele Fehler, auf die ich gestoßen bin, die einfach nicht passiert wären, wenn wir einen Vertrag zwischen dem Server und dem Client gehabt hätten.