Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Zaawansowane CI/CD - matrix builds, artifacts i cache

Testy przechodzą u Ciebie. U kolegi nie - bo ma inną wersję Node'a. Na serwerze też nie, bo ktoś zapomniał przebudować obrazu. Każda z tych trzech sytuacji kosztuje pół dnia, a wszystkie mają wspólną przyczynę: sprawdzenie zależy od tego, kto i gdzie je uruchomił.

Continuous Integration usuwa tę zależność: to automatyczne budowanie i testowanie kodu po każdym push. Nie ręczne wdrażanie raz w tygodniu, nie monitorowanie pamięci i nie kopie zapasowe bazy - tylko jedno sprawdzenie, zawsze tak samo, na maszynie, której nikt nie skonfigurował po swojemu.

Kiedy uruchomić

Workflow zaczyna się od wskazania zdarzeń, które go budzą:

1name: CI
2
3on:
4  push:
5    branches: [main]
6  pull_request:

on: pull_request
uruchamia workflow przy tworzeniu pull requesta - i to jest ten moment, o który chodzi najbardziej, bo sprawdzenie odbywa się przed wejściem zmiany do gałęzi głównej.

on: push
reaguje na wypchnięcie kodu; oba stosuje się zwykle razem. Istnieją też inne wyzwalacze, ale robią co innego:
on: schedule
uruchamia workflow o wyznaczonej porze, a
on: deployment
- przy zdarzeniu wdrożenia. Żaden z nich nie zadziała, gdy ktoś otworzy pull request.

Kolejność zadań

Pipeline dzieli się na joby, a ich kolejność nie jest dowolna:

  1. Lint - sprawdzenie formatowania.
  2. Build - kompilacja TypeScriptu.
  3. Test - testy jednostkowe.
  4. Docker build & push - zbudowanie i wysłanie obrazu.

Reguła porządkująca brzmi: najtańsze i najszybsze najpierw. Lint kończy się w kilka sekund, więc nie ma sensu czekać pięciu minut na testy, żeby dowiedzieć się o brakującym średniku. Kompilacja poprzedza testy, bo test nieskompilowanego kodu i tak nie ruszy. Obraz Dockera budujemy na końcu - to najdroższy krok i nie ma po co go zaczynać, jeśli cokolwiek wcześniej padło.

Krok po kroku

Każdy job składa się z kroków, a pierwszym jest zawsze pobranie kodu:

1jobs:
2  test:
3    runs-on: ubuntu-latest
4    steps:
5      - name: Checkout kodu
6        uses: actions/checkout@v4
7
8      - name: Setup Node
9        uses: actions/setup-node@v4
10        with:
11          node-version: 20
12
13      - name: Instalacja zaleznosci
14        run: yarn install --frozen-lockfile
15
16      - name: Testy
17        run: yarn test --coverage

Budowa kroku jest stała:

- name: Checkout kodu
, potem
uses:
, dalej
actions/checkout@v4
.
name
to opis widoczny w interfejsie,
uses
wskazuje gotową akcję, a
run
- polecenie do wykonania w powłoce.

Kolejność w jobie testowym też jest niezmienna:

actions/checkout@v4
pobiera kod,
actions/setup-node@v4
instaluje Node'a,
yarn install --frozen-lockfile
wciąga zależności, a na końcu
yarn test --coverage
uruchamia testy.

Flaga

--frozen-lockfile
zasługuje na uwagę: każe zainstalować dokładnie wersje z pliku blokady i przerwać, gdyby się nie zgadzały. Bez niej pipeline mógłby cicho zainstalować nowszą wersję zależności i przetestować coś innego, niż uruchomisz na produkcji.

Sekrety

Pipeline potrzebuje haseł - do rejestru obrazów, do serwera, do bazy testowej. Przechowujemy je w Settings > Secrets and variables > Actions, w ustawieniach repozytorium.

Nie w pliku

.env
w repozytorium, nie w komentarzu do commita, nie w
README.md
- wszystkie te miejsca są publiczne dla każdego, kto ma dostęp do kodu, a historii Gita nie da się tak po prostu wyczyścić.

Zapisany sekret wstawiasz do workflow przez odwołanie, nigdy wprost:

1      - name: Logowanie do rejestru
2        run: docker login -u ${{ secrets.REGISTRY_USER }} -p ${{ secrets.REGISTRY_TOKEN }}

Wartość zostanie podstawiona przy uruchomieniu, a w logach pojawi się w postaci zamaskowanej.

Trzy usprawnienia

Na koniec trzy rzeczy, które odróżniają pipeline działający od pipeline'u, na który da się czekać.

Matrix uruchamia ten sam job na kilku konfiguracjach naraz:

1    strategy:
2      matrix:
3        node-version: [18, 20, 22]

Trzy wersje Node'a, trzy równoległe przebiegi, jeden opis. To odpowiedź na problem z początku lekcji - „u kolegi nie działa" wychodzi teraz przy pull requeście, a nie po tygodniu.

Cache zapamiętuje pobrane zależności między przebiegami. Bez niego każdy przebieg ściąga te same paczki od nowa; z nim instalacja skraca się z minut do sekund.

Artifacts to pliki, które job zostawia po sobie - raport pokrycia, zbudowana paczka, zrzuty ekranu z testów. Przeżywają zakończenie przebiegu i można je pobrać z interfejsu, co bywa jedynym sposobem, by zrozumieć, dlaczego coś padło tylko na maszynie CI, @name.

Podsumowanie

Sprawdzenie przestało zależeć od tego, kto je uruchamia:

  • CI to automatyczne budowanie i testowanie kodu po każdym push - nie ręczne wdrażanie, nie monitoring pamięci, nie kopie zapasowe,
  • on: pull_request
    uruchamia workflow przy tworzeniu pull requesta
    ;
    on: schedule
    działa o wyznaczonej porze,
    on: deployment
    przy wdrożeniu,
  • kolejność jobów: Lint → Build → Test → Docker build & push, według zasady „najtańsze najpierw",
  • budowa kroku:
    - name:
    , potem
    uses:
    , potem nazwa akcji;
    run:
    wykonuje polecenie powłoki,
  • job testowy w kolejności:
    actions/checkout@v4
    ,
    actions/setup-node@v4
    ,
    yarn install --frozen-lockfile
    ,
    yarn test --coverage
    ,
  • --frozen-lockfile
    instaluje dokładnie wersje z pliku blokady i przerywa przy niezgodności,
  • sekrety trzymamy w Settings > Secrets and variables > Actions - nie w
    .env
    , nie w komentarzu, nie w
    README.md
    ,
  • matrix uruchamia job na kilku konfiguracjach równolegle, cache skraca instalację zależności, artifacts zachowują pliki po zakończeniu przebiegu.

W następnej lekcji zejdziemy z poziomu liczb do pojedynczego żądania - poznasz distributed tracing. A na razie zapamiętaj: pipeline to jedno sprawdzenie, wykonywane zawsze tak samo - a matrix sprawia, że „u mnie działa" przestaje być argumentem.

Przejdź do CodeWorlds