Poniższa scenka z familoka jest satyrą. Problemy techniczne, które opisuje, mogą przytrafić się całkiem serio.
U programisty na laptopie testy świecą na zielono. W fikcyjnym familoku ktoś już nastawia wodę na kawę, bo robota skończona. Chwilę później automat sprawdzający ten sam kod pokazuje czerwony krzyżyk. Sąsiad z góry pyta, czy komputer w chmurze jest zepsuty. Niekoniecznie. Może po prostu przyszedł na szychtę bez narzędzi, które od dawna leżą w lokalnej szufladzie.
CI, czyli ciągła integracja, to sposób automatycznego sprawdzania zmian w kodzie. Po określonym zdarzeniu, na przykład wysłaniu zmian do repozytorium, uruchamia się zestaw zadań: instalacja zależności, budowanie programu, testy. Taki ciąg kroków często nazywamy pipeline'em. Zielony wynik mówi, że przeszły konkretne kontrole, nie że program jest wolny od wszystkich błędów. Czerwony zaś jest wskazówką do sprawdzenia, a nie osobistą obrazą.
Na laptopie jest szuflada, na runnerze niekoniecznie
W GitHub Actions zadania wykonuje runner, czyli maszyna albo środowisko uruchomieniowe. GitHub udostępnia własne runnery, można też utrzymywać swoje. Większość runnerów hostowanych przez GitHub dostaje nową maszynę wirtualną; dokumentacja wskazuje wyjątek dla runnerów z jednym CPU, które działają w kontenerze na współdzielonej maszynie. Nie należy więc zakładać, że kolejne zadanie zastanie stan poprzedniego.
Laptop tymczasem pamięta więcej niż jego właściciel. Ma ręcznie doinstalowane narzędzie, katalog z wcześniejszej budowy albo plik konfiguracyjny, którego nie ma w repozytorium. Przez miesiące wszystko działa, bo lokalny warsztat został po cichu wyposażony. Świeże środowisko zadaje niewygodne pytanie: co właściwie jest potrzebne, żeby ten projekt ruszył?
Na początek warto porównać system operacyjny, wersję środowiska uruchomieniowego i polecenia wykonywane w obu miejscach. Nawet różnica wielkości liter w nazwie pliku może ujawnić się inaczej na różnych systemach plików. GitHub opisuje wyposażenie konkretnego obrazu runnera w logu: w sekcji „Set up job”, pod „Runner Image”, znajdziemy odnośnik „Included Software”. To lepszy punkt wyjścia niż zakładanie, że obraz zawsze zawiera tę samą wersję narzędzia.
Zależności nie przychodzą na słowo honoru
Program rzadko pracuje sam. Korzysta z bibliotek, a te potrafią potrzebować kolejnych bibliotek. Jeżeli lokalnie mamy już katalog z paczkami, test może korzystać z tego, co zostało tam wcześniej. Na CI instalacja od zera może odsłonić brak deklaracji albo niespójny plik blokady wersji, tak zwany lockfile.
W projektach npm jednym ze sposobów na bardziej powtarzalną instalację jest npm ci. Polecenie wymaga istniejącego package-lock.json albo npm-shrinkwrap.json; gdy plik blokady nie zgadza się z package.json, kończy się błędem zamiast go aktualizować. Usuwa też istniejący node_modules przed instalacją. Dlatego przed odpaleniem go lokalnie trzeba pamiętać, że katalog zostanie odtworzony, a projekt musi mieć poprawny plik blokady.
Dobrze jest przechowywać plik blokady w repozytorium i uzgodnić wersję Node.js oraz npm używaną przez zespół i CI. Jeśli projekt wymaga szczególnych ustawień instalacji, trzeba je świadomie udokumentować i stosować spójnie. Sam lockfile nie rozwiąże każdej różnicy między systemami ani awarii sieci. Jest jednak porządnym spisem części, a nie kartką z napisem „jakoś to skręcimy”.
Zmienna była, ale tylko u sąsiada
Inny klasyk to zmienna środowiskowa: nazwana wartość, z której program odczytuje konfigurację. Lokalnie może wskazywać adres testowej usługi albo tryb pracy aplikacji. Jeśli istnieje tylko w ustawieniach powłoki programisty, runner sam jej nie odgadnie. Równie dobrze sąsiad mógłby zgadywać, gdzie schowano klucz do piwnicy.
W GitHub Actions niesekretne wartości można określić w env dla całego workflow, pojedynczego zadania albo konkretnego kroku. Zakres ma znaczenie: wartość dostępna w jednym kroku nie musi być widoczna w innym. Dochodzi składnia powłoki: dokumentacja pokazuje $NAME dla bash oraz $env:NAME dla PowerShella. Zanim uznamy zmienną za zaginioną, sprawdźmy jej nazwę, zakres i powłokę.
Dane poufne wymagają osobnego traktowania. Nie wpisujmy haseł do repozytorium ani nie wypisujmy ich w logach w ramach diagnostyki. Korzystajmy z mechanizmu sekretów dostawcy CI, ograniczając dostęp do niezbędnych zadań. Do sprawdzenia konfiguracji zwykle wystarczy bezpiecznie ustalić, czy wymagana wartość jest dostępna, bez ujawniania jej treści. Nie obchodźmy też zabezpieczeń tylko po to, żeby kontrolka zrobiła się zielona.
Pierwszy czerwony krok, nie ostatni okrzyk
Gdy pipeline padnie, otwórz logi i znajdź pierwszy krok, który zgłosił błąd. Końcowy komunikat może informować jedynie, że cała robota się nie udała; wcześniejszy bywa znacznie bardziej konkretny. Sprawdź, czy zawiodła instalacja, kompilacja, sam test czy dostęp do usługi. Następnie porównaj to samo polecenie uruchomione lokalnie, najlepiej w czystym środowisku z właściwymi wersjami narzędzi.
Nie każda czerwień oznacza rozjazd konfiguracji. Test mógł wykryć prawdziwy błąd w kodzie, usługa testowa mogła być niedostępna, a test zależny od czasu mógł zachować się niestabilnie. Ponowne uruchomienie bywa użyteczną wskazówką, ale bez poznania przyczyny nie jest naprawą. Jeśli problem wraca, zapisz warunki, w których występuje, i dodaj sprawdzenie, które pozwoli go wcześnie zauważyć.
Na koniec zostawmy po sobie instrukcję uruchomienia projektu: wersje narzędzi, sposób instalacji, wymagane niesekretne zmienne i komendy testowe. Niech nowa osoba z zespołu umie odtworzyć warsztat bez pożyczania cudzego laptopa. Wtedy familokowy pipeline może dalej czasem ponarzekać, ale przynajmniej wiadomo, gdzie szukać jego przepustki.
Dokumentacja GitHub Actions: https://docs.github.com/en/actions/concepts/runners/github-hosted-runners oraz https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-variables.
