Czy precommit hooks to zło?
Narzędzia są różne - lepsze i gorsze, lubiane i znienawidzone, ale zawsze powstają w jakimś konkretnym celu. Dziś porozmawiamy o jednym z takich narzędzi - czy precommit hook jest naprawdę tak fatalny?

Z tego artykułu dowiesz się:
Co to precommit hook?
Ile kosztuje uruchomienie procesu CI?
Jaki jest koszt opóźnionego feedbacku?
Dlaczego --no-verify jest problematyczne?
Istnieje takie bardzo modne określenie, które opisuje jakość współpracy programisty z różnymi narzędziami i bibliotekami. Tym określeniem jest developer experience - im wyższy tym lepiej, im gorszy... no cóż. Warto podkreślić, że poziom satysfakcji z korzystania z danego narzędzia jest bardzo subiektywny. Jeden programista lubi Visual Studio Code i uważa, że nie ma nic lepszego - drugi stwierdzi, że VSC nie ma podjazdu do narzędzi od JetBrains.
Pytanie, które chciałbym dziś zadać pośrednio dotyczy właśnie developer experience. Czy narzędzia takie jak precommit hook, mające z zasady podnosić jakość naszej pracy, są złe? Wkraczam w ten temat, bo wiem, że zdania są mocno podzielone, polaryzują społeczność prawie tak samo jak temat taby vs spacje. Spójrzmy zatem głębiej na problem.
Czym jest precommit hook?
System kontroli wersji jakim jest GIT ma jedno podstawowe zadanie, a jest nim możliwość utrzymania wysokiej jakości kodu poprzez zarządzanie dostarczaniem kolejnych iteracji oprogramowania. Według mnie GIT i jakość to słowa, które można postawić obok siebie - oczywiście zdaję sobie sprawę, że to tylko narzędzie i dużo zależy od tego, jak jego użyjemy. Aczkolwiek czymś naturalnym jest traktowanie GITa jako części jakościowogo środowiska pracy z kodem.
Nic zatem dziwnego, że GIT dostarcza nam dodatkowych narzędzi, których założeniem jest tę jakość podnosić. `precommit` hook to nic innego jak skrypt, który zostanie uruchomiony tuż przed stworzeniem commita. Innymi słowy polecenie `git commit` odpali go, o ile został zadeklarowany. Mechanizm hook'ów został umieszczony w GIT wraz z pierwszymi jego wydaniami. `precommit` hook może wykonać na kodzie w zasadzie wszystkie operacje, które pod niego podepniemy - czyszczenie ze zbędnych elementów, jego formatowanie, testowanie itd. Brzmi super i rzeczywiście tak własnie mogłoby być w idealnym świecie.
A co z CI/CD?
Jeśli spojrzymy na listę procesów, o których wspomniałem powyżej to zauważymy, że często są to te same procesy, które wykonujemy w ramach CI / CD. Czy zatem możemy stosować zamiennie oba mechanizmy? Czy byłoby w porządku, aby zastąpić jeden mechanizm drugim? Procesy CI / CD mają zapewnić bezpieczne dostarczenie naszej aplikacji na określone środowisko, zapobiec publikacji niedziałającej aplikacji i wykryć potencjalne problemy. Pisałem już o tym kilkukrotnie - między innymi w artykule o tym, jaką rolę pełnią CI i CD w procesie deweloperskim.
Zastąpienie CI / CD pre-commit hookami to na pewno kiepski pomysł. Przede wszystkim dlatego, że środowisko lokalne w ramach, którego uruchamiany jest pre-commit może różnić się od finalnego środowiska produkcyjnego. Mechanizmy Continous Integrations oraz Continous Deployment mają sporo zabezpieczeń, które nie pozwalają np. na pominięcie kolejnych kroków - co mogłoby być dość problematyczne przy wypuszczaniu zmian na produkcję.
Czas, czas, czas
W sieci znajdziemy dużo krytycznych uwag na temat stosowania precommit hooków. Chciałbym jednak zacząć od wskazania możliwych zalet ich wykorzystywania. Jednym z największych plusów jest wczesne wykrywanie błędów - jeszcze w czasie dewelopmentu, lokalnie przed przygotowaniem commita. Błędów zwracanych przez różne narzędia - przykładowo lintery, formattery czy przez testy. Wczesne ich wykrycie pozwala nam zaoszczędzić czas w przyszłości, być może uniknąć tworzenia kolejnego commita tylko ze zmianami stylistycznymi.
W mojej opinii ma to wiele sensu. Wyobraźmy sobie sytuację, w której mamy przygotowany cały kod, tworzymy commita, wypychamy naszego brancha a następnie tworzymy Pull Request i prosimy zespół o wykonanie code review. Kolejne etapy dzieją się już równolegle - code review jest wykonywane oraz uruchamiane są procesy wpisane w pipeline w CI / CD.
Dobre code review nie polega na wyłapywaniu brakujących średników, nadmiarowych spacji i literówek. Dobre code review to sprawdzenie implementacji, logiki działania i wydajności rozwiązania. Według wielu źródeł nasze mózgi widząc proste pomyłki - skupiają się na nich, pomijając bardziej złożone aspekty. Jeśli wyeliminujemy drobne błędy, takie jak pomyłki stylistyczne przed publikacją Pull Requesta, wówczas jest szansa na to, że osoby sprawdzające skupią się na ważniejszych elementach systemu.
Zakładając jednak, że zespół jest bardzo dojrzały i nie rozpraszają go tego typu rzeczy, i zweryfikuje kod poprzez akceptację - wówczas wywalający się na brakującym średniku proces CI zmusi sprawdzających do ponownego zajrzenia do PRa i ponownej jego akceptacji. W kontekście stosowania precommit hooków - mówi się często o marnowaniu czasu - czy zatem wrzucania stylistycznej poprawki do zaakceptowanego PRa nie jest tym samym? Czy nie byłoby łatwiej sprawdzić ten kod lokalnie?
Zasoby
Kolejnym aspektem dyskutowanym są zasoby. Część deweloperów uważa, że precommity są złe, bo zjadają zasoby ich komputerów, spowalniają pracę a cały proces może się zadziać na serwerze w ramach CI. Racja może - ale jaki jest realny koszt takiego przesunięcia? Spróbujmy to przeliczyć.
Na polskim rynku stawki godzinowe deweloperów można uśrednić do poziomiu 140 zł netto na godzinę (założyłem B2B oraz zakres od 100 do 180 zł na godzinę). Deweloper pracuje średnio w miesiącu około 160 godzin. Przeciętny koszt programisty w Polsce to zatem około 22 400 zł netto, co daje nam kwotę 5 600 USD.
Nasz typowy projekt, który weźmiemy do obliczeń, składa się z 5 programistów. Każdy z nich wypycha tygodniowo 4 branche. W skali miesiąca jest to około 80 pull requestów, czyli przynajmniej 80 razy zostanie uruchomiony pipeline. Ostrożnie założmy, że jeden pipeline wykonuje się około 5 minut. Przy takim wykorzystaniu usługi (wykluczając darmowe limity) AWS CodeBuild oraz GitLab CI/CD będą nas kosztować około 4 dolary miesięcznie, natomiast Azure DevOps nawet czterdzieści.
Do powyższego dodajmy, że AWS CodeBuild i GitLab CI/CD mają darmowe limity minut. Przykładowo AWS oferuje 100 minut za darmo, ale to i tak nie pokryje zapotrzebowania naszego zespołu. Azure DevOps może kosztować więcej - płatność za agenta bez własnego hostingu to 40 dolarów za miesiąc.
Koszt opóźnionego feedbacku
Skoro wiemy ile może kosztować nas wykonanie zadań po stronie serwera, zastanówmy się jak to wpływa na czas pracy programisty. Założmy, że 50% uruchomionych pipeline'ów wykryje błędy, które mogłyby być wykryte w czasie precommit'a. Informacja zwrotna z wykonanego pipeline'a trafi do dewelopera średnio po 5 - 10 minutach. Programista, potrzebuje następnie czas na powrót do błędnego branch'a, zmianę kontekstu, wprowadzenie poprawki i ponowne wypchnięcie. W idealnych warunkach, gdy pipeline'y nie są kolejkowane i czekają kilka godzin na wykonanie, może to zająć dodatkowe 15 minut programiście.
Marnowany czas jednego programisty miesięcznie, licząc według powyższych zmiennych - to 8 błędów razy 15 minut, czyli w sumie 120 minut (2h). Średni koszt na jednego inżyniera to zatem 280 zł netto (70 USD). Cały zespół zatem może tracić w ten sposób około 350 dolarów - tylko z powodu braku szybkiego feedbacku. Pre-commity sprawdzające tylko błędy stylistyczne i składniowe trwają około 3 sekund. Kilka sekund wykonanych lokalnie, które mogą zaoszczędzić kilkaset dolarów.
Wolność Tomku w swoim domku
Spotkać możemy się też ze stwierdzeniem, że tu chodzi o wolność i zaufanie. Nie wolno swoim programistom narzucać tego, jak mają pracować.Prawdziwi (sic!) seniorzy powinni rzucić papierami, gdy tylko ktoś ich zmusza do uruchomienia precommit hooka. Dość zabawne - unoszenie się dumą z powodu tego, że ktoś kazał nam użyć narzędzia, które bądź co bądź wpływa na wydajność i może zaoszczędzić realne pieniadze w jakimś okresie czasu. Czasami mam wrażenie, że programiści idą troszkę za daleko z poczuciem swojej wyjątkowości - oczywiście rozumiem, że posiadana przez nas wiedza i doświadczenie może poniekąd wpływać na taki osąd, ale nie przesadzajmy. Programista, lekarz, taksówkarz czy trener personalny - każdy z nich posiada bardzo konkretny kawałek wiedzy. Nie czujmy się lepsi, bo tacy nie jesteśmy.
--no-verify
Argumentacja przeciw precommit hookom często sprowadza się też do tego, że każdy może je świadomie pominąć, używając do tego flagi --no-verify
. Dodaje się ją na końcu polecenia, np. git commit -m "initial commit" --no-verify
. Rzeczywiście każdy może pominąć weryfikację z poziomu swojego środowiska. Świadome pomijanie sprawdzania kodu może wynikać z kilku aspektów: świadomego łamania ustalonych przez zespół zasad lub z problemów z aplikacją. O ile w tym drugim przypadku jest to dla mnie jasne - czasami chcemy pominąć lokalną weryfikację, ponieważ błąd pojawia się tylko na okreslonym środowisku, o tym łamanie wspólnie ustalonych kontraktów jest już conajmniej nieodpowiedzialne.
Redundancja
Wykonywanie tych samych operacji na CI oraz lokalnie w precommit hooku czasami może być niepotrzebnym przepalaniem zasobów. Będzie to najzwyklejsze powielanie zadań, które można spokojnie wykonać tylko raz. I tutaj częściowo mogę się zgodzić z przeciwnikami precommit hooków - niektóre operacje powinny być wykonane tylko raz i to najlepiej na pipeline. Czasami redukcja powielonych zadań to oszczędność zasobów i czasu. Weźmy jednak pod uwagę, że nie zawsze duplikacja jest zła - zduplikowane zadania czasami mają różne cele do zrealizowania.
Cóż czynić?
Pozbycie się CI i pozostawienie tylko precommit hooków to kiepski pomysł. Dostaniemy bardzo szybki feedback, natomiast nie mamy kontroli nad tym co znajduje się w repozytorium - wszak ktoś mógł świadomie pominąć hooki. A co gdybyśmy natomiast pozbyli się precommit hooków i zdecydowali się tylko na wykonywanie zadań po stronie pipeline'a? Na pierwszy rzut oka bardzo kusząca opcja. Niestety przy takim zestawieniu koszty zużycia zasobów mogą wzrosnąć, a programistyczna wolność może doprowadzić do anarchii. Część deweloperów będzie ignorować zasady. Może to być nie do zaakceptowania w niektórych organizacjach, nawet pomimo istotnych zalet - jakimi są prostsze środowisko programistyczne.
Pozostaje nam wariant hybrydowy. Otrzymujemy wczesny feedback, a dokładne wykonanie testów uruchamiamy na CI. Istnieje nadal ryzyko rozjechania się wersji reguł np. linterów. Osobiście najbardziej optuję za takim podejściem. Bez zbędnych dram, ale ze świadomym zrozumieniem ryzyk i zalet obu podejść.
Podsumowanie
Precommit hooki są super dla szybkich i lekkich operacji, takich jak lintowanie i formatowanie kodu. CI natomiast o wiele bardziej nadaje się do wykonywania testów integracyjnych, skanowania kodu pod kątem bezpieczeństwa, budowania aplikacji czy wykonywania deploymentu. Każde narzędzie ma swoje plusy i minusy, warto je znać i rozumieć - dzięki temu możemy wyciagnąć z nich jak najwięcej korzyści dla naszego projektu.
Komentarze (0)
Jeszcze nikt nic nie napisał, ale to znaczy że... możesz być pierwszy/pierwsza.