Poznajemy GitHub Actions w praktyce

Poznajemy GitHub Actions w praktyce

Obecnie na rynku występuje wiele dobrych narzędzi do automatycznej integracji, budowy i wdrażania naszego kodu. W dzisiejszym wpisie chciałbym skupić się na GitHub Actions. Pokażę w praktyce jedno z najlepszych narzędzi do CI/CD, które w najbliższych latach będzie tylko umacniało swoją pozycję na rynku. Posiadając wiedzę o tym narzędziu będziecie mogli w łatwiejszy, szybszy i bardziej elastyczny sposób wspomagać wasze codzienne obowiązki DevOpsa. Serdecznie zapraszam do materaiłu.

Słów kilka o GitHub Actions

Serwisu GitHub chyba nie trzeba nikomu przedstawiać. Każdy kto pracował z kodem wie, że jest to dominujący serwis służący do kontroli naszego kodu. Posiada on dziesiątki milionów aktywnych użytkowników i repozytoriów. Pod koniec 2018 roku Microsoft sfinalizował zakup GitHuba i w tym samym roku oddano użytkownikom nową usługę, a mianowicie GitHub Actions.

GitHub Actions to proste, ale bardzo potężne narzędzie do automatyzacji. Jest porównywalne do innych narzędzi dostępnych w internecie. Zasada działania jest następująca: do naszego repozytorium dodajemy w odpowiednim folderze plik yaml, który jest recepturą zawierającą instrukcję do wykonania na zdalnym serwerze. Podobne rozwiązania widzieliśmy już w innych narzędziach CI/CD takich jak Azure Pieplines czy Concurse.

To co odróżnia opisywane narzędzie od pozostałych są tzw. „Akcje„, o których więcej dowiecie się z dalszej części artykułu. GitHub Actions jest zdecydowanie jednym z przyjemniejszych narzędzi, z których korzystałem z racji tego, że mamy pełną integrację z naszym repozytorium kodu. W jednym miejscu mamy dostęp do logów z budowy, obsługę sekretów oraz czytelne statusy wykonywanych zadań. Dodatkowo wszystkie te elementy są dostępne w naszym panelu GitHub co ułatwia współpracę z innymi użytkownikami i zarządzanie uprawnieniami.

Github Acions: odrobina teorii i słowa kluczowe

Pierwszym ważnym słówkiem jest workflows. Jest to właśnie nasz plik yaml receptura, grupa instrukcji, która ma zostać wykonana na wirtualnych maszynach (VM). Mogą to być np. zadania związane z budową kodu, testowaniem, tworzeniem release-u, wdrożeniem na zdalny serwer itd. itp. W tym pliku znajdują się też inne ważne konfiguracje takie jak rodzaj maszyny, na której te polecenia mają być wykonywane lub informacje co będzie inicjować wykonanie workflow.

GitHub udostępnia kilka rodzajów wirtualnych maszyn takich jak Linux, Windows, MacOs. Pozwala też na wykonywanie poleceń w kontenerach.
Możemy również wykorzystać włąsne maszyny jeśli potrzebujemy mieć pełną władzę nad konfiguracją VM. Jedynym wymogiem jest zainstalowanie na niej agenta GitHub Actions runner. Jest to podobne do dodawania self-hosted agen z Azure Piepline.

Podstawowe słowa kluczowe:
jobs – jest to zbiór zadań
job – pojedyńcze zadania do wykonania na danej VM; w jej skład wchodzą kroki steps
steps – w tym kluczu definiujemy jeden lub więcej kroków do wykonania

Przyjrzyjmy się prostemu przykładowi:

name: GitHub Actions Demo
on: [push]
jobs:
  First-Job:
    runs-on: ubuntu-latest
    steps:
      - name: First Step
        run: echo "Hello Red-DevOps"
      - name: Second Step
        run: |
          pwd
          ls -a

Na samej górze widnieje nazwa naszego workflow GitHub Actions Demo. Następnie mamy klucz on, który określa jaka akcja będzie wyzwalała nasz workflow (potok). Widzimy, że push znajduje się w nawiasach kwadratowych więc możemy w tym miejscu podać listę wyzwalaczy. Możemy też inicjować nasze zadania za pomocą innych metod m.in. push, fork, pull_request.

Dla klucza jobs podaliśmy obiekt First-Job jest to nazwa zadania, poniżej definiujemy na jakiej VM będzie uruchamiane zadanie za pomocą klucza runs-on.

Gdybyśmy chcieli użyć kontenera ten fragment kodu powinien wyglądać jak niżej.

...
jobs:
  First-Job:
    runs-on: ubuntu-latest
    container: node:14.16
    steps:
...

Następnie dla sekcji steps mamy listę step do wykonania. Klucz name dla steps nie jest wymagany, ale przydaje się przy czytaniu statusów na GitHub. Run określa komendy do wykonania w powłoce bash. W naszym wypadku pierwszy krok wyświetla napis „Hello Red-DevOps” , a drugi wykonuje dwa polecenia pwd oraz ls -a.

By uruchomić nasz potok, powyższy plik musimy dodać do repozytorium w folderze .github/workflows/ i wypchnąć zmiany do GitHub.

Na powyższym zdjęciu widzimy second push, który został zaczerpnięty z komentarza wypchniętego commita. Analiza logów wskazuje, że kroki wykonały swoje zadania.
Warto pamiętać, że wszystkie jobs są wykonywane asynchronicznie oraz, że nasz kod repozytorium domyślnie nie jest klonowany do VM. Naturalnie możemy tak skonfigurować workflow by zadania następowały po sobie.

Synchroniczne wykonywanie zadań

Jeśli chcemy by nasze zadania wykonywały się po kolei musimy nieco zmodyfikować kod i użyć klucza needs, który przyjmuje listę zadań do spełnienia przed rozpoczęciem danego task-a.

name: GitHub Actions Demo
on: [push]
jobs:
  First-Job:
    runs-on: ubuntu-latest
    steps:
      - name: First Step
        run: echo "Hello Red-DevOps"
      - name: Second Step
        run: |
          pwd
          ls -a
  Second-Job:
    needs: [First-Job]
    runs-on: ubuntu-latest
    steps:
      - run: echo "Second Job Run !"

Second-Job posiada klucz needs. W nawiasie kwadratowym umieściliśmy First-Job, dzięki czemu zadanie drugie zacznie się wykonywać dopiero po poprawnym zakończeniu działania zadnia pierwszego. Po wypchnięciu powyższego pliku powinniśmy w zakładce action ujrzeć taki rezultat.

Zmienne środowiskowe, warunki, sekrety i debugowanie

W tej sekcji wpisu dowiecie się podstaw dotyczących zmiennych środowiskowych, warunków logicznych jakie możemy umieszczać w workflow, a także jak przekazywać sekrety oraz podstawy debugowania. Oczywiście jeden wpis to zbyt mało by omówić ważniejsze elementy Github Actions. Chcę jednak przybliżyć podstawowe pojęcia gdyż wykorzystamy ich część w ostatnim fragmencie tego wpisu.

W tym artykule nie znajdziecie informacji na temat pojęć takich jak strategie, matrix i wiele innych. Osoby zainteresowane powyższą terminologią zachęcam do przejrzenia dokumentacji.

Zmienne środowiskowe

Github Actions pozwala na tworzenie własnych zmiennych. Zasięg zmiennych środowiskowych dodanych do naszego workflow będzie różny w zależności od miejsca deklaracji. Jeśli zmienna będzie zdefiniowana na poziomie jobs wtedy ta zmienna będzie dostępna dla wszystkich zadań. Z kolei jeśli zdefiniujemy ją na poziomie danego job wówczas dostęp do niej będą miały tylko elementy danego zadania. Analogicznie jeśli zmienna zostanie zadeklarowana dla steps.

name: GitHub Actions Demo
on: [push]
env:
  WF_ENV_GLOBAL: Global env variable
jobs:
  First-Job:
    env:
      WF_ENV_FIRST: First env variable
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "WF_ENV_GLOBAL: ${WF_ENV_GLOBAL}"
          echo "WF_ENV_FIRST: ${WF_ENV_FIRST}"
  Second-Job:
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "WF_ENV_GLOBAL: ${WF_ENV_GLOBAL}"
          echo "WF_ENV_FIRST: ${WF_ENV_FIRST}"
     

Używając składni ${<VAR>} z dolarem i nawiasami klamrowymi dokonujemy ewaluacji zmiennych.

Wynikiem powyższego uruchomienia będą następujące logi.

Widzimy, że drugi job nie ma dostępu do zmiennej z joba pierwszego. Pole pozostało puste.

GitHub domyślnie dostarcza wielu przydatnych zmiennych, które możemy wykorzystać w naszym kodzie. Są to przykładowo GITHUB_WORKSPACE, GITHUB_REPOSITORY, GITHUB_REF_NAME, GITHUB_SHA, GITHUB_ACTOR i wiele więcej. Pełna lista jest dostępna w dokumentacji.

Sekrety

Sekrety w Github Actions dodajemy przechodząc do zakładki Settings naszego repozytorium, a następnie w sekcji Seciurity -> Secrets -> Actions wybieramy przycisk „New repository secret

W celu wstrzyknięcia sekretu do naszego kodu używamy składni ${{ secrets.MY_TEST_SECRET }}

name: GitHub Actions Demo
on: [push]
env:
  MY_TEST_SECRET: ${{ secrets.MY_TEST_SECRET }}
jobs:
  First-Job:
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "MY_TEST_SECRET: ${MY_TEST_SECRET}"
          

Sekret nie będzie widoczny w logach co jest zachowaniem pożądanym. Wiemy jednak jak go wykorzystać gdy będziemy potrzebowali umieścić w kodzie tokeny, hasła lub inne dane wrażliwe.

GitHub udostępnia też inne sekrety, które nie są widoczne w zakładce z sekretami np. secrets.GITHUB_TOKEN, dzięki któremu VM może uzyskać dostęp do naszych repozytoriów.

Warunki wykonywania

GitHub Actions pozwala na dynamiczne sterowanie przepływem potoku za pomocą warunków wykonania. Poniżej widnieje przykład, w którym zadania zostaną wykonane w zależności od gałęzi na którą wypchniemy zmiany.

name: GitHub Actions Demo
on: 
  push:
    branches:       
      - dev
      - master
jobs:
  Master-Job:
    if: ${{ github.ref == 'refs/heads/master' }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "This job run for branch master"
      
  Dev-Job:
    if: ${{ github.ref == 'refs/heads/dev' }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "This job run for branch dev"
      

GitHub Actions wykonał zadanie Dev-Job, które wymagało aktywnego branch-a dev. Analogicznie sytuacja będzie wyglądała po wypchnięciu zmian na branch master.

Debugowanie

W trakcie analizy workflow mogliśmy zauważyć, że poziom logów jest całkiem satysfakcjonujący. Możemy jeszcze bardziej zwiększyć poziom logowania jeśli dodamy zmienną ACTIONS_STEP_DEBUG do secrets i ustawimy jej wartość na true. Na fioletowo zostaną oznaczone dodatkowe linijki logów dostępne po zwiększeniu poziomu szczegółowości.

Czas na Akcje

Wszystko co do tej pory zostało przedstawione jest przejrzyste, proste w obsłudze, intuicyjne lecz znane. Te same funkcjonalności udostępnia min. Azure Pielines. Wspomniałem na początku artykułu, że to właśnie „Akcje” odróżniają narzędzie, któremu poświęcony został dzisiejszy wpis od konkurencji i jest to jego najważniejsza funkcjonalność.

Akcje to zamknięte kawałki kodu, funkcje, moduły, które możemy wykorzystywać podobnie jak moduły w Ansible. Akcje mogą być napisane w języku JavaScript lub uruchamiane w kontenerach. W tym drugim przypadku wydłuża to jednak czas bo potrzebujemy postawić kontener przed wykonaniem kodu zamieszczonego w nim. Każda akcja, w serwisie GitHub posiadająca adres według składni actions/<nazwa_akcji> jest dostępna do użycia. Akcje mogą przyjmować zmienne jak również je zwracać.

By skorzystać z akcji musimy użyć nieco innej składni niż do tej pory. Zamiast słowa kluczowego run używamy uses.

  First-Job:
    runs-on: ubuntu-latest
    steps:
      - name: Clone repository
        uses: actions/checkout@v3

Powyższa akcja pozwala na sklonowanie aktywnego repozytorium i wykonanie git checkout na dany branch. Zauważcie, że po znaku @ występuje v3 odnoszący się do konkretnego tagu. Dzięki takiemu rozwiązaniu ściśle kontrolujemy wykorzystywaną wersję. Możemy po @ wskazać też np. branch lub hash commita. Nie zaleca się wybierania brancha bo w przypadku aktualizacji może to popsuć nasze workflow.

Akcja checkout nie zwraca żadnej wartości jako output, ale pozwala na przekazanie kilku danych wejściowych (input). Gdybyśmy przykładowo chcieli by zawsze przełączać się na branch dev dodalibyśmy następujący fragment kodu:

...
      - name: Clone repository
        uses: actions/checkout@v3
        with:
          ref: dev

Do przekazywania danych do akcji służy słowo with. Wszystkie dostępne wejścia i wyjścia powinny być zawsze opisane w dokumentacji, link do docku checkout.
Ostatnią rzeczą, o której chcę wspomnieć jest GitHub Marketplace. Możemy tam znaleźć ogromną ilość gotowych akcji tworzonych przez użytkowników jak i przez duże organizacje. W połowie 2021 roku GitHub chwalił się, że na marketplace dostępnych jest już ponad 10 000 akcji.

Budujemy CI dla aplikacji Charlenes Coffee Corner

Poznaliśmy jedne z podstawowych możliwości GitHub Actions. Nadszedł czas by wykorzystać nabytą wiedzę w praktyce. Jakiś czas temu musiałem stworzyć prostą aplikację w języku Java, która posiadała testy jednostkowe i budowa pojedynczy plik jar. Posłużę się tym przykładem do zademonstrowania możliwości GitHub Actions.

UWAGA NA ZMIANY W REPOZYTORIACH !!!

Proszę zwrócić uwagę na daty commitów, które znajdziecie we wszystkich repozytoriach dostępnych na https://github.com/red-devops. Często korzystam z wcześniejszego kodu w kolejnych wpisach, dostosowując go do bieżących zadań. W związku z tym może się zdarzyć, że kod prezentowany w tym wpisie będzie różnił się od tego, który znajdziecie w najnowszym commicie repozytorium. Niemniej jednak wszystkie główne commity zostawiam, dlatego proszę porównywać daty publikacji wpisu z datami commitów.

Naszym zadaniem będzie napisanie CI do aplikacji CharlenesCoffeeCorner. Aplikacja ma na celu obliczać należność do zamówienia w barze z kanapkami i kawą. By zbliżyć się do prawidłowego toku budowania aplikacji i przepływu kodu stworzymy następujący schemat widoczny na rysunku poniżej.

Nasze repozytorium kodu będzie posiadało dwa stałe branch-e master i dev. Na oba branch-e nie jesteśmy w stanie bezpośrednio wypychać commitów. W celu aktualizacji tych gałęzi musimy wykonać pull request (PR) najpierw z tymczasowej gałęzi feature na dev, a następnie z dev na master.
PR będzie wyzwalał workflow, który będzie sprawdzać testy jednostkowe. Jeśli testy przejdą użytkownik będzie mógł wykonać merge na branch.
Prawidłowy merge będzie wyzwalał kolejny workflow sprawdzający czy aplikacja prawidłowo się buduje. Dodatkowo w przypadku gałęzi master po prawidłowym zbudowaniu aplikacji plik jar zostanie udostępniony jako artefakt.

Zanim zaczniemy chciałbym dodać, że do budowy wykorzystuję VM windows zamiast linuxa ponieważ podczas tworzenia tego projektu używałem windowsa. Zaś dla VM linux część testów nie przechodzi. Pozwoliłem sobie na ten niewielki fortel ponieważ celem tego wpisu jest nauka GitHub Actions.

...
jobs:
  CharlenesCoffeeCorner-Job:
    runs-on: windows-latest
    steps:
      - name: Clone repository
        uses: actions/checkout@v3
      
      - name: Setup java 8
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '8'
...

Kroki, które są wykonywane dla powyższego zadnia to sklonowanie repozytorium CharlenesCoffeeCorner do VM. Plik yaml znajduje się w repozytorium CharlenesCoffeeCorner daltego domyślnie to właśnie to repo jest klonowane i niema potrzeby podawania go jako daną wejściową do akcji. Następnie za pomocą akcji setup-java instalujemy java w wersji 8.

...
      - name: Maven Test
        if: ${{ github.event_name == 'pull_request' }}
        run: mvn test
...

W zadaniu Maven Test jeśli workflow zostało wyzwolone za pomocą PR będziemy wykonywać komendę mvn test, która sprawdzi testy jednostkowe.

Z kolei jeśli potok został wyzwolony za pomocą push, co będzie miało miejsce przy każdym merge aplikacja zostanie sprawdzona pod kątem budowy za pomocą mvn package.

...
      - name: Maven Package
        if: ${{ github.event_name == 'push' }}
        run: mvn package
...

Ostatnim krokiem, który będzie wykonywany tylko dla gałęzi master jest udostępnienie gotowego pliku jar jako artefaktu.

...
      - name: Upload Artifact
        uses: actions/upload-artifact@v3
        if: ${{ ( github.event_name == 'push' )  &&  ( github.ref == 'refs/heads/master' ) }}
        with:
          name: CharlenesCoffeeCorner-Artifact
          path: target/CharlenesCoffeeCorner-1.0-SNAPSHOT.jar

Gotowy plik yaml prezentuje się następująco:

name: CharlenesCoffeeCorner CI
on: 
  pull_request:
    branches: [dev, master]
  push:
    branches: [dev, master]
jobs:
  CharlenesCoffeeCorner-Job:
    runs-on: windows-latest
    steps:
      - name: Clone repository
        uses: actions/checkout@v3
      
      - name: Setup java 8
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '8'

      - name: Maven Test
        if: ${{ github.event_name == 'pull_request' }}
        run: mvn test 

      - name: Maven Package
        if: ${{ github.event_name == 'push' }}
        run: mvn package

      - name: Upload Artifact
        uses: actions/upload-artifact@v3
        if: ${{ ( github.event_name == 'push' )  &&  ( github.ref == 'refs/heads/master' ) }}
        with:
          name: CharlenesCoffeeCorner-Artifact
          path: target/CharlenesCoffeeCorner-1.0-SNAPSHOT.jar
        

Pozwoliłem sobie pominąć semantyczne wersjonowanie aplikacji i zostawiłem domyślną nazwę dla artefaktu „SNAPSHOT”.

Stwórzmy teraz nowy branch feature/new-price-extra-milk., który będzie zmieniał należność za dodatkowe mleko do kawy. Zmieniamy cenę z 0.3 CHF na 0.8 CHF w klasie Constant oraz pamiętamy o aktualizacji testów jednostkowych.

Wypychamy zmianę do zdalnego repozytorium na nową gałąź feature/new-price-add-milk i wykonujemy PR na branch dev co spowoduje pierwsze wyzwolenie potoku i wykonanie mvn test dla naszego kodu.

Przy panelu pull request pojawił się komunikat „Some checks haven’t completed yet” co oznacza, że w tle wyzwolony został odpowiedni potok.

Testy przechodzą prawidłowo i możemy wykonać merge , co powoduje wykonanie push dla gałęzi dev i wyzwolenie kolejnego workflow, który widać niżej. W tym miejscu muszę dodać, że powinniśmy tak skonfigurować GitHub by przy prawidłowym merge, branch feature był automatycznie usuwany w celu utrzymania porządku. Nie zrobiłem tego dla celów dokumentacyjnych.

Na poniższym obrazie widzimy, że potok wykonał tylko krok Maven Package. Dla osób, które nie znają narzędzia maven, muszę wspomnieć, że polecenie mvn package powoduje wykonanie mvn test oraz buduje plik jar. Nie ma zatem potrzeby podwójnego wykonywania kroku Maven Test oraz Maven Package.

Pozostało nam już tylko przeniesienie zmian z dev na master. Tworzymy PR z gałęzi dev na master, która jest naszym branch-em realese-owym. Wykonuje się krok Maven Test, następnie łączymy zmiany.

To pozwala na wyzwolenie ostatniej części workflow. Zbudowanie aplikacji oraz udostępnienie jest w formie pliku jar.

Po zakończeniu workflow w szczegółach wykonywania powinniśmy zobaczyć artefakt gotowy do pobrania. Możliwe, że w chwili gdy przeglądacie owo repozytorium tego artefaktu tam nie będzie. Jest to spowodowane tym, że w bezpłatnym planie nasze artefakty są przechowywane w GitHub tylko przez 90 dni.

Tak przygotowany artefakt możemy pobrać, rozpakować i sprawdzić czy zmiany zostały wprowadzone.

Jar-ka zawiera zmiany, które przed chwilą znajdowały sie w branchu feature/new-price-add-milk. W ten sposób pokazałem jak łatwo możemy stworzyć w Github Actions CI dla prostej aplikacji.

Mam nadzieję, że wpis nie okaże się zbyt długi gdyż faktem jest, że zagadnienia i elementy, które znalazły w nim swoje miejsce stanowią ułamek możliwości jakie daje nam opisywane narzędzie. Jeśli macie ochotę poznać pełne jego możliwości odsyłam osoby zainteresowane do dokumentacji. Gorąco też zachęcam was do używania go w projektach, nad którymi pracujecie.

Comments are closed.