Pierwszy pipeline CI CD: zbuduj automatyzację testów i wdrożeń w GitHub Actions krok po kroku

0
53
4/5 - (1 vote)

Nawigacja:

Po co początkującemu pierwszy pipeline CI/CD

Prosty obraz CI/CD na przykładzie pracy grupowej

Wyobraź sobie mały zespół pracujący nad jedną aplikacją. Każdy robi zmiany na swoim komputerze. Jeden programista aktualizuje bibliotekę, drugi poprawia widok, trzeci dopisuje nową funkcję. Każdy testuje „po swojemu”, gdy ma czas. Na końcu ktoś zbiera zmiany, wypycha na serwer i modli się, żeby wszystko działało.

CI (Continuous Integration) to automatyczne sprawdzenie, czy te zmiany w ogóle do siebie pasują. Za każdym razem, gdy ktoś wypchnie kod do repozytorium, pipeline CI odpala testy, sprawdza build i daje szybki feedback: „zielono, możesz iść dalej” albo „czerwono, coś zepsułeś, napraw zanim to połączymy”.

CD (Continuous Delivery / Continuous Deployment) to etap, w którym poprawnie zbudowany i przetestowany kod trafia na środowisko testowe, staging albo produkcję – w zautomatyzowany, powtarzalny sposób. Koniec z kopiowaniem plików przez FTP, ręcznym wklepywaniem komend na serwerze i zgadywaniem, co zostało wdrożone.

Pierwszy pipeline CI/CD w GitHub Actions ma jeden cel: zautomatyzować minimum, które usuwa największy chaos – automatyczne testy i proste wdrożenie, bez wielkiej architektury DevOps.

Typowe problemy, które rozwiązuje pipeline CI/CD

Bez pipeline’u projekty szybko zamieniają się w serię niespodzianek. Typowe z nich:

  • „U mnie działa” – na Twoim komputerze wszystko przechodzi, na komputerze kolegi już nie, na serwerze testowym aplikacja się wywala.
  • Testy odpalane od święta – wszyscy wiedzą, że testy są, ale mało kto je puszcza przed commitem. Szkoda czasu, a i tak „przecież działa”.
  • Chaotyczne wdrożenia – brak stałego procesu publikacji. Raz ktoś deployuje z lokalnej maszyny, innym razem z serwera CI, czasem przez FTP.
  • Brak informacji, co faktycznie jest na serwerze – trudno odpowiedzieć, która wersja kodu znajduje się aktualnie na stagingu czy produkcji.

Pipeline CI/CD w GitHub Actions rozwiązuje te problemy, bo:

  • testy odpalają się automatycznie przy każdym push lub pull_request,
  • build i deploy są opisane w jednym, wersjonowanym pliku (workflow YAML),
  • proces wdrożeniowy jest powtarzalny i przewidywalny,
  • logi z każdego uruchomienia są dostępne w jednym miejscu – zakładce Actions.

CI i CD kontra „git push na produkcję”

Wiele małych zespołów przez długi czas robi „CD” tak: lokalnie pull, szybki test w przeglądarce, a potem ssh na serwer i git pull origin main. Albo zero SSH, za to wypychanie przez FTP. Technicznie kod trafia na serwer, ale:

  • nie ma automatycznych testów przed wdrożeniem,
  • nie ma śledzenia, która wersja została wdrożona,
  • często brakuje builda (np. bundlowanie frontendu, migracje bazy).

CI dba, żeby każda zmiana przechodziła te same kroki – checkout, instalacja zależności, testy, build. CD podnosi poprzeczkę: deployment dzieje się dopiero po zielonym CI, w kontrolowany sposób (np. tylko z brancha main albo po ręcznym kliknięciu).

„Git push na produkcję” to skrót bez kontroli jakości. Nawet prosty pipeline CI/CD na GitHub Actions wnosi automatyczną weryfikację i uporządkowany proces wdrożenia.

Co realnie daje najprostszy pipeline

Nawet najprostszy workflow GitHub Actions z testami i prostym deployem daje kilka wymiernych korzyści:

  • Szybszy feedback – widzisz, że testy padły kilka sekund po push, a nie podczas ręcznego klikania aplikacji.
  • Mniej regresji – te same testy odpalają się konsekwentnie dla każdego commita i brancha.
  • Większy spokój przy merge’ach – zielony status CI przy PR to sygnał, że kod przynajmniej „składa się” i przechodzi bazowe testy.
  • Wersjonowany proces – plik YAML z workflow siedzi w repo, więc historia zmian w samym pipeline jest równie przejrzysta jak w kodzie.
  • Łatwiejsze wdrożenia – jasne zasady: np. push do main → build → deploy na staging; ręczne uruchomienie → deploy na produkcję.

Wymagania wstępne: co przygotować przed pierwszym pipeline

Podstawowe konto i repozytorium na GitHubie

Do zbudowania pierwszego pipeline CI/CD w GitHub Actions potrzebne są tylko elementarne narzędzia:

  • Konto GitHub – darmowe konto w zupełności wystarczy.
  • Repozytorium z kodem – może to być:
    • prosty projekt „Hello World” w Node.js, Pythonie czy .NET,
    • aplikacja frontowa (np. React, Vue),
    • backendowy serwis API.
  • Podstawowa znajomość gita – umiejętność wykonania:
    • git clone, git add, git commit, git push,
    • pracy na branchach: git checkout -b feature/x.

Jeżeli repo jeszcze nie istnieje, najprościej stworzyć je w GitHubie, sklonować lokalnie i dodać najprostszy kod z jednym testem jednostkowym. Pipeline łatwiej budować na czymś, co faktycznie da się przetestować.

Środowisko lokalne i znajomość własnego stacku

GitHub Actions bierze na siebie uruchamianie testów i builda, ale konfigurację opierasz na tym, co robisz lokalnie. Przydają się więc:

  • Edytor kodu – Visual Studio Code, IntelliJ, Rider, PyCharm, WebStorm lub dowolny inny.
  • Podstawy terminala – uruchamianie poleceń typu npm test, pytest, dotnet test.
  • Znajomość podstaw swojego stacku – przynajmniej:
    • komenda do uruchomienia testów,
    • komenda do zbudowania aplikacji.

Przykłady:

  • Node.js: testy – npm test lub npm run test, build – npm run build.
  • Python: testy – pytest lub python -m unittest, build zwykle zbędny (chyba że tworzysz paczkę).
  • .NET: testy – dotnet test, build – dotnet publish.
  • Java/Maven: testy – mvn test, build – mvn package.

Branch main / master, pull request i ich rola w CI

Pipeline CI/CD zwykle nie działa „tak samo” dla wszystkich branchy. Typowy schemat:

  • feature branch – pełne testy, ewentualnie build, ale bez automatycznego deployu,
  • develop lub staging – testy + deploy na środowisko testowe,
  • main / master – testy + deploy na produkcję lub staging.

Branch główny (main lub master) to zwykle miejsce, gdzie kod jest stabilny. Pull request to wniosek o połączenie zmian z feature brancha do głównej gałęzi. CI bardzo mocno łączy się z PR:

  • workflow GitHub Actions uruchamia się przy pull_request,
  • status testów (zielony/czerwony) jest widoczny bezpośrednio przy PR,
  • można wymusić zielone CI jako warunek merge’u (branch protection rules).

Gdzie w GitHubie będzie widać pipeline

Po utworzeniu pierwszego workflow pojawi się nowa aktywność w zakładce Actions w repozytorium. W tej karcie widać:

  • listę workflow (po nazwie zdefiniowanej w pliku YAML),
  • historię uruchomień (każde powiązane z konkretnym commitem/PR),
  • status każdego joba i kroku,
  • szczegółowe logi – przydatne przy debugowaniu problemów.

Do tego status ostatniego uruchomienia (zielony/czerwony) widać bezpośrednio przy commitach i pull requestach, co ułatwia szybkie orientowanie się, czy pipeline CI/CD działa prawidłowo.

GitHub Actions w pigułce: kluczowe pojęcia

Workflow, job i step – prosta analogia

Dobrze jest mieć prostą mapę pojęć, zanim powstanie pierwszy plik YAML. Najczęściej używane definicje:

  • Workflow – cała „recepta” na pipeline. Opisuje, kiedy ma się uruchomić i jakie zadania (jobs) wykonać. To jeden plik YAML w katalogu .github/workflows.
  • Job – zestaw kroków wykonywany na jednym runnerze (maszynie). Job może:
    • być niezależny,
    • albo zależeć od innych jobów (uruchamia się dopiero po nich).
  • Step – pojedyncza instrukcja w jobie. Może to być:
    • uruchomienie komendy shellowej (run),
    • albo użycie gotowej akcji (uses).
  • Runner – maszyna (wirtualna lub fizyczna), na której GitHub uruchamia joby.

Analogia kulinarna:

  • workflow to przepis jako całość („Zrób obiad dla 4 osób”),
  • jobs to równoległe zadania („ugotuj makaron”, „przygotuj sos”),
  • steps to konkretne kroki („wlej wodę do garnka”, „wrzuć makaron”, „podsmaż cebulę”).

Runnery: GitHub-hosted vs self-hosted

GitHub Actions potrzebuje maszyny, na której wykona komendy. Do wyboru są dwa główne typy runnerów:

  • GitHub-hosted – domyślne, zarządzane przez GitHub:
    • systemy: Linux (Ubuntu), Windows, macOS,
    • krótkotrwałe: za każdym razem świeża maszyna,
    • dobry wybór na start – zero konfiguracji serwerów.
  • Self-hosted – własne serwery lub maszyny:
    • przydatne, gdy potrzebne są specyficzne zasoby,
    • wymagają samodzielnej konfiguracji, aktualizacji i zabezpieczenia.

Dla pierwszego pipeline’u CI/CD najbezpieczniej skupić się na GitHub-hosted runners, np. ubuntu-latest. Dzięki temu odpada cała administracja infrastrukturą.

Marketplace akcji – gotowe klocki

Zamiast pisać wszystko w surowym shellu, można wykorzystywać gotowe akcje z GitHub Marketplace. To małe moduły, które wykonują konkretne zadanie, np.:

  • actions/checkout – klonuje kod repo do runnera,
  • actions/setup-node – instaluje Node.js w zadanej wersji,
  • actions/setup-python – konfiguruje Pythona,
  • actions/upload-artifact i actions/download-artifact – praca z artefaktami builda,
  • specyficzne akcje do deployu na GitHub Pages, Heroku itd.

W pierwszym pipeline korzysta się z podstawowych klocków. Z czasem można je zastępować własnymi skryptami lub bardziej zaawansowanymi akcjami.

Eventy wyzwalające workflow

Workflow w GitHub Actions uruchamia się w reakcji na konkretne zdarzenia w repozytorium. Najczęściej używane w pierwszym pipeline:

  • push – każde wypchnięcie commita na wybrane branche.
  • pull_request – zmiany w PR (otwarcie, commit, synchronizacja z bazowym branchem).
  • workflow_dispatch – ręczne uruchomienie workflow z poziomu GitHuba, np. do kontrolowanego deployu.

Konfigurując pipeline CI/CD, można wybrać, które eventy mają go uruchamiać. Typowy schemat to:

  • CI (testy) – na push i pull_request w większości branchy,
  • CD (deploy) – na push do main lub tylko na workflow_dispatch.

Logi i statusy: jak czytać wyniki Actions

Interpretacja wyników, ponowne uruchamianie i debugowanie

Po pierwszym uruchomieniu workflow najważniejsza umiejętność to szybkie odczytanie, co poszło dobrze, a co źle. Interfejs Actions jest dość przejrzysty, ale na początku łatwo się zgubić.

  • Na liście runów (zakładka Actions) widać:
    • status całego workflow (zielony/czerwony/szary),
    • gałąź i commit, które go wyzwoliły,
    • kto był autorem zmian.
  • Po wejściu w konkretny run:
    • po lewej – lista jobów i ich status,
    • po kliknięciu joba – lista kroków (steps), każdy z logami.

Najczęstsze czynności przy debugowaniu:

  • Odnalezienie pierwszego czerwonego kroku – to on przerwał resztę.
  • Przewinięcie logów do końca – tam zwykle jest najczytelniejszy komunikat błędu (np. nieznana komenda, test nie przeszedł).
  • Porównanie z lokalnym środowiskiem – uruchom ten sam zestaw poleceń w terminalu lokalnie i sprawdź, czy da się odtworzyć błąd.

Wybrane narzędzia ułatwiające życie:

  • Re-run jobs – możliwość ponownego uruchomienia całego workflow lub pojedynczego joba, np. gdy testy padły przez flaki.
  • Log grouping – akcje i kroki potrafią grupować logi, co ułatwia orientację w długich wyjściach.
  • Annotations – niektóre akcje i test-runnery dodają komentarze „inline” do PR (np. konkretny plik i linia z błędem).
Osoba pracuje przy laptopie obok oznaczonych płyt CD na biurku
Źródło: Pexels | Autor: cottonbro studio

Struktura pierwszego workflow: jak czytać i pisać YAML w Actions

Minimalny szkielet pliku workflow

Workflow to plik w katalogu .github/workflows. Na początek wystarczy jeden plik, np. ci.yml. Minimalny szkielet, z którym da się coś zrobić:

name: CI

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repo
        uses: actions/checkout@v4

      - name: Uruchom testy
        run: echo "Tu za chwilę będą testy"

W tym szablonie są już wszystkie kluczowe elementy:

  • name – czytelna nazwa workflow, widoczna w zakładce Actions.
  • on – definicja eventów, które uruchamiają workflow (push, pull_request) i na jakich branchach.
  • jobs – słownik jobów, tutaj jeden: build-and-test.
  • runs-on – typ runnera (w tym przypadku Ubuntu na infrastrukturze GitHuba).
  • steps – sekwencja kroków, wykonywanych po kolei.

Nazwy jobów i kroków versus identyfikatory

W YAML występują dwie warstwy nazw:

  • Identyfikator joba (np. build-and-test) – techniczna nazwa używana do zależności (needs:) i w logach.
  • Label joba – pojawia się jako name: wewnątrz joba, to czytelna etykieta.
  • Nazwa kroku (name: w stepie) – widoczna w UI, łatwiej znaleźć miejsce błędu.

Przykład z bardziej opisowymi nazwami:

jobs:
  tests:
    name: Uruchom testy jednostkowe
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repo
        uses: actions/checkout@v4

      - name: Instalacja zależności
        run: npm ci

      - name: Testy jednostkowe
        run: npm test

Dobrze nazwane kroki oszczędzają czas, gdy za pół roku będziesz szukać, czemu coś „nagle” się wywala.

Użycie gotowych akcji vs komendy shellowe

Każdy step ma jedną z dwóch form:

  • uses – odwołanie do gotowej akcji, np. actions/checkout@v4.
  • run – komendy shellowe uruchomione na runnerze.

Często te dwa typy miesza się w jednym jobie:

steps:
  - name: Checkout repo
    uses: actions/checkout@v4

  - name: Ustaw Node.js
    uses: actions/setup-node@v4
    with:
      node-version: "20"

  - name: Instalacja zależności
    run: npm ci

  - name: Lint + testy
    run: |
      npm run lint
      npm test

Wiele prostych zadań (testy, budowanie, lintery) można spokojnie obsłużyć samym run. Gotowe akcje przydają się przy instalacji środowisk, pracy z cache czy deployem.

Zmienna środowiskowe i secrets w pliku YAML

Każdy pipeline prędzej czy później potrzebuje konfiguracji i haseł. W Actions robi się to dwiema drogami:

  • env: – zwykłe zmienne środowiskowe, jawne w YAML, np. NODE_ENV=test.
  • secrets – poufne dane, przechowywane szyfrowane w repo (Settings → Secrets and variables → Actions).

Fragment z wykorzystaniem obu:

env:
  NODE_ENV: test

jobs:
  tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Konfiguracja bazy testowej
        run: npm run db:migrate
        env:
          DB_CONNECTION: ${{ secrets.TEST_DB_CONNECTION_STRING }}

Takie podejście pozwala trzymać konfigurację deployu i klucze API poza repozytorium, ale wciąż wygodnie używać ich w pipeline.

Pierwszy krok CI: automatyczne testy przy każdym pushu

Najprostszy scenariusz: Node.js z npm test

Na początku lepiej trzymać się jednego konkretnego stacku. Przykład dla Node.js z testami pod npm test:

name: CI

on:
  push:
    branches: [ "main", "develop", "feature/*" ]
  pull_request:
    branches: [ "main", "develop" ]

jobs:
  test-node:
    name: Testy Node.js
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repo
        uses: actions/checkout@v4

      - name: Ustaw Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Instalacja zależności
        run: npm ci

      - name: Uruchom testy
        run: npm test

Co tu się dzieje praktycznie:

  • Workflow odpala się na push do main, develop i branchy pasujących do wzorca feature/* oraz na PR-y do main/develop.
  • Job test-node używa Ubuntu jako runnera, instaluje wybraną wersję Node.js i cache’uje katalog ~/.npm, przyspieszając kolejne buildy.
  • Testy uruchamia dokładnie tak, jak lokalnie – npm test.

Przykład dla Pythona z pytest

Analogicznie można zbudować pipeline dla Pythona:

name: CI Python

on:
  push:
    branches: [ "main", "develop" ]
  pull_request:
    branches: [ "main", "develop" ]

jobs:
  test-python:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Ustaw Pythona
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Zainstaluj zależności
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt

      - name: Uruchom pytest
        run: pytest

Jeśli lokalnie korzystasz z innych komend (np. tox), wystarczy podmienić sekcję run w ostatnim kroku.

Równoległe testy w wielu wersjach runtime (matrix)

W praktyce często trzeba wspierać kilka wersji Node.js, Pythona czy .NET. GitHub Actions ma do tego matrix strategy:

jobs:
  test-node:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [ "18", "20" ]

    steps:
      - uses: actions/checkout@v4

      - name: Ustaw Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}

      - name: Instalacja zależności
        run: npm ci

      - name: Testy
        run: npm test

W ten sposób jeden job logiczny uruchamia się fizycznie kilka razy – po razie na każdą wersję Node.js. Błędy kompatybilności wychodzą szybko, zanim trafią na produkcję.

Ograniczenie testów do zmian w konkretnych katalogach

Przy większych monorepo lub mikroserwisach w jednym repo dobrze jest nie uruchamiać ciężkich testów zawsze. Można ograniczyć workflow do zmian w określonych ścieżkach:

on:
  push:
    branches: [ "main", "develop" ]
    paths:
      - "backend/**"
      - ".github/workflows/backend-ci.yml"
  pull_request:
    branches: [ "main", "develop" ]
    paths:
      - "backend/**"

Dzięki temu modyfikacja dokumentacji nie odpala od razu pełnego test-suite’a dla backendu.

Rozszerzenie pipeline: build aplikacji i artefakty

Dodanie kroku build do istniejącego joba

Po stabilnym etapie testów naturalny krok to dodanie builda. W prostym projekcie frontowym (np. React) pipeline może wyglądać tak:

name: CI Frontend

on:
  push:
    branches: [ "main", "develop" ]
  pull_request:
    branches: [ "main", "develop" ]

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Instalacja zależności
        run: npm ci

      - name: Testy jednostkowe
        run: npm test -- --watch=false

      - name: Build produkcyjny
        run: npm run build

W takim wariancie testy i build są w jednym jobie. Build nie odpali się, jeśli wcześniej testy padną.

Oddzielenie testów i builda na dwa joby

Przy bardziej rozbudowanych projektach wygodniej jest rozdzielić testy oraz build na osobne joby i połączyć je zależnością:

jobs:
  tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - run: npm test

  build:
    runs-on: ubuntu-latest
    needs: tests
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - run: npm run build

Job build uruchomi się tylko, jeśli tests zakończy się sukcesem. To prosty mechanizm „gate’owania” kolejnych etapów pipeline.

Zapisywanie wyników builda jako artefaktów

Bezpośrednio po buildzie warto zapisać wynik jako artefakt. Dzięki temu można:

  • pobrać paczkę i uruchomić ją lokalnie z dokładnie tego samego commit’a,
  • przekazać ją do joba deployującego (CD), zamiast budować drugi raz.

Przykład dla aplikacji frontowej, gdzie build trafia do katalogu build:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - run: npm run build

      - name: Zapisz artefakt builda
        uses: actions/upload-artifact@v4
        with:
          name: frontend-build
          path: build/

Artefakt pojawi się w UI runa, skąd można go pobrać. W kolejnym jobie (w tym samym workflow) można go odtworzyć:

jobs:
  deploy:
    runs-on: ubuntu-latest
    needs: build

    steps:
      - name: Pobierz artefakt
        uses: actions/download-artifact@v4
        with:
          name: frontend-build
          path: ./dist

Definicja artefaktów dla aplikacji backendowych

Dla backendu artefakt może być np. paczką JAR (Java), paczką NuGet/zipem (NET) czy zbudowanym kontenerem (metadata o obrazie). Przykład dla .NET:

Przykładowy artefakt dla .NET (build + publikacja)

Dla aplikacji .NET typowy artefakt to katalog po dotnet publish. Minimalny przykład:

name: CI Backend .NET

on:
  push:
    branches: [ "main", "develop" ]

jobs:
  build-backend:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Ustaw .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: "8.0.x"

      - name: Przywróć pakiety
        run: dotnet restore

      - name: Build + test
        run: |
          dotnet build --configuration Release --no-restore
          dotnet test --configuration Release --no-build

      - name: Publish
        run: dotnet publish src/Api/Api.csproj -c Release -o ./publish

      - name: Zapisz artefakt backendu
        uses: actions/upload-artifact@v4
        with:
          name: backend-published
          path: publish/

Artefakt backend-published można później zdeployować na serwer, do kontenera lub na dowolną PaaS.

Artefakt jako paczka Docker (metadane obrazu)

Coraz częściej backend to kontener. Sam obraz ląduje w rejestrze (np. GitHub Container Registry), natomiast pipeline trzyma jako artefakt informacje o tagu i repozytorium. Prosty schemat:

jobs:
  build-image:
    runs-on: ubuntu-latest

    permissions:
      contents: read
      packages: write

    env:
      IMAGE_NAME: ghcr.io/${{ github.repository }}/app

    steps:
      - uses: actions/checkout@v4

      - name: Zaloguj do GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Zbuduj obraz
        run: |
          TAG=${GITHUB_SHA::7}
          docker build -t $IMAGE_NAME:$TAG .
          echo "image=$IMAGE_NAME:$TAG" >> $GITHUB_OUTPUT
        id: build

      - name: Wypchnij obraz
        run: docker push ${{ steps.build.outputs.image }}

      - name: Zapisz metadane obrazu
        uses: actions/upload-artifact@v4
        with:
          name: image-meta
          path: meta.txt
        env:
          IMAGE_REF: ${{ steps.build.outputs.image }}

Plik meta.txt może zawierać np. jedną linię z pełnym refem obrazu. Job wdrożeniowy użyje go, by uruchomić właściwą wersję kontenera.

Proste wdrożenie na środowisko testowe lub staging

Strategia: build raz, deploy wiele razy

Bezpieczniej jest budować aplikację raz, zapisać artefakt i tę samą paczkę deployować na staging/produkcję. Dzięki temu nie ma sytuacji, w której staging i produkcja stoją na dwóch różnych buildach tego samego commit’a.

Minimalny wzorzec:

  • job build – testy + build + artefakt,
  • job deploy-staging – pobiera artefakt, wdraża na środowisko testowe.

Wdrożenie frontendu na prosty hosting (np. serwer z SSH)

Najczęstszy pierwszy krok: statyczny frontend na prostym serwerze (VPS, shared hosting). Załóżmy, że serwer przyjmuje pliki po SSH/SCP i serwuje je z katalogu /var/www/staging.

Konfiguracja secrets w repo (Settings → Secrets and variables → Actions):

  • STAGING_HOST – adres serwera,
  • STAGING_USER – użytkownik SSH,
  • STAGING_SSH_KEY – prywatny klucz SSH,
  • STAGING_PATH – ścieżka docelowa, np. /var/www/staging.

Workflow łączący build i deploy może wyglądać tak:

name: Frontend CI Staging

on:
  push:
    branches: [ "develop" ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - run: npm ci

      - run: npm test -- --watch=false

      - run: npm run build

      - name: Zapisz artefakt
        uses: actions/upload-artifact@v4
        with:
          name: frontend-build
          path: build/

  deploy-staging:
    runs-on: ubuntu-latest
    needs: build

    steps:
      - name: Pobierz artefakt buildu
        uses: actions/download-artifact@v4
        with:
          name: frontend-build
          path: ./build

      - name: Skonfiguruj klucz SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.STAGING_SSH_KEY }}" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa

      - name: Deploy na staging przez rsync
        run: |
          rsync -az --delete ./build/ ${{ secrets.STAGING_USER }}@${{ secrets.STAGING_HOST }}:${{ secrets.STAGING_PATH }}

Mechanizm jest prosty: po każdym pushu na develop buduje się frontend i ląduje od razu na stagingu. Zespół testowy widzi zmiany bez ręcznego przepychania plików.

Wdrożenie backendu .NET na serwer (SSH + systemd)

Dla prostego środowiska stagingowego z backendem .NET uruchamianym jako usługa systemd scenariusz często wygląda tak:

  1. spakowanie katalogu po dotnet publish do archiwum,
  2. przesłanie archiwum na serwer,
  3. rozpakowanie w docelowej ścieżce,
  4. restart usługi systemd.

Fragment joba deployującego (po zbudowaniu artefaktu backend-published):

jobs:
  deploy-backend-staging:
    runs-on: ubuntu-latest
    needs: build-backend

    steps:
      - name: Pobierz artefakt backendu
        uses: actions/download-artifact@v4
        with:
          name: backend-published
          path: ./publish

      - name: Spakuj publikację
        run: |
          tar czf app.tar.gz -C publish .

      - name: Skonfiguruj SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.STAGING_SSH_KEY }}" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa

      - name: Wyślij paczkę na serwer
        run: |
          scp app.tar.gz ${{ secrets.STAGING_USER }}@${{ secrets.STAGING_HOST }}:/tmp/app.tar.gz

      - name: Rozpakuj i zrestartuj usługę
        run: |
          ssh ${{ secrets.STAGING_USER }}@${{ secrets.STAGING_HOST }} 
            "sudo mkdir -p /var/www/app-staging 
             && sudo tar xzf /tmp/app.tar.gz -C /var/www/app-staging 
             && sudo systemctl restart app-staging.service"

Pattern jest ten sam, co w przypadku frontu: wszystko idzie z jednego workflow run, który w UI GitHuba pokazuje zarówno wynik builda, jak i status wdrożenia.

Warunkowe uruchamianie jobów deployujących

Na początku dobrze jest pilnować, by deploy nie odpalał się do każdej gałęzi. Kontrolę zapewniają trzy mechanizmy:

  • filtrowanie po gałęzi w sekcji on:,
  • warunki if: po stronie joba lub kroku,
  • ręczne zatwierdzenie (environments z protection rules).

Przykład, który deployuje na staging tylko, gdy commit wyląduje na develop i wszystkie wcześniejsze joby przejdą:

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    needs: [ build ]
    if: github.ref == 'refs/heads/develop' && success()

Warunek success() jest domyślny (jeśli go nie ma, job i tak nie ruszy, gdy needs zakończy się niepowodzeniem), ale jawne zapisanie bywa czytelniejsze dla zespołu.

Środowiska (environments) i manualne zatwierdzanie

GitHub Actions ma pojęcie środowiska (environment). Można do nich przypisać:

  • secrets specyficzne dla danego środowiska (np. inne connection stringi),
  • zasady zatwierdzania (kto musi kliknąć „Approve” przed deployem),
  • limity dostępu (tylko określone branch’e mogą deployować).

Przykład prostej definicji joba deployującego na environment staging z przypiętymi secrets:

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    needs: build
    environment: staging

    steps:
      - name: Checkout (czasem potrzebny)
        uses: actions/checkout@v4

      - name: Użyj secretów środowiskowych
        run: |
          echo "Host: ${{ secrets.STAGING_HOST }}"
          # tu normalny deploy...

Environment i jego zasady konfiguruje się w Settings → Environments. Przy bardziej wrażliwych środowiskach (np. produkcja) można wymusić ręczne zatwierdzenie, zanim workflow ruszy z jobem deploy.

Prosty deployment kontenera na staging (Docker host)

Jeśli staging stoi jako zwykły serwer z Dockerem, workflow może po buildzie obrazu wykonać:

  1. pobranie metadanych obrazu,
  2. zalogowanie się na hosta przez SSH,
  3. ściągnięcie obrazu z rejestru,
  4. podmianę kontenera (stop starego, uruchom nowy).

Job wdrażający przy założeniu, że wcześniej zbudowano i wypchnięto obraz do GHCR:

jobs:
  deploy-container-staging:
    runs-on: ubuntu-latest
    needs: build-image

    steps:
      - name: Pobierz metadane obrazu
        uses: actions/download-artifact@v4
        with:
          name: image-meta
          path: .

      - name: Odczytaj ref obrazu
        id: meta
        run: |
          IMAGE_REF=$(cat meta.txt)
          echo "image=$IMAGE_REF" >> $GITHUB_OUTPUT

      - name: Skonfiguruj SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.STAGING_SSH_KEY }}" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa

      - name: Deploy kontenera na staging
        run: |
          ssh ${{ secrets.STAGING_USER }}@${{ secrets.STAGING_HOST }} "
            docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} &&
            docker pull ${{ steps.meta.outputs.image }} &&
            docker stop app-staging || true &&
            docker rm app-staging || true &&
            docker run -d --name app-staging -p 8080:80 ${{ steps.meta.outputs.image }}
          "

Dla małego projektu to już w pełni działające CI/CD: commit → build obrazu → staging sam się aktualizuje.

Rozdzielenie workflow: osobny plik dla CI i osobny dla CD

Gdy pipeline rośnie, wygodniej jest oddzielić pliki:

  • .github/workflows/ci.yml – testy, build, artefakty,
  • .github/workflows/cd-staging.yml – same deploye, odpalane ręcznie lub przy release.

Prosty workflow do ręcznego deployu na staging z konkretnego artefaktu (np. gdy QA chce przetestować build z określonego runa):

name: Deploy Staging Manual

on:
  workflow_dispatch:
    inputs:
      run-id:
        description: "ID runa CI z artefaktem"
        required: true

jobs:
  deploy-staging:
    runs-on: ubuntu-latest

    steps:
      - name: Pobierz artefakt z innego runa
        uses: dawidd6/action-download-artifact@v3
        with:
          workflow: ci.yml
          run_id: ${{ github.event.inputs.run-id }}
          name: frontend-build
          path: ./build

      # Dalej standardowy deploy (rsync / scp / docker...) 

Taki układ dobrze się sprawdza, gdy CI działa często, a CD ma być kontrolowane ręcznie, np. tylko przez lidera zespołu.

Dodanie prostych smoke testów po wdrożeniu

Sam deploy to nie wszystko. Dobrze jest od razu sprawdzić, czy aplikacja wstała. Na początek wystarczy prosty smoke test HTTP.

Przykład dla frontu na stagingu, który powinien zwracać kod 200 na /:

  deploy-staging:
    runs-on: ubuntu-latest
    needs: build
    steps:
      # ...kroki deployu...

      - name: Smoke test aplikacji
        run: |
          for i in {1..10}; do
            STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://staging.example.com/ || echo "000")
            echo "Proba $i, status: $STATUS"
            if [ "$STATUS" = "200" ]; then
              echo "OK"
              exit 0
            fi
            sleep 5
          done
          echo "Aplikacja nie odpowiada poprawnie"
          exit 1

Taki prosty krok już wychwyci typowe problemy: błędną konfigurację Nginxa, błąd w startupie aplikacji czy brak dostępu do bazy.

Najczęściej zadawane pytania (FAQ)

Co daje pierwszy pipeline CI/CD w GitHub Actions początkującemu programiście?

Pierwszy pipeline CI/CD usuwa największy chaos: testy odpalają się same, a wdrożenie nie jest już ręcznym kopiowaniem plików na serwer. Zamiast „u mnie działa” masz jasny sygnał: zielono – można mergować, czerwono – coś się posypało i trzeba poprawić.

W praktyce zyskujesz szybszy feedback po każdym pushu, mniej niespodzianek przy merge’ach i powtarzalny sposób budowania oraz wdrażania aplikacji. Nawet jeśli projekt jest mały, taki pipeline szybko zwraca się w oszczędzonym czasie i nerwach.

Jakie są minimalne wymagania, żeby zacząć z CI/CD w GitHub Actions?

Potrzebujesz darmowego konta GitHub, repozytorium z działającym projektem oraz podstaw gita (clone, commit, push, praca na branchach). Projekt może być prosty: „Hello World” z jednym testem jednostkowym w Node.js, Pythonie, .NET czy innym stacku.

Do tego przydaje się:

  • edytor kodu (np. VS Code),
  • umiejętność odpalenia lokalnie testów i builda (np. npm test, pytest, dotnet test),
  • podstawowe obycie z terminalem.

Na tym fundamencie łatwo przełożyć lokalne komendy na kroki w workflow YAML.

Czym różni się pipeline CI/CD od prostego „git push na produkcję”?

„Git push na produkcję” to manualny skrót: kod ląduje na serwerze, ale bez automatycznych testów, bez jasnej informacji, która wersja została wdrożona i często bez pełnego procesu builda (np. bundlowanie frontendu, migracje bazy).

Pipeline CI/CD:

  • przy każdym pushu lub pull requeście uruchamia te same testy i build,
  • wdraża tylko to, co przeszło zielony CI,
  • zostawia ślad w logach – wiadomo, co, kiedy i z jakiego commita trafiło na serwer.
  • To różnica między „wrzućmy szybko i trzymajmy kciuki” a kontrolowanym procesem jakości.

Jak skonfigurować pierwszy prosty workflow GitHub Actions do testów?

Najprostsza ścieżka to przeniesienie lokalnych kroków do pliku YAML w katalogu .github/workflows. W praktyce:

  • ustawiasz trigger, np. on: [push, pull_request],
  • definiujesz job na runnerze, np. runs-on: ubuntu-latest,
  • dodajesz kroki: checkout kodu, instalacja zależności, komenda testów.

To zazwyczaj kilka–kilkanaście linijek YAML, które od razu dają automatyczne testy przy każdym commicie.

Dla początkujących dobrym startem jest: „uruchom dokładnie to, co robisz lokalnie, tylko na runnerze GitHuba”. Dopiero później dokładasz build i prosty deploy.

Jak ustawić, żeby CI/CD działał inaczej dla brancha main i feature brancha?

W GitHub Actions sterujesz tym przez sekcję on oraz warunki w jobach. Typowy schemat:

  • dla feature/* – uruchamiasz testy i ewentualny build, ale bez deployu,
  • dla main – po przejściu testów uruchamiasz job deployujący.

Możesz zdefiniować osobne workflow dla różnych branchy albo w jednym workflow użyć warunków typu if: github.ref == 'refs/heads/main' przy jobie deployującym.

W efekcie każdy commit jest testowany, ale tylko zmiany na głównej gałęzi trafiają automatycznie na staging czy produkcję.

Gdzie w GitHubie sprawdzić status i logi mojego pipeline’u?

W repozytorium przechodzisz do zakładki Actions. Widzisz tam listę workflow, historię ich uruchomień, status każdego joba oraz szczegółowe logi poszczególnych kroków. To pierwsze miejsce, do którego zaglądasz, gdy pipeline się wyłoży.

Dodatkowo status ostatniego uruchomienia jest widoczny przy commitach i pull requestach (zielony znaczek – OK, czerwony – błąd). W praktyce wystarcza rzut oka na PR, żeby wiedzieć, czy można bezpiecznie robić merge.

Czy warto stawiać pipeline CI/CD w małym zespole lub solo projekcie?

Tak, nawet w jednoosobowym projekcie szybko widać różnicę. Automatyczne testy przy każdym pushu wychwytują regresje, które łatwo przeoczyć przy „szybkim klikaniu w przeglądarce”. Przy pierwszym większym refaktoringu taki pipeline zwykle ratuje dzień.

W małym zespole zyskujesz też:

  • jasne kryterium merge’u (PR bez zielonego CI nie wchodzi),
  • przejrzystą historię wdrożeń,
  • mniej konfliktów w stylu „u mnie działa, u ciebie nie”.
  • To prosty nawyk, który dobrze procentuje, gdy projekt i zespół zaczynają rosnąć.

Najważniejsze punkty

  • Najprostszy pipeline CI/CD w GitHub Actions porządkuje chaos w projekcie: automatyzuje testy i wdrożenia, zamiast polegać na „u mnie działa” i ręcznych deployach.
  • CI uruchamia zawsze ten sam zestaw kroków (checkout, instalacja zależności, testy, build) przy każdym pushu lub pull requeście, dzięki czemu szybko łapiesz konflikty i regresje.
  • CD dopiero po zielonym CI wypycha kod na odpowiednie środowisko (test, staging, produkcja) w powtarzalny sposób, bez FTP, ręcznych komend i zgadywania, co jest na serwerze.
  • Workflow zapisany w jednym pliku YAML jest wersjonowany razem z kodem, więc cały proces build/deploy jest przejrzysty, powtarzalny i łatwy do modyfikacji przez zespół.
  • Nawet prosty pipeline daje wymierne efekty: szybszy feedback po pushu, mniej błędów przy merge’ach, spójne testy dla każdego commita i jasne zasady, kiedy i skąd robiony jest deploy.
  • Do startu wystarczy podstawowy zestaw: darmowe konto GitHub, repo z prostym projektem i jednym testem, znajomość gita oraz komend do uruchamiania testów i builda w swoim stacku.
  • Dobrze zaprojektowany pipeline różnicuje zachowanie między branchami: na feature branchach tylko testy i ewentualny build, na main/develop dodatkowo automatyczny deploy na wskazane środowiska.
Poprzedni artykułMonorepo w praktyce: Nx, Turborepo i pnpm workspaces w jednym projekcie
Następny artykułTesty jednostkowe w Go: jak pisać czytelne testy i mocki bez frustracji
Marta Rutkowski
Marta Rutkowski zajmuje się tematami AI/ML i analizą danych, łącząc podejście inżynierskie z krytycznym spojrzeniem na trendy. W tutorialach prowadzi od przygotowania danych po wdrożenie, pokazując, jak mierzyć jakość modeli, unikać przecieków danych i kontrolować koszty obliczeń. Zamiast obietnic „magii AI” stawia na powtarzalne eksperymenty, czytelne metryki i rzetelne źródła, w tym publikacje oraz dokumentację narzędzi. Interesują ją także kwestie etyki, prywatności i bezpieczeństwa w systemach uczących się, dlatego wnioski zawsze osadza w realnych ograniczeniach.