Jak zbudować prosty pipeline CI/CD w GitHub Actions krok po kroku

0
105
5/5 - (2 votes)

Nawigacja:

Po co w ogóle CI/CD i dlaczego akurat GitHub Actions

Prosty obraz CI/CD na przykładzie małej aplikacji

Wyobraź sobie małą aplikację webową: kilku programistów, parę endpointów API, trochę frontendu. Bez CI/CD każdy commit to ręczna procedura: pobierz zmiany, uruchom testy lokalnie, zbuduj aplikację, skopiuj pliki na serwer, zrestartuj usługę. Jeden pominięty krok i zaczynają się kłopoty – brakujące migracje, niedziałający build, niespójne wersje bibliotek.

Ciągła integracja (Continuous Integration) sprowadza się do tego, że każda zmiana w kodzie jest automatycznie budowana i testowana. Nie „kiedyś tam”, tylko przy każdym pushu lub pull requeście. Ciągłe dostarczanie (lub wdrażanie, Continuous Delivery/Deployment) idzie o krok dalej – sprawdzony kod automatycznie ląduje na środowisku testowym lub produkcyjnym. Dzięki temu przepływ wygląda bardziej jak taśma w fabryce niż improwizacja na kolanie.

Przy małej aplikacji często pojawia się pokusa: „zrobimy to ręcznie, przecież to tylko kilka plików”. Do momentu, gdy przy piątym hotfiksie w piątek wieczorem ktoś pomyli katalog na serwerze. Prosty pipeline CI/CD w GitHub Actions usuwa z tego równania większość ludzkich pomyłek i powtarzalnych, nudnych czynności.

Zalety automatyzacji w codziennej pracy

Automatyzacja przez prosty pipeline CI/CD przekłada się na bardzo konkretne korzyści:

  • Szybka informacja zwrotna – commit nie przechodzi testów? Dowiadujesz się o tym po kilku minutach, a nie po tygodniu, gdy ktoś zmerguje gałąź do main.
  • Stała jakość – ten sam zestaw testów, ten sam proces builda, ta sama konfiguracja środowiska. Znika problem „u mnie działa”.
  • Mniej ręcznych kroków – nie musisz pamiętać, jakiego polecenia użyć do builda, gdzie wrzucić artefakt czy jaki serwer wybrać. Workflow GitHub Actions robi to za Ciebie.
  • Historia i przejrzystość – każdy pipeline zostawia ślad: kto zainicjował run, co się uruchomiło, na jakim kroku się wyłożyło.

Przy większym zespole automatyzacja staje się wręcz koniecznością. Bez niej część programistów zamienia się w „pseudo-adminów”, którzy cały dzień wgrywają paczki, restartują kontenery i poprawiają drobne błędy wdrożenia zamiast rozwijać produkt.

Dlaczego GitHub Actions jest dobrym wyborem na start

Jeśli kod jest już w GitHubie, GitHub Actions jest jednym z najbardziej naturalnych wyborów. Workflow GitHub Actions jest zapisany w repozytorium jako plik YAML. To oznacza, że konfiguracja pipeline’u jest wersjonowana razem z kodem, przechodzi PR-y, review, revert. Jedna zmiana – jedna historia.

Kilka mocnych stron GitHub Actions na początek:

  • Ścisła integracja z GitHubem – triggerujesz pipeline na push, pull_request, tagu, release czy manualnie. Nie trzeba stawiać osobnych webhooków i kombajnu CI.
  • Darmowy próg – dla publicznych repozytoriów masz sporą pulę minut za darmo. Dla małych projektów komercyjnych podstawowy pakiet często w zupełności wystarcza.
  • Ogromna społeczność i Marketplace – typowe zadania, jak checkout kodu, cache zależności, logowanie do chmury, mają już gotowe actions.

Na etapie „chcę zbudować prosty pipeline CI/CD krok po kroku” trudno znaleźć coś prostszego przy istniejącym repo na GitHubie.

Kiedy GitHub Actions ma sens, a kiedy poszukać alternatywy

GitHub Actions jest świetne dla:

  • małych i średnich projektów,
  • zespołów, które już używają GitHuba,
  • aplikacji webowych/mikroserwisów, które można zbudować i wdrożyć w ~kilkanaście minut,
  • projektów open source, gdzie publiczne repozytoria mają sporo darmowych minut.

Są jednak sytuacje, gdy warto sięgnąć po inne rozwiązania, jak GitLab CI czy Jenkins:

Jeśli organizacja pracuje wyłącznie na GitLabie, ma rozbudowaną infrastrukturę on-premise i wymagania audytowe, GitLab CI będzie naturalnym wyborem. Z kolei Jenkins wciąż ma sens w środowiskach bardzo złożonych, z dziesiątkami specyficznych integracji i wymogiem pełnej kontroli nad serwerem CI. Bywa cięższy w utrzymaniu, ale daje ogromną elastyczność – pod warunkiem, że ktoś ma czas go doglądać.

Na start, do zbudowania prostego pipeline’u CI/CD, GitHub Actions zwykle wygrywa prostotą i szybkością wdrożenia. Zamiast tygodni konfiguracji maszyl CI, dodajesz folder .github/workflows, plik YAML i pipeline zaczyna żyć.

Podstawowe pojęcia w GitHub Actions, które trzeba mieć „w głowie”

Repozytorium, workflow, job i step – jak puzzle w układance

W GitHub Actions wszystko kręci się wokół repozytorium. Repozytorium przechowuje kod, testy i konfigurację workflow. W środku znajduje się folder .github/workflows, a w nim jeden lub więcej plików YAML – każdy z nich to osobny workflow.

Workflow to cała „scenka” automatyzacji: reaguje na określone zdarzenie (np. push) i zawiera jeden lub więcej jobów. Job to z kolei zestaw kroków wykonywanych na jednym runnerze. W jobie definiujesz, na jakim systemie chcesz działać (runs-on: ubuntu-latest, windows-latest, macos-latest) oraz jakie stepy mają się wykonać.

Najprostsze skojarzenie:

  • Repozytorium – jak projekt w IDE.
  • Workflow – scenariusz „co zrobić przy puszu / PR-ze / wdrożeniu”.
  • Job – konkretny etap: np. „build & test”, „deploy na staging”.
  • Step – pojedynczy krok: „checkout kodu”, „instalacja zależności”, „uruchomienie testów”.

Runner i środowisko wykonania

Runner to maszyna (wirtualna lub fizyczna), na której wykonywany jest job. GitHub oferuje:

  • Hosted runners – zarządzane przez GitHub, nazywane np. ubuntu-latest, windows-latest. To z nich korzysta się najczęściej.
  • Self-hosted runners – hostowane u Ciebie (np. w prywatnej sieci, na własnym serwerze), gdy potrzebujesz specjalistycznego środowiska lub pełnej kontroli.

Dla prostego pipeline’u CI/CD zdecydowanie wystarczą hosted runners. Jedna linijka runs-on: ubuntu-latest i masz nową, czystą maszynę z zestawem narzędzi developerskich. Każdy job dostaje świeże środowisko, więc nie trzeba bać się „śmieci” po poprzednim runie.

Workflow a action – co jest czym

Łatwo pomylić dwa terminy: workflow i action. Workflow to cała definicja pipeline’u, przechowywana jako plik YAML. Action to pojedynczy element, którego możesz użyć w stepie, np.:

  • actions/checkout – pobiera kod z repo do runnera,
  • actions/cache – zarządza cache zależności,
  • actions/upload-artifact – wrzuca artefakty z joba do przechowania.

Action może być:

  • gotową akcją z GitHub Marketplace,
  • projektem w Twoim własnym repozytorium,
  • definicją skryptu napisanego w Node.js lub jako composite action (złożony ze stepów).

Workflow łączy różne actions i komendy powłoki w jeden spójny proces. Można to porównać do kuchni: workflow to przepis, a actions to gotowe składniki i narzędzia kuchenne.

Publiczne i prywatne repozytoria a limity GitHub Actions

GitHub Actions jest szczególnie hojny dla projektów open source. Publiczne repozytoria dostają duży limit minut działania pipeline’u za darmo. To dlatego wiele popularnych projektów open source buduje się i testuje przy każdym PR-ze bez dodatkowych opłat.

W przypadku prywatnych repozytoriów obowiązują konkretne limity w zależności od planu (Free, Pro, Teams, Enterprise). Dla małego zespołu zwykle spokojnie wystarczy podstawowy pakiet, o ile pipeline’y są rozsądne i nie ładują długich, resource-heavy jobów na każdą drobną zmianę.

Przy tworzeniu prostego pipeline’u CI/CD dobrze trzymać się zasady: krótkie, szybkie joby na każdy push, cięższe joby na wybrane gałęzie lub przy release’ach. Dzięki temu zużycie minut pozostaje pod kontrolą, a feedback dla programistów jest szybki.

Typowe eventy wyzwalające workflow

Najczęściej spotykane eventy w prostym pipeline to:

  • push – uruchamia workflow przy każdym puszu na wskazane gałęzie; idealne miejsce na podstawowy build i testy.
  • pull_request – pipeline odpala się przy utworzeniu/aktualizacji PR-a; pomaga ocenić jakość zmian przed mergem.
  • workflow_dispatch – manualne wywołanie workflow z GUI GitHuba; świetne do ręcznego deploymentu lub okazyjnych zadań.
  • schedule – uruchamianie workflow o określonych godzinach (CRON), np. nocne testy e2e, backupy, skany bezpieczeństwa.

Dla prostego pipeline’u CI/CD sensowny start to: on: push na gałęzie developerskie + on: push na main z dodatkowym jobem deploy. Wraz z rozwojem projektu można dodać pull_request do pre-merge checków i workflow_dispatch do manualnych wdrożeń.

Dłonie przy płytach CD na biurku w technicznym środowisku pracy
Źródło: Pexels | Autor: cottonbro studio

Przygotowanie repozytorium pod prosty pipeline CI/CD

Struktura repozytorium i folder .github/workflows

Podstawą prostego pipeline’u jest odpowiednia struktura repozytorium. Nie musi być idealna, ale kilka elementów ułatwia życie:

  • src/ lub app/ – kod aplikacji,
  • tests/ – testy jednostkowe/integracyjne,
  • README.md – opis projektu i podstawowe komendy uruchomienia.

Do tego dochodzi nowy folder:

.github/
  workflows/
    ci.yml

Folder .github/workflows zawiera pliki YAML, które opisują workflow GitHub Actions. Nazwa pliku (ci.yml, build-and-test.yml) jest dowolna, ale dobrze, by mówiła coś o przeznaczeniu. System wykrywa wszystkie pliki YAML w tym folderze i każdy z nich staje się osobnym workflow widocznym na zakładce „Actions” w repozytorium.

Minimalne wymagania: kod i testy jednostkowe

Do sensownego pipeline’u CI/CD przyda się choćby minimalny zestaw testów. Bez nich pipeline staje się tylko zautomatyzowanym „uruchomieniem builda”, a nie prawdziwą kontrolą jakości. Dla prostego projektu wystarczy:

  • kilka testów jednostkowych, które sprawdzają kluczowe funkcje (np. logikę biznesową),
  • komenda, którą można uruchomić z linii komend, np. npm test, pytest, mvn test.

Częsty scenariusz z praktyki: zespół postanawia „wdrożyć CI”, ale nie ma jeszcze testów. Powstaje workflow, który tylko buduje aplikację. Po kilku miesiącach wszyscy przyzwyczajają się, że „CI świeci na zielono”, ale zielone światło oznacza jedynie tyle, że kompilator nie krzyczy. Gdy wreszcie dodaje się testy, okazuje się, że połowa aplikacji nie jest weryfikowana, a pipeline trzeba przebudowywać od zera.

Dlatego lepiej dodać nawet kilka prostych testów i od razu wpiąć je w pipeline. Niewielki zysk na początku, ale ogromna różnica w nawykach zespołu.

Prosta strategia gałęzi na potrzeby pipeline’u

Pipeline CI/CD dobrze współgra z jasną strategią gałęzi. Nie musi to być od razu pełny GitFlow – na początek wystarczy prosty układ:

  • main – gałąź stabilna, odzwierciedla to, co jest (lub za chwilę będzie) na produkcji,
  • gałęzie funkcjonalne (feature/xyz) – miejsce, gdzie powstają zmiany.

W takim podejściu:

  • każdy push na gałąź feature uruchamia pipeline z buildem i testami – to część CI,
  • push/merge na main uruchamia dodatkowy job deploy – to część CD.

Dzięki temu na gałęziach developerskich skupiasz się na jakości kodu, a na main – na jakości wdrożenia. Nawet przy jednym programiście pomaga to utrzymać porządek i nie wrzucać eksperymentalnych zmian wprost na produkcję.

Tworzenie pierwszego workflow: od pustego pliku do prostego joba

Najprostszy możliwy workflow „czy w ogóle działa?”

Dobrym początkiem jest absolutne minimum – coś, co tylko odpali się na push i wypisze komunikat. Trochę jak pierwsze hello world w nowym języku.

Utwórz plik .github/workflows/ci.yml z taką zawartością:

name: CI

on:
  push:

jobs:
  hello:
    runs-on: ubuntu-latest

    steps:
      - name: Przywitanie
        run: echo "Pierwszy workflow działa!"

Kilka rzeczy dzieje się tu „magicznie”:

  • name to przyjazna nazwa workflow – będzie widoczna na zakładce „Actions”.
  • on: push oznacza, że workflow odpali się przy każdym puszu na dowolną gałąź.
  • jobs.hello definiuje job o nazwie hello, który będzie wykonywany na maszynie ubuntu-latest.
  • W sekcji steps znajduje się jeden krok, który uruchamia komendę powłoki echo.

Po zacommitowaniu i wypchnięciu tego pliku na GitHuba w zakładce „Actions” pojawi się nowy workflow. Jeśli w logach biegu zobaczysz swój komunikat, można iść dalej.

Dodanie checkoutu kodu – fundament wszystkich dalszych kroków

Większość sensownych pipeline’ów potrzebuje kodu źródłowego. Sam runner nie „widzi” go automatycznie, dlatego w prawie każdym workflow pojawia się ten sam krok – checkout:

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

  - name: Przywitanie
    run: echo "Repozytorium zostało pobrane."

Po dodaniu tego kroku cały kod repozytorium ląduje na maszynie CI w bieżącym katalogu roboczym. Od tego momentu komendy typu npm install, pytest czy mvn test działają tak, jak na Twoim lokalnym komputerze.

Konkretny przykład: prosty job dla projektu Node.js

Załóżmy, że masz projekt w Node.js, a lokalnie używasz komend:

  • npm install – instalacja zależności,
  • npm test – uruchomienie testów.

Pierwszy „prawie-docelowy” workflow może wyglądać tak:

name: CI

on:
  push:
    branches:
      - main
      - 'feature/*'

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

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

      - name: Użycie Node.js 20
        uses: actions/setup-node@v4
        with:
          node-version: '20'

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

      - name: Uruchomienie testów
        run: npm test

Taka konfiguracja sprawi, że build i testy odpalą się na każdą zmianę w gałęzi main oraz w gałęziach feature/*. Deweloper wypycha zmiany i po chwili widzi, czy niczego nie połamał – bez ręcznego grzebania w terminalu.

Dodawanie build i testów – serce prostego pipeline CI

Oddzielenie etapów: instalacja, build, testy

Kuszące jest wrzucenie wszystkiego w jeden krok: „zainstaluj, zbuduj, przetestuj”. Na początku działa, ale gdy coś się sypnie, trudno od razu zrozumieć, w którym miejscu. Lepiej rozbić proces na osobne kroki:

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

  - name: Setup środowiska
    uses: actions/setup-node@v4
    with:
      node-version: '20'

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

  - name: Build aplikacji
    run: npm run build

  - name: Uruchomienie testów
    run: npm test

Zaleta takiego podejścia jest prosta: patrzysz na logi i od razu wiesz, czy padła instalacja, build czy testy. Niby drobiazg, a przy pierwszej awarii oszczędza kilkanaście minut szukania.

Strategia testów: co uruchamiać „zawsze”, a co tylko czasami

Nie każdy zestaw testów musi lecieć przy każdym puszu. W praktyce często spotyka się taki podział:

  • testy jednostkowe – szybkie, uruchamiane przy każdym push i pull_request,
  • testy integracyjne / e2e – cięższe, odpalane np. tylko na main albo w nocnych buildach.

Przykładowy workflow, który na każdą gałąź odpala tylko unit testy, a na main dodatkowo integracje:

name: CI

on:
  push:
    branches:
      - main
      - 'feature/*'

jobs:
  unit-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
        name: Testy jednostkowe

  integration-tests:
    runs-on: ubuntu-latest
    needs: unit-tests
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run test:integration
        name: Testy integracyjne

Job integration-tests ma dwa ograniczenia: odpala się dopiero po udanych unit-tests (needs) i tylko na gałęzi main (if). Prosty zapis, a pipeline od razu robi się bardziej „inteligentny”.

Równoległe joby – szybszy feedback

Jeśli testów robi się dużo, warto rozbić je na kilka jobów. Każdy runner działa niezależnie, więc cały pipeline kończy się szybciej. Przykładowy scenariusz:

  • job lint – analiza statyczna kodu,
  • job unit-tests – testy jednostkowe,
  • job build – build aplikacji frontowej.

W YAML może to wyglądać tak:

name: CI

on:
  push:
    branches:
      - main
      - 'feature/*'

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

  unit-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:
      - lint
      - unit-tests
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run build

Tutaj build uruchamia się dopiero wtedy, gdy lint i testy przejdą pozytywnie. Przydaje się to zwłaszcza tam, gdzie build jest kosztowny czasowo – nie ma sensu go odpalać, jeśli kod nawet nie przechodzi podstawowej weryfikacji.

Optymalizacja czasu: cache zależności

Jeżeli przy każdym runie instalacja zależności trwa wieczność, można sięgnąć po cache. GitHub Actions udostępnia gotową akcję actions/cache, która zapisuje i odtwarza katalogi między runami. Klasyczny przykład dla Node.js:

steps:
  - uses: actions/checkout@v4

  - uses: actions/setup-node@v4
    with:
      node-version: '20'

  - name: Cache npm
    uses: actions/cache@v4
    with:
      path: ~/.npm
      key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
      restore-keys: |
        ${{ runner.os }}-node-

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

Klucz cache opiera się o package-lock.json. Jeśli zależności się nie zmieniły, runner odzyska cache i instalacja znacząco przyspieszy. Gdy pojawi się nowy lockfile, klucz się zmieni, więc zależności zostaną zainstalowane od zera.

Zespół programistów pracuje nad kodem w nowoczesnym biurze tech
Źródło: Pexels | Autor: cottonbro studio

Dodanie prostego CD: automatyczne wdrożenie na środowisko testowe lub produkcyjne

Oddzielenie części CI od CD w jednym workflow

Najczęściej wdrożenie nie powinno startować przy każdym puszu na gałąź developerską. Zwykle cały pipeline dzieli się mentalnie na dwie części:

  • CI – build + testy na wielu gałęziach,
  • CD – deployment tylko z wybranych gałęzi (np. main) lub po wydaniu taga.

Dobrym kompromisem jest jeden workflow z dwoma jobami: build-and-test (CI) i deploy (CD). Deployment zależy od sukcesu CI i odpala się tylko na main:

name: CI/CD

on:
  push:
    branches:
      - main
      - 'feature/*'

jobs:
  build-and-test:
    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
      - run: npm test

  deploy:
    runs-on: ubuntu-latest
    needs: build-and-test
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4

      - name: Deploy na środowisko testowe
        run: |
          echo "Tutaj wywołaj swój skrypt deploy.sh"
          ./scripts/deploy.sh

W takim układzie gałęzie feature dostają tylko szybki feedback z CI, a prawdziwy deploy leci wyłącznie z main. To prosty bufor bezpieczeństwa.

Przykład: deployment na serwer przez SSH

W wielu małych projektach „CD” to po prostu skopiowanie paczki na prosty VPS i odpalenie restartu serwisu. Taki scenariusz da się zautomatyzować jednym jobem z użyciem gotowej akcji do SSH, np. appleboy/ssh-action:

name: Deploy

on:
  push:
    branches:
      - main

jobs:
  deploy-to-vps:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout kodu
        uses: actions/checkout@v4

      - name: Zbuduj artefakt
        run: |
          npm ci
          npm run build
          tar czf app.tar.gz dist/

      - name: Deploy przez SSH
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_KEY }}
          port: 22
          script: |
            cd /var/www/app
            rm -rf dist_old
            mv dist dist_old || true
            tar xzf /tmp/app.tar.gz -C .
            systemctl restart my-app.service

Tutaj pojawia się nowy element – sekrety (SSH_HOST, SSH_USER, SSH_KEY). To one zabezpieczają dostęp do serwera, a do YAML-a trafiają tylko jako odwołania, nigdy „wprost”.

Deployment warunkowy na staging vs produkcję

Częsty wzorzec to dwa środowiska: staging i production. Można je obsłużyć jednym workflow, rozdzielając joby po gałęziach:

name: Deploy

on:
  push:
    branches:
      - develop
      - main

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/develop'
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/deploy-staging.sh
        env:
          API_URL: https://api-staging.example.com
          NODE_ENV: staging

  deploy-production:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/deploy-production.sh
        env:
          API_URL: https://api.example.com
          NODE_ENV: production

W praktyce wygląda to tak: merge do develop odpala automatyczne wdrożenie na środowisko testowe, a merge do main – na produkcję. Równocześnie cała logika wdrożenia tkwi w skryptach deploy-*.sh, więc łatwiej ją rozwijać i testować lokalnie.

Manualny trigger deployu – workflow_dispatch

Nie zawsze chcesz, żeby każde scalenie na main automatycznie lądowało na produkcji. Czasem wygodniej jest, aby pipeline zbudował i przetestował aplikację, ale sam deployment był odpalany ręcznie, np. przez tech leada po krótkiej weryfikacji.

Do tego służy workflow_dispatch:

name: Manualny deploy

on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Środowisko docelowe'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production

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

      - name: Deploy na wybrane środowisko
        run: ./scripts/deploy-${{ github.event.inputs.environment }}.sh

Po zapisaniu takiego workflow na zakładce „Actions” pojawi się przycisk „Run workflow”, który otworzy formularz z wyborem środowiska. Cały proces nadal jest powtarzalny i logowany, ale wyzwala go człowiek, a nie każdy commit.

Bezpieczne przechowywanie sekretów i konfiguracji w GitHub Actions

Rodzaje sekretów: repozytorium, organizacja, environment

GitHub ma kilka „poziomów” sekretów. Dzięki temu da się oddzielić dane wspólne dla wielu projektów od tych ściśle projektowych albo środowiskowych.

  • Secrets repozytorium – dostępne tylko w konkretnym repo. Idealne na klucze do jednego projektu, np. token do prywatnego npm-a czy hasło do bazy testowej.
  • Secrets organizacji – współdzielone między repozytoriami. Sprawdzają się przy wspólnych integracjach, np. jeden token do systemu issue tracker dla wszystkich usług.
  • Secrets środowisk (Environments) – przypisane do konkretnego environmentu, np. staging, production. Tutaj zwykle lądują rzeczy takie jak DATABASE_URL, API_KEY lub dane logowania do serwera.

Typowy układ w małym zespole wygląda tak: część wspólnych integracji ląduje na poziomie organizacji, techniczne szczegóły projektu (np. token do CDN) – w secrets repozytorium, a dane różniące się między środowiskami – w secrets environmentów.

Dodawanie sekretów w praktyce

Sekrety konfiguruje się z poziomu interfejsu GitHuba. Nie trzeba nic kombinować z Git-em ani commitami. Ogólny schemat:

  1. Wejdź do repozytorium i przejdź do Settings > Secrets and variables > Actions.
  2. Kliknij New repository secret.
  3. Podaj nazwę, np. SSH_KEY, i wklej wartość (pełny klucz prywatny).
  4. Zapisz – od tego momentu sekret jest dostępny w workflow jako ${{ secrets.SSH_KEY }}.

Na podobnej zasadzie dodaje się sekrety organizacji (zakładka Organization settings) albo dla environmentów (zakładka Environments w ustawieniach repo). Kluczowy punkt: wartości nie są już potem widoczne w UI – jeśli je zgubisz, musisz wprowadzić je ponownie.

Użycie sekretów w jobach – zmienne środowiskowe i parametry akcji

W YAML sekrety wykorzystuje się najczęściej na dwa sposoby:

  • jako env (zmienne środowiskowe) w krokach,
  • jako parametry with w gotowych akcjach.

Przykład pierwszego wariantu – przekazanie sekretu do skryptu deploy:

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

      - name: Uruchom deploy ze zmiennymi
        run: ./scripts/deploy.sh
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          API_TOKEN: ${{ secrets.API_TOKEN }}

Skrypt deploy.sh może wtedy normalnie korzystać z $DATABASE_URL i $API_TOKEN, jak z każdej innej zmiennej środowiskowej.

Drugi wariant, przekazywanie sekretów jako argumentów gotowych akcji, wygląda na przykład tak:

steps:
  - name: Deploy przez SSH
    uses: appleboy/ssh-action@v1.0.3
    with:
      host: ${{ secrets.SSH_HOST }}
      username: ${{ secrets.SSH_USER }}
      key: ${{ secrets.SSH_KEY }}

Sekrety nigdzie nie są wypisywane w logach (o ile sam ich nie echoujesz) i nie trafiają do artefaktów. GitHub dodatkowo maskuje ich fragmenty w outputach – jeśli gdzieś przypadkiem coś wycieknie, zobaczysz w logu gwiazdki.

Environments i approvals – dodatkowy „bezpiecznik” przed deployem

Environments w GitHubie służą nie tylko do przechowywania sekretów, ale też do kontroli, kiedy można wykonać deployment. Da się ustawić tzw. required reviewers, czyli osoby, które muszą kliknąć „Approve”, żeby job mógł przejść dalej.

Wyobraź sobie produkcję, na którą nie chcesz deployować o każdej godzinie. Możesz utworzyć environment production, podpiąć do niego sekrety i wymusić zatwierdzenie przez Tech Leada. W YAML wygląda to tak:

name: Deploy z approvalem

on:
  workflow_dispatch:

jobs:
  deploy-production:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://app.example.com
    steps:
      - uses: actions/checkout@v4

      - name: Deploy na produkcję
        run: ./scripts/deploy-production.sh
        env:
          API_URL: ${{ vars.API_URL }}
          API_KEY: ${{ secrets.API_KEY }}

Environment production można skonfigurować tak, aby wymagał ręcznego zatwierdzenia. Workflow dochodzi do joba, zatrzymuje się, a wybrana osoba musi kliknąć „Approve and deploy”. Dobrze sprawdza się to w mniejszych zespołach bez rozbudowanego procesu release’ów, ale z potrzebą szybkiej, ludzkiej weryfikacji.

Variables vs secrets – gdy konfiguracja nie musi być tajna

Nie każda konfiguracja wymaga sekretu. Wiele rzeczy można opisać jako zwykłe variables (dostępne także w sekcji Secrets and variables). Różnica:

  • secrets – szyfrowane, maskowane w logach, niewidoczne po zapisaniu,
  • variables – zwykłe zmienne konfiguracyjne, widoczne w UI, nieukrywane w logach.

Dobry podział:

  • Do vars wrzucasz rzeczy typu API_BASE_URL, NODE_ENV, nazwy bucketów, flage feature’ów.
  • Do secrets trafiają hasła, tokeny, klucze prywatne, dane dostępowe do baz, brokerów, chmur.

Przykład użycia variables:

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/deploy-staging.sh
        env:
          API_BASE_URL: ${{ vars.API_BASE_URL }}
          FEATURE_X_ENABLED: ${{ vars.FEATURE_X_ENABLED }}

Dzięki temu YAML nie jest „twardo zaszyty” na konkretne adresy i parametry. Zmianę adresu API można zrobić z poziomu ustawień, bez edycji workflow.

Typowe pułapki przy pracy z sekretami

Przy pierwszych podejściach do sekretów powtarzają się te same błędy. Kilka z nich dobrze mieć z tyłu głowy:

  • Logowanie sekretów przez echo – debugując skrypt, łatwo wrzucić echo $API_KEY. Takie coś od razu ląduje w logu i może być skopiowane. Jeśli naprawdę trzeba sprawdzić, czy zmienna jest ustawiona, pokazuj tylko część, np. echo ${API_KEY:0:4}, albo zupełnie inny znacznik.
  • Wrzucanie sekretów do artefaktów – np. plik .env z sekretami wchodzi do archiwum builda. Potem taki artefakt trafia do bucketu S3 albo jest współdzielony między zespołami. Lepiej generować .env na serwerze, a nie w samym pipeline.
  • Hardcodowane sekrety w YAML – kuszące, gdy coś ma „tylko zadziałać”. Niestety taki plik trafia do historii Git-a i nawet po usunięciu ślad zostaje. Do takich sytuacji powstały narzędzia typu git-secrets czy skanery wbudowane w GitHuba, ale wygodniej od razu tego unikać.
  • Używanie tych samych sekretów dla staging i produkcji – na skróty działa, ale potem staging może przypadkiem modyfikować dane produkcyjne. Osobne klucze i bazy na environment to tanie ubezpieczenie.

Rozbudowa pipeline: artefakty, raporty i warunkowe uruchamianie kroków

Czym są artefakty i po co je w ogóle trzymać

Artefakt to po prostu plik lub paczka, którą pipeline „zostawia” po sobie: zbudowana aplikacja, raport testów, logi z długotrwałego procesu. Tak jak po pieczeniu chleba zostaje bochenek, a nie tylko wspomnienie, tak po CI może zostać gotowa paczka deployowa.

W GitHub Actions artefakty są przechowywane przez ograniczony czas (domyślnie kilka dni) i można je pobrać z zakładki „Actions” jako zwykły plik. Daje to kilka korzyści:

  • jedno miejsce, z którego deploy pobiera zbudowaną aplikację,
  • możliwość debugowania – np. pobranie logów z awaryjnego runu,
  • odseparowanie fazy „build” od „deploy” – budujesz raz, wdrażasz ten sam artefakt na kilka środowisk.

Tworzenie i pobieranie artefaktów

Najprostsza forma to użycie akcji actions/upload-artifact i actions/download-artifact. Przykładowy układ: osobny job build, który pakuje aplikację, i osobny deploy, który z tego korzysta.

name: Build i deploy z artefaktem

on:
  push:
    branches:
      - main

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: Spakuj artefakt
        run: tar czf app.tar.gz dist/
      - name: Upload artefaktu
        uses: actions/upload-artifact@v4
        with:
          name: app-bundle
          path: app.tar.gz

  deploy:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Pobierz artefakt
        uses: actions/download-artifact@v4
        with:
          name: app-bundle
          path: .

      - name: Deploy na serwer
        run: |
          ls -la
          # tutaj wykorzystaj app.tar.gz
          ./scripts/deploy-from-tar.sh app.tar.gz

W ten sposób build nie musi być powtarzany w jobie deploy. Oszczędzasz czas, unikasz niespodzianek typu „na stagingu zbudowało się inaczej niż lokalnie” i możesz w każdej chwili pobrać paczkę z zakładki „Actions”.

Raporty z testów: JUnit, coverage i statyczna analiza

Same logi testów często nie wystarczają. Dobrze jest mieć czytelny raport: które testy padły, jak długo trwało ich wykonanie, jaki jest coverage. Wielu runnerów testów potrafi generować raporty w formacie JUnit lub HTML, wystarczy je „podnieść” jako artefakty.

Dla Node.js może to wyglądać tak:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - name: Uruchom testy z raportem JUnit
        run: npm test -- --reporter=junit --reporter-options output=reports/junit.xml
      - name: Upload raportów testów
        uses: actions/upload-artifact@v4
        with:
          name: test-reports
          path: reports/

Jeśli framework generuje raport HTML (np. coverage z nyc / jest), również można go dodać jako artefakt. W większych organizacjach raporty często są zbierane centralnie przez dedykowane narzędzia (SonarQube, Codecov), ale na start zwykły artefakt w zupełności wystarcza.

Warunkowe kroki: when, if i strategie „szybkiego wyjścia”

Nie każdy krok musi uruchamiać się zawsze. Czasem chcesz ominąć kawałek pipeline’u przy drobnych zmianach w dokumentacji, albo zareagować tylko wtedy, gdy testy padną. GitHub Actions ma prosty mechanizm if:, który można użyć zarówno na poziomie joba, jak i pojedynczego kroku.

Warunkowy krok wygląda tak:

steps:
  - name: Uruchom tylko dla PR-ów
    if: github.event_name == 'pull_request'
    run: npm run lint

Można też reagować na wynik poprzednich kroków:

steps:
  - name: Testy
    id: tests
    run: npm test

  - name: Wyślij powiadomienie o błędzie
    if: failure()
    run: ./scripts/notify-on-failure.sh

Funkcje success(), failure(), cancelled() odnoszą się do stanu całego joba do danego momentu. Dzięki nim da się np. wysłać powiadomienie na Slacka tylko wtedy, gdy coś faktycznie się wysypało.

Warunkowe joby – różne ścieżki dla taga, PR-a, zwykłego pusha

W praktyce pipeline często ma kilka ścieżek: inną dla pull requestów, inną dla pushy do main, jeszcze inną dla release tagów (np. v1.2.3). Zamiast tworzyć trzy osobne workflow, można w jednym pliku rozdzielić joby warunkami.

name: CI/CD z warunkami

on:
  push:
    branches:
      - main
    tags:
      - 'v*.*.*'
  pull_request:

jobs:
  ci-pr:
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

  ci-main:
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && startsWith(github.

Najczęściej zadawane pytania (FAQ)

Co to jest CI/CD i po co mi prosty pipeline w GitHub Actions?

CI/CD to skrót od Continuous Integration i Continuous Delivery/Deployment. W praktyce oznacza to, że każda zmiana w kodzie jest automatycznie budowana, testowana, a często także wdrażana na wybrane środowisko – bez ręcznego klikania i logowania się na serwery.

Prosty pipeline w GitHub Actions przejmuje nudne, powtarzalne kroki: uruchamia testy przy każdym pushu, buduje aplikację, przygotowuje artefakty, a nawet wysyła je na serwer. Zamiast „czy ja na pewno zrobiłem wszystkie kroki?”, masz jeden, powtarzalny proces, który albo przechodzi, albo jasno pokazuje, gdzie się wywrócił.

Dlaczego do CI/CD wybrać GitHub Actions, a nie inne narzędzia?

Jeśli kod leży już na GitHubie, GitHub Actions jest najprostszą drogą do startu. Workflow zapisujesz w repozytorium jako plik YAML, więc cała konfiguracja pipeline’u jest wersjonowana tak samo jak kod: przechodzi PR-y, review i revert. Jedna zmiana w procesie – jedna historia w gicie.

Dodatkowo dostajesz ścisłą integrację z GitHubem (triggery na push, pull_request, release, tag), darmowe minuty dla publicznych repo i ogromny Marketplace z gotowymi akcjami: od checkoutu kodu po logowanie do chmury. Dzięki temu pierwszy działający pipeline da się często postawić w godzinę, a nie w tydzień.

Kiedy GitHub Actions ma sens, a kiedy lepiej użyć GitLab CI albo Jenkinsa?

GitHub Actions świetnie sprawdza się w małych i średnich projektach, szczególnie webowych lub mikroserwisach, które budują się i testują w kilkanaście minut. To dobry wybór, gdy zespół i tak pracuje na GitHubie i nie potrzebuje rozbudowanej, on-premisowej infrastruktury CI z dziesiątkami egzotycznych integracji.

GitLab CI będzie naturalny, gdy cała organizacja siedzi na GitLabie i ma własne wymagania audytowe. Jenkins z kolei bywa sensowny przy bardzo złożonych środowiskach, gdzie kluczowa jest pełna kontrola nad serwerem CI i niestandardowe integracje. Trzeba się jednak liczyć z większym nakładem na utrzymanie – ktoś musi te serwery i pluginy regularnie doglądać.

Jak wygląda najprostszy pipeline CI w GitHub Actions dla małej aplikacji?

Najprostsza wersja pipeline’u CI to workflow uruchamiany na każde push lub pull_request, który robi trzy rzeczy: pobiera kod, instaluje zależności i uruchamia testy. W YAML-u sprowadza się to do jednego joba typu „build & test” z kilkoma krokami (stepami).

Przykładowo: job działa na runs-on: ubuntu-latest, pierwszy step używa actions/checkout do pobrania kodu, drugi instaluje zależności (np. npm install albo pip install -r requirements.txt), a trzeci odpala testy (npm test, pytest itd.). Taki prosty scenariusz już eliminuje sytuacje typu „u mnie działa, na serwerze nie”.

Czym różni się workflow od action w GitHub Actions?

Workflow to cały przepis na automatyzację zapisany w pliku YAML w folderze .github/workflows. Określa, kiedy pipeline ma się uruchomić (np. na push do maina) i z jakich jobów oraz kroków ma się składać.

Action to pojedynczy „klocek”, którego używasz wewnątrz workflowu w konkretnym stepie. Może to być gotowa akcja z Marketplace (np. actions/checkout, actions/cache, actions/upload-artifact) albo własna akcja napisana w Twoim repo. Można to potraktować jak kuchnię: workflow to cały przepis, a poszczególne actions to gotowe narzędzia i półprodukty, które przyspieszają pracę.

Co to jest runner w GitHub Actions i jak wybrać właściwy?

Runner to maszyna (wirtualna lub fizyczna), na której wykonuje się job w workflow. GitHub oferuje hosted runners, czyli gotowe środowiska pod nazwami typu ubuntu-latest, windows-latest, macos-latest, oraz self-hosted runners, które stawiasz samodzielnie w swojej infrastrukturze.

Do prostego pipeline’u CI/CD zwykle wystarczy hosted runner, najczęściej ubuntu-latest. Każdy job startuje na świeżej maszynie z typowymi narzędziami developerskimi, więc nie musisz się martwić „śmieciami” po poprzednich runach. Self-hosted runners przydają się dopiero wtedy, gdy potrzebujesz specjalnego sprzętu, prywatnej sieci lub niestandardowego środowiska.

Jakie są limity GitHub Actions dla publicznych i prywatnych repozytoriów?

Dla publicznych repozytoriów GitHub jest bardzo hojny – projekty open source dostają sporą pulę darmowych minut na buildy i testy. Dlatego wiele popularnych bibliotek czy frameworków może bez kosztów odpalać CI na każdym PR-ze.

W prywatnych repozytoriach limity zależą od planu (Free, Pro, Teams, Enterprise). Przy małych zespołach zwykle wystarcza domyślny pakiet, o ile pipeline’y są rozsądnie zaprojektowane: krótsze, szybkie joby na każdy push, a cięższe rzeczy (np. pełne testy e2e, duże buildy produkcyjne) jedynie na wybrane gałęzie lub przy release’ach.