Dostarczanie wydajnego oprogramowania: automatyzacja CI/CD 

Mateusz Mirecki | Rozwój oprogramowania | 24.05.2024

Continuous Integration, Delivery and Deployment sprowadzają się do automatyzacji procesu testowania i wdrażania, minimalizacji (lub całkowitego eliminowania) potrzeby ingerencji człowieka, redukcji ryzyka wystąpienia błędów oraz ułatwienia tworzenia i wdrażania oprogramowania.

Continuous Integration, Delivery and Deployment to praktyki rozwoju oprogramowania, które w ostatnich latach zdobyły dużą popularność. Gdybyśmy mieli je podsumować jednym słowem, brzmiałoby to: automatyzacja. Wszystkie te trzy praktyki sprowadzają się do automatyzacji procesu testowania i wdrażania, minimalizacji (lub całkowitego eliminowania) potrzeby ingerencji człowieka, redukcji ryzyka wystąpienia błędów oraz ułatwienia tworzenia i wdrażania oprogramowania do tego stopnia, że może to zrobić każdy programista w zespole. Poznaj kluczowe praktyki i korzyści w zakresie automatyzacji CI/CD.  

Definicja Continuous Integration (CI) oraz Continuous Delivery (CD)

Ciągła integracja

Continuous Integration, CI jest praktyką stosowaną w procesie wytwarzania oprogramowania polegającą na częstym, regularnym integrowaniu bieżących zmian w kodzie do głównego repozytorium aplikacji. Zwykle programiści włączają swoje zmiany co najmniej raz dziennie, co przy zespole składającym się z kilku lub kilkunastu osób skutkuje wieloma integracjami wykonywanymi każdego dnia. Każda integracja jest weryfikowana przez zastosowanie automatyzacji w projekcie (włączając również testy automatyczne) w celu jak najszybszego wykrycia ewentualnych błędów integrowanego kodu. Stosowanie tego podejścia prowadzi do znacznego zmniejszenia liczby problemów związanych z integracją i umożliwia szybsze tworzenie spójnego oprogramowania.

Ciągłe dostarczanie

Continuous Delivery, CD – jest praktyką inżynierii oprogramowania polegającą na regularnym wydawaniu kolejnych wersji oprogramowania w krótkich cyklach. Oznacza to, że z każdą kompilacją budowana jest nowa wersja całego tworzonego oprogramowania. Nie znaczy to jednak, że oprogramowanie jest od razu wypuszczane na środowisko produkcyjne. Nic jednak nie stoi na przeszkodzie, aby to zrobić, gdyby zaszła taka potrzeba. Ta nieznaczna różnica dotycząca automatycznego wdrażania zmian bezpośrednio na środowisku produkcyjnym odróżnia ciągłe dostarczanie od ciągłego wdrażania (Continuous Deployment) – w którym to każda kompilacja całego oprogramowania kończy się wydaniem kolejnej wersji na środowisku produkcyjnym. Zespoły developerskie dbają o to, aby nowe funkcje były dodawane w małych pakietach, dzięki czemu ewentualne wydanie nowej wersji oprogramowania może nastąpić szybko i w dowolnym momencie. W praktyce maksymalizuje to możliwość uzyskania szybkiej informacji zwrotnej na temat oprogramowania zarówno z perspektywy biznesu, jak i technicznej.

Wyzwania CI/CD

O ciągłej integracji po raz pierwszy napisał Kent Beck w swojej książce pod tytułem „Extreme Programming Explained”, wydanej po raz pierwszy w 1999 roku. Ideą CI było: jeżeli regularna integracja kodu jest dobra – dlaczego nie wykonywać jej przez cały czas? „Cały czas” oznacza: za każdym razem, gdy ktoś wprowadza zmianę w systemie kontroli wersji.

Problem z ciągłą integracją, dostarczaniem i wdrażaniem polega jednak na tym, że konfiguracja nie należy do najprostszych czynności i zajmuje dużo czasu, zwłaszcza jeśli zespół ma z nią do czynienia po raz pierwszy lub postanowiono wdrożyć proces do istniejącego już projektu.

Jednak korzyści płynące z wprowadzenia tych praktyk są warte poświęconego czasu. Przejawi się to w mniejszej liczbie błędów, ułatwieniu ich identyfikacji i naprawy, a ostatecznie w stworzeniu oprogramowania lepszej jakości.

Czym jest ciągłe dostarczanie i automatyzacja CI/CD?

Ciągła integracja (CI).

Kluczowe aspekty skutecznej ciągłej integracji

Przyjrzyjmy się CI jeszcze bliżej. Ciągła integracja na przestrzeni ostatnich kilku lat stała się w zasadzie standardem w zwinnych metodykach wytwarzania oprogramowania. Początkowo, w połączeniu z uruchamianiem automatycznych testów jednostkowych na lokalnym środowisku programisty, miała na celu zapewnienie, że programista nie włączy kodu, który mógłby powodować błędy w aplikacji. Z biegiem czasu metoda ta ewoluowała.

Obecnie kompilacje uruchamiane są na serwerach ciągłej integracji, które budują projekt oraz uruchamiają zautomatyzowane testy – automatycznie, przy każdej zmianie dodanej przez programistów, lub okresowo. Wynik (w postaci raportów, logów) generowany jest po zakończeniu procesu budowania. Obecnie podejście ciągłej integracji jest wykorzystywane nie tylko w metodykach zwinnych, ale również w innych metodykach wytwarzania oprogramowania.

Oprócz przeprowadzania testów jednostkowych i integracyjnych można też uruchamiać testy statyczne i dynamiczne, mierzyć i profilować wydajność, tworzyć dokumentację bazującą na kodzie źródłowym oraz upraszczać manualne procesy zapewnienia jakości. W ten sposób ciągła integracja zapewnia również ciągłą kontrolę jakości oprogramowania.

Dobrze funkcjonująca ciągła integracja opiera się na spełnieniu pewnych warunków wstępnych, a także przestrzeganiu kilku podstawowych praktyk. Przy jej wdrażaniu należy zadbać przede wszystkim o trzy rzeczy:

  1. Narzędzie kontroli wersji
    Wszystko w projekcie musi być ewidencjonowane w jednym narzędziu kontroli wersji: kod, skrypty, bazy danych, skrypty kompilacji i wdrażania oraz wszystko inne, co jest niezbędne do tworzenia, instalowania, uruchamiania i testowania aplikacji.
  2. Narzędzie do zautomatyzowanej kompilacji
    Należy mieć możliwość uruchomienia kompilacji za pomocą wiersza poleceń. Podstawowym narzędziem może być prosty program wiersza poleceń, który skompiluje tworzony program, a następnie uruchomi testy. Może to być też złożona kolekcja skryptów kompilacji, które wywołują się nawzajem w odpowiedniej kolejności. Niezależnie od mechanizmu, osoba lub komputer musi mieć możliwość uruchomienia kompilacji, budowania, uruchamiania testów i wdrażania w sposób zautomatyzowany za pomocą wiersza poleceń. Skrypty powinny być traktowane jak kod – muszą być testowane i stale refaktoryzowane, aby były stabilne, uporządkowane i łatwe do zrozumienia.
  3. Zgoda całego zespołu
    Ciągła integracja jest praktyką, a nie narzędziem. Wymaga od zespołu programistycznego dyscypliny i zaangażowania. Każdy w zespole musi często wprowadzać swoje zmiany do repozytorium i przyjąć założenie, że najważniejszym zadaniem w trakcie pracy nad projektem jest naprawa wszelkich zmian, które psują aplikację. Jeśli zespół nie będzie przestrzegał tych zasad, poprawne wdrożenie ciągłej integracji może nie być efektywne i nie doprowadzi do satysfakcjonującej poprawy jakości wytwarzania oprogramowania.

Zalety ciągłej integracji w porównaniu z podejściem tradycyjnym

W porównaniu do tradycyjnego podejścia, gdzie zmiany wprowadzane w kodzie źródłowym są kompilowane, testowane i integrowane raz na jakiś czas (raz w tygodniu lub rzadziej), wykorzystanie podejścia ciągłej integracji zapewnia wiele korzyści:

  • wcześniejsze wykrywanie problemów kompilacji/integracji, dzięki czemu są naprawiane w sposób ciągły, a nie w ostatniej chwili, tuż zanim główna wersja oprogramowania zostanie przygotowana do wydania, 
  • zespół otrzymuje natychmiastową informację zwrotną na temat jakości, funkcjonalności oraz wpływu pisanego kodu na cały system, 
  • w ramach tego procesu wykonywane są zautomatyzowane testy. Dzięki nim możliwa jest natychmiastowa kontrola poprawności architektury projektu i jakości kodu – są one wykonywane w ramach procesów kompilacji, budowania i wdrażania zmian (deployment) na różne środowiska, 
  • wczesny dostęp do aktualnej wersji oprogramowania (przed oficjalnym wydaniem) do celów testowych lub demonstracyjnych. 

Kluczowe praktyki w procesie ciągłej integracji

Optymalne korzyści z procesu ciągłej integracji można uzyskać, gdy przestrzega się ustalonych w zespole procedur, co wymaga dyscypliny, jak wspomniałem wcześniej. Istnieją też sprawdzone, dobre praktyki, których przestrzeganie znacząco zwiększy sukces we wdrożeniu tego procesu. Składają się na nie poniższe aspekty: 

  1. Częsta integracja kodu – najważniejsza praktyka w ciągłej integracji. Kod powinno się integrować przynajmniej kilka razy dziennie. Regularne wysyłanie zmian niesie ze sobą inne korzyści – zmiany są przesyłane w mniejszych pakietach, co zmniejsza prawdopodobieństwo błędu w kompilacji, a także ułatwia poszukiwanie ewentualnych zmian (mniejsza liczba plików do przeszukiwania). To minimalizuje też prawdopodobieństwo wystąpienia konfliktów z kodem innych programistów w przypadku pracy wymagającej zmiany w wielu plikach. W przypadku awarii lokalnej stacji roboczej, która spowodowałyby utratę lokalnych zmian w kodzie – programista nie musi pisać wszystkiego od początku, lecz może zacząć od etapu zmian wprowadzonych w ostatniej integracji.
  2. Zarządzanie obszarem roboczym – ważne jest, aby środowisko programistyczne było starannie zarządzane – przede wszystkim zwiększa to produktywność. Deweloperzy powinni zawsze rozpoczynać nową pracę od aktualnej, działającej wersji kodu z głównej gałęzi repozytorium. Powinni móc uruchomić kompilację, testy i być w stanie wdrożyć aplikację w swoim lokalnym środowisku. Uruchamianie aplikacji w środowisku lokalnym powinno wykorzystywać te same zautomatyzowane procesy, co w serwerach ciągłej integracji.
  3. Kompleksowy zestaw testów automatycznych – konieczne jest posiadanie zestawu testów automatycznych, aby mieć pewność, że aplikacja działa poprawnie. Najważniejsze z punktu widzenia ciągłej integracji są testy: jednostkowe, integracyjne oraz akceptacyjne. Ich kombinacja powinna zapewnić bardzo wysoki poziom pewności co do tego, że żadna z wprowadzonych zmian nie naruszyła stabilności aplikacji.   
  4. Krótki proces budowania i testowania projektu – kompilacja, budowanie i wykonanie testów nie powinny trwać dłużej niż 5-10 minut. Jeśli z jakiegoś powodu proces ten zajmuje zbyt dużo czasu, mogą wystąpić problemy takie jak: 
    • niecierpliwość programistów – mogą oni przestać wykonywać pełną procedurę (na przykład pomijać etap testów), co będzie powodować więcej błędów kompilacji na serwerze ciągłej integracji. 
    • proces ciągłej integracji może trwać tak długo, że do czasu ponownego uruchomienia kompilacji może zostać zintegrowanych wiele zmian – znalezienie tej, która powoduje błędy, może być bardzo czasochłonne. 
    • rzadsza integracja zmian przez programistów z powodu czasochłonności procesu – czekanie kilku godzin na zakończenie kompilacji i testów jest stratą czasu i zasobów. 

Continuous Delivery (CD) 

Ciągła integracja stanowi podstawę solidnego, nowoczesnego wytwarzania oprogramowania. Jest to czynnik umożliwiający wdrożenie jeszcze bardziej zaawansowanych technik. W ciągłej integracji, gdy wszystkie testy zakończą się pomyślnie, zyskujemy pewność, że oprogramowanie działa. Jednakże – w jaki sposób upewnić się, że oprogramowanie będzie działało w środowisku docelowym? Czy będzie współpracowało z innymi aplikacjami? Wreszcie – jak dostarczyć je do użytkowników końcowych? Z pomocą przychodzi podejście ciągłego dostarczania (Continuous Delivery, CD). 

W swej istocie ciągłe dostarczanie polega na minimalizowaniu czasu potrzebnego do wprowadzenia zmian w aplikacji oraz minimalizowaniu ryzyka wystąpienia problemów, które także mogą pojawić się w trakcie tego procesu. Może składać się z kilku faz, takich jak: 

  • Odkrywanie – w której analizuje się pomysły, ogólne działanie, funkcjonalności – aby uzyskać szersze zrozumienie przestrzeni problemu i zastanowić się nad potencjalnymi rozwiązaniami. 
  • Definiowanie – gdzie formalne kryteria akceptacji i prace projektowe stanowią bazę do dalszych prac rozwojowych. 
  • Rozwój – w której produkt jest budowany i testowany funkcjonalnie. 
  • Akceptacja – w tej fazie przeprowadzane są testy akceptacyjne wysokiego poziomu i uzyskuje się potwierdzenia od akcjonariuszy. 
  • Wdrożenie – zmiana jest wprowadzana na produkcję. 
  • Weryfikacja – w ramach której, na podstawie wszelkich danych, analizuje się zmiany, aby upewnić się, że osiągnęły one zamierzony wpływ. 

Każda faza stanowi swego rodzaju listę kontrolną określającą, co jest potrzebne, aby zmiany szły naprzód. Czasem przywołane fazy wysokiego poziomu są podzielone na bardziej szczegółowe. Na przykład, faza rozwoju może mieć wyszczególnione podfazy:  

  • architektury technicznej,  
  • budowania lub kompilacji,  
  • i przeglądu kodu.  

Niezależnie od złożoności poszczególnych faz, zadania związane z pracą nad nimi mogą być podejmowane przez wiele osób o różnych umiejętnościach. Zadania mogą być więc wykonywane równolegle, lecz powinny zachować niezależność. 

Continuous Delivery automatyzuje proces wdrażania oprogramowania na różne środowiska w projekcie. Niektóre z nich można wykorzystać do przeprowadzania różnych testów automatycznych, takich jak:  

  • testy integracyjne,  
  • testy akceptacyjne,  
  • czy też testy niefunkcjonalne, jak na przykład testy wydajnościowe lub penetracyjne.  

Nie należy również zapominać o przeprowadzeniu testów manualnych, które mogą wykryć nowe klasy defektów, których automatyczne testy zwykle nie są w stanie wychwycić. 

Korzyści ciągłego dostarczania (CD) 

Wdrożenie podejścia ciągłego dostarczania niesie wiele korzyści, do których należą:

#1. Oszczędność czasu

W średnich i dużych firmach programy oraz ich infrastruktura są zwykle tworzone i obsługiwane przez wiele oddzielnych zespołów. Każde wdrożenie nowej wersji aplikacji musi być skoordynowane między nimi. Należy ustalić między zespołami datę wprowadzenia nowej zmiany, takiej, która obu zespołom będzie odpowiadała.

Ponadto należy rozpowszechnić informację na temat nowej wersji (na przykład: jakie obszary aplikacji zostaną poddane zmianom, czy jest wymagana nowa konfiguracja, zmiany w integracji itp.). Dodatkowo zespoły muszą udostępnić niezbędne pliki, i tak dalej – procedury mogą się różnić w zależności od stopnia formalności i dojrzałości procesów w organizacji.

Generalnie wszystkie niezbędne działania z pewnością będą trwały kilka godzin, czasem nawet kilka dni. Często procesowi temu towarzyszą również przestoje spowodowane różnymi czynnikami. A ponieważ przestojów należy unikać w godzinach pracy, zdarza się, że wdrożenia odbywają się w nocy lub w weekend. Może to działać demotywująco na zespoły operacyjne odpowiedzialne za wdrożenie, a stąd już tylko krok do popełniania błędów. Wprowadzenie ciągłego wdrażania poprzez automatyzację zadań może zaoszczędzić wiele czasu i skrócić proces z kilku dni do nawet kilku minut oraz oszczędzić nerwów ludziom z zespołów operacyjnych. Dobrze skonfigurowane narzędzie pozwoli na praktycznie bezobsługowe wdrożenie, koordynowane przez jedną lub kilka osób bez potrzeby angażowania całych zespołów.

#2. Krótsze cykle wdrożeń

Firmy wykonujące wydania i wdrożenia manualnie robią to zwykle raz w tygodniu lub nawet rzadziej. Rzadkie wydania doprowadzają do przedłużających się procesów rozwoju, a w ostateczności spowolnienia czasu wprowadzenia produktu na rynek. Jeżeli na przykład oprogramowanie jest aktualizowane raz na kwartał, wprowadzenie nowej, małej funkcji lub istotnej poprawki może niepotrzebnie rozciągnąć się w czasie i spowodować odpływ klientów. Na przykład właściciel sklepu internetowego, w którym pewien etap procesu zakupowego został źle zaprojektowany (co powoduje trudności w zakupach) będzie musiał czekać 3 miesiące, zanim poprawka zostanie wdrożona. W tym czasie sklep może stracić ogromną liczbę klientów, a co za tym idzie – realne pieniądze. Automatyczne wdrażanie może całkowicie wyeliminować ten rodzaj problemów.

#3. Krótszy czas otrzymania informacji zwrotnej (feedback)

Najlepszym sposobem na uzyskanie wartościowych opinii na temat produktu jest wdrożenie go na środowisko produkcyjne, gdzie będzie ono używane przez prawdziwych użytkowników. Dzięki temu można mierzyć ich zaangażowanie w różnych częściach systemu. Użytkownicy często wyrażają również opinie na temat aplikacji i sugerują zmiany.

Niestety, zadanie to nie jest takie proste, gdy tworzy się na przykład narzędzia do użytku wewnętrznego w firmie. Można wtedy poprosić kilka osób, aby przetestowały je na jednym ze środowisk testowych, lecz to rodzi wyzwania. Potrzeba na to wykorzystania ich roboczogodzin, środowisko testowe musi być w pełni przygotowane i skonfigurowane z wszystkimi niezbędnymi danymi, które na końcu i tak zostaną usunięte. Jest to proces czasochłonny i skomplikowany.

Przy ręcznych wydaniach, co często idzie w parze z nieczęstym okresem wdrażania, cykl otrzymania feedbacku jest bardzo powolny, co jest sprzeczne z całą ideą zwinnego procesu wytwarzania oprogramowania. Komunikacja między ludźmi również nie zawsze jest idealna – praca w grupie to zawsze ryzyko nieporozumień. Zdarza się, że pierwsza implementacja funkcjonalności nie spełnia pierwotnych oczekiwań. W takiej sytuacji dyskusje (a więc informacja zwrotna) są nieuniknione. Powolne cykle wdrożeniowe prowadzą zatem do spowolnienia rozwoju aplikacji, frustrując nie tylko zespół, ale także interesariuszy. Użytkownicy docelowi, zdając sobie sprawę z czasu trwania wdrażania kolejnych aktualizacji, mogą nawet nie prosić o poprawki lub nie sugerować różnych udogodnień – prędzej przeniosą się do aplikacji konkurencyjnej, której autorzy lepiej dbają o ten aspekt. Na dłuższą metę powolne cykle wdrożeniowe przekładają się na utratę jakości aplikacji i odpływ użytkowników.

#4. Niezawodność wydań

Manualne wdrażanie zazwyczaj nie odbywa się często. To oznacza, że każde wydanie zawiera dużo zmian, co z kolei zwiększa ryzyko niepowodzenia. Gdy duże aktualizacje są źródłem zbyt wielu problemów, inżynierowie i managerowie szukają sposobów na poprawę stabilności kolejnego wydania, niejednokrotnie poprzez tworzenie kolejnych procedur i list kontrolnych.

Tworzy to błędne koło, ponieważ więcej procesów wymaga włożenia jeszcze większego wysiłku w kolejne wydania, a co za tym idzie – więcej czasu. Sprawia to, że cykle wydawania oprogramowania są jeszcze dłuższe, co z kolei rodzi potrzebę zmiany procedur, aby… ten czas skrócić. Sposobem na wyjście z tego błędnego koła jest automatyzacja etapów lub nawet całego procesu wydawania. Gdy więc proces ten stanie się niezawodny i szybszy, nic nie będzie stało na przeszkodzie, aby zwiększyć liczbę wydań, które jednorazowo będą zawierały mniejsze zmiany. Czas zaoszczędzony dzięki automatyzacji uwalnia zasoby, które można wykorzystać, aby jeszcze bardziej usprawnić zautomatyzowany proces wydawania.

#5. Mniejsze pakiety ułatwiają znalezienie źródła problemu

Kiedy wdrożenie spowoduje błąd w oprogramowaniu (a zawierało ono tylko jedną lub dwie nowe funkcje lub same poprawki błędów), dość łatwo można ustalić, która ze zmian stanowi źródło błędu. Duże pakiety, gdzie na jedno wydanie przypada wiele zmian w różnych częściach aplikacji, utrudniają znalezienie źródła problemów i wydłużają czas poszukiwań oraz naprawy. Ta, ze względu na wiele zmian zawartych w wydaniu (poziom skomplikowania pakietu przez różne zależności) trwa dłużej.

#6. Swoboda architektoniczna

Obecne trendy w inżynierii oprogramowania odchodzą od monolitycznych aplikacji na rzecz systemów rozproszonych, składających się z małych komponentów (mikroserwisów). Mniejsze aplikacje lub usługi są łatwiejsze w utrzymaniu, a wymagania dotyczące skalowalności wymuszają, aby działały na różnych urządzeniach, często na wielu jednocześnie. Ręczne wydawanie poszczególnych mikroserwisów, w przypadku gdy są ich setki, wydaje się być niemożliwe do przeprowadzenia w sposób bezawaryjny w satysfakcjonującym czasie. Automatyzacja wdrożeń pozwala konfigurować wydawanie aplikacji w sposób optymalny, tak, aby zapewnić niezawodność działania oprogramowania.

#7. Zaawansowane techniki zapewnienia jakości

Wyobraźmy sobie wyszukiwarkę wycieczek, której właściciel chce ulepszyć algorytm wyszukiwania. Można wdrożyć zarówno starą, jak i nową wersję silnika jednocześnie i uruchamiać zapytania na obu. Definiując przy tym odpowiednie metryki, można w prosty sposób oceniać działanie nowego algorytmu. Dzięki nim, w przypadku, gdy stary silnik będzie zwracał na przykład lepsze wyniki na niektóre zapytania – można wykorzystać te dane w celu ulepszenia nowego. Automatyczne wdrażanie nie jest samo w sobie gwarantem zapewnienia jakości, ale stanowi bazę do zastosowania zaawansowanych technik w celu jego osiągnięcia.

Automatyzacja vs. manualne wdrażanie

Główne praktyki stosowane w obu podejściach (Continuous Integration i Continuous Deployment) są jednakowe. Różnicę stanowi etap zastosowania automatyzacji wdrożenia do środowiska produkcyjnego, co obrazuje poniższy rysunek.  

CI/CD diagram pokazujący proces automatyczny i proces manualny

Jak już wspomniałem, celem podejścia Continuous Delivery jest automatyzacja każdego etapu cyklu wdrażania do ostatniego środowiska przedprodukcyjnego, dzięki czemu zmiany można wprowadzić na docelowe środowisko w dowolnym momencie. W Continuous Deployment zmiany są dostarczane bezpośrednio na środowisko produkcyjne. Różnica to sposób wdrażania – automatyczny lub manualny.

Praktyka ciągłego wdrażania jest skomplikowana, procesy wykorzystywane w ciągłej integracji nie są w stanie pokryć wszystkich niezbędnych etapów koniecznych do jej poprawnego zaimplementowania w projekcie. Niezbędne jest użycie dodatkowych narzędzi, pozwalających na testowanie i kontrolowanie różnych części systemu (na przykład wydajności, gotowości do działania, weryfikacja poprawności integracji itp.).

Podsumowanie

Dziś bez praktyk CI/CD trudno wyobrazić sobie jakikolwiek nowoczesny projekt IT. Automatyzacja procesów ciągłej integracji i ciągłego dostarczania pozwala nam skrócić czas wdrażania zmian, wydawać oprogramowanie częściej i w sposób niezawodny. Dodatkowo szybciej uzyskujemy feedback od użytkowników. Już teraz zachęcam was do lektury drugiej części artykułu. Przyjrzę się w nim bliżej procesowi dostarczania oprogramowania z wykorzystaniem CI/CD i omówię konkretne praktyki testerskie wraz z przeglądem najlepszych narzędzi CI/CD.

TOPICS