Pierwszy CI/CD, który skonfigurowałem, to był Jenkins w 2019. Trzy tygodnie setupu, custom serwer, plugin hell. Po każdej aktualizacji coś padało. Kiedy w końcu działało, deploy zajmował 20 minut.

W 2020 przeszedłem na GitHub Actions. Setup: 15 minut. Deploy pierwszego projektu: 4 minuty. Zero infrastruktury do utrzymania.

Dziś każdy projekt w agencji ma GitHub Actions. Pipeline’y build, test, deploy, monitoring. Wszystko definiowane w YAML w tym samym repo co kod. Zero mental overhead.

Ten post pokaże Ci mój dokładny setup dla stacka Next.js + Strapi + Postgres w monorepo. Konkretne pliki, konkretne komendy, dlaczego robimy tak, a nie inaczej.

Co to jest GitHub Actions

GitHub Actions to CI/CD wbudowany w GitHub. Pipeline’y definiujesz jako YAML w .github/workflows/. Trigger’y (push, PR, cron, manual) uruchamiają workflow’y.

Free tier: 2000 minut miesięcznie dla private repos. Public repos = unlimited. Dla większości małych i średnich projektów bezpłatne.

Ekosystem: dziesiątki tysięcy prebuilt actions (deploy do AWS, notify Slack, run tests). Composability jest killer feature.

Dlaczego GitHub Actions, nie inne

CircleCI. Klasyk, świetne performance. Ale drogie, wymaga osobnego konta.

GitLab CI. Świetne, jeśli używasz GitLab. Bezsensowne, jeśli kod jest na GitHub.

Jenkins. Klasyk klasyków. Self-hosted, ogromna flexibility, ale utrzymanie to full-time job.

CircleCI, Travis, Drone. Wszystkie działają, ale mniejszy ekosystem, więcej frykcji.

Vercel/Netlify built-in. Dobre dla stricte frontendowych apps. Ale nie do backendu, testów, complex flows.

Dla większości projektów w 2026 GitHub Actions to default. Kod na GitHub → CI/CD na GitHub. Zero switching, zero extra kont.

Podstawowa struktura

Workflow files w .github/workflows/:

.github/
└── workflows/
    ├── ci.yml           # Tests + linting na każdy PR
    ├── deploy-web.yml   # Deploy Next.js
    ├── deploy-cms.yml   # Deploy Strapi
    ├── security.yml     # Scan raz w tygodniu
    └── cleanup.yml      # Delete stare artefakty raz w miesiącu

Każdy plik = jeden workflow. Może mieć wiele jobs. Każdy job ma wiele steps.

Mój CI workflow

Pierwszy, najważniejszy. Uruchamiany na każdym PR i push do main.

# .github/workflows/ci.yml
name: CI

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

concurrency:
  group: $-$
  cancel-in-progress: true

jobs:
  install:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: pnpm/action-setup@v3
        with:
          version: 9
      
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      
      - run: pnpm install --frozen-lockfile
      
      - name: Cache turbo
        uses: actions/cache@v4
        with:
          path: .turbo
          key: turbo-$
          restore-keys: |
            turbo-

  lint:
    needs: install
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
        with:
          version: 9
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint

  typecheck:
    needs: install
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
        with:
          version: 9
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm typecheck

  test:
    needs: install
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_PASSWORD: test
          POSTGRES_DB: test
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 5s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
        with:
          version: 9
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm test
        env:
          DATABASE_URL: postgresql://postgres:test@localhost:5432/test

  build:
    needs: [lint, typecheck, test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
        with:
          version: 9
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

Kluczowe rzeczy tutaj:

  • Concurrency: nowe pushe kancelują stare buildy. Oszczędność minut.
  • Parallel jobs: lint, typecheck, test lecą równolegle. Szybciej.
  • Postgres service: baza testowa uruchamiana w kontenerze dla testów.
  • Turbo cache: monorepo z TurboRepo cache’uje buildy między runami.

Deploy Next.js na Vercel

Vercel ma dedicated GitHub App - deploy jest automatyczny bez własnego workflow. Ale kiedy chcesz custom flow (feature flags, notifications), używasz Actions.

# .github/workflows/deploy-web.yml
name: Deploy Web

on:
  push:
    branches: [main]
    paths:
      - 'apps/web/**'
      - 'packages/**'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Install Vercel CLI
        run: npm install --global vercel@latest
      
      - name: Pull Vercel Environment
        run: vercel pull --yes --environment=production --token=$
      
      - name: Build
        run: vercel build --prod --token=$
      
      - name: Deploy
        run: vercel deploy --prebuilt --prod --token=$
      
      - name: Notify Slack
        if: success()
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": "🚀 Web deployed to production by $"
            }
        env:
          SLACK_WEBHOOK_URL: $
      
      - name: Notify Sentry
        uses: getsentry/action-release@v1
        env:
          SENTRY_AUTH_TOKEN: $
          SENTRY_ORG: my-org
          SENTRY_PROJECT: my-app
        with:
          environment: production

Kluczowe:

  • Path filter: workflow uruchamia się tylko, gdy zmiany w apps/web/ lub packages/. Bez tego każdy commit trigger’uje pełny build.
  • Vercel CLI: zamiast git push, controlowany deploy przez CLI.
  • Notifications: Slack po deploy, Sentry o nowej wersji.

Deploy Strapi na własny serwer

Strapi zwykle nie idzie na Vercela (nie wspiera long-running processes). Deploy przez Docker + SSH.

# .github/workflows/deploy-cms.yml
name: Deploy CMS

on:
  push:
    branches: [main]
    paths:
      - 'apps/cms/**'
      - 'packages/**'

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: $
          password: $
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          file: apps/cms/Dockerfile
          push: true
          tags: |
            ghcr.io/$/cms:latest
            ghcr.io/$/cms:$
          cache-from: type=gha
          cache-to: type=gha,mode=max
  
  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - name: SSH deploy
        uses: appleboy/ssh-action@v1
        with:
          host: $
          username: deploy
          key: $
          script: |
            cd /var/apps/cms
            docker pull ghcr.io/$/cms:latest
            docker compose up -d --no-deps cms
            docker system prune -f

Flow:

  1. Build image z Dockera
  2. Push do GHCR (GitHub Container Registry, free)
  3. SSH do serwera
  4. Pull nowej wersji, restart kontenera
  5. Cleanup starych images

Cały flow: 5-8 minut. Automatyczne, powtarzalne.

Cache: gdzie kończy się większość minut

Bez cache buildy są wolne. Z cache błyskawiczne.

Node modules cache:

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

Setup-node action automatycznie cache’uje. Pierwszy run: 60 sekund pnpm install. Następne: 5 sekund.

Turbo cache:

- name: Cache turbo
  uses: actions/cache@v4
  with:
    path: .turbo
    key: turbo-$
    restore-keys: |
      turbo-

Turbo build z cache: 2 sekundy dla niezmienionych paczek. Bez cache: 45 sekund.

Docker layer cache:

cache-from: type=gha
cache-to: type=gha,mode=max

Layers Docker cache’owane w GitHub Actions storage. Kolejne buildy 3-5x szybsze.

Bez cache ważne pipeline’y potrafią zajmować 10-15 minut. Z cache 2-4 minuty. Różnica w free tier minutes: 3-4x.

Secrets management

Sekrety (API keys, tokens, passwords) nigdy w repo. GitHub Actions Secrets:

Repo → Settings → Secrets and Variables → Actions → New repository secret.

Używanie:

env:
  DATABASE_URL: $

Organization secrets dla wielu repo. Environment secrets dla różnych environmentów (staging vs prod, różne wartości).

Nie kompromituj się:

  • Nie print’uj secretów w logach
  • Nie commituj .env (add do .gitignore)
  • Rotate secrety co 3-6 miesięcy
  • Delete deprecated secrety

Matrix builds

Testowanie na wielu środowiskach naraz:

strategy:
  matrix:
    node: [18, 20, 22]
    os: [ubuntu-latest, macos-latest]

To uruchomi test 6 razy (3 node × 2 OS). Świetnie do libraries. Overkill dla aplikacji.

Dla mojego stacka (dedykowany node 20 na Ubuntu w prod) matrix nie potrzebny.

Feature flags przez branchowanie

Chcesz deploy tylko dla części userów?

Ephemeral environments: każdy PR ma własny preview URL na Vercelu. Testujesz zmiany przed merge.

Staging: develop branch → staging environment. Prod-like data, real testing.

Production: main branch → prod. Only after staging approval.

Konfiguracja:

on:
  push:
    branches:
      - main       # → production
      - develop    # → staging
  pull_request:    # → preview (Vercel auto)

Trzy environmenty, trzy różne workflow triggers.

Cost management

GitHub Actions free tier:

  • Public repos: unlimited
  • Private repos: 2000 minutes/month (Free), 3000 (Team), 50k (Enterprise)

Naive setup potrafi zjeść 2000 minut w tydzień na aktywnym projekcie.

Techniki oszczędzania:

Concurrency cancellation: nowe pushe kancelują stare. Save 30-50% minut.

Path filters: builduj tylko to, co się zmieniło.

Cache. Cache. Cache.

Self-hosted runners: własny serwer jako runner. Minuty nie liczone. Setup: 30 minut. Wybór dla dużych zespołów.

Skip CI w commitach WIP: [skip ci] w commit message pomija workflow.

Monitoring pipeline’ów

Twój pipeline pada? Bez notyfikacji nie wiesz.

GitHub notifications: email/mobile push na failed builds. Włącz w settings.

Slack integration: notyfikacje w team channel. Konkretny format:

- name: Notify Slack on failure
  if: failure()
  uses: slackapi/slack-github-action@v1
  with:
    payload: |
      {
        "text": "❌ Build failed in $ by $\n$"
      }

Datadog / New Relic: metrics na build times, success rates. Dla zespołów, gdzie CI/CD to first-class citizen.

Security best practices

Pin action versions:

Złe: uses: actions/checkout@v4 (tag, może się zmienić).
Lepsze: uses: actions/checkout@a1b2c3... (commit SHA, immutable).

Attacker, który przejmuje action, nie wpłynie na Twoje buildy.

Minimal permissions:

permissions:
  contents: read
  packages: write

Nie przyznawaj więcej, niż potrzeba.

Restricted workflows: pull_request_target niebezpieczne (dostęp do secretów z PR forks). Używaj pull_request zamiast, jeśli nie musisz koniecznie.

Dependabot: automatyczne update actions. Włącz w repo settings.

Częste błędy

Brak concurrency cancellation. Push 3 razy w minucie, 3 buildy leci równolegle. Cost, wait time. Dodaj concurrency block.

Buildowanie wszystkiego naraz. Push do frontend triggeruje build backendu. Marnujesz minuty. Path filters.

Testy bez isolated env. Testy używają real DB. Testy modyfikują state. Kolejny test nie działa. Postgres service w Compose dla każdego job’a.

Ignorowanie failed tests. continue-on-error: true dla całego pipeline. Failed testy nikogo nie obchodzą. Fix testy albo delete.

Deploy bez rollback strategy. Deploy padnie na produkcji, brak sposobu na powrót do poprzedniej wersji. Docker images taged with SHA, docker compose up z tag = rollback.

Nie test’owanie workflow lokalnie. Push to GitHub, czekasz 5 minut, dowiedujesz się, że YAML jest zły. act (open source) uruchamia workflow lokalnie.

Duplicate installation steps. Każdy job powtarza checkout, setup-node, install. Composite actions albo reusable workflows redukują boilerplate.

Reusable workflows

Jeśli masz 5 workflow’ów z tym samym setupem:

# .github/workflows/reusable-setup.yml
name: Setup

on:
  workflow_call:

jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
      - uses: actions/setup-node@v4
      - run: pnpm install --frozen-lockfile

Wywoływanie:

jobs:
  setup:
    uses: ./.github/workflows/reusable-setup.yml
  
  test:
    needs: setup
    ...

DRY dla CI. Change w jednym miejscu propaguje wszędzie.

Co czytać dalej

  • GitHub Actions docs. Najlepsze dokumentacja CI/CD w branży.
  • awesome-actions na GitHubie: curated lista prebuilt actions.
  • Julien Renaux blog: praktyczne case studies na Actions.

Nie kupuj kursów. Dokumentacja + 3 real workflows = wystarczy.

Od czego zacząć

Jeśli nie masz jeszcze CI/CD:

  1. Stwórz .github/workflows/ci.yml z basic testami.
  2. Dodaj lint i typecheck.
  3. Włącz protected branches w GitHub settings (wymóg passed CI przed merge).
  4. Po tygodniu dodaj deploy workflow.
  5. Po miesiącu dodaj cache, concurrency, path filters.
  6. Po 3 miesiącach rozważ reusable workflows, jeśli masz dużo pipeline’ów.

GitHub Actions to unfair advantage w 2026. Zero infrastruktury do utrzymania, potężny ekosystem, dobra integracja z resztą GitHub.

Pisałem o mojej stack agencji w Docker, Next.js, TurboRepo. GitHub Actions to klej, który spina te elementy w automatyczny deployment flow.

3 godziny setupu, save 3-5 godzin tygodniowo na manualnym deployowaniu. ROI liczony w tygodniach.