Przejdź do treści

Trunk Based Development - co to takiego?

Trunk Based Development to model pracy w zespole, w ramach wspólnego repozytorium kodu, skupiający się na rozwijaniu jednej gałęzi (branch'a) nazywanej trunk (lub main jeśli używasz github'a).

Celem Trunk Based Development jest zminimalizowanie rozwoju oprogramowania w osobnych gałęziach (tzw. feature branch'ach) i stosowanie technik, które pozwalają unikać większości problemów z merge'owaniem i konfliktami, a jednocześnie gwarantują stabilność oprogramowania i ciągłą gotowość do wydania nowej wersji na produkcję.

Przykłady problemów

1. Wszyscy rozwijają wspólny "release branch"

gitGraph
  commit id: "init"
  commit id: "1.0.0" tag: "1.0.0"
  branch release-1.0.1
  checkout release-1.0.1
  commit id: "bugfix-1"
  commit id: "bugfix-2"
  commit id: "bugfix-3"
  checkout main
  merge release-1.0.1 tag: "1.0.1"
  branch release-1.0.2
  checkout release-1.0.2
  commit id: "bugfix-4"
  commit id: "bugfix-5"
  commit id: "bugfix-6"
  checkout main
  merge release-1.0.2 tag: "1.0.2"

W takim przypadku nie ma logicznego sensu, by posiadać osobne release branch'e. Wszystkie commit'y można realizować w głównej gałęzi:

%%{init: { 'logLevel': 'debug', 'gitGraph': {'mainBranchName': 'trunk'}} }%%
gitGraph
    commit id: "init"
    commit id: "1.0.0" tag: "1.0.0"
    commit id: "bugfix-1"
    commit id: "bugfix-2"
    commit id: "bugfix-3" tag: "1.0.1"
    commit id: "bugfix-4"
    commit id: "bugfix-5"
    commit id: "bugfix-6" tag: "1.0.2"

2. Gitflow i feature branch'e

%%{init: { 'logLevel': 'debug', 'gitGraph': {'showBranches': true}, 'themeVariables': {
              'git3': '#bb0000'
       }} }%%
gitGraph
  commit id: "init"
  commit id: "1.0.0" tag: "1.0.0"
  branch featureA
  branch featureB
  branch featureC
  checkout featureA
  commit id: "a-1"
  checkout featureB
  commit id: "b-1"
  checkout featureA
  commit id: "a-2"
  checkout featureB
  commit id: "b-2"
  checkout featureC
  commit id: "c-1"
  checkout featureA
  commit id: "a-3"
  checkout featureC
  commit id: "c-2"
  checkout featureB
  commit id: "b-3"
  checkout featureC
  commit id: "c-3"
  checkout main
  merge featureA
  merge featureB type: REVERSE
  merge featureC type: REVERSE

Gdy kilka zespołów lub developerów realizuje prace na osobnych branch'ach, szczęście ma ten, który merge'uje swoje zmiany jako pierwszy.

Ten, który będzie następny, musi rozwiązać wszystkie potencjalne konflikty. Jeśli branch'e były rozwijane przez wiele tygodni i są bardzo duże, może to doprowadzić do długotrwałych opóźnień, związanych z nieprzewidzianą pracą zespołu.

Problem narasta wraz z ilością aktywnych branch'y.

3. Feature branch tak długo był rozwijany, że wymknął się spod kontroli

%%{init: { 'logLevel': 'debug', 'gitGraph': {'showBranches': true}, 'themeVariables': {
              'git3': '#bb0000'
       }} }%%
gitGraph
  commit id: "init"
  commit id: "1.0.0" tag: "1.0.0"
  branch featureA
  branch featureB
  checkout featureA
  commit id: "a-1"
  checkout featureB
  commit id: "b-1"
  checkout featureA
  commit id: "a-2"
  branch featureC
  commit id: "c-1"
  checkout featureB
  commit id: "b-2"
  checkout main
  merge featureB
  checkout featureA
  merge main
  checkout featureC
  commit id: "c-2"
  merge featureA
  checkout featureA
  commit id: "a-3"
  checkout featureC
  commit id: "c-3"
  checkout featureA
  merge featureC
  commit id: "a-4"

To sytuacja, w której branch featureA był rozwijany tak długo, że zaczynają powstawać branch'e wychodzące od niego, i do niego są merge'owane. Posiadanie gałęzi głównej przestało mieć sens, bo naturalnie jej cykl życia wygasł.

Zastosowanie Trunk Based Development

Z założenia wszystkie osoby pracujące z repozytorium powinny pracować na gałęzi głównej (trunk), wielokrotnie synchronizując zmiany, które wprowadził zespół w ciągu dnia. Aby było to możliwe, konieczne jest zastosowanie systemu kontroli wersji, który będzie szybko synchronizował zmiany.

Osoby, które wykonały zmiany w kodzie i mają pewność, że te zmiany nie zagrażają stabilności branch'a, commit'ują swoje zmiany do gałęzi głównej. Stabilność jest tutaj kluczowa i powinna być poparta testami automatycznymi, które każdy może uruchomić lokalnie.

Czasami uruchomienie testów lokalnie nie jest możliwe lub wysyłany kod chcemy najpierw zrecenzować w procesie code-review. W takiej sytuacji uzasadnione jest stworzenie krótko żyjącego branch'a i pull request'a. Częstą praktyką jest uruchamianie testów automatycznych w momencie otwarcia nowego pull request'a. Warto jednak pamiętać, że zazwyczaj dużo szybsze jest uruchomienie testów lokalnie.

Co do zasady, wysyłane zmiany powinny być małe (maksymalnie kilka godzin pracy), jednak jest to kwestia, którą zespół musi sam wypracować.

1. Wszyscy commit'ują do trunk'a, wdrożenie z trunk'a.

%%{init: { 'logLevel': 'debug', 'gitGraph': {'mainBranchName': 'trunk'}} }%%
gitGraph
    commit id: "init"
    commit id: "1.0.0" tag: "1.0.0"
    commit id: "bugfix-1"
    commit id: "feature-1"
    commit id: "bugfix-2" tag: "1.0.1"
    commit id: "feature-2"
    commit id: "bugfix-3"
    commit id: "bugfix-4" tag: "1.0.2"

Wszyscy commit'ują do wspólnej gałęzi głównej. Każdy pilnuje, aby mieć aktualny kod. Konflikty są rzadkie, a jeśli już występują, to tylko przy próbie pobrania (pull) zmian z repozytorium.

Zakres konfliktu nie przekracza zakresu zmian wprowadzonych od ostatniego commit'a.

Wdrożenia realizujemy z gałęzi głównej. Zakres testów obejmuje wszystkie commit'y, jakie trafiły do repozytorium. Gałąź główna musi być zatem zawsze stabilna.

Przy zachowaniu rygoru stabilności, ta technika pozwala na zmaksymalizowanie tempa wydawania zmian na produkcję, ze względu na możliwość pełnej automatyzacji procesu wdrożenia.

Ten wariant jest przewidziany dla zespołów, o dużym tempie wdrażania zmian, które mają kompetencje do szybkiego ustabilizowania gałęzi głównej (np. w ciągu godziny).

2. Wszyscy commit'ują do trunk'a, wdrożenie z release branch'y.

%%{init: { 'logLevel': 'debug', 'gitGraph': {'mainBranchName': 'trunk'}} }%%
gitGraph
    checkout trunk
    commit id: "init"
    commit id: "1.0.0" tag: "1.0.0"
    branch release-1.0.1
    checkout trunk
    commit id: "feature-1"
    commit id: "bugfix-1"
    checkout release-1.0.1
    cherry-pick id: "bugfix-1"
    checkout trunk
    commit id: "feature-2" tag: "1.1.0"
    branch release-1.1.1
    checkout trunk
    commit id: "bugfix-2"
    checkout release-1.1.1
    cherry-pick id: "bugfix-2"
    checkout trunk
    commit id: "bugfix-3"
    checkout release-1.1.1
    cherry-pick id: "bugfix-3"
    checkout trunk
    commit id: "feature-3"

W tym scenariuszu nadal wszyscy commit'ują bezpośrednio do gałęzi głównej, ale wdrożenia realizujemy, wybierając pojedyncze zmiany (commit'y) i umieszczając je (cherry-pick) w gałęzi, która będzie służyć wydaniu konkretnej wersji (release branch). Po wydaniu, gałąź powinna zostać usunięta.

Nikt nie wysyła zmian bezpośrednio do gałęzi wydawniczej (release branch'a).

Zakres testowania gałęzi wydawniczej jest mniejszy i istnieje mniejsze ryzyko destabilizacji, bo zawiera tylko bugfix'y.

Ze względu na operacje cherry-pick, zespoły, które stosują tę technikę, mają więcej manualnej pracy przy wdrożeniu, co utrudnia automatyzację procesu wydawniczego.

3. Trunk Based Development na większą skalę

%%{init: { 'logLevel': 'debug', 'gitGraph': {'mainBranchName': 'trunk'}, 'themeVariables': {
              'git3': '#bb0000'
       }} }%%
gitGraph
    commit id: "init"
    commit id: "1.0.0" tag: "1.0.0"
    branch release-1.0.1 order: 5
    checkout trunk
    branch featureA-1 order: 3
    checkout featureA-1
    commit id: "feature-1"
    checkout trunk
    merge featureA-1
    commit id: "bugfix-1"
    checkout release-1.0.1
    cherry-pick id: "bugfix-1"
    checkout trunk
    branch featureA-2 order:3
    checkout featureA-2
    commit id: "feature-2"
    checkout trunk
    merge featureA-2 tag: "1.1.0"
    branch release-1.1.1 order: 6
    checkout trunk
    commit id: "bugfix-2"
    checkout release-1.1.1
    cherry-pick id: "bugfix-2"
    checkout trunk
    commit id: "bugfix-3"
    checkout release-1.1.1
    cherry-pick id: "bugfix-3"
    checkout trunk
    branch featureB-1 order:3
    commit id: "feature-3"
    checkout trunk
    merge featureB-1

Jeśli zespół developerów jest duży lub mamy wiele zespołów pracujących z tym samym repozytorium, pozwalamy na krótko żyjące branch'e, które służą tylko do wprowadzenia ustabilizowanych zmian i procesowi code-review. Ich długość życia musi zostać wypracowana przez zespół, ale nie powinna przekraczać 3 dni.

Zmiany w branch'ach są niewielkie, co minimalizuje ryzyko konfliktów, a jeśli te wystąpią, to ich zakres jest niewielki.

Zastosowanie krótko żyjących branch'y pozwala też na stosowanie pull request'ów i klasyczny proces code-review.

Gdy kodu jest mało, code-review jest przyjemniejsze.

Branch'e stosowane do code-review powinny być usuwane po merge'u.

Dla zespołów, które stosowały wcześniej pracę w feature branche'ach może być to nieintuicyjne i może istnieć pokusa zachowania brancha "na potem, bo jeszcze będzie rozwijany". Częstym błędem jest też ciągła praca z tym samym pull request'em, który rozrasta się z każdym dniem.

Warto zachować historię pull request'ów, komentarze i uwagi, ale raz zaakceptowany i zamknięty pull request nie powinien być ponownie otwierany, a branch nie powinien żyć dłużej niż to wymagane.